var isUserSeek = false; // Track if seek was user-initiated var lastToggleSpeed = {}; // Store last toggle speeds per video var speederShared = typeof SpeederShared === "object" && SpeederShared ? SpeederShared : {}; var controllerUtils = speederShared.controllerUtils || {}; var keyBindingUtils = speederShared.keyBindings || {}; var siteRuleUtils = speederShared.siteRules || {}; var sharedSettingsDefaults = typeof vscGetSettingsDefaults === "function" ? vscGetSettingsDefaults() : null; function getSharedDefault(key, fallback) { if ( sharedSettingsDefaults && Object.prototype.hasOwnProperty.call(sharedSettingsDefaults, key) ) { var value = sharedSettingsDefaults[key]; if (Array.isArray(value)) { return value.map(function(item) { return item && typeof item === "object" ? Object.assign({}, item) : item; }); } return value; } return fallback; } function getPrimaryVideoElement(mediaElements) { var candidates = Array.isArray(mediaElements) ? mediaElements : tc.mediaElements; if (!candidates || candidates.length === 0) return null; var best = null; var bestScore = -Infinity; candidates.forEach(function(el, index) { if (!el || !el.vsc || !el.isConnected) return; var rect = null; try { rect = el.getBoundingClientRect(); } catch (_error) {} var width = rect && Number(rect.width) > 0 ? Number(rect.width) : 0; var height = rect && Number(rect.height) > 0 ? Number(rect.height) : 0; var win = el.ownerDocument && el.ownerDocument.defaultView; var viewportWidth = (win && win.innerWidth) || width; var viewportHeight = (win && win.innerHeight) || height; var visibleWidth = rect ? Math.max(0, Math.min(rect.right, viewportWidth) - Math.max(rect.left, 0)) : 0; var visibleHeight = rect ? Math.max(0, Math.min(rect.bottom, viewportHeight) - Math.max(rect.top, 0)) : 0; var visibleArea = visibleWidth * visibleHeight; var visuallyAvailable = visibleArea > 0; try { var computed = win && win.getComputedStyle(el); if ( computed && (computed.display === "none" || computed.visibility === "hidden" || Number(computed.opacity) === 0) ) { visuallyAvailable = false; } } catch (_error) {} var score = visibleArea; if (visuallyAvailable) score += 1e12; if (!el.paused && !el.ended) { score += visuallyAvailable ? 1e11 : 1e6; } if (el.vsc.lastActivityAt) { score += Math.max(0, 1e9 - (Date.now() - el.vsc.lastActivityAt)); } if (el.ended) score -= 1e11; // Stable tie-break: prefer the most recently registered media element. score += index / 1000; if (score > bestScore) { best = el; bestScore = score; } }); return best; } var tc = { settings: { lastSpeed: getSharedDefault("lastSpeed", 1.0), enabled: getSharedDefault("enabled", true), speeds: {}, rememberSpeed: getSharedDefault("rememberSpeed", false), forceLastSavedSpeed: getSharedDefault("forceLastSavedSpeed", false), audioBoolean: getSharedDefault("audioBoolean", false), showAmbientLoopControls: getSharedDefault( "showAmbientLoopControls", false ), startHidden: getSharedDefault("startHidden", false), hideWithYouTubeControls: getSharedDefault( "hideWithYouTubeControls", false ), hideWithControls: getSharedDefault("hideWithControls", false), hideWithControlsTimer: getSharedDefault("hideWithControlsTimer", 2.0), controllerLocation: getSharedDefault("controllerLocation", "top-left"), controllerOpacity: getSharedDefault("controllerOpacity", 0.3), controllerMarginTop: getSharedDefault("controllerMarginTop", 0), controllerMarginRight: getSharedDefault("controllerMarginRight", 0), controllerMarginBottom: getSharedDefault("controllerMarginBottom", 65), controllerMarginLeft: getSharedDefault("controllerMarginLeft", 0), shortcutTargetMode: getSharedDefault("shortcutTargetMode", "closest"), keyBindings: getSharedDefault("keyBindings", []), siteRules: getSharedDefault("siteRules", []), controllerButtons: getSharedDefault("controllerButtons", [ "rewind", "slower", "faster", "advance", "display" ]), defaultLogLevel: 3, logLevel: 3, enableSubtitleNudge: getSharedDefault("enableSubtitleNudge", false), subtitleNudgeEnabledByDefault: getSharedDefault( "subtitleNudgeEnabledByDefault", true ), subtitleNudgeInterval: getSharedDefault("subtitleNudgeInterval", 250), subtitleNudgeAmount: getSharedDefault("subtitleNudgeAmount", 0.001), customButtonIcons: {} }, mediaElements: [], isNudging: false, pendingLastSpeedSave: null, pendingLastSpeedValue: null, lastSpeedSaveRetries: 0, lastSpeedWriteInFlight: false, lastSpeedWriteValue: null, pendingRememberedSpeedsSave: null, rememberedSpeedsSaveRetries: 0, rememberedSpeedsWriteEpoch: 0, rememberedSpeedsWriteInFlight: null, rememberedSpeedsResetAt: 0, speedAccessTimes: {}, persistedLastSpeed: 1.0, activeSiteRule: null, siteRuleBase: null, runtimeSettingsHydrated: false, pendingMediaCandidates: [], settingsReloadRetries: 0, lastPointerPosition: null, lastInteractedMedia: null, frameToken: window.crypto && typeof window.crypto.randomUUID === "function" ? window.crypto.randomUUID() : String(Date.now()) + "-" + Math.random().toString(36).slice(2) }; var MIN_SPEED = Number(keyBindingUtils.MIN_SPEED) || 0.1; var MAX_SPEED = Number(keyBindingUtils.MAX_SPEED) || 16; var YT_NATIVE_MIN = 0.25; var YT_NATIVE_MAX = 2.0; var YT_NATIVE_STEP = 0.05; var vscObservedRoots = new WeakSet(); var vscObservedRootList = []; var vscConnectedScannedRoots = new WeakSet(); var vscInitializedDocuments = new WeakSet(); var vscSourceObjectIds = new WeakMap(); var vscNextSourceObjectId = 1; var requestIdle = typeof window.requestIdleCallback === "function" ? window.requestIdleCallback.bind(window) : function(callback, options) { return setTimeout(callback, (options && options.timeout) || 1); }; var controllerLocations = Array.isArray(controllerUtils.controllerLocations) ? controllerUtils.controllerLocations.slice() : [ "top-left", "top-center", "top-right", "middle-right", "bottom-right", "bottom-center", "bottom-left", "middle-left" ]; var defaultControllerLocation = controllerUtils.defaultControllerLocation || controllerLocations[0]; var controllerLocationStyles = { "top-left": { top: "10px", left: "15px", transform: "translate(0, 0)" }, "top-center": { top: "10px", left: "50%", transform: "translate(-50%, 0)" }, "top-right": { top: "10px", left: "calc(100% - 10px)", transform: "translate(-100%, 0)" }, "middle-right": { top: "50%", left: "calc(100% - 10px)", transform: "translate(-100%, -50%)" }, "bottom-right": { top: "calc(100% - 0px)", left: "calc(100% - 10px)", transform: "translate(-100%, -100%)" }, "bottom-center": { top: "calc(100% - 0px)", left: "50%", transform: "translate(-50%, -100%)" }, "bottom-left": { top: "calc(100% - 0px)", left: "15px", transform: "translate(0, -100%)" }, "middle-left": { top: "50%", left: "15px", transform: "translate(0, -50%)" } }; /* `label` fallback only when ui-icons has no path for the action. */ var controllerButtonDefs = { rewind: { label: "", name: "Rewind", className: "rw" }, slower: { label: "", name: "Decrease speed", className: "" }, faster: { label: "", name: "Increase speed", className: "" }, advance: { label: "", name: "Advance", className: "rw" }, display: { label: "", name: "Show or hide controller", className: "hideButton" }, reset: { label: "\u21BB", name: "Reset speed", className: "" }, fast: { label: "", name: "Toggle preferred speed", className: "" }, nudge: { label: "", name: "Toggle subtitle nudge", className: "" }, pause: { label: "", name: "Play or pause", className: "" }, muted: { label: "", name: "Mute or unmute", className: "" }, louder: { label: "", name: "Increase volume", className: "" }, softer: { label: "", name: "Decrease volume", className: "" }, mark: { label: "", name: "Mark position", className: "" }, jump: { label: "", name: "Jump to marked position", className: "" }, settings: { label: "", name: "Open Speeder settings", className: "" } }; function createDefaultBinding(action, code, value) { return { action: action, code: code, value: value, force: false, predefined: true }; } function defaultKeyBindings(storage) { return [ createDefaultBinding( "slower", "KeyS", Number(storage.speedStep) || 0.1 ), createDefaultBinding( "faster", "KeyD", Number(storage.speedStep) || 0.1 ), createDefaultBinding( "rewind", "KeyZ", Number(storage.rewindTime) || 10 ), createDefaultBinding( "advance", "KeyX", Number(storage.advanceTime) || 10 ), createDefaultBinding( "reset", "KeyR", 1.0 ), createDefaultBinding( "fast", "KeyG", Number(storage.fastSpeed) || 1.8 ), createDefaultBinding( "move", "KeyP", 0 ), createDefaultBinding( "toggleSubtitleNudge", "KeyN", 0 ) ]; } function ensureDefaultKeyBinding(action, code, value) { if (tc.settings.keyBindings.some((binding) => binding.action === action)) { return false; } tc.settings.keyBindings.push( createDefaultBinding(action, code, value) ); return true; } function getLegacyKeyCode(binding) { return keyBindingUtils.getLegacyKeyCode(binding); } function normalizeControllerLocation(location) { return controllerUtils.normalizeControllerLocation( location, defaultControllerLocation ); } var CONTROLLER_MARGIN_MAX_PX = 200; function normalizeControllerMarginPx(value, fallback) { return controllerUtils.clampControllerMarginPx(value, fallback); } function applyControllerMargins(controller) { if (!controller) return; var d = tc.settings; var loc = controller.dataset.location; var manual = controller.dataset.positionMode === "manual"; var isTopAnchored = !manual && (loc === "top-left" || loc === "top-center" || loc === "top-right"); var isBottomAnchored = !manual && (loc === "bottom-right" || loc === "bottom-center" || loc === "bottom-left"); var isMiddleRow = !manual && (loc === "middle-left" || loc === "middle-right"); var mt = normalizeControllerMarginPx(d.controllerMarginTop, 0); var mb = normalizeControllerMarginPx(d.controllerMarginBottom, 65); if (isTopAnchored || isBottomAnchored || isMiddleRow) { mt = 0; mb = 0; } controller.style.marginTop = mt + "px"; var ml = normalizeControllerMarginPx(d.controllerMarginLeft, 0); var mr = normalizeControllerMarginPx(d.controllerMarginRight, 0); if (!manual) { ml = 0; mr = 0; } controller.style.marginRight = mr + "px"; controller.style.marginBottom = mb + "px"; controller.style.marginLeft = ml + "px"; } function getNextControllerLocation(location) { return controllerUtils.getNextControllerLocation(location); } function getControllerElement(videoOrController) { if (!videoOrController) return null; if ( videoOrController.shadowRoot && typeof videoOrController.shadowRoot.querySelector === "function" ) { return videoOrController.shadowRoot.querySelector("#controller"); } if ( videoOrController.div && videoOrController.div.shadowRoot && typeof videoOrController.div.shadowRoot.querySelector === "function" ) { return videoOrController.div.shadowRoot.querySelector("#controller"); } return null; } function applyControllerLocationToElement(controller, location) { if (!controller) return defaultControllerLocation; var normalizedLocation = normalizeControllerLocation(location); var styles = controllerLocationStyles[normalizedLocation]; controller.dataset.location = normalizedLocation; controller.dataset.positionMode = "anchored"; var top = styles.top; if ( normalizedLocation === "top-left" || normalizedLocation === "top-center" || normalizedLocation === "top-right" ) { var insetTop = normalizeControllerMarginPx( tc.settings.controllerMarginTop, 0 ); top = "calc(10px + " + insetTop + "px)"; } if ( normalizedLocation === "bottom-right" || normalizedLocation === "bottom-center" || normalizedLocation === "bottom-left" ) { var lift = normalizeControllerMarginPx( tc.settings.controllerMarginBottom, 65 ); top = "calc(100% - " + lift + "px)"; } // If in fullscreen, move the controller down to avoid overlapping video titles if ( document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement ) { if (normalizedLocation.startsWith("top-")) { var insetTopFs = normalizeControllerMarginPx( tc.settings.controllerMarginTop, 0 ); top = "calc(63px + " + insetTopFs + "px)"; } } controller.style.top = top; var left = styles.left; switch (normalizedLocation) { case "top-left": case "middle-left": case "bottom-left": left = "15px"; break; case "top-right": case "middle-right": case "bottom-right": left = "calc(100% - 10px)"; break; case "top-center": case "bottom-center": left = "50%"; break; default: break; } controller.style.left = left; controller.style.transform = styles.transform; applyControllerMargins(controller); return normalizedLocation; } function applyControllerLocation(videoController, location) { if (!videoController) return; var controller = getControllerElement(videoController); if (!controller) return; videoController.controllerLocation = applyControllerLocationToElement( controller, location ); } function captureSiteRuleBase() { tc.siteRuleBase = { startHidden: tc.settings.startHidden, hideWithControls: tc.settings.hideWithControls, hideWithControlsTimer: tc.settings.hideWithControlsTimer, controllerLocation: tc.settings.controllerLocation, rememberSpeed: tc.settings.rememberSpeed, forceLastSavedSpeed: tc.settings.forceLastSavedSpeed, audioBoolean: tc.settings.audioBoolean, showAmbientLoopControls: tc.settings.showAmbientLoopControls, controllerOpacity: tc.settings.controllerOpacity, controllerMarginTop: tc.settings.controllerMarginTop, controllerMarginBottom: tc.settings.controllerMarginBottom, shortcutTargetMode: tc.settings.shortcutTargetMode, enableSubtitleNudge: tc.settings.enableSubtitleNudge, subtitleNudgeEnabledByDefault: tc.settings.subtitleNudgeEnabledByDefault, subtitleNudgeInterval: tc.settings.subtitleNudgeInterval, controllerButtons: Array.isArray(tc.settings.controllerButtons) ? tc.settings.controllerButtons.slice() : tc.settings.controllerButtons, keyBindings: Array.isArray(tc.settings.keyBindings) ? tc.settings.keyBindings.map(function(binding) { return Object.assign({}, binding); }) : tc.settings.keyBindings }; } function resetSettingsFromSiteRuleBase() { if (!tc.siteRuleBase) return; var base = tc.siteRuleBase; tc.settings.startHidden = base.startHidden; tc.settings.hideWithControls = base.hideWithControls; tc.settings.hideWithControlsTimer = base.hideWithControlsTimer; tc.settings.controllerLocation = base.controllerLocation; tc.settings.rememberSpeed = base.rememberSpeed; tc.settings.forceLastSavedSpeed = base.forceLastSavedSpeed; tc.settings.audioBoolean = base.audioBoolean; tc.settings.showAmbientLoopControls = base.showAmbientLoopControls; tc.settings.controllerOpacity = base.controllerOpacity; tc.settings.controllerMarginTop = base.controllerMarginTop; tc.settings.controllerMarginBottom = base.controllerMarginBottom; tc.settings.shortcutTargetMode = base.shortcutTargetMode; tc.settings.enableSubtitleNudge = base.enableSubtitleNudge; tc.settings.subtitleNudgeEnabledByDefault = base.subtitleNudgeEnabledByDefault; tc.settings.subtitleNudgeInterval = base.subtitleNudgeInterval; tc.settings.controllerButtons = Array.isArray(base.controllerButtons) ? base.controllerButtons.slice() : base.controllerButtons; tc.settings.keyBindings = Array.isArray(base.keyBindings) ? base.keyBindings.map(function(binding) { return Object.assign({}, binding); }) : base.keyBindings; } function clearManualControllerPosition(videoController) { if (!videoController) return; applyControllerLocation( videoController, videoController.controllerLocation || tc.settings.controllerLocation ); } function convertControllerToManualPosition(videoController) { if (!videoController) return null; var controller = getControllerElement(videoController); if (!controller) return null; controller.dataset.positionMode = "manual"; var offsetParent = controller.offsetParent; if (offsetParent) { var controllerRect = controller.getBoundingClientRect(); var offsetParentRect = offsetParent.getBoundingClientRect(); controller.style.setProperty( "left", controllerRect.left - offsetParentRect.left + "px", "important" ); controller.style.setProperty( "top", controllerRect.top - offsetParentRect.top + "px", "important" ); } else { controller.style.setProperty( "left", controller.offsetLeft + "px", "important" ); controller.style.setProperty( "top", controller.offsetTop + "px", "important" ); } controller.style.setProperty("transform", "none", "important"); return controller; } function cycleControllerLocation(video) { if (!video || !video.vsc) return; video.vsc.controllerLocation = getNextControllerLocation( video.vsc.controllerLocation || tc.settings.controllerLocation ); clearManualControllerPosition(video.vsc); } function normalizeBindingKey(key) { return keyBindingUtils.normalizeBindingKey(key); } function legacyBindingKeyToCode(key) { return keyBindingUtils.legacyBindingKeyToCode(key); } function legacyKeyCodeToCode(keyCode) { return keyBindingUtils.legacyKeyCodeToCode(keyCode); } function inferBindingCode(binding, fallbackCode) { return keyBindingUtils.inferBindingCode(binding, fallbackCode); } function normalizeStoredBinding(binding, fallbackCode) { if (!binding) { if (!fallbackCode) return null; return { code: fallbackCode, disabled: false, value: 0, force: "false", predefined: false }; } if ( binding.disabled === true || (binding.code === null && binding.key === null && binding.keyCode === null) ) { return { action: binding.action, code: null, disabled: true, value: keyBindingUtils.sanitizeActionValue( binding.action, binding.value, 0 ), force: String(binding.force) === "true" ? "true" : "false", predefined: Boolean(binding.predefined) }; } var normalizedCode = inferBindingCode(binding, fallbackCode); if (!normalizedCode) { return null; } var normalized = { action: binding.action, code: normalizedCode, shiftKey: binding.shiftKey === true, disabled: false, value: keyBindingUtils.sanitizeActionValue( binding.action, binding.value, 0 ), force: String(binding.force) === "true" ? "true" : "false", predefined: Boolean(binding.predefined) }; return normalized; } function isValidSpeed(speed) { return !isNaN(speed) && speed >= MIN_SPEED && speed <= MAX_SPEED; } function sanitizeSpeed(speed, fallback) { var numericSpeed = Number(speed); return isValidSpeed(numericSpeed) ? numericSpeed : fallback; } function getVideoSourceKey(video) { if (!video) return "unknown_src"; var sourceObject = video.srcObject; if ( sourceObject && (typeof sourceObject === "object" || typeof sourceObject === "function") ) { if (!vscSourceObjectIds.has(sourceObject)) { vscSourceObjectIds.set(sourceObject, vscNextSourceObjectId++); } return "src_object:" + vscSourceObjectIds.get(sourceObject); } return video.currentSrc || video.src || "unknown_src"; } function getControllerTargetSpeed(video) { if (!video || !video.vsc) return null; if (!isValidSpeed(video.vsc.targetSpeed)) return null; var currentSourceKey = getVideoSourceKey(video); var targetSourceKey = video.vsc.targetSpeedSourceKey; // SPA sites (e.g. YouTube) can reuse the same