您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
speed up video on any site
当前为
// ==UserScript== // @name video, faster // @namespace Violentmonkey Scripts // @include *://*/* // @grant none // @version 3.5 // @author KraXen72 // @description speed up video on any site // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @license MIT // ==/UserScript== // TODO // highlight currently selected playbackRate // per-site speed & remember toggles, with a default remember toggle? could be useful // use ratechange event // NOTE: to get compact video speed buttons, add this css in violentMonkey custom css in settings /* [data-message="video, faster"] + .submenu-buttons + .submenu-commands { display: flex; justify-content: center; } [data-message="video, faster"] + .submenu-buttons + .submenu-commands .menu-item { padding: 0.5rem; margin: 0px; width: auto; } [data-message="video, faster"] + .submenu-buttons + .submenu-commands .menu-item .icon { display: none; } */ GM_addStyle(` .userscript-video-top-bar { box-sizing: border-box; background-color: black; color: white; position: absolute; top: 0; left: 0; width: 100%; height: 22px; z-index: 100; display: flex; padding: 0; transition: opacity 0.2s ease-in-out; display: grid; grid-template-columns: max-content max-content 1fr; grid-template-rows: min-content; } .userscript-hoverinv { opacity: 0; } .userscript-hoverinv:hover { opacity: 1 } .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; line-height: 14px; width: min-content; } .userscript-bar-wrap { display: flex; height: 22px; padding: 2px 16px; 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-hl-rate.userscript-simple-btn { background-color: #3aa99fb2 !important; border-color: #3aa99f !important; } .userscript-simple-btn:hover { background: #303030 !important; border-color: #383838 !important; } .userscript-simple-btn:active { background: #383838 !important; } .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-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; } .userscript-cb-wrap > *:not(::last-child) { margin-right: 3px; } .userscript-speed-display { min-width: 32px; max-width: max-content; pointer-events: none; } `); const jumpVal = 5 let videoElem = null const rates = [1, 1.5, 2, 2.5, 2.75, 3, 3.5, 4] const rateButtons = {} const commands = {} for (const r of rates) { commands[`${r}x`] = () => playbackRate(r) } // const commands = { // "1x": () => playbackRate(1), // "1.5x": () => playbackRate(1.5), // "2x": () => playbackRate(2), // "2.5x": () => playbackRate(2.5), // "2.75x": () => playbackRate(2.75), // "3x": () => playbackRate(3), // "3.5x": () => playbackRate(3.5), // "4x": () => playbackRate(4) // } function findVideoElement(debug = false) { if (!document.body) return; if (document.body.contains(videoElem)) return; let qs = "video"; videoElem = document.querySelector(qs); if (videoElem !== null) { if (debug) console.log("found video elem", videoElem, qs); } else { // Look for video elements within iframes const iframes = document.querySelectorAll("iframe"); iframes.forEach(iframe => { const iframeDoc = iframe.contentDocument || iframe.contentWindow.document; const iframeVideoElem = iframeDoc.querySelector("video"); if (iframeVideoElem) { videoElem = iframeVideoElem; if (debug) console.log("found video elem in iframe", videoElem, iframe.src); } }); } } function get_uuid() { return window.btoa(String(new Date().getTime())) } function playbackRate(rate, e = null, video = null) { if (video == null) { findVideoElement() video = videoElem } if (e != null) cancelEvent(e); // let wasplaying = !video.paused // if (wasplaying) video.pause() video.playbackRate = rate // if (wasplaying && video.paused) video.play() GM_setValue("lastSpeed", rate) } function registerCommands() { Object.keys(commands).forEach(command => { try { GM_unregisterMenuCommand(command) } catch (e) { console.error(e) } }) Object.entries(commands).forEach(command => { GM_registerMenuCommand(command[0], command[1]) }) } registerCommands() findVideoElement() // new part of script function cancelEvent(e) { if (!e || e == null) return; e.preventDefault() e.stopPropagation() } function ff(vid = null, e) { cancelEvent(e); if (!vid) vid = videoElem; vid.currentTime += jumpVal; } function rw(vid = null, e) { cancelEvent(e); if (!vid) vid = videoElem; vid.currentTime -= jumpVal; } function pp(vid = null, e) { cancelEvent(e); if (vid.paused) { vid.play() } else { vid.pause() } } const btnDefaults = { classList: "userscript-simple-btn" } let seekHotkeysEnabled = false 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 } function addVideoTopBar(video) { if (video.dataset.vfUserscriptBar) return; if (video.previousElementSibling !== null && video.previousElementSibling.classList.contains("userscript-video-top-bar")) return; const topBar = document.createElement('div'); topBar.classList.add('userscript-video-top-bar') const shouldBePinned = GM_getValue("pinned", false) if (!shouldBePinned) topBar.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.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; fallbackRateDisplay.textContent = `${_rate}x` document.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") } else { fallbackRateDisplay.style.opacity = 1 } }) rightdiv.appendChild(fallbackRateDisplay) rightdiv.appendChild(checkBox("💾🐇", "rememberSpeed", "remember video speed across sites.")) // const hotkeyCb = checkBox("⏪⏩", "add left/right 5s seek hotkeys (this site)") // hotkeyCb.onclick = e => {e.stopPropagation(); console.log(e.target.checked)} // rightdiv.appendChild(hotkeyCb) rightdiv.appendChild(Object.assign(document.createElement("button"), { ...btnDefaults, onclick: function(e) { cancelEvent(e); topBar.classList.toggle("userscript-hoverinv"); const isPinned = !topBar.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) const pe = video.parentElement if (pe.querySelectorAll("video").length > 1) { // wrapper-less videos topBar.style.position = "relative" topBar.style.overflowX = "auto" video.addEventListener("resize", () => { topBar.style.width = `${video.clientWidth}px`; }) topBar.querySelectorAll(".userscript-bar-wrap").forEach(wrapper => { Object.assign(wrapper.style, { paddingLeft: 0, paddingRight: 0 })}) topBar.style.width = `${video.clientWidth}px`; pe.insertBefore(topBar, video); } else { if (pe.classList.contains("html5-video-container") && (pe?.parentElement?.classList?.contains("ytp-embed") ?? false)) { // yt embeds pe.parentElement.insertBefore(topBar, pe); } else { // rest pe.insertBefore(topBar, video); } } video.dataset.vfUserscriptBar = get_uuid() const lastSpeed = GM_getValue("lastSpeed", 0) // video.addEventListener("ratechange", (e) => console.log("ratechange", e, video.playbackRate)) // video.addEventListener("loadeddata", (e) => console.log("loadeddata", e, video.playbackRate)) // video.addEventListener("timeupdate", (e) => console.log("timeupdate", e, video.playbackRate)) if (GM_getValue("rememberSpeed", false) && lastSpeed) { if (!!video.paused) { // TODO remove repeated code? video.addEventListener("play", () => { if (GM_getValue("rememberSpeed", false) === false) return; const lastSpeed = GM_getValue("lastSpeed", 0) const restoreSpeed = () => playbackRate(Number(lastSpeed), null, video); setTimeout(restoreSpeed, 1) // hopefully run after any external code }, { once: true }) video.addEventListener("loadeddata", () => { if (GM_getValue("rememberSpeed", false) === false) return; const lastSpeed = GM_getValue("lastSpeed", 0) const restoreSpeed = () => playbackRate(Number(lastSpeed), null, video); setTimeout(() => { if (video.playbackRate !== Number(lastSpeed)) restoreSpeed() }, 1) }, { once: true }) } else { restoreSpeed() } } } let observer = null; if (observer === null) { observer = new MutationObserver(MOCallback) observer.observe(document.body, { childList: true, subtree: true }); } else { console.log("observer is already defined:", observer, "restarting it...") observer.disconnect() // script has re-ran for whatever reason, but the observer is defined. restart observer to be safe observer.observe(document.body, { childList: true, subtree: true }) } function MOCallback(mutationsList, observer) { for(const mutation of mutationsList) { if (mutation.type !== 'childList') continue; mutation.addedNodes.forEach(node => { if (node.tagName && node.tagName.toLowerCase() === 'video') { console.log('A video element has been added to the document:', node); addVideoTopBar(node) } }); } }; document.querySelectorAll("video").forEach(addVideoTopBar)
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址