Files
videospeed/inject.js
2025-07-06 19:40:25 -04:00

918 lines
29 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

var regStrip = /^[\r\t\f\v ]+|[\r\t\f\v ]+$/gm;
var isUserSeek = false; // Track if seek was user-initiated
var lastToggleSpeed = {}; // Store last toggle speeds per video
var tc = {
settings: {
lastSpeed: 1.0,
enabled: true,
speeds: {},
displayKeyCode: 86,
rememberSpeed: false,
forceLastSavedSpeed: false,
audioBoolean: false,
startHidden: false,
controllerOpacity: 0.3,
keyBindings: [],
blacklist: `\
www.instagram.com
twitter.com
vine.co
imgur.com
teams.microsoft.com
`.replace(regStrip, ""),
defaultLogLevel: 4,
logLevel: 5, // Set to 5 to see your debug logs
enableSubtitleNudge: true,
subtitleNudgeInterval: 25,
subtitleNudgeAmount: 0.001
},
mediaElements: [],
isNudging: false
};
/* Log levels */
function log(message, level) {
verbosity = tc.settings.logLevel;
if (typeof level === "undefined") level = tc.settings.defaultLogLevel;
if (verbosity >= level) {
let prefix = "[VSC] ";
if (level === 2) console.log(prefix + "ERROR: " + message);
else if (level === 3) console.log(prefix + "WARNING: " + message);
else if (level === 4) console.log(prefix + "INFO: " + message);
else if (level === 5) console.log(prefix + "DEBUG: " + message);
else if (level === 6) {
console.log(prefix + "DEBUG (VERBOSE): " + message);
console.trace();
}
}
}
chrome.storage.sync.get(tc.settings, function (storage) {
// Original initialization from your code
tc.settings.keyBindings = storage.keyBindings;
if (storage.keyBindings.length == 0) {
tc.settings.keyBindings.push({
action: "slower",
key: Number(storage.slowerKeyCode) || 83,
value: Number(storage.speedStep) || 0.1,
force: false,
predefined: true
});
tc.settings.keyBindings.push({
action: "faster",
key: Number(storage.fasterKeyCode) || 68,
value: Number(storage.speedStep) || 0.1,
force: false,
predefined: true
});
tc.settings.keyBindings.push({
action: "rewind",
key: Number(storage.rewindKeyCode) || 90,
value: Number(storage.rewindTime) || 10,
force: false,
predefined: true
});
tc.settings.keyBindings.push({
action: "advance",
key: Number(storage.advanceKeyCode) || 88,
value: Number(storage.advanceTime) || 10,
force: false,
predefined: true
});
tc.settings.keyBindings.push({
action: "reset",
key: Number(storage.resetKeyCode) || 82,
value: 1.0,
force: false,
predefined: true
});
tc.settings.keyBindings.push({
action: "fast",
key: Number(storage.fastKeyCode) || 71,
value: Number(storage.fastSpeed) || 1.8,
force: false,
predefined: true
});
tc.settings.version = "0.5.3";
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, "")
});
}
tc.settings.lastSpeed = Number(storage.lastSpeed);
tc.settings.displayKeyCode = Number(storage.displayKeyCode);
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.startHidden = Boolean(storage.startHidden);
tc.settings.controllerOpacity = Number(storage.controllerOpacity);
tc.settings.blacklist = String(storage.blacklist);
tc.settings.enableSubtitleNudge =
typeof storage.enableSubtitleNudge !== "undefined"
? Boolean(storage.enableSubtitleNudge)
: tc.settings.enableSubtitleNudge;
tc.settings.subtitleNudgeInterval =
Number(storage.subtitleNudgeInterval) || 25;
tc.settings.subtitleNudgeAmount =
Number(storage.subtitleNudgeAmount) || tc.settings.subtitleNudgeAmount;
if (
tc.settings.keyBindings.filter((x) => x.action == "display").length == 0
) {
tc.settings.keyBindings.push({
action: "display",
key: Number(storage.displayKeyCode) || 86,
value: 0,
force: false,
predefined: true
});
}
initializeWhenReady(document);
});
function getKeyBindings(action, what = "value") {
try {
return tc.settings.keyBindings.find((item) => item.action === action)[what];
} catch (e) {
return false;
}
}
function setKeyBindings(action, value) {
tc.settings.keyBindings.find((item) => item.action === action)["value"] =
value;
}
function defineVideoController() {
tc.videoController = function (target, parent) {
if (target.vsc) return target.vsc;
tc.mediaElements.push(target);
target.vsc = this;
this.video = target;
this.parent = target.parentElement || parent;
this.nudgeIntervalId = null;
let storedSpeed = tc.settings.speeds[target.currentSrc];
if (!tc.settings.rememberSpeed) {
if (!storedSpeed) {
storedSpeed = 1.0;
}
} else {
storedSpeed = tc.settings.lastSpeed;
}
if (tc.settings.forceLastSavedSpeed) {
storedSpeed = tc.settings.lastSpeed;
}
target.playbackRate = storedSpeed;
this.div = this.initializeControls();
// Make the controller visible for 5 seconds on startup
runAction("blink", 5000, null, this.video);
// Rewritten mediaEventAction to prevent speed reset on pause.
var mediaEventAction = function (event) {
// Handle subtitle nudging based on the event type first.
if (event.type === "play") {
this.startSubtitleNudge();
// Reapply the current speed to ensure it doesn't get reset
const currentSpeed = event.target.playbackRate;
if (currentSpeed !== 1.0) {
// Only reapply if it's not already at the correct speed
setTimeout(() => {
if (Math.abs(event.target.playbackRate - currentSpeed) > 0.01) {
event.target.playbackRate = currentSpeed;
}
}, 0);
}
} else if (event.type === "pause" || event.type === "ended") {
this.stopSubtitleNudge();
}
// For seek events, don't mess with speed
if (event.type === "seeked" && isUserSeek) {
isUserSeek = false;
return;
}
};
target.addEventListener(
"play",
(this.handlePlay = mediaEventAction.bind(this))
);
target.addEventListener(
"pause",
(this.handlePause = mediaEventAction.bind(this))
);
target.addEventListener(
"ended",
(this.handleEnded = mediaEventAction.bind(this))
);
target.addEventListener(
"seeked",
(this.handleSeek = mediaEventAction.bind(this))
);
var srcObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (
mutation.type === "attributes" &&
(mutation.attributeName === "src" ||
mutation.attributeName === "currentSrc")
) {
if (this.div) {
this.stopSubtitleNudge();
if (!mutation.target.src && !mutation.target.currentSrc) {
this.div.classList.add("vsc-nosource");
} else {
this.div.classList.remove("vsc-nosource");
if (!mutation.target.paused) this.startSubtitleNudge();
}
}
}
});
});
srcObserver.observe(target, { attributeFilter: ["src", "currentSrc"] });
if (!target.paused && target.playbackRate !== 1.0)
this.startSubtitleNudge();
};
tc.videoController.prototype.remove = function () {
this.stopSubtitleNudge();
if (this.div) this.div.remove();
if (this.video) {
this.video.removeEventListener("play", this.handlePlay);
this.video.removeEventListener("pause", this.handlePause);
this.video.removeEventListener("ended", this.handleEnded);
this.video.removeEventListener("seeked", this.handleSeek);
delete this.video.vsc;
}
let idx = tc.mediaElements.indexOf(this.video);
if (idx != -1) tc.mediaElements.splice(idx, 1);
};
tc.videoController.prototype.startSubtitleNudge = function () {
const isYouTube =
(this.video &&
this.video.currentSrc &&
this.video.currentSrc.includes("googlevideo.com")) ||
location.hostname.includes("youtube.com");
if (!isYouTube) return;
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 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.`, 5);
clearInterval(this.nudgeIntervalId);
this.nudgeIntervalId = null;
}
};
tc.videoController.prototype.initializeControls = function () {
const doc = this.video.ownerDocument;
const speed = this.video.playbackRate.toFixed(2);
// Fix for videos rendered after page load - use relative positioning
var top = "10px",
left = "10px";
// Try to get actual position, but fallback to default if not available
if (this.video.offsetTop > 0 || this.video.offsetLeft > 0) {
top = Math.max(this.video.offsetTop, 0) + "px";
left = Math.max(this.video.offsetLeft, 0) + "px";
}
var wrapper = doc.createElement("div");
wrapper.classList.add("vsc-controller");
if (!this.video.src && !this.video.currentSrc)
wrapper.classList.add("vsc-nosource");
if (tc.settings.startHidden) wrapper.classList.add("vsc-hidden");
var shadow = wrapper.attachShadow({ mode: "open" });
shadow.innerHTML = `<style> @import "${chrome.runtime.getURL("shadow.css")}"; </style><div id="controller" style="top:${top}; left:${left}; opacity:${tc.settings.controllerOpacity}"><span data-action="drag" class="draggable">${speed}</span><span id="controls"><button data-action="rewind" class="rw">«</button><button data-action="slower"></button><button data-action="faster">+</button><button data-action="advance" class="rw">»</button><button data-action="display" class="hideButton">×</button></span></div>`;
this.speedIndicator = shadow.querySelector(".draggable");
shadow.querySelector(".draggable").addEventListener(
"mousedown",
(e) => {
runAction(
e.target.dataset["action"],
getKeyBindings(e.target.dataset["action"], "value"),
e
);
e.stopPropagation();
},
true
);
shadow.querySelectorAll("button").forEach((button) => {
button.addEventListener(
"click",
(e) => {
runAction(
e.target.dataset["action"],
getKeyBindings(e.target.dataset["action"]),
e
);
e.stopPropagation();
},
true
);
});
shadow
.querySelector("#controller")
.addEventListener("click", (e) => e.stopPropagation(), false);
shadow
.querySelector("#controller")
.addEventListener("mousedown", (e) => e.stopPropagation(), false);
var fragment = doc.createDocumentFragment();
fragment.appendChild(wrapper);
const parentEl = this.parent || this.video.parentElement;
if (!parentEl || !parentEl.parentNode) {
doc.body.appendChild(fragment);
return wrapper;
}
switch (true) {
case location.hostname == "www.amazon.com":
case location.hostname == "www.reddit.com":
case /hbogo\./.test(location.hostname):
parentEl.parentElement.insertBefore(fragment, parentEl);
break;
case location.hostname == "www.facebook.com":
let p =
parentEl.parentElement.parentElement.parentElement.parentElement
.parentElement.parentElement.parentElement;
if (p && p.firstChild) p.insertBefore(fragment, p.firstChild);
else parentEl.insertBefore(fragment, parentEl.firstChild);
break;
case location.hostname == "tv.apple.com":
const r = parentEl.getRootNode();
const s = r && r.querySelector ? r.querySelector(".scrim") : null;
if (s) s.prepend(fragment);
else parentEl.insertBefore(fragment, parentEl.firstChild);
break;
default:
parentEl.insertBefore(fragment, parentEl.firstChild);
}
return wrapper;
};
}
function escapeStringRegExp(str) {
const m = /[|\\{}()[\]^$+*?.]/g;
return str.replace(m, "\\$&");
}
function isBlacklisted() {
let b = false;
const l = tc.settings.blacklist ? tc.settings.blacklist.split("\n") : [];
l.forEach((m) => {
if (b) return;
m = m.replace(regStrip, "");
if (m.length == 0) return;
let r;
if (m.startsWith("/") && m.lastIndexOf("/") > 0) {
try {
const ls = m.lastIndexOf("/");
r = new RegExp(m.substring(1, ls), m.substring(ls + 1));
} catch (e) {
log(`Invalid regex: ${m}. ${e.message}`, 2);
return;
}
} else r = new RegExp(escapeStringRegExp(m));
if (r && r.test(location.href)) b = true;
});
if (b) log(`Page ${location.href} blacklisted.`, 4);
return b;
}
var coolDown = false;
function refreshCoolDown() {
if (coolDown) clearTimeout(coolDown);
coolDown = setTimeout(function () {
coolDown = false;
}, 1000);
}
function setupListener() {
if (document.vscRateListenerAttached) return;
function updateSpeedFromEvent(video, fromUserInput = false) {
if (!video.vsc || !video.vsc.speedIndicator) return;
var speed = Number(video.playbackRate.toFixed(2));
video.vsc.speedIndicator.textContent = speed.toFixed(2);
tc.settings.speeds[video.currentSrc || "unknown_src"] = speed;
tc.settings.lastSpeed = speed;
chrome.storage.sync.set({ lastSpeed: speed }, () => {});
if (fromUserInput) {
runAction("blink", 1000, null, video);
}
if (video.vsc) {
if (speed === 1.0 || video.paused) video.vsc.stopSubtitleNudge();
else video.vsc.startSubtitleNudge();
}
}
document.addEventListener(
"ratechange",
function (event) {
if (tc.isNudging) return;
if (coolDown) {
event.stopImmediatePropagation();
return;
}
var video = event.target;
if (!video || typeof video.playbackRate === "undefined" || !video.vsc)
return;
if (tc.settings.forceLastSavedSpeed) {
if (event.detail && event.detail.origin === "videoSpeed") {
video.playbackRate = event.detail.speed;
updateSpeedFromEvent(video, event.detail.fromUserInput === true);
} else {
video.playbackRate = tc.settings.lastSpeed;
}
event.stopImmediatePropagation();
} else {
updateSpeedFromEvent(video, video.vscIsDirectlySettingRate === true);
if (video.vscIsDirectlySettingRate)
delete video.vscIsDirectlySettingRate;
}
},
true
);
document.vscRateListenerAttached = true;
}
var vscInitializedDocuments = new Set();
function initializeWhenReady(doc) {
if (vscInitializedDocuments.has(doc) || !doc.body) return;
if (doc.readyState === "complete") {
initializeNow(doc);
} else {
doc.addEventListener("DOMContentLoaded", () => initializeNow(doc), {
once: true
});
}
}
function inIframe() {
try {
return window.self !== window.top;
} catch (e) {
return true;
}
}
function getShadow(parent) {
let r = [];
function gC(p) {
if (p.firstElementChild) {
var c = p.firstElementChild;
do {
r.push(c);
gC(c);
if (c.shadowRoot) r.push(...getShadow(c.shadowRoot));
c = c.nextElementSibling;
} while (c);
}
}
gC(parent);
return r;
}
function initializeNow(doc) {
if (vscInitializedDocuments.has(doc) || !doc.body) return;
if (!tc.settings.enabled) return;
if (!doc.body.classList.contains("vsc-initialized"))
doc.body.classList.add("vsc-initialized");
if (typeof tc.videoController === "undefined") defineVideoController();
setupListener();
var docs = Array(doc);
try {
if (inIframe()) docs.push(window.top.document);
} catch (e) {}
docs.forEach(function (d) {
if (d.vscKeydownListenerAttached) return; // Prevent duplicate listeners
d.addEventListener(
"keydown",
function (event) {
var keyCode = event.keyCode;
if (
!event.getModifierState ||
event.getModifierState("Alt") ||
event.getModifierState("Control") ||
event.getModifierState("Fn") ||
event.getModifierState("Meta") ||
event.getModifierState("Hyper") ||
event.getModifierState("OS")
)
return;
if (
event.target.nodeName === "INPUT" ||
event.target.nodeName === "TEXTAREA" ||
event.target.isContentEditable
)
return;
if (!tc.mediaElements.length) return;
var item = tc.settings.keyBindings.find((item) => item.key === keyCode);
if (item) {
runAction(item.action, item.value, event);
if (item.force === "true") {
event.preventDefault();
event.stopPropagation();
}
}
return false;
},
true
);
d.vscKeydownListenerAttached = true;
});
if (!doc.vscMutationObserverAttached) {
const observer = new MutationObserver(function (mutations) {
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 }
);
});
function checkForVideo(node, parent, added) {
if (!added && document.body.contains(node)) return;
if (
node.nodeName === "VIDEO" ||
(node.nodeName === "AUDIO" && tc.settings.audioBoolean)
) {
if (added) {
if (!node.vsc) node.vsc = new tc.videoController(node, parent);
} else {
if (node.vsc) node.vsc.remove();
}
} 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);
}
}
}
observer.observe(doc, {
attributeFilter: ["aria-hidden"],
childList: true,
subtree: true
});
doc.vscMutationObserverAttached = true;
}
const q = tc.settings.audioBoolean ? "video,audio" : "video";
const foundVideos = doc.querySelectorAll(q);
foundVideos.forEach((v) => {
if (!v.vsc) new tc.videoController(v, v.parentElement);
});
Array.from(doc.getElementsByTagName("iframe")).forEach((f) => {
if (f.vscLoadListenerAttached) return;
f.addEventListener("load", () => {
try {
if (f.contentDocument) {
initializeWhenReady(f.contentDocument);
}
} catch (e) {
// Silently ignore CORS errors
}
});
f.vscLoadListenerAttached = true;
try {
if (f.contentDocument) {
initializeWhenReady(f.contentDocument);
}
} catch (e) {
// Silently ignore CORS errors
}
});
vscInitializedDocuments.add(doc);
}
function setSpeed(video, speed, isInitialCall = false, isUserKeyPress = 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}. UserKeyPress: ${isUserKeyPress}`,
4
);
tc.settings.lastSpeed = numericSpeed;
video.vsc.speedIndicator.textContent = numericSpeed.toFixed(2);
if (isUserKeyPress && !isInitialCall && video.vsc && video.vsc.div) {
runAction("blink", 1000, null, video); // Pass video to blink
}
if (tc.settings.forceLastSavedSpeed) {
video.dispatchEvent(
new CustomEvent("ratechange", {
detail: {
origin: "videoSpeed",
speed: numericSpeed.toFixed(2),
fromUserInput: isUserKeyPress
}
})
);
} else {
if (Math.abs(video.playbackRate - numericSpeed) > 0.001) {
if (isUserKeyPress && !isInitialCall) {
video.vscIsDirectlySettingRate = true; // Set flag for ratechange listener
}
video.playbackRate = numericSpeed;
}
}
if (!isInitialCall) refreshCoolDown();
if (video.vsc) {
if (numericSpeed === 1.0 || video.paused) video.vsc.stopSubtitleNudge();
else video.vsc.startSubtitleNudge();
}
}
function runAction(action, value, e) {
log("runAction Begin", 5);
var mediaTagsToProcess;
const specificVideo = arguments[3] || null;
if (specificVideo) {
mediaTagsToProcess = [specificVideo];
} else if (e && e.target && e.target.getRootNode) {
// Event-driven action
const docContext = e.target.ownerDocument || document;
mediaTagsToProcess = tc.mediaElements.filter(
(v) => v.ownerDocument === docContext
);
const targetController = e.target.getRootNode().host;
if (targetController) {
// If it's a click on a controller, only use that one video
const videoFromController = tc.mediaElements.find(
(v) => v.vsc && v.vsc.div === targetController
);
if (videoFromController) mediaTagsToProcess = [videoFromController];
}
} else {
mediaTagsToProcess = tc.mediaElements;
}
if (mediaTagsToProcess.length === 0 && action !== "display") return;
mediaTagsToProcess.forEach(function (v) {
if (!v.vsc) return; // Don't process videos without a controller
var controller = v.vsc.div;
const userDrivenActionsThatShowController = [
"rewind",
"advance",
"faster",
"slower",
"reset",
"fast",
"pause",
"muted",
"mark",
"jump",
"drag"
];
if (userDrivenActionsThatShowController.includes(action)) {
showController(controller);
}
if (v.classList.contains("vsc-cancelled")) return;
const numValue = parseFloat(value);
switch (action) {
case "rewind":
isUserSeek = true;
v.currentTime -= numValue;
break;
case "advance":
isUserSeek = true;
v.currentTime += numValue;
break;
case "faster":
setSpeed(
v,
Math.min(
(v.playbackRate < 0.07 ? 0.07 : v.playbackRate) + numValue,
16
),
false,
true
);
break;
case "slower":
setSpeed(v, Math.max(v.playbackRate - numValue, 0.07), false, true);
break;
case "reset":
resetSpeed(v, 1.0, false); // Use enhanced resetSpeed
break;
case "fast":
resetSpeed(v, numValue, true); // Use enhanced resetSpeed
break;
case "display":
controller.classList.add("vsc-manual");
controller.classList.toggle("vsc-hidden");
break;
case "blink":
if (
controller.classList.contains("vsc-hidden") ||
controller.blinkTimeOut !== undefined
) {
clearTimeout(controller.blinkTimeOut);
controller.classList.remove("vsc-hidden");
controller.blinkTimeOut = setTimeout(() => {
if (
!(
controller.classList.contains("vsc-manual") &&
!controller.classList.contains("vsc-hidden")
)
) {
controller.classList.add("vsc-hidden");
}
controller.blinkTimeOut = undefined;
}, numValue || 1000); // FIXED: Use numValue for consistency
}
break;
case "drag":
if (e) handleDrag(v, e);
break;
case "pause":
pause(v);
break;
case "muted":
muted(v);
break;
case "mark":
setMark(v);
break;
case "jump":
jumpToMark(v);
break;
}
});
log("runAction End", 5);
}
function pause(v) {
if (v.paused) v.play().catch((e) => log(`Play err:${e.message}`, 2));
else v.pause();
}
function resetSpeed(v, target, isFastKey = false) {
const videoId = v.currentSrc || v.src || "default";
const currentSpeed = v.playbackRate;
if (isFastKey) {
// G key: Toggle between current speed and preferred speed (e.g., 1.8)
const preferredSpeed = target;
const lastToggle = lastToggleSpeed[videoId] || currentSpeed;
if (Math.abs(currentSpeed - preferredSpeed) < 0.01) {
// Currently at preferred speed, toggle to the last speed
setSpeed(v, lastToggle, false, true);
} else {
// Not at preferred speed, save current as toggle speed and go to preferred
lastToggleSpeed[videoId] = currentSpeed;
setSpeed(v, preferredSpeed, false, true);
}
} else {
// R key: Toggle between current speed and 1.0
const resetSpeedValue = 1.0;
const lastToggle = lastToggleSpeed[videoId] || currentSpeed;
if (Math.abs(currentSpeed - resetSpeedValue) < 0.01) {
// Currently at 1.0, toggle to the last speed (or 1.8 if no history)
const speedToRestore =
Math.abs(lastToggle - 1.0) < 0.01
? getKeyBindings("fast") || 1.8
: lastToggle;
setSpeed(v, speedToRestore, false, true);
} else {
// Not at 1.0, save current as toggle speed and go to 1.0
lastToggleSpeed[videoId] = currentSpeed;
setSpeed(v, resetSpeedValue, false, true);
}
}
}
function muted(v) {
v.muted = !v.muted;
}
function setMark(v) {
v.vsc.mark = v.currentTime;
}
function jumpToMark(v) {
if (v.vsc && typeof v.vsc.mark === "number") v.currentTime = v.vsc.mark;
}
function handleDrag(video, e) {
const c = video.vsc.div;
const sC = c.shadowRoot.querySelector("#controller");
var pE = c.parentElement;
while (
pE.parentNode &&
pE.parentNode.offsetHeight === pE.offsetHeight &&
pE.parentNode.offsetWidth === pE.offsetWidth
)
pE = pE.parentNode;
video.classList.add("vcs-dragging");
sC.classList.add("dragging");
const iXY = [e.clientX, e.clientY],
iCXY = [parseInt(sC.style.left), parseInt(sC.style.top)];
const sD = (e) => {
let s = sC.style;
s.left = iCXY[0] + e.clientX - iXY[0] + "px";
s.top = iCXY[1] + e.clientY - iXY[1] + "px";
};
const eD = () => {
pE.removeEventListener("mousemove", sD);
pE.removeEventListener("mouseup", eD);
pE.removeEventListener("mouseleave", eD);
sC.classList.remove("dragging");
video.classList.remove("vcs-dragging");
};
pE.addEventListener("mouseup", eD);
pE.addEventListener("mouseleave", eD);
pE.addEventListener("mousemove", sD);
}
var timer = null;
function showController(controller) {
if (!controller || typeof controller.classList === "undefined") return;
controller.classList.add("vsc-show");
if (timer) clearTimeout(timer);
timer = setTimeout(function () {
controller.classList.remove("vsc-show");
timer = false;
}, 2000);
}