您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
speed up video on any site
当前为
// ==UserScript== // @name video, faster // @namespace Violentmonkey Scripts // @include *://*/* // @exclude https://*svelte.dev* // @grant none // @version 5.0 // @author KraXen72 // @description speed up video on any site // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @grant GM_getValue // @grant GM_setValue // @grant GM_addStyle // @license MIT // ==/UserScript== // per-domain speed remember with an option to reset all/manage them? const vfCSS = /*css*/` div.userscript-video-top-bar { box-sizing: border-box; background-color: black; color: white; width: 100%; height: 22px; padding: 0 16px; display: grid; column-gap: 16px; grid-template-columns: max-content max-content 1fr; grid-template-rows: min-content; overflow-x: auto; transition: opacity 0.2s ease-in-out; scrollbar-width: none; } .userscript-video-top-bar::-webkit-scrollbar { display: none } .userscript-video-top-bar button, .userscript-simple-btn { margin: 0 3px; height: 100%; padding: 2px; font-size: 14px !important; line-height: 14px !important; width: max-content; } .userscript-bar-wrap { display: flex !important; position: relative !important; height: 22px !important; padding: 2px 0px !important; box-sizing: border-box; } .userscript-simple-btn { background: #262626 !important; border: 1px solid #191919 !important; border-radius: 2px; font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono', monospace; font-weight: normal; } .userscript-simple-btn, .userscript-simple-btn:hover, .userscript-simple-btn:active { color: white !important; background-image: none !important; box-sizing: border-box !important; box-shadow: none !important; text-shadow: none !important; } .userscript-simple-btn:hover { background: #303030 !important; border-color: #383838 !important; } .userscript-simple-btn:active { opacity: 0.8; } .userscript-hl-rate.userscript-simple-btn, .userscript-hl-rate.userscript-simple-btn:hover, .userscript-hl-rate.userscript-simple-btn:active { background-color: #3aa99fb2 !important; border-color: #3aa99f !important; } .userscript-bar-separator { border-left: 2px solid #616161e5; width: 0px; margin: 0 6px; } .userscript-cb-wrap { display: flex; white-space: nowrap; padding: 0 3px; align-items: center; } .userscript-cb-wrap, .userscript-cb-wrap > * { width: max-content; margin-top: 0; margin-bottom: 0; user-select: none; } .userscript-cb-wrap > *:not(::last-child) { margin-right: 3px; } .userscript-speed-display { min-width: 0; max-width: max-content; pointer-events: none; } ` const pageCSS = /*css*/` .userscript-hoverinv { opacity: 0; } .userscript-hoverinv:hover { opacity: 1 } .userscript-video-shadow-host { box-sizing: border-box; position: absolute; top: 0; left: 0; z-index: 100 !important; width: 100% !important; height: 22px !important; transition: opacity 0.2s ease-in-out !important; } .userscript-video-shadow-host::-webkit-scrollbar { display: none } ` function ensureCSSInjected() { if (vfStyleTag !== null) return; vfStyleTag = GM_addStyle(pageCSS) } let vfStyleTag = null // YouTube-specific variables let lastYouTubeUrl = window.location.href; let currentYouTubeVideo = null; const jumpVal = 5 const rates = [1, 1.5, 1.75, 2, 2.25, 2.5, 2.75, 3, 3.5] const commands = {} const barAborts = {} for (const r of rates) { commands[`${r}x`] = () => playbackRate(r) } function get_uuid() { return window.btoa(String(new Date().getTime())) } function get_restoreSpeed(video) { const lastSpeed = GM_getValue("lastSpeed", 0) return { lastSpeed, restoreSpeed: () => playbackRate(Number(lastSpeed), null, video) } } // Update the playbackRate function to always store the last speed function playbackRate(rate, e = null, video = null) { if (e != null) cancelEvent(e); video = video || currentYouTubeVideo; if (video) { video.playbackRate = rate; } GM_setValue("lastSpeed", rate); // Always store the last speed for playlist navigation } function cancelEvent(e) { if (!e || e == null) return; e.preventDefault() e.stopPropagation() } function ff(vid = null, e) { cancelEvent(e); vid.currentTime += jumpVal; } function rw(vid = null, e) { cancelEvent(e); vid.currentTime -= jumpVal; } function pp(vid = null, e) { cancelEvent(e); if (vid.paused) { vid.play() } else { vid.pause() } } const btnDefaults = { classList: "userscript-simple-btn" } function checkBox(emoji, valueKey = "", title = "", defaultv = false) { const wrap = document.createElement("span") wrap.classList.add("userscript-simple-btn", "userscript-cb-wrap") wrap.textContent = emoji wrap.onclick = cancelEvent if (title) wrap.title = title const cb = Object.assign(document.createElement("input"), { type: "checkbox" }) cb.style.marginLeft = "7px" cb.checked = valueKey ? GM_getValue(valueKey, defaultv) : false cb.onclick = (e) => { if (!valueKey) return; e.stopPropagation(); GM_setValue(valueKey, e.target.checked) } wrap.appendChild(cb) return wrap } // YouTube-specific functions function handleYouTubeVideoChange(video) { if (!window.location.hostname.includes('youtube.com')) return; if (video === currentYouTubeVideo) return; // Store the previous URL before updating const previousUrl = lastYouTubeUrl; lastYouTubeUrl = window.location.href; if (currentYouTubeVideo) { currentYouTubeVideo.removeEventListener('loadeddata', handleYouTubeVideoLoaded); currentYouTubeVideo.removeEventListener('play', handleYouTubeVideoPlay); } currentYouTubeVideo = video; video.addEventListener('loadeddata', handleYouTubeVideoLoaded); video.addEventListener('play', handleYouTubeVideoPlay); // Only check immediately if this is a new video in the same session (playlist) if (!video.paused && video.readyState >= 2 && previousUrl !== window.location.href) { checkAndRestoreYouTubeSpeed(video); } } function handleYouTubeVideoLoaded(e) { checkAndRestoreYouTubeSpeed(e.target); } function handleYouTubeVideoPlay(e) { setTimeout(() => checkAndRestoreYouTubeSpeed(e.target), 50); } function checkAndRestoreYouTubeSpeed(video) { const rememberSpeed = GM_getValue("rememberSpeed", false); const expectedSpeed = GM_getValue("lastSpeed", 1); const currentSpeed = video.playbackRate; // Only restore speed if rememberSpeed is enabled OR this is a playlist navigation if ((rememberSpeed || window.location.href !== lastYouTubeUrl) && expectedSpeed > 1 && currentSpeed === 1) { console.log(`YouTube speed reset detected. Restoring ${expectedSpeed}x`); video.playbackRate = expectedSpeed; video.dispatchEvent(new Event('ratechange')); } } function addVideoTopBar(video) { if (video.dataset.vfUserscriptBar) return; if (video.previousElementSibling !== null && video.previousElementSibling.classList.contains("userscript-video-top-bar")) return; if (video.parentElement === document.body && [...video.parentElement.children].filter(node => node.nodeName && node.nodeName.toLowerCase() === "video").length === 1) return; // don't inject auto-generated video sites ??? ensureCSSInjected() const videoUUID = get_uuid() barAborts[videoUUID] = new AbortController() const rateButtons = {} // so they're bound to the current bar const pe = video.parentElement // don't inject to shorts hover previews (might not work) if (pe.parentElement && pe.parentElement.classList.contains("ytp-inline-preview-mode") && pe.parentElement.classList.contains("ytp-tiny-mode")) return; const shadowHost = document.createElement('div') const shadow = shadowHost.attachShadow({ mode: "open" }); shadowHost.classList.add("userscript-video-shadow-host") const topBar = document.createElement('div'); topBar.classList.add('userscript-video-top-bar') const styleSheet = new CSSStyleSheet(); styleSheet.replaceSync(vfCSS) shadow.adoptedStyleSheets.push(styleSheet); const shouldBePinned = GM_getValue("pinned", false) if (!shouldBePinned) shadowHost.classList.add("userscript-hoverinv"); const leftdiv = Object.assign(document.createElement("div"), { classList: "userscript-bar-wrap" }) const centerdiv = Object.assign(document.createElement("div"), { classList: "userscript-bar-wrap" }) const rightdiv = Object.assign(document.createElement("div"), { classList: "userscript-bar-wrap", style: "justify-content: end" }) for (const r of rates) { const btn = document.createElement("button") btn.textContent = `${r}x` btn.classList = "userscript-simple-btn" btn.dataset.rate = r btn.onclick = (e) => playbackRate(Number(r), e, video) rateButtons[String(r)] = btn leftdiv.appendChild(btn) } centerdiv.appendChild(Object.assign(document.createElement("button"), {...btnDefaults, textContent: "<<", onclick: (e) => rw(video, e), title:`rewind ${jumpVal}s` })) centerdiv.appendChild(Object.assign(document.createElement("button"), {...btnDefaults, textContent: "⏯", onclick: (e) => pp(video, e), title:`play/pause` })) centerdiv.appendChild(Object.assign(document.createElement("button"), {...btnDefaults, textContent: ">>", onclick: (e) => ff(video, e), title:`forward ${jumpVal}s` })) const fallbackRateDisplay = Object.assign(document.createElement("span"), { classList: "userscript-speed-display userscript-simple-btn", textContent: `${video.playbackRate}x`, style: "opacity: 0" }) video.addEventListener("ratechange", (e) => { const _rate = video?.playbackRate ?? e?.target?.playbackRate ?? 1; fallbackRateDisplay.textContent = `${_rate}x` shadow.querySelectorAll(".userscript-hl-rate").forEach(el => el.classList.remove("userscript-hl-rate")) if (rates.includes(_rate)) { fallbackRateDisplay.style.opacity = 0 rateButtons[String(_rate)].classList.add("userscript-hl-rate") console.log("ratechange", _rate, rateButtons[String(_rate)], rateButtons[String(_rate)].classList.toString()) } else { fallbackRateDisplay.style.opacity = 1 } }, { signal: barAborts[videoUUID].signal }) rightdiv.appendChild(fallbackRateDisplay) rightdiv.appendChild(checkBox("💾🐇", "rememberSpeed", "remember video speed across sites.")) rightdiv.appendChild(Object.assign(document.createElement("button"), { ...btnDefaults, onclick: function(e) { cancelEvent(e); shadowHost.classList.toggle("userscript-hoverinv"); const isPinned = !shadowHost.classList.contains("userscript-hoverinv") this.textContent = isPinned ? "📍" : "📌" this.title = isPinned ? "unpin" : "pin" GM_setValue("pinned", isPinned) }, textContent: shouldBePinned ? "📍": "📌", title: shouldBePinned ? "unpin" : "pin", style: "postition: relative;" })) topBar.appendChild(leftdiv) topBar.appendChild(centerdiv) topBar.appendChild(rightdiv) video.dataset.vfUserscriptBar = videoUUID shadowHost.id = `vf-userscript-bar-${videoUUID}` // small video, yeet paddings if (video.clientWidth < 540) { topBar.style.paddingLeft = "4px"; topBar.style.paddingRight = "4px"; topBar.querySelectorAll(".userscript-bar-wrap").forEach(wrapper => { Object.assign(wrapper.style, { paddingLeft: 0, paddingRight: 0 })}) topBar.style.columnGap = "8px" } shadow.appendChild(topBar) if ([...pe.children].filter(node => node.nodeName && node.nodeName.toLowerCase() === "video").length > 1) { // wrapper-less videos shadowHost.style.position = "relative" video.addEventListener("resize", () => { shadowHost.style.width = `${video.clientWidth}px`; }, { signal: barAborts[videoUUID].signal }) shadowHost.style.width = `${video.clientWidth}px`; pe.insertBefore(shadowHost, video); } else { // yt / yt embeds if (pe.classList.contains("html5-video-container") && ( (pe?.parentElement?.classList?.contains("ytp-embed") ?? false) || (Array.from(pe?.parentElement?.classList) ?? []).some(cl => cl.startsWith("ytp-")) )) { pe.parentElement.insertBefore(shadowHost, pe); } else { // rest if (pe.style.position !== 'relative') pe.style.position = 'relative' pe.insertBefore(shadowHost, video); } } const { lastSpeed, restoreSpeed } = get_restoreSpeed(video) if (GM_getValue("rememberSpeed", false) && lastSpeed) { if (!!video.paused) { video.addEventListener("play", () => { if (GM_getValue("rememberSpeed", false) === false) return; const { lastSpeed, restoreSpeed } = get_restoreSpeed(video) setTimeout(restoreSpeed, 1) // hopefully run after any external code }, { once: true, signal: barAborts[videoUUID].signal }) video.addEventListener("loadeddata", () => { if (GM_getValue("rememberSpeed", false) === false) return; console.log("loadeddata") const { lastSpeed, restoreSpeed } = get_restoreSpeed(video) setTimeout(() => { if (video.playbackRate !== Number(lastSpeed)) restoreSpeed() }, 1) }, { once: true, signal: barAborts[videoUUID].signal }) } else { restoreSpeed() } } if (window.location.origin === 'https://www.prageru.com') { video.style.height = 'unset' } console.log(`injected bar for newly added video, id: ${video.dataset.vfUserscriptBar}`) // YouTube-specific handling for newly added video if (window.location.hostname.includes('youtube.com')) { handleYouTubeVideoChange(video); } } // YouTube-specific setup if (window.location.hostname.includes('youtube.com')) { const originalHistory = window.history; // Create a proxy for the history object to detect URL changes const historyProxy = new Proxy(originalHistory, { get(target, prop, receiver) { const originalMethod = Reflect.get(target, prop, receiver); // Intercept pushState and replaceState calls to detect navigation if (prop === 'pushState' || prop === 'replaceState') { return function(...args) { const result = Reflect.apply(originalMethod, target, args); setTimeout(() => { if (window.location.href !== lastYouTubeUrl) { lastYouTubeUrl = window.location.href; const video = document.querySelector('video'); if (video) handleYouTubeVideoChange(video); } }, 100); return result; }; } // For other properties/methods, return them as-is return typeof originalMethod === 'function' ? originalMethod.bind(target) : originalMethod; } }); // Replace the window.history with our proxy Object.defineProperty(window, 'history', { value: historyProxy, writable: false, // Prevent modification configurable: true // Allow redefinition if needed }); // Handle back/forward navigation window.addEventListener('popstate', () => { setTimeout(() => { if (window.location.href !== lastYouTubeUrl) { lastYouTubeUrl = window.location.href; const video = document.querySelector('video'); if (video) handleYouTubeVideoChange(video); } }, 100); }); // Handle YouTube's internal navigation events window.addEventListener('yt-navigate-finish', () => { setTimeout(() => { const video = document.querySelector('video'); if (video) handleYouTubeVideoChange(video); }, 100); }); // Initialize for existing video on YouTube const initialVideo = document.querySelector('video'); if (initialVideo) { handleYouTubeVideoChange(initialVideo); } } function MOCallback(mutationsList, observer) { for(const mutation of mutationsList) { if (mutation.type !== 'childList') { // Handle YouTube attribute changes if (window.location.hostname.includes('youtube.com') && mutation.type === 'attributes' && mutation.target.tagName && mutation.target.tagName.toLowerCase() === 'video') { if (mutation.attributeName === 'src' || mutation.attributeName === 'currentSrc') { handleYouTubeVideoChange(mutation.target); } } continue; } mutation.addedNodes.forEach(node => { if (node.tagName && node.tagName.toLowerCase() === 'video') { addVideoTopBar(node); // YouTube-specific handling if (window.location.hostname.includes('youtube.com')) { handleYouTubeVideoChange(node); } } }); mutation.removedNodes.forEach(node => { if (!node.tagName || node.tagName.toLowerCase() !== "video") return; const uuid = node.dataset.vfUserscriptBar if (!uuid) return; try { document.getElementById(`vf-userscript-bar-${uuid}`).remove(); barAborts[uuid].abort(); } catch (e) { console.warn(`couldn't remove bar id: ${uuid}, was likely removed by site`); } if (uuid in barAborts) delete barAborts[uuid]; // YouTube-specific cleanup if (window.location.hostname.includes('youtube.com') && node === currentYouTubeVideo) { currentYouTubeVideo = null; } }); } } let observer = null; if (observer === null) { observer = new MutationObserver(MOCallback); const observerOptions = { childList: true, subtree: true }; // Add attribute observation for YouTube if (window.location.hostname.includes('youtube.com')) { observerOptions.attributes = true; observerOptions.attributeFilter = ['src', 'currentSrc']; } observer.observe(document.body, observerOptions); } else { console.log("observer is already defined:", observer, "restarting it..."); observer.disconnect(); observer.observe(document.body, { childList: true, subtree: true }); } GM_registerMenuCommand('force add bars to all videos', () => document.querySelectorAll("video").forEach(addVideoTopBar)); document.querySelectorAll("video").forEach(addVideoTopBar);
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址