您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Add YouTube-style speed controls & shortcuts to Instagram videos (feed, reels, profiles).
// ==UserScript== // @name Instagram Video Speed control (YouTube-like) // @namespace https://yourscripts.example/instagram-speed // @version 1.2.0 // @description Add YouTube-style speed controls & shortcuts to Instagram videos (feed, reels, profiles). // @author X0john // @match https://www.instagram.com/* // @match https://m.instagram.com/* // @run-at document-idle // @grant none // ==/UserScript== (function () { 'use strict'; // ----- Config ----- const STEP = 0.25; const MIN_RATE = 0.1; const MAX_RATE = 16; const STORAGE_KEY = 'ig_speed_rate'; const UI_CLASS = 'ig-speed-control'; const UI_ATTR = 'data-ig-speed-bound'; const ACTIVE_ATTR = 'data-ig-active-video'; const TOAST_ID = 'ig-speed-toast'; // ----- Utilities ----- const clamp = (v, min, max) => Math.min(max, Math.max(min, v)); const round2 = v => Math.round(v * 100) / 100; const getSavedRate = () => { const v = parseFloat(localStorage.getItem(STORAGE_KEY)); return Number.isFinite(v) ? clamp(v, MIN_RATE, MAX_RATE) : 1.0; }; const saveRate = r => localStorage.setItem(STORAGE_KEY, String(r)); const isTypingInInput = () => { const a = document.activeElement; if (!a) return false; const tag = a.tagName?.toLowerCase(); return ( a.isContentEditable || tag === 'input' || tag === 'textarea' || tag === 'select' ); }; const createStyle = () => { if (document.getElementById('ig-speed-style')) return; const css = ` .${UI_CLASS} { position: absolute; bottom: 8px; left: 8px; z-index: 99999; display: inline-flex; gap: 6px; align-items: center; background: rgba(0,0,0,0.55); backdrop-filter: blur(2px); border-radius: 14px; padding: 6px 8px; font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; font-size: 12px; line-height: 1; color: #fff; user-select: none; opacity: 0; transform: translateY(4px); transition: opacity .15s ease, transform .15s ease; pointer-events: auto; } video:hover + .${UI_CLASS}, .${UI_CLASS}:hover, [${ACTIVE_ATTR}="true"] + .${UI_CLASS} { opacity: 1; transform: translateY(0); } .${UI_CLASS} button { border: none; padding: 6px 8px; border-radius: 10px; background: rgba(255,255,255,0.12); color: #fff; font-weight: 600; cursor: pointer; } .${UI_CLASS} button:active { transform: scale(0.96); } .${UI_CLASS} .rate { min-width: 34px; text-align: center; font-weight: 700; letter-spacing: .2px; } /* Toast */ #${TOAST_ID}{ position: fixed; left: 50%; bottom: 8%; transform: translateX(-50%); z-index: 999999; background: rgba(0,0,0,0.75); color: #fff; padding: 10px 14px; border-radius: 12px; font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; font-size: 14px; pointer-events: none; opacity: 0; transition: opacity .12s ease; } #${TOAST_ID}.show { opacity: 1; } `; const style = document.createElement('style'); style.id = 'ig-speed-style'; style.textContent = css; document.head.appendChild(style); }; const showToast = (msg) => { let t = document.getElementById(TOAST_ID); if (!t) { t = document.createElement('div'); t.id = TOAST_ID; document.body.appendChild(t); } t.textContent = msg; t.classList.add('show'); clearTimeout(showToast._timer); showToast._timer = setTimeout(() => t.classList.remove('show'), 700); }; // Keep track of current "active" video (last interacted/visible) let globalRate = getSavedRate(); let lastActiveVideo = null; const setVideoRate = (video, rate, {silent=false} = {}) => { const r = clamp(round2(rate), MIN_RATE, MAX_RATE); if (!video) return; if (video.playbackRate !== r) video.playbackRate = r; const ui = video.nextSibling; if (ui?.classList?.contains(UI_CLASS)) { const rateEl = ui.querySelector('.rate'); if (rateEl) rateEl.textContent = `${r}×`; } if (!silent) showToast(`${r}×`); }; const setGlobalRate = (rate, opts={}) => { const r = clamp(round2(rate), MIN_RATE, MAX_RATE); globalRate = r; saveRate(r); // Apply to all videos currently in DOM document.querySelectorAll('video').forEach(v => setVideoRate(v, r, {silent:true})); if (!opts.silent) showToast(`${r}×`); }; const buildUI = (video) => { if (!video || video[UI_ATTR]) return; // Some IG containers don't have position context; insert right after video and rely on :hover video + UI const ui = document.createElement('div'); ui.className = UI_CLASS; ui.innerHTML = ` <button class="dec" title="Slow down (Shift+,)">–</button> <div class="rate" title="Current speed">${round2(video.playbackRate || globalRate)}×</div> <button class="inc" title="Speed up (Shift+.)">+</button> <button class="reset" title="Reset speed (Shift+Backspace)">1x</button> `; const handler = (delta) => { const current = video.playbackRate || 1.0; setVideoRate(video, current + delta); setGlobalRate(current + delta, {silent:true}); }; ui.querySelector('.dec').addEventListener('click', e => { e.stopPropagation(); handler(-STEP); }); ui.querySelector('.inc').addEventListener('click', e => { e.stopPropagation(); handler(+STEP); }); ui.querySelector('.reset').addEventListener('click', e => { e.stopPropagation(); setVideoRate(video, 1.0); setGlobalRate(1.0, {silent:true}); }); // Scroll to change speed when hovering the control ui.addEventListener('wheel', (e) => { e.preventDefault(); const dir = e.deltaY > 0 ? -STEP : +STEP; handler(dir); }, { passive: false }); // mark as bound video.setAttribute(UI_ATTR, '1'); // mark active video on hover/focus/play const markActive = () => { if (lastActiveVideo && lastActiveVideo !== video) { lastActiveVideo.removeAttribute(ACTIVE_ATTR); } lastActiveVideo = video; video.setAttribute(ACTIVE_ATTR, 'true'); }; ['mouseenter','play','pointerdown','touchstart','focus'].forEach(ev => video.addEventListener(ev, markActive, { passive: true }) ); // Insert right after the video so CSS sibling selector works video.parentElement?.insertBefore(ui, video.nextSibling); // Ensure initial rate reflects global preference setVideoRate(video, video.playbackRate || globalRate, {silent:true}); }; const bindVideo = (video) => { if (!(video instanceof HTMLVideoElement)) return; // Skip tiny/hidden placeholders const rect = video.getBoundingClientRect(); if (rect.width < 60 || rect.height < 60) { // still bind so it updates when sized later } // Apply saved/global rate video.playbackRate = globalRate; // Add UI overlay buildUI(video); // When a video starts playing, sync rate and mark active video.addEventListener('play', () => { setVideoRate(video, globalRate, {silent:true}); if (lastActiveVideo && lastActiveVideo !== video) { lastActiveVideo.removeAttribute(ACTIVE_ATTR); } lastActiveVideo = video; video.setAttribute(ACTIVE_ATTR, 'true'); }, { passive: true }); // If IG swaps the source, re-apply speed video.addEventListener('loadeddata', () => setVideoRate(video, globalRate, {silent:true}), { passive: true }); }; const scan = (root = document) => { root.querySelectorAll('video').forEach(bindVideo); }; const observe = () => { const mo = new MutationObserver((muts) => { for (const m of muts) { if (m.type === 'childList') { m.addedNodes.forEach(n => { if (n.nodeType !== 1) return; if (n.tagName === 'VIDEO') { bindVideo(n); } else { // search within const vids = n.querySelectorAll?.('video'); vids?.forEach(bindVideo); } }); } else if (m.type === 'attributes' && m.target?.tagName === 'VIDEO') { // Re-apply if IG toggles attributes/sources const v = m.target; setVideoRate(v, globalRate, {silent:true}); } } }); mo.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['src', 'poster'] }); }; const onKeydown = (e) => { // Match YouTube: Shift + . (faster), Shift + , (slower) if (isTypingInInput()) return; // Some IG overlays capture keys; use capture to intercept earlier via addEventListener below. if (e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) { const key = e.key; if (key === '.' || key === '>') { e.preventDefault(); setGlobalRate(globalRate + STEP); return; } if (key === ',' || key === '<') { e.preventDefault(); setGlobalRate(globalRate - STEP); return; } if (key === 'Backspace') { e.preventDefault(); setGlobalRate(1.0); return; } } }; // ----- Init ----- const init = () => { createStyle(); globalRate = getSavedRate(); scan(); observe(); // Capture early in the chain window.addEventListener('keydown', onKeydown, true); // Heuristic: keep the "most visible" video as active const visObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { const v = entry.target; if (entry.isIntersecting && entry.intersectionRatio > 0.6) { if (lastActiveVideo && lastActiveVideo !== v) { lastActiveVideo.removeAttribute(ACTIVE_ATTR); } lastActiveVideo = v; v.setAttribute(ACTIVE_ATTR, 'true'); setVideoRate(v, globalRate, {silent:true}); } }); }, { threshold: [0.6] }); document.querySelectorAll('video').forEach(v => visObserver.observe(v)); // Periodic safety net in case IG tears down nodes aggressively setInterval(() => { document.querySelectorAll('video').forEach(v => { if (!v.hasAttribute(UI_ATTR)) bindVideo(v); if (Math.abs((v.playbackRate || 1) - globalRate) > 0.001) { setVideoRate(v, globalRate, {silent:true}); } }); }, 2000); }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init, { once: true }); } else { init(); } })();