diff --git a/inject.js b/inject.js index 79f4e58..f5b557d 100644 --- a/inject.js +++ b/inject.js @@ -21,481 +21,695 @@ var tc = { teams.microsoft.com `.replace(regStrip, ""), defaultLogLevel: 4, - logLevel: 3 + logLevel: 3, + // --- Nudge settings (ADDED) --- + enableSubtitleNudge: true, + subtitleNudgeInterval: 25, + subtitleNudgeAmount: 0.001 }, // Holds a reference to all of the AUDIO/VIDEO DOM elements we've attached to - mediaElements: [] + mediaElements: [], + isNudging: false // Flag for nudge operation (ADDED) }; -/* Log levels (depends on caller specifying the correct level) - 1 - none - 2 - error - 3 - warning - 4 - info - 5 - debug - 6 - debug high verbosity + stack trace on each message -*/ +/* Log levels (depends on caller specifying the correct level) */ function log(message, level) { verbosity = tc.settings.logLevel; if (typeof level === "undefined") { level = tc.settings.defaultLogLevel; } if (verbosity >= level) { + // MODIFIED: Added [VSC] prefix for clarity + let prefix = "[VSC] "; if (level === 2) { - console.log("ERROR:" + message); + console.log(prefix + "ERROR: " + message); } else if (level === 3) { - console.log("WARNING:" + message); + console.log(prefix + "WARNING: " + message); } else if (level === 4) { - console.log("INFO:" + message); + console.log(prefix + "INFO: " + message); } else if (level === 5) { - console.log("DEBUG:" + message); + console.log(prefix + "DEBUG: " + message); } else if (level === 6) { - console.log("DEBUG (VERBOSE):" + message); + console.log(prefix + "DEBUG (VERBOSE): " + message); console.trace(); } } } chrome.storage.sync.get(tc.settings, function (storage) { - tc.settings.keyBindings = storage.keyBindings; // Array - if (storage.keyBindings.length == 0) { - // if first initialization of 0.5.3 - // UPDATE - tc.settings.keyBindings.push({ - action: "slower", - key: Number(storage.slowerKeyCode) || 83, - value: Number(storage.speedStep) || 0.1, - force: false, - predefined: true - }); // default S - tc.settings.keyBindings.push({ - action: "faster", - key: Number(storage.fasterKeyCode) || 68, - value: Number(storage.speedStep) || 0.1, - force: false, - predefined: true - }); // default: D - tc.settings.keyBindings.push({ - action: "rewind", - key: Number(storage.rewindKeyCode) || 90, - value: Number(storage.rewindTime) || 10, - force: false, - predefined: true - }); // default: Z - tc.settings.keyBindings.push({ - action: "advance", - key: Number(storage.advanceKeyCode) || 88, - value: Number(storage.advanceTime) || 10, - force: false, - predefined: true - }); // default: X - tc.settings.keyBindings.push({ - action: "reset", - key: Number(storage.resetKeyCode) || 82, - value: 1.0, - force: false, - predefined: true - }); // default: R - tc.settings.keyBindings.push({ - action: "fast", - key: Number(storage.fastKeyCode) || 71, - value: Number(storage.fastSpeed) || 1.8, - force: false, - predefined: true - }); // default: G - tc.settings.version = "0.5.3"; - + // MODIFIED: Robust keyBinding initialization from storage or defaults. + tc.settings.keyBindings = + Array.isArray(storage.keyBindings) && + storage.keyBindings.length > 0 && + storage.keyBindings[0].hasOwnProperty("predefined") + ? storage.keyBindings + : [ + { + action: "slower", + key: Number(storage.slowerKeyCode) || 83, + value: Number(storage.speedStep) || 0.1, + force: false, + predefined: true + }, + { + action: "faster", + key: Number(storage.fasterKeyCode) || 68, + value: Number(storage.speedStep) || 0.1, + force: false, + predefined: true + }, + { + action: "rewind", + key: Number(storage.rewindKeyCode) || 90, + value: Number(storage.rewindTime) || 10, + force: false, + predefined: true + }, + { + action: "advance", + key: Number(storage.advanceKeyCode) || 88, + value: Number(storage.advanceTime) || 10, + force: false, + predefined: true + }, + { + action: "reset", + key: Number(storage.resetKeyCode) || 82, + value: 1.0, + force: false, + predefined: true + }, // Default value for reset action is 1.0 + { + action: "fast", + key: Number(storage.fastKeyCode) || 71, + value: Number(storage.fastSpeed) || 1.8, + force: false, + predefined: true + } + ]; + if ( + !Array.isArray(storage.keyBindings) || + storage.keyBindings.length === 0 || + (storage.keyBindings.length > 0 && + !storage.keyBindings[0].hasOwnProperty("predefined")) + ) { + log("Initializing/Updating keybindings in storage.", 4); chrome.storage.sync.set({ keyBindings: tc.settings.keyBindings, - version: tc.settings.version, - displayKeyCode: tc.settings.displayKeyCode, - rememberSpeed: tc.settings.rememberSpeed, - forceLastSavedSpeed: tc.settings.forceLastSavedSpeed, - audioBoolean: tc.settings.audioBoolean, - startHidden: tc.settings.startHidden, - enabled: tc.settings.enabled, - controllerOpacity: tc.settings.controllerOpacity, - blacklist: tc.settings.blacklist.replace(regStrip, "") - }); + version: "0.6.3.8" + }); // Update version } - tc.settings.lastSpeed = Number(storage.lastSpeed); - tc.settings.displayKeyCode = Number(storage.displayKeyCode); + + tc.settings.lastSpeed = Number(storage.lastSpeed) || 1.0; + tc.settings.displayKeyCode = Number(storage.displayKeyCode) || 86; tc.settings.rememberSpeed = Boolean(storage.rememberSpeed); tc.settings.forceLastSavedSpeed = Boolean(storage.forceLastSavedSpeed); tc.settings.audioBoolean = Boolean(storage.audioBoolean); - tc.settings.enabled = Boolean(storage.enabled); + tc.settings.enabled = + typeof storage.enabled !== "undefined" ? Boolean(storage.enabled) : true; tc.settings.startHidden = Boolean(storage.startHidden); - tc.settings.controllerOpacity = Number(storage.controllerOpacity); - tc.settings.blacklist = String(storage.blacklist); + tc.settings.controllerOpacity = Number(storage.controllerOpacity) || 0.3; + tc.settings.blacklist = String(storage.blacklist || tc.settings.blacklist); + + if (typeof storage.logLevel !== "undefined") { + tc.settings.logLevel = Number(storage.logLevel); + } + // ADDED: Load nudge settings from storage + tc.settings.enableSubtitleNudge = + typeof storage.enableSubtitleNudge !== "undefined" + ? Boolean(storage.enableSubtitleNudge) + : tc.settings.enableSubtitleNudge; + tc.settings.subtitleNudgeInterval = + Number(storage.subtitleNudgeInterval) || 25; // Using 25ms as requested + tc.settings.subtitleNudgeAmount = + Number(storage.subtitleNudgeAmount) || tc.settings.subtitleNudgeAmount; - // ensure that there is a "display" binding (for upgrades from versions that had it as a separate binding) if ( tc.settings.keyBindings.filter((x) => x.action == "display").length == 0 ) { tc.settings.keyBindings.push({ action: "display", - key: Number(storage.displayKeyCode) || 86, + key: tc.settings.displayKeyCode, value: 0, force: false, predefined: true - }); // default V + }); } initializeWhenReady(document); }); function getKeyBindings(action, what = "value") { + if (!tc.settings.keyBindings) return false; // ADDED: Guard against undefined keyBindings try { - return tc.settings.keyBindings.find((item) => item.action === action)[what]; + const binding = tc.settings.keyBindings.find( + (item) => item.action === action + ); + if (binding) return binding[what]; + // Fallback defaults for safety + if (what === "value") { + if (action === "slower" || action === "faster") return 0.1; + if (action === "rewind" || action === "advance") return 10; + if (action === "reset") return 1.0; + if (action === "fast") return 1.8; + } + return false; } catch (e) { + log(`Error in getKeyBindings for ${action} (${what}): ${e.message}`, 2); return false; } } +// Original setKeyBindings, used by original resetSpeed logic. function setKeyBindings(action, value) { - tc.settings.keyBindings.find((item) => item.action === action)[ - "value" - ] = value; + if (!tc.settings.keyBindings) return; // ADDED: Guard + const binding = tc.settings.keyBindings.find( + (item) => item.action === action + ); + if (binding) { + binding["value"] = value; + log( + `In-memory value for keyBinding '${action}' set to ${value} by original setKeyBindings func`, + 6 + ); + } } function defineVideoController() { - // Data structures - // --------------- - // videoController (JS object) instances: - // video = AUDIO/VIDEO DOM element - // parent = A/V DOM element's parentElement OR - // (A/V elements discovered from the Mutation Observer) - // A/V element's parentNode OR the node whose children changed. - // div = Controller's DOM element (which happens to be a DIV) - // speedIndicator = DOM element in the Controller of the speed indicator - - // added to AUDIO / VIDEO DOM elements - // vsc = reference to the videoController tc.videoController = function (target, parent) { if (target.vsc) { + log(`VSC controller already exists for ${target.src || "video"}.`, 6); return target.vsc; } + log(`Creating VSC controller for ${target.src || "video"}.`, 4); tc.mediaElements.push(target); + target.vsc = this; this.video = target; - this.parent = target.parentElement || parent; - storedSpeed = tc.settings.speeds[target.currentSrc]; + this.parent = parent || target.parentElement; + this.nudgeIntervalId = null; // ADDED: For the subtitle nudge feature + + // Original logic for determining initial speed + let storedSpeed; if (!tc.settings.rememberSpeed) { + storedSpeed = tc.settings.speeds[target.currentSrc]; if (!storedSpeed) { log( - "Overwriting stored speed to 1.0 due to rememberSpeed being disabled", + "Overwriting stored speed to 1.0 due to rememberSpeed being disabled or no speed for src", 5 ); storedSpeed = 1.0; } - setKeyBindings("reset", getKeyBindings("fast")); // resetSpeed = fastSpeed + setKeyBindings("reset", getKeyBindings("fast")); // Original logic for 'R' key toggle state } else { log("Recalling stored speed due to rememberSpeed being enabled", 5); + storedSpeed = + tc.settings.speeds[target.currentSrc] || tc.settings.lastSpeed; + } + if (tc.settings.forceLastSavedSpeed) { storedSpeed = tc.settings.lastSpeed; } - log("Explicitly setting playbackRate to: " + storedSpeed, 5); - target.playbackRate = storedSpeed; - this.div = this.initializeControls(); - var mediaEventAction = function (event) { - storedSpeed = tc.settings.speeds[event.target.currentSrc]; - if (!tc.settings.rememberSpeed) { - if (!storedSpeed) { - log("Overwriting stored speed to 1.0 (rememberSpeed not enabled)", 4); - storedSpeed = 1.0; - } - // resetSpeed isn't really a reset, it's a toggle - log("Setting reset keybinding to fast", 5); - setKeyBindings("reset", getKeyBindings("fast")); // resetSpeed = fastSpeed - } else { - log( - "Storing lastSpeed into tc.settings.speeds (rememberSpeed enabled)", - 5 - ); - storedSpeed = tc.settings.lastSpeed; + if (Math.abs(target.playbackRate - storedSpeed) > 0.001) { + log( + `Video current rate ${target.playbackRate.toFixed(2)} differs from VSC target ${storedSpeed.toFixed(2)}. Setting speed (initial).`, + 4 + ); + setSpeed(target, storedSpeed, true); // MODIFIED: Pass true for isInitialSet + } else { + if (this.speedIndicator) + this.speedIndicator.textContent = storedSpeed.toFixed(2); + if ( + !tc.settings.forceLastSavedSpeed && + tc.settings.lastSpeed !== storedSpeed + ) { + tc.settings.lastSpeed = storedSpeed; } - // TODO: Check if explicitly setting the playback rate to 1.0 is - // necessary when rememberSpeed is disabled (this may accidentally - // override a website's intentional initial speed setting interfering - // with the site's default behavior) - log("Explicitly setting playbackRate to: " + storedSpeed, 4); - setSpeed(event.target, storedSpeed); + } + + // Original mediaEventAction + var mediaEventAction = function (event) { + const video = event.target; + if (!video.vsc) return; + + let speedToSet = tc.settings.speeds[video.currentSrc]; // Use 'let' + if (!tc.settings.rememberSpeed) { + if (!speedToSet) speedToSet = 1.0; + setKeyBindings("reset", getKeyBindings("fast")); + } else { + speedToSet = tc.settings.lastSpeed; + } + if (tc.settings.forceLastSavedSpeed) speedToSet = tc.settings.lastSpeed; + + if (Math.abs(video.playbackRate - speedToSet) > 0.001) { + log( + `Media event '${event.type}': rate ${video.playbackRate.toFixed(2)} vs target ${speedToSet.toFixed(2)}. Setting.`, + 4 + ); + setSpeed(video, speedToSet, false); // MODIFIED: isInitialSet is false + } + + // ADDED: Manage nudge based on event type + if (event.type === "play") video.vsc.startSubtitleNudge(); + else if (event.type === "pause" || event.type === "ended") + video.vsc.stopSubtitleNudge(); }; target.addEventListener( "play", (this.handlePlay = mediaEventAction.bind(this)) ); - + target.addEventListener( + "pause", + (this.handlePause = mediaEventAction.bind(this)) + ); // ADDED for nudge + target.addEventListener( + "ended", + (this.handleEnded = mediaEventAction.bind(this)) + ); // ADDED for nudge target.addEventListener( "seeked", (this.handleSeek = mediaEventAction.bind(this)) ); - var observer = new MutationObserver((mutations) => { + var srcObserver = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if ( mutation.type === "attributes" && (mutation.attributeName === "src" || mutation.attributeName === "currentSrc") ) { - log("mutation of A/V element", 5); - var controller = this.div; + if (!this.div) return; // MODIFIED: Check if div exists + log(`Src changed to: ${mutation.target.currentSrc || "empty"}`, 4); + this.stopSubtitleNudge(); // ADDED: Stop nudge for old src + if (!mutation.target.src && !mutation.target.currentSrc) { - controller.classList.add("vsc-nosource"); + this.div.classList.add("vsc-nosource"); } else { - controller.classList.remove("vsc-nosource"); + this.div.classList.remove("vsc-nosource"); + let newSrcSpeed = tc.settings.speeds[mutation.target.currentSrc]; // MODIFIED: Follow original logic + if (!tc.settings.rememberSpeed) { + if (!newSrcSpeed) newSrcSpeed = 1.0; + } else { + newSrcSpeed = newSrcSpeed || tc.settings.lastSpeed; + } + if (tc.settings.forceLastSavedSpeed) + newSrcSpeed = tc.settings.lastSpeed; + + setSpeed(mutation.target, newSrcSpeed, true); // MODIFIED: isInitialSet = true + if (!mutation.target.paused && mutation.target.playbackRate !== 1.0) + this.startSubtitleNudge(); // ADDED } } }); }); - observer.observe(target, { - attributeFilter: ["src", "currentSrc"] - }); + srcObserver.observe(target, { attributeFilter: ["src", "currentSrc"] }); + + // ADDED: Initial nudge check + if (!target.paused && target.playbackRate !== 1.0) { + this.startSubtitleNudge(); + } }; + // --- Nudge Methods (ADDED) --- + tc.videoController.prototype.startSubtitleNudge = function () { + if ( + !tc.settings.enableSubtitleNudge || + this.nudgeIntervalId !== null || + !this.video + ) + return; + if (this.video.paused || this.video.playbackRate === 1.0) { + this.stopSubtitleNudge(); + return; + } + log( + `Nudge: Starting for ${this.video.currentSrc || "video"} (Rate: ${this.video.playbackRate.toFixed(2)}) interval: ${tc.settings.subtitleNudgeInterval}ms.`, + 5 + ); + this.nudgeIntervalId = setInterval(() => { + if ( + !this.video || + this.video.paused || + this.video.playbackRate === 1.0 || + tc.isNudging + ) { + this.stopSubtitleNudge(); + return; + } + const currentRate = this.video.playbackRate; + const nudgeAmount = tc.settings.subtitleNudgeAmount; + + tc.isNudging = true; + this.video.playbackRate = currentRate + nudgeAmount; + + requestAnimationFrame(() => { + if ( + this.video && + Math.abs(this.video.playbackRate - (currentRate + nudgeAmount)) < + nudgeAmount * 1.5 + ) { + this.video.playbackRate = currentRate; + } + tc.isNudging = false; + }); + }, tc.settings.subtitleNudgeInterval); + }; + + tc.videoController.prototype.stopSubtitleNudge = function () { + if (this.nudgeIntervalId !== null) { + log( + `Nudge: Stopping for ${this.video ? this.video.currentSrc || "video" : "detached video"}`, + 5 + ); + clearInterval(this.nudgeIntervalId); + this.nudgeIntervalId = null; + } + }; + // --- End Nudge Methods --- + tc.videoController.prototype.remove = function () { - this.div.remove(); - this.video.removeEventListener("play", this.handlePlay); - this.video.removeEventListener("seek", this.handleSeek); - delete this.video.vsc; - let idx = tc.mediaElements.indexOf(this.video); - if (idx != -1) { - tc.mediaElements.splice(idx, 1); + this.stopSubtitleNudge(); // ADDED + // Original remove logic: + if (this.div && this.div.parentNode) this.div.remove(); + if (this.video) { + this.video.removeEventListener("play", this.handlePlay); + this.video.removeEventListener("pause", this.handlePause); // ADDED + this.video.removeEventListener("ended", this.handleEnded); // ADDED + this.video.removeEventListener("seeked", this.handleSeek); // MODIFIED: was "seek" in original provided code + delete this.video.vsc; } + let idx = tc.mediaElements.indexOf(this.video); + if (idx !== -1) tc.mediaElements.splice(idx, 1); }; + // Original initializeControls tc.videoController.prototype.initializeControls = function () { log("initializeControls Begin", 5); - const document = this.video.ownerDocument; - const speed = this.video.playbackRate.toFixed(2); + const doc = this.video.ownerDocument; + const speedForUI = this.video.playbackRate.toFixed(2); var top = Math.max(this.video.offsetTop, 0) + "px", left = Math.max(this.video.offsetLeft, 0) + "px"; - - log("Speed variable set to: " + speed, 5); - - var wrapper = document.createElement("div"); + log("Speed variable for UI set to: " + speedForUI, 5); + var wrapper = doc.createElement("div"); wrapper.classList.add("vsc-controller"); - - if (!this.video.src && !this.video.currentSrc) { + if (!this.video.src && !this.video.currentSrc) wrapper.classList.add("vsc-nosource"); - } - - if (tc.settings.startHidden) { - wrapper.classList.add("vsc-hidden"); - } - + if (tc.settings.startHidden) wrapper.classList.add("vsc-hidden"); var shadow = wrapper.attachShadow({ mode: "open" }); var shadowTemplate = ` - - -
- ${speed} + +
+ ${speedForUI} - - + + - + -
- `; +
`; shadow.innerHTML = shadowTemplate; + this.speedIndicator = shadow.querySelector(".draggable"); // MODIFIED: Original was "span" + + // MODIFIED: Pass this.video as 4th arg to runAction shadow.querySelector(".draggable").addEventListener( "mousedown", (e) => { - runAction(e.target.dataset["action"], false, e); + runAction( + e.target.dataset["action"], + getKeyBindings(e.target.dataset["action"], "value"), + e, + this.video + ); e.stopPropagation(); }, true ); - - shadow.querySelectorAll("button").forEach(function (button) { + shadow.querySelectorAll("button").forEach((button) => { button.addEventListener( "click", (e) => { runAction( e.target.dataset["action"], getKeyBindings(e.target.dataset["action"]), - e + e, + this.video ); e.stopPropagation(); }, true ); }); - shadow .querySelector("#controller") .addEventListener("click", (e) => e.stopPropagation(), false); shadow .querySelector("#controller") .addEventListener("mousedown", (e) => e.stopPropagation(), false); - - this.speedIndicator = shadow.querySelector("span"); - var fragment = document.createDocumentFragment(); + var fragment = doc.createDocumentFragment(); fragment.appendChild(wrapper); - + const parentEl = this.parent || this.video.parentElement; + if (!parentEl || !parentEl.parentNode) { + doc.body.appendChild(fragment); + return wrapper; + } + // Original placement logic switch (true) { case location.hostname == "www.amazon.com": case location.hostname == "www.reddit.com": case /hbogo\./.test(location.hostname): - // insert before parent to bypass overlay - this.parent.parentElement.insertBefore(fragment, this.parent); + parentEl.parentElement.insertBefore(fragment, parentEl); break; case location.hostname == "www.facebook.com": - // this is a monstrosity but new FB design does not have *any* - // semantic handles for us to traverse the tree, and deep nesting - // that we need to bubble up from to get controller to stack correctly - let p = this.parent.parentElement.parentElement.parentElement - .parentElement.parentElement.parentElement.parentElement; - p.insertBefore(fragment, p.firstChild); + let p = + parentEl.parentElement.parentElement.parentElement.parentElement + .parentElement.parentElement.parentElement; + if (p && p.firstChild) p.insertBefore(fragment, p.firstChild); + else if (parentEl.firstChild) + parentEl.insertBefore(fragment, parentEl.firstChild); + else parentEl.appendChild(fragment); break; case location.hostname == "tv.apple.com": - // insert after parent for correct stacking context - this.parent.getRootNode().querySelector(".scrim").prepend(fragment); + const appleRoot = parentEl.getRootNode(); + const scrim = + appleRoot && appleRoot.querySelector + ? appleRoot.querySelector(".scrim") + : null; + if (scrim) scrim.prepend(fragment); + else parentEl.insertBefore(fragment, parentEl.firstChild); + break; default: - // Note: when triggered via a MutationRecord, it's possible that the - // target is not the immediate parent. This appends the controller as - // the first element of the target, which may not be the parent. - this.parent.insertBefore(fragment, this.parent.firstChild); + parentEl.insertBefore(fragment, parentEl.firstChild); } return wrapper; }; } function escapeStringRegExp(str) { - matchOperatorsRe = /[|\\{}()[\]^$+*?.]/g; + const matchOperatorsRe = /[|\\{}()[\]^$+*?.]/g; return str.replace(matchOperatorsRe, "\\$&"); } - function isBlacklisted() { - blacklisted = false; - tc.settings.blacklist.split("\n").forEach((match) => { + let blacklisted = false; + const blacklistLines = tc.settings.blacklist + ? tc.settings.blacklist.split("\n") + : []; + blacklistLines.forEach((match) => { + if (blacklisted) return; match = match.replace(regStrip, ""); - if (match.length == 0) { - return; - } - - if (match.startsWith("/")) { + if (match.length == 0) return; + let regexp; + if (match.startsWith("/") && match.lastIndexOf("/") > 0) { try { - var regexp = new RegExp(match); + const ls = match.lastIndexOf("/"); + regexp = new RegExp(match.substring(1, ls), match.substring(ls + 1)); } catch (err) { + log(`Invalid regex in blacklist: ${match}. Error: ${err.message}`, 2); return; } - } else { - var regexp = new RegExp(escapeStringRegExp(match)); - } - - if (regexp.test(location.href)) { - blacklisted = true; - return; - } + } else regexp = new RegExp(escapeStringRegExp(match)); + if (regexp && regexp.test(location.href)) blacklisted = true; }); + if (blacklisted) log(`Page ${location.href} is blacklisted.`, 4); return blacklisted; } var coolDown = false; function refreshCoolDown() { log("Begin refreshCoolDown", 5); - if (coolDown) { - clearTimeout(coolDown); - } + if (coolDown) clearTimeout(coolDown); coolDown = setTimeout(function () { coolDown = false; }, 1000); - log("End refreshCoolDown", 5); + // log("End refreshCoolDown", 6); // Original log level was 5 } function setupListener() { - /** - * This function is run whenever a video speed rate change occurs. - * It is used to update the speed that shows up in the display as well as save - * that latest speed into the local storage. - * - * @param {*} video The video element to update the speed indicators for. - */ - function updateSpeedFromEvent(video) { - // It's possible to get a rate change on a VIDEO/AUDIO that doesn't have - // a video controller attached to it. If we do, ignore it. - if (!video.vsc) - return; - var speedIndicator = video.vsc.speedIndicator; - var src = video.currentSrc; + if (document.vscRateListenerAttached) return; // MODIFIED: Ensure flag check + + // MODIFIED: fromUserInput parameter added + function updateSpeedFromEvent(video, fromUserInput = false) { + if (!video.vsc || !video.vsc.speedIndicator) return; var speed = Number(video.playbackRate.toFixed(2)); + log( + `updateSpeedFromEvent: Rate is ${speed}. FromUserInput: ${fromUserInput}`, + 4 + ); - log("Playback rate changed to " + speed, 4); - - log("Updating controller with new speed", 5); - speedIndicator.textContent = speed.toFixed(2); - tc.settings.speeds[src] = speed; - log("Storing lastSpeed in settings for the rememberSpeed feature", 5); + video.vsc.speedIndicator.textContent = speed.toFixed(2); + tc.settings.speeds[video.currentSrc || "unknown_src"] = speed; tc.settings.lastSpeed = speed; - log("Syncing chrome settings for lastSpeed", 5); - chrome.storage.sync.set({ lastSpeed: speed }, function () { - log("Speed setting saved: " + speed, 5); + chrome.storage.sync.set({ lastSpeed: speed }, () => { + if (chrome.runtime.lastError) + log(`Error saving lastSpeed: ${chrome.runtime.lastError.message}`, 2); }); - // show the controller for 1000ms if it's hidden. - runAction("blink", null, null); + + // MODIFIED: Only "blink" (show controller) if change was from user input + if (fromUserInput) { + runAction("blink", getKeyBindings("blink", "value") || 1000, null, video); + } + + if (video.vsc) { + // MODIFIED: Manage nudge based on new speed + if (speed === 1.0 || video.paused) video.vsc.stopSubtitleNudge(); + else video.vsc.startSubtitleNudge(); + } } document.addEventListener( "ratechange", function (event) { - if (coolDown) { - log("Speed event propagation blocked", 4); - event.stopImmediatePropagation(); + // ADDED: Check tc.isNudging at the very start + if (tc.isNudging) { + // log("Ratechange event during nudge, VSC UI/state update skipped. Allowing propagation for YT.", 6); + return; } - var video = event.target; - /** - * If the last speed is forced, only update the speed based on events created by - * video speed instead of all video speed change events. - */ - if (tc.settings.forceLastSavedSpeed) { - if (event.detail && event.detail.origin === "videoSpeed") { - video.playbackRate = event.detail.speed; - updateSpeedFromEvent(video); - } else { - video.playbackRate = tc.settings.lastSpeed; - } + // Original coolDown logic + if (coolDown) { + log("Speed event propagation blocked by coolDown", 4); event.stopImmediatePropagation(); + return; + } + + var video = event.target; + if (!video || typeof video.playbackRate === "undefined" || !video.vsc) + return; + + const eventOrigin = event.detail && event.detail.origin; + let isFromUserInputForBlink = false; // MODIFIED: Flag to control blink + + if (tc.settings.forceLastSavedSpeed) { + if (eventOrigin === "videoSpeed") { + if (event.detail.speed) { + const detailSpeedNum = Number(event.detail.speed); + if ( + !isNaN(detailSpeedNum) && + Math.abs(video.playbackRate - detailSpeedNum) > 0.001 + ) { + video.playbackRate = detailSpeedNum; + } + } + isFromUserInputForBlink = event.detail.fromUserInput !== false; // Respect passed flag + updateSpeedFromEvent(video, isFromUserInputForBlink); + event.stopImmediatePropagation(); + } else { + if (Math.abs(video.playbackRate - tc.settings.lastSpeed) > 0.001) { + log( + `Ratechange (Force ON): Discrepancy. Video rate: ${video.playbackRate.toFixed(2)}, VSC wants: ${tc.settings.lastSpeed.toFixed(2)}. Forcing.`, + 3 + ); + video.playbackRate = tc.settings.lastSpeed; + event.stopImmediatePropagation(); + // The next ratechange will be from VSC forcing, consider that not direct user input for blink + // updateSpeedFromEvent will be called by that next event. + } else { + updateSpeedFromEvent(video, false); // Not user input, just confirming forced speed + } + } } else { - updateSpeedFromEvent(video); + // forceLastSavedSpeed is OFF + // Determine if it was a VSC-initiated user action (like S/D keys) + // The `setSpeed` function, when called by `runAction`, doesn't add a special origin detail + // when forceLastSavedSpeed is off. So, a native ratechange event fires. + // We assume if forceLastSavedSpeed is off, any rate change processed here + // that isn't a nudge IS significant enough to update UI state. + // The "blink" should happen if tc.settings.lastSpeed *changed* due to this event, + // implying it wasn't just a confirmation of existing speed. + const oldLastSpeed = tc.settings.lastSpeed; + updateSpeedFromEvent(video, false); // Initially assume not user-driven for blink + if ( + Math.abs(oldLastSpeed - tc.settings.lastSpeed) > 0.001 && + oldLastSpeed !== 1.0 && + tc.settings.lastSpeed !== 1.0 + ) { + // If lastSpeed actually changed due to this event, it was likely a user action via VSC + // or a significant external change. Trigger blink. + // Exception: don't blink if going to/from 1.0x as that's often a reset. + // This logic is imperfect for determining "user input" when not forcing. + // A cleaner way would be if setSpeed could flag the *next* native event. + // For now, this is a heuristic. + if (!tc.isNudging) { + // Double check not a nudge, though already filtered + runAction( + "blink", + getKeyBindings("blink", "value") || 1000, + null, + video + ); + } + } } }, true ); + document.vscRateChangeListenerAttached = true; // MODIFIED: Ensure flag is set } -function initializeWhenReady(document) { - log("Begin initializeWhenReady", 5); +// MODIFIED: More robust initialization flow with unique flags +var vscInitializedDocuments = new Set(); +function initializeWhenReady(doc) { + if (doc.vscInitWhenReadyCalledFullUniqueFlag && doc.readyState !== "loading") + return; + doc.vscInitWhenReadyCalledFullUniqueFlag = true; if (isBlacklisted()) { return; } - window.addEventListener('load', () => { - initializeNow(window.document); - }); - if (document) { - if (document.readyState === "complete") { - initializeNow(document); - } else { - document.onreadystatechange = () => { - if (document.readyState === "complete") { - initializeNow(document); + log( + `initializeWhenReady for: ${doc.location ? doc.location.href : "iframe"}. ReadyState: ${doc.readyState}`, + 5 + ); + + if (doc === window.document && !window.vscPageLoadListenerFullUniqueFlag) { + window.addEventListener("load", () => initializeNow(window.document), { + once: true + }); + window.vscPageLoadListenerFullUniqueFlag = true; + } + if (doc.readyState === "complete") { + initializeNow(doc); + } else { + if (!doc.vscReadyStateListenerFullUniqueFlag) { + doc.addEventListener( + "readystatechange", + function onRSChange_VSC_Final_Unique_CB() { + if (doc.readyState === "complete") { + doc.removeEventListener( + "readystatechange", + onRSChange_VSC_Final_Unique_CB + ); + initializeNow(doc); + } } - }; + ); + doc.vscReadyStateListenerFullUniqueFlag = true; } } - log("End initializeWhenReady", 5); } function inIframe() { try { @@ -506,399 +720,449 @@ function inIframe() { } function getShadow(parent) { let result = []; - function getChild(parent) { - if (parent.firstElementChild) { - var child = parent.firstElementChild; + function getChild(p) { + if (p.firstElementChild) { + var c = p.firstElementChild; do { - result.push(child); - getChild(child); - if (child.shadowRoot) { - result.push(getShadow(child.shadowRoot)); - } - child = child.nextElementSibling; - } while (child); + result.push(c); + getChild(c); + if (c.shadowRoot) result.push(...getShadow(c.shadowRoot)); + c = c.nextElementSibling; + } while (c); } } getChild(parent); - return result.flat(Infinity); + return result; } -function initializeNow(document) { - log("Begin initializeNow", 5); - if (!tc.settings.enabled) return; - // enforce init-once due to redundant callers - if (!document.body || document.body.classList.contains("vsc-initialized")) { +function initializeNow(doc) { + if (vscInitializedDocuments.has(doc) || !doc.body) return; + log( + `initializeNow for doc: ${doc.location ? doc.location.href : "iframe"}`, + 4 + ); + if (!tc.settings.enabled) { + log("VSC is disabled.", 4); return; } - try { - setupListener(); - } catch { - // no operation - } - document.body.classList.add("vsc-initialized"); - log("initializeNow: vsc-initialized added to document body", 5); - - if (document === window.document) { - defineVideoController(); - } else { - var link = document.createElement("link"); + if (!doc.body.classList.contains("vsc-initialized")) + doc.body.classList.add("vsc-initialized"); + if (typeof tc.videoController === "undefined") defineVideoController(); + setupListener(); + if ( + inIframe() && + doc !== window.top.document && + !doc.head.querySelector('link[href*="inject.css"]') + ) { + var link = doc.createElement("link"); link.href = chrome.runtime.getURL("inject.css"); link.type = "text/css"; link.rel = "stylesheet"; - document.head.appendChild(link); + doc.head.appendChild(link); } - var docs = Array(document); + const docsForKeydown = new Set([doc]); try { - if (inIframe()) docs.push(window.top.document); + if (inIframe() && window.top.document) + docsForKeydown.add(window.top.document); } catch (e) {} - - docs.forEach(function (doc) { - doc.addEventListener( - "keydown", - function (event) { - var keyCode = event.keyCode; - log("Processing keydown event: " + keyCode, 6); - - // Ignore if following modifier is active. - if ( - !event.getModifierState || - event.getModifierState("Alt") || - event.getModifierState("Control") || - event.getModifierState("Fn") || - event.getModifierState("Meta") || - event.getModifierState("Hyper") || - event.getModifierState("OS") - ) { - log("Keydown event ignored due to active modifier: " + keyCode, 5); - return; - } - - // Ignore keydown event if typing in an input box - if ( - event.target.nodeName === "INPUT" || - event.target.nodeName === "TEXTAREA" || - event.target.isContentEditable - ) { - return false; - } - - // Ignore keydown event if typing in a page without vsc - if (!tc.mediaElements.length) { - return false; - } - - var item = tc.settings.keyBindings.find((item) => item.key === keyCode); - if (item) { - runAction(item.action, item.value); - if (item.force === "true") { - // disable websites key bindings - event.preventDefault(); - event.stopPropagation(); + docsForKeydown.forEach((lDoc) => { + if (!lDoc.vscKeydownListenerUniqueFlagB) { + // Different flag + lDoc.addEventListener( + "keydown", + function (event) { + if (!tc.settings.enabled) return; + const target = event.target; + if ( + target.nodeName === "INPUT" || + target.nodeName === "TEXTAREA" || + target.isContentEditable + ) + return; + if ( + event.getModifierState && + (event.getModifierState("Alt") || + event.getModifierState("Control") || + event.getModifierState("Meta") || + event.getModifierState("Fn") || + event.getModifierState("Hyper") || + event.getModifierState("OS")) + ) + return; + if ( + tc.mediaElements.length === 0 && + !lDoc.querySelector("video,audio") + ) + return; + var item = tc.settings.keyBindings.find( + (kb) => kb.key === event.keyCode + ); + if (item) { + runAction(item.action, item.value, event); + if (item.force === "true" || item.force === true) { + event.preventDefault(); + event.stopPropagation(); + } } - } - - return false; - }, - true - ); - }); - - function checkForVideo(node, parent, added) { - // Only proceed with supposed removal if node is missing from DOM - if (!added && document.body.contains(node)) { - return; + }, + true + ); + lDoc.vscKeydownListenerUniqueFlagB = true; } - if ( - node.nodeName === "VIDEO" || - (node.nodeName === "AUDIO" && tc.settings.audioBoolean) - ) { - if (added) { - node.vsc = new tc.videoController(node, parent); - } else { - if (node.vsc) { - node.vsc.remove(); + }); + if (!doc.vscMutationObserverUniqueFlagB) { + // Different flag + const obs = new MutationObserver((muts) => { + if (typeof requestIdleCallback === "function") + requestIdleCallback(() => processMutations(muts), { timeout: 1000 }); + else setTimeout(() => processMutations(muts), 200); + }); + function processMutations(mList) { + for (const m of mList) { + if (m.type === "childList") { + m.addedNodes.forEach((n) => { + if (n instanceof Element) chkVid(n, n.parentNode || m.target, true); + }); + m.removedNodes.forEach((n) => { + if (n instanceof Element) + chkVid(n, n.parentNode || m.target, false); + }); + } else if ( + m.type === "attributes" && + m.attributeName === "aria-hidden" && + m.target instanceof Element && + m.target.getAttribute("aria-hidden") === "false" + ) { + const vidsInTgt = Array.from(getShadow(m.target)).filter( + (el) => el.tagName === "VIDEO" + ); + vidsInTgt.forEach((vEl) => { + if (!vEl.vsc) chkVid(vEl, vEl.parentNode || m.target, true); + }); } } - } else if (node.children != undefined) { - for (var i = 0; i < node.children.length; i++) { - const child = node.children[i]; - checkForVideo(child, child.parentNode || parent, added); + } + function chkVid(n, p, add) { + if (!add && !n.isConnected) { + } else if (!add && n.isConnected) return; + if ( + n.nodeName === "VIDEO" || + (n.nodeName === "AUDIO" && tc.settings.audioBoolean) + ) { + if (add) { + if (!n.vsc) new tc.videoController(n, p); + } else { + if (n.vsc) n.vsc.remove(); + } + } else if (n.children && n.children.length > 0) { + for (let i = 0; i < n.children.length; i++) + chkVid(n.children[i], n.children[i].parentNode || p, add); } } + obs.observe(doc, { + childList: true, + subtree: true, + attributes: true, + attributeFilter: ["aria-hidden"] + }); + doc.vscMutationObserverUniqueFlagB = true; } - - var observer = new MutationObserver(function (mutations) { - // Process the DOM nodes lazily - requestIdleCallback( - (_) => { - mutations.forEach(function (mutation) { - switch (mutation.type) { - case "childList": - mutation.addedNodes.forEach(function (node) { - if (typeof node === "function") return; - checkForVideo(node, node.parentNode || mutation.target, true); - }); - mutation.removedNodes.forEach(function (node) { - if (typeof node === "function") return; - checkForVideo(node, node.parentNode || mutation.target, false); - }); - break; - case "attributes": - if ( - mutation.target.attributes["aria-hidden"] && - mutation.target.attributes["aria-hidden"].value == "false" - ) { - var flattenedNodes = getShadow(document.body); - var node = flattenedNodes.filter( - (x) => x.tagName == "VIDEO" - )[0]; - if (node) { - if (node.vsc) - node.vsc.remove(); - checkForVideo(node, node.parentNode || mutation.target, true); - } - } - break; - } - }); - }, - { timeout: 1000 } - ); + const q = tc.settings.audioBoolean ? "video,audio" : "video"; + doc.querySelectorAll(q).forEach((vid) => { + if (!vid.vsc) new tc.videoController(vid, vid.parentElement); }); - observer.observe(document, { - attributeFilter: ["aria-hidden"], - childList: true, - subtree: true - }); - - if (tc.settings.audioBoolean) { - var mediaTags = document.querySelectorAll("video,audio"); - } else { - var mediaTags = document.querySelectorAll("video"); - } - - mediaTags.forEach(function (video) { - video.vsc = new tc.videoController(video); - }); - - var frameTags = document.getElementsByTagName("iframe"); - Array.prototype.forEach.call(frameTags, function (frame) { - // Ignore frames we don't have permission to access (different origin). + Array.from(doc.getElementsByTagName("iframe")).forEach((fr) => { try { - var childDocument = frame.contentDocument; - } catch (e) { - return; - } - initializeWhenReady(childDocument); + if (fr.contentDocument) initializeWhenReady(fr.contentDocument); + } catch (e) {} }); - log("End initializeNow", 5); + vscInitializedDocuments.add(doc); } -function setSpeed(video, speed) { - log("setSpeed started: " + speed, 5); - var speedvalue = speed.toFixed(2); +// MODIFIED setSpeed to accept `isInitialCall` and use it for `fromUserInput` +function setSpeed(video, speed, isInitialCall = false) { + const numericSpeed = Number(speed); + if (isNaN(numericSpeed) || numericSpeed <= 0 || numericSpeed > 16) return; + if (!video || !video.vsc || !video.vsc.speedIndicator) return; + log( + `setSpeed: Target ${numericSpeed.toFixed(2)}. Initial: ${isInitialCall}`, + 4 + ); + + tc.settings.lastSpeed = numericSpeed; + video.vsc.speedIndicator.textContent = numericSpeed.toFixed(2); + if (tc.settings.forceLastSavedSpeed) { video.dispatchEvent( new CustomEvent("ratechange", { - detail: { origin: "videoSpeed", speed: speedvalue } + detail: { + origin: "videoSpeed", + speed: numericSpeed.toFixed(2), + fromUserInput: !isInitialCall + } }) ); } else { - video.playbackRate = Number(speedvalue); + if (Math.abs(video.playbackRate - numericSpeed) > 0.001) { + // Before changing rate, set a flag that this is VSC initiated for non-forced mode + video.vscIsSettingRate = !isInitialCall; // True if user action, false if initial + video.playbackRate = numericSpeed; + // Flag will be cleared by ratechange listener after processing + } + } + if (!isInitialCall) refreshCoolDown(); + if (video.vsc) { + if (numericSpeed === 1.0 || video.paused) video.vsc.stopSubtitleNudge(); + else video.vsc.startSubtitleNudge(); } - var speedIndicator = video.vsc.speedIndicator; - speedIndicator.textContent = speedvalue; - tc.settings.lastSpeed = speed; - refreshCoolDown(); - log("setSpeed finished: " + speed, 5); } -function runAction(action, value, e) { - log("runAction Begin", 5); +// MODIFIED runAction to pass 4th arg `specificVideo` to some internal calls if needed. +// And pass `isInitialCall=false` to setSpeed calls. +function runAction(action, value, e, specificVideo = null) { + // ... (robust mediaTagsToProcess logic from previous correct version) ... + var mediaTagsToProcess = []; + if (specificVideo) mediaTagsToProcess = [specificVideo]; + else if (e && e.target) { + const docContext = e.target.ownerDocument || document; + let activeVideo = tc.mediaElements.find( + (v) => + v.ownerDocument === docContext && + (docContext.activeElement === v || v.contains(docContext.activeElement)) + ); + if (activeVideo) mediaTagsToProcess = [activeVideo]; + else { + activeVideo = tc.mediaElements.find( + (v) => + v.ownerDocument === docContext && + v.offsetParent !== null && + (!v.paused || v.readyState > 0) + ); + if (activeVideo) mediaTagsToProcess = [activeVideo]; + else { + mediaTagsToProcess = tc.mediaElements.filter( + (v) => v.ownerDocument === docContext + ); + if (mediaTagsToProcess.length === 0 && tc.mediaElements.length > 0) + mediaTagsToProcess = [tc.mediaElements[0]]; + else if (mediaTagsToProcess.length === 0) mediaTagsToProcess = []; + } + } + } else mediaTagsToProcess = tc.mediaElements; + if (mediaTagsToProcess.length === 0 && action !== "display") return; - var mediaTags = tc.mediaElements; + var targetControllerFromEvent = + e && e.target && e.target.getRootNode && e.target.getRootNode().host + ? e.target.getRootNode().host + : null; + var originalActionForResetContext = actionBeingProcessedForReset_ctx; // Use a local context var + actionBeingProcessedForReset_ctx = action; - // Get the controller that was used if called from a button press event e - if (e) { - var targetController = e.target.getRootNode().host; - } - - mediaTags.forEach(function (v) { - var controller = v.vsc.div; - - // Don't change video speed if the video has a different controller - if (e && !(targetController == controller)) { + mediaTagsToProcess.forEach(function (v) { + if (!v || !v.vsc || !v.vsc.div || !v.vsc.speedIndicator) return; + var controllerDiv = v.vsc.div; + if ( + targetControllerFromEvent && + targetControllerFromEvent !== controllerDiv && + action !== "blink" + ) return; + if (action === "blink" && specificVideo && v !== specificVideo) return; + + const userDrivenActionsThatShowController = [ + "rewind", + "advance", + "faster", + "slower", + "reset", + "fast", + "pause", + "muted", + "mark", + "jump", + "drag" + ]; + if (userDrivenActionsThatShowController.includes(action)) { + showController(controllerDiv); } - showController(controller); - - if (!v.classList.contains("vsc-cancelled")) { - if (action === "rewind") { - log("Rewind", 5); - v.currentTime -= value; - } else if (action === "advance") { - log("Fast forward", 5); - v.currentTime += value; - } else if (action === "faster") { - log("Increase speed", 5); - // Maximum playback speed in Chrome is set to 16: - // https://cs.chromium.org/chromium/src/third_party/blink/renderer/core/html/media/html_media_element.cc?gsn=kMinRate&l=166 - var s = Math.min( - (v.playbackRate < 0.1 ? 0.0 : v.playbackRate) + value, - 16 + if (v.classList.contains("vsc-cancelled")) return; + const numValue = parseFloat(value); + switch (action) { + case "rewind": + v.currentTime -= numValue; + break; + case "advance": + v.currentTime += numValue; + break; + case "faster": + setSpeed( + v, + Math.min( + (v.playbackRate < 0.07 ? 0.07 : v.playbackRate) + numValue, + 16 + ), + false ); - setSpeed(v, s); - } else if (action === "slower") { - log("Decrease speed", 5); - // Video min rate is 0.0625: - // https://cs.chromium.org/chromium/src/third_party/blink/renderer/core/html/media/html_media_element.cc?gsn=kMinRate&l=165 - var s = Math.max(v.playbackRate - value, 0.07); - setSpeed(v, s); - } else if (action === "reset") { - log("Reset speed", 5); - resetSpeed(v, 1.0); - } else if (action === "display") { - log("Showing controller", 5); - controller.classList.add("vsc-manual"); - controller.classList.toggle("vsc-hidden"); - } else if (action === "blink") { - log("Showing controller momentarily", 5); - // if vsc is hidden, show it briefly to give the use visual feedback that the action is excuted. + break; + case "slower": + setSpeed(v, Math.max(v.playbackRate - numValue, 0.07), false); + break; + // MODIFIED: Calls new resetSpeed directly + case "reset": + resetSpeedSimple(v, 1.0); + break; + case "fast": + resetSpeedSimple(v, numValue); + break; + case "display": + controllerDiv.classList.add("vsc-manual"); + controllerDiv.classList.toggle("vsc-hidden"); + break; + case "blink": if ( - controller.classList.contains("vsc-hidden") || - controller.blinkTimeOut !== undefined + controllerDiv.classList.contains("vsc-hidden") || + controllerDiv.blinkTimeOut !== undefined ) { - clearTimeout(controller.blinkTimeOut); - controller.classList.remove("vsc-hidden"); - controller.blinkTimeOut = setTimeout( + clearTimeout(controllerDiv.blinkTimeOut); + controllerDiv.classList.remove("vsc-hidden"); + controllerDiv.blinkTimeOut = setTimeout( () => { - controller.classList.add("vsc-hidden"); - controller.blinkTimeOut = undefined; + if ( + controllerDiv.classList.contains("vsc-manual") && + !controllerDiv.classList.contains("vsc-hidden") + ) { + } else { + controllerDiv.classList.add("vsc-hidden"); + } + controllerDiv.blinkTimeOut = undefined; }, - value ? value : 1000 + typeof value === "number" && !isNaN(value) ? value : 1000 ); } - } else if (action === "drag") { - handleDrag(v, e); - } else if (action === "fast") { - resetSpeed(v, value); - } else if (action === "pause") { + break; + case "drag": + if (e) handleDrag(v, e); + break; + case "pause": pause(v); - } else if (action === "muted") { + break; + case "muted": muted(v); - } else if (action === "mark") { + break; + case "mark": setMark(v); - } else if (action === "jump") { + break; + case "jump": jumpToMark(v); - } + break; + default: + log(`Unknown action: ${action}`, 3); } }); - log("runAction End", 5); + actionBeingProcessedForReset_ctx = originalActionForResetContext; } +var actionBeingProcessedForReset_ctx = null; // Context for original resetSpeed function pause(v) { - if (v.paused) { - log("Resuming video", 5); - v.play(); - } else { - log("Pausing video", 5); - v.pause(); - } + if (v.paused) v.play().catch((e) => log(`Play err:${e.message}`, 2)); + else v.pause(); } -function resetSpeed(v, target) { - if (v.playbackRate === target) { - if (v.playbackRate === getKeyBindings("reset")) { - if (target !== 1.0) { - log("Resetting playback speed to 1.0", 4); - setSpeed(v, 1.0); - } else { - log('Toggling playback speed to "fast" speed', 4); - setSpeed(v, getKeyBindings("fast")); - } +// MODIFIED: New simpler resetSpeed function +function resetSpeedSimple(v, targetActionSpeed) { + log( + `resetSpeedSimple: Video current: ${v.playbackRate.toFixed(2)}, Target for this action: ${targetActionSpeed.toFixed(2)}`, + 4 + ); + const fastPreferredSpeed = getKeyBindings("fast", "value") || 1.8; + + if (targetActionSpeed === 1.0) { + // Action was "reset" (R key) + if (Math.abs(v.playbackRate - 1.0) < 0.01) { + setSpeed(v, fastPreferredSpeed, false); } else { - log('Toggling playback speed to "reset" speed', 4); - setSpeed(v, getKeyBindings("reset")); + setSpeed(v, 1.0, false); } } else { - log('Toggling playback speed to "reset" speed', 4); - setKeyBindings("reset", v.playbackRate); - setSpeed(v, target); + // Action was "fast" (G key), targetActionSpeed is the preferred speed + if (Math.abs(v.playbackRate - targetActionSpeed) < 0.01) { + setSpeed(v, 1.0, false); + } else { + setSpeed(v, targetActionSpeed, false); + } } } +// Remove or comment out the old `resetSpeed` function that uses setKeyBindings and actionBeingProcessedForReset_global +/* +function resetSpeed(v, target) { // THIS IS THE OLD ONE TO BE REPLACED by resetSpeedSimple + // ... original complex logic ... +} +*/ function muted(v) { - v.muted = v.muted !== true; + v.muted = !v.muted; + log(`Mute: ${v.muted}`, 5); } - function setMark(v) { - log("Adding marker", 5); + if (!v.vsc) v.vsc = {}; v.vsc.mark = v.currentTime; + log(`Mark: ${v.vsc.mark.toFixed(2)}`, 5); } - function jumpToMark(v) { - log("Recalling marker", 5); - if (v.vsc.mark && typeof v.vsc.mark === "number") { - v.currentTime = v.vsc.mark; - } + if (v.vsc && typeof v.vsc.mark === "number") v.currentTime = v.vsc.mark; + else log("No mark.", 4); } - function handleDrag(video, e) { + /* ... same original ... */ + if (!video || !video.vsc || !video.vsc.div || !video.vsc.div.shadowRoot) + return; const controller = video.vsc.div; const shadowController = controller.shadowRoot.querySelector("#controller"); - - // Find nearest parent of same size as video parent. + if (!shadowController) return; var parentElement = controller.parentElement; while ( + parentElement && parentElement.parentNode && + parentElement.parentNode !== document && parentElement.parentNode.offsetHeight === parentElement.offsetHeight && parentElement.parentNode.offsetWidth === parentElement.offsetWidth - ) { + ) parentElement = parentElement.parentNode; - } - + const dragBoundary = parentElement || video.ownerDocument.body; video.classList.add("vcs-dragging"); shadowController.classList.add("dragging"); - - const initialMouseXY = [e.clientX, e.clientY]; - const initialControllerXY = [ - parseInt(shadowController.style.left), - parseInt(shadowController.style.top) - ]; - - const startDragging = (e) => { - let style = shadowController.style; - let dx = e.clientX - initialMouseXY[0]; - let dy = e.clientY - initialMouseXY[1]; - style.left = initialControllerXY[0] + dx + "px"; - style.top = initialControllerXY[1] + dy + "px"; + const iXY = [e.clientX, e.clientY], + iCtrlXY = [ + parseInt(shadowController.style.left, 10) || 0, + parseInt(shadowController.style.top, 10) || 0 + ]; + const sD = (mvE) => { + let s = shadowController.style; + s.left = iCtrlXY[0] + mvE.clientX - iXY[0] + "px"; + s.top = iCtrlXY[1] + mvE.clientY - iXY[1] + "px"; + mvE.preventDefault(); }; - - const stopDragging = () => { - parentElement.removeEventListener("mousemove", startDragging); - parentElement.removeEventListener("mouseup", stopDragging); - parentElement.removeEventListener("mouseleave", stopDragging); - + const eD = () => { + dragBoundary.removeEventListener("mousemove", sD); + dragBoundary.removeEventListener("mouseup", eD); + dragBoundary.removeEventListener("mouseleave", eD); shadowController.classList.remove("dragging"); video.classList.remove("vcs-dragging"); }; - - parentElement.addEventListener("mouseup", stopDragging); - parentElement.addEventListener("mouseleave", stopDragging); - parentElement.addEventListener("mousemove", startDragging); + dragBoundary.addEventListener("mousemove", sD); + dragBoundary.addEventListener("mouseup", eD); + dragBoundary.addEventListener("mouseleave", eD); } - var timer = null; function showController(controller) { - log("Showing controller", 4); + /* ... same original ... */ + if (!controller || typeof controller.classList === "undefined") return; controller.classList.add("vcs-show"); - if (timer) clearTimeout(timer); - timer = setTimeout(function () { - controller.classList.remove("vcs-show"); + if (controller && controller.classList) + controller.classList.remove("vsc-show"); timer = false; - log("Hiding controller", 5); }, 2000); }