fix: reduce page overhead and preserve media targeting

This commit is contained in:
joshpatra committed 2026-09-03 11:57:39 -04:00
1 parent 7c9259e5ea
commit 9a4ea94dcd
3 files changed
+1942 -226

No files matched your search

+1034 -224
View File
@@ -27,9 +27,63 @@ function getSharedDefault(key, fallback) {
return fallback;
}
function getViewportScrollEpoch(win) {
return Number(win && win.vscViewportScrollEpoch) || 0;
}
function getViewportScrollX(win) {
var scrollX = Number(win && win.scrollX);
if (Number.isFinite(scrollX)) return scrollX;
return Number(win && win.pageXOffset) || 0;
}
function getViewportScrollY(win) {
var scrollY = Number(win && win.scrollY);
if (Number.isFinite(scrollY)) return scrollY;
return Number(win && win.pageYOffset) || 0;
}
function cacheVideoRect(wrapper, video, rect) {
if (!wrapper || !rect) return null;
var win =
video && video.ownerDocument ? video.ownerDocument.defaultView : window;
wrapper.vscVideoRect = {
left: Number(rect.left) || 0,
top: Number(rect.top) || 0,
right: Number(rect.right) || 0,
bottom: Number(rect.bottom) || 0,
width: Number(rect.width) || 0,
height: Number(rect.height) || 0,
viewportScrollEpoch: getViewportScrollEpoch(win),
viewportScrollX: getViewportScrollX(win),
viewportScrollY: getViewportScrollY(win)
};
return wrapper.vscVideoRect;
}
function getCachedVideoRect(video) {
var wrapper = video && video.vsc && video.vsc.div;
return (wrapper && wrapper.vscVideoRect) || null;
var rect = (wrapper && wrapper.vscVideoRect) || null;
if (!rect || !video) return rect;
var win = video.ownerDocument && video.ownerDocument.defaultView;
var currentScrollEpoch = getViewportScrollEpoch(win);
if (
rect.viewportScrollEpoch === currentScrollEpoch &&
rect.viewportScrollX === getViewportScrollX(win) &&
rect.viewportScrollY === getViewportScrollY(win)
) {
return rect;
}
// Scrolling changes getBoundingClientRect() without changing player layout.
// Refresh lazily on the next action so targeting stays exact for normal,
// fixed, sticky, and nested-scroll players without layout work per scroll.
try {
return cacheVideoRect(wrapper, video, video.getBoundingClientRect());
} catch (_error) {
return rect;
}
}
function getPrimaryVideoElement(mediaElements) {
@@ -242,8 +296,11 @@ var tc = {
activeSiteRule: null,
tabPaused: false,
siteRuleBase: null,
siteRuleAppliedHref: null,
runtimeSettingsHydrated: false,
rawSyncSettingsSnapshot: null,
pendingMediaCandidates: [],
pendingMediaCandidateNodes: new WeakSet(),
settingsReloadRetries: 0,
lastPointerPosition: null,
lastInteractedMedia: null,
@@ -268,6 +325,7 @@ function applyTabPausedState(paused) {
if (!tc.runtimeSettingsHydrated) return;
if (tc.tabPaused) {
clearAllSpeedRestoreEnforcement();
clearDeferredMediaCandidates();
tc.mediaElements.slice().forEach(function(media) {
removeController(media);
});
@@ -283,10 +341,15 @@ var YT_NATIVE_MAX = 2.0;
var YT_NATIVE_STEP = 0.05;
var vscObservedRoots = new WeakSet();
var vscObservedRootList = [];
var vscSupportsWeakRootReferences = typeof WeakRef === "function";
var vscSuspendedObservedRoots = new WeakSet();
var vscSuspendedObservedRootList = [];
var vscConnectedScannedRoots = new WeakSet();
var vscInitializedDocuments = new WeakSet();
var vscSourceObjectIds = new WeakMap();
var vscNextSourceObjectId = 1;
var vscControllersByMount = new WeakMap();
var vscDeferredMediaCandidates = new Set();
var requestIdle =
typeof window.requestIdleCallback === "function"
? window.requestIdleCallback.bind(window)
@@ -605,6 +668,7 @@ function applyControllerLocation(videoController, location) {
}
function captureSiteRuleBase() {
tc.siteRuleAppliedHref = null;
tc.siteRuleBase = {
startHidden: tc.settings.startHidden,
hideWithControls: tc.settings.hideWithControls,
@@ -1094,6 +1158,11 @@ function schedulePersistLastSpeed(speed) {
var MAX_PERSISTED_SOURCE_SPEEDS = 200;
function rememberToggleSpeed(sourceKey, speed) {
if (!sourceKey || !isValidSpeed(speed)) return;
lastToggleSpeed[sourceKey] = Number(speed);
}
function isPersistableSourceKey(sourceKey) {
return Boolean(
sourceKey &&
@@ -1500,6 +1569,63 @@ function isMediaElement(node) {
);
}
function isMediaSourceAttributeMutation(mutation, media) {
if (!mutation || mutation.type !== "attributes" || !media) return false;
if (mutation.target === media) {
return (
mutation.attributeName === "src" ||
mutation.attributeName === "currentSrc"
);
}
return Boolean(
mutation.attributeName === "src" &&
mutation.target &&
mutation.target.nodeName === "SOURCE" &&
mutation.target.parentElement === media
);
}
function observeMediaSourceNodes(media) {
if (!media || media.vscBootstrapSourceObserver) return;
var observer = new MutationObserver(function(mutations) {
if (!media.isConnected) {
observer.disconnect();
delete media.vscBootstrapSourceObserver;
return;
}
var sourceChanged = false;
for (var i = 0; i < mutations.length; i += 1) {
if (isMediaSourceAttributeMutation(mutations[i], media)) {
sourceChanged = true;
break;
}
}
if (sourceChanged) {
ensureController(media, media.parentElement || media.parentNode);
}
});
media.vscBootstrapSourceObserver = observer;
observer.observe(media, {
attributes: true,
subtree: true,
attributeFilter: ["src", "currentSrc"]
});
}
function disconnectBootstrapMediaSourceObserver(media) {
if (!media || !media.vscBootstrapSourceObserver) return;
media.vscBootstrapSourceObserver.disconnect();
delete media.vscBootstrapSourceObserver;
}
function clearDeferredMediaCandidates() {
vscDeferredMediaCandidates.forEach(function(media) {
disconnectBootstrapMediaSourceObserver(media);
});
vscDeferredMediaCandidates.clear();
}
function hasInteractivePlayerChrome(node) {
if (!node || node.nodeName !== "VIDEO") return false;
if (node.controls === true) return true;
@@ -1590,12 +1716,12 @@ function hasUsableMediaSource(node) {
}
if (node.querySelectorAll) {
return Array.from(node.querySelectorAll("source[src]")).some(function(
source
) {
var sources = node.querySelectorAll("source[src]");
for (var i = 0; i < sources.length; i += 1) {
var source = sources[i];
var src = source.getAttribute("src");
return typeof src === "string" && src.trim().length > 0;
});
if (typeof src === "string" && src.trim().length > 0) return true;
}
}
return false;
@@ -1615,10 +1741,9 @@ function ensureController(node, parent) {
if (
node &&
(node.nodeName === "VIDEO" || node.nodeName === "AUDIO") &&
!tc.pendingMediaCandidates.some(function(candidate) {
return candidate.node === node;
})
!tc.pendingMediaCandidateNodes.has(node)
) {
tc.pendingMediaCandidateNodes.add(node);
tc.pendingMediaCandidates.push({ node: node, parent: parent });
}
return null;
@@ -1626,15 +1751,18 @@ function ensureController(node, parent) {
if (!isMediaElement(node)) return null;
if (typeof tc.videoController === "undefined") defineVideoController();
// href selects site rules; re-run on every new/usable media so all runtime
// paths agree on activation and effective settings.
applySiteRuleOverrides();
if (!isSpeederActiveForCurrentPage()) {
// The URL is the only page input to site-rule matching. Reapply once when
// it changes, while keeping every media discovery path synchronous.
if (!reapplySiteRulesAndControllerGeometry()) {
disconnectBootstrapMediaSourceObserver(node);
vscDeferredMediaCandidates.delete(node);
if (node.vsc) removeController(node);
return null;
}
if (isAmbientLoopMedia(node)) {
if (node.vsc) removeController(node);
disconnectBootstrapMediaSourceObserver(node);
vscDeferredMediaCandidates.add(node);
return null;
}
@@ -1650,6 +1778,8 @@ function ensureController(node, parent) {
}
if (node.vsc) {
vscDeferredMediaCandidates.delete(node);
disconnectBootstrapMediaSourceObserver(node);
var existingController = node.vsc;
var hasSource = hasUsableMediaSource(node);
if (existingController.div) {
@@ -1676,6 +1806,8 @@ function ensureController(node, parent) {
}
if (!hasUsableMediaSource(node)) {
vscDeferredMediaCandidates.add(node);
observeMediaSourceNodes(node);
log(
`Deferring controller creation for ${node.tagName}: no usable source yet`,
5
@@ -1683,13 +1815,13 @@ function ensureController(node, parent) {
return null;
}
refreshAllControllerGeometry();
log(
`Creating controller for ${node.tagName}: ${node.src || node.currentSrc || "no src"}`,
4
);
try {
vscDeferredMediaCandidates.delete(node);
disconnectBootstrapMediaSourceObserver(node);
node.vsc = new tc.videoController(
node,
parent || node.parentElement || node.parentNode
@@ -1697,11 +1829,15 @@ function ensureController(node, parent) {
} catch (error) {
log(`Unable to create media controller: ${error.message}`, 2);
removeController(node);
vscDeferredMediaCandidates.add(node);
observeMediaSourceNodes(node);
return null;
}
if (!node.vsc || !node.vsc.div) {
log("Controller initialization returned without a control host", 3);
removeController(node);
vscDeferredMediaCandidates.add(node);
observeMediaSourceNodes(node);
return null;
}
if (
@@ -1717,6 +1853,7 @@ function ensureController(node, parent) {
function flushPendingMediaCandidates() {
var pending = tc.pendingMediaCandidates.splice(0);
pending.forEach(function(candidate) {
if (candidate.node) tc.pendingMediaCandidateNodes.delete(candidate.node);
if (!candidate.node || !candidate.node.isConnected) return;
ensureController(
candidate.node,
@@ -1735,7 +1872,10 @@ function ensureControllerForMediaChild(node) {
}
function removeController(node) {
if (!node || !node.vsc) return;
if (!node) return;
vscDeferredMediaCandidates.delete(node);
disconnectBootstrapMediaSourceObserver(node);
if (!node.vsc) return;
var controller = node.vsc;
if (typeof controller.remove === "function") {
try {
@@ -1745,6 +1885,14 @@ function removeController(node) {
log(`Partial controller cleanup failed: ${error.message}`, 3);
}
}
if (controller.controllerHostCleanup) {
try {
controller.controllerHostCleanup(true);
} catch (_error) {}
controller.controllerHostCleanup = null;
}
unregisterControllerMount(controller, controller.controllerHostMount);
controller.controllerHostSchedule = null;
if (controller.div && typeof controller.div.remove === "function") {
controller.div.remove();
}
@@ -1768,8 +1916,9 @@ function scanNodeForMedia(node, parent, added) {
return;
}
var ownerDocument = node.ownerDocument || document;
if (!added && ownerDocument.body && ownerDocument.body.contains(node)) return;
// A removed-node record can represent a move. isConnected works across
// shadow boundaries; document.body.contains() does not.
if (!added && node.isConnected) return;
// Check if the node itself is a media element
if (isMediaElement(node)) {
@@ -1809,6 +1958,39 @@ function scanNodeForMedia(node, parent, added) {
observeRoot(node.shadowRoot);
}
if (
added &&
!vscSupportsWeakRootReferences &&
typeof node.querySelectorAll === "function"
) {
// Engines without WeakRef cannot retain detached roots safely. Rediscover
// nested open roots only when their containing subtree is reinserted.
rescanOpenShadowRoots(node, undefined, false);
}
}
function compactMutationScanCandidates(candidates) {
var candidateByNode = new Map();
candidates.forEach(function(candidate) {
if (!candidate || !candidate.node) return;
// Keep the latest mutation target for a node that moved more than once.
candidateByNode.set(candidate.node, candidate);
});
var candidateNodes = new Set(candidateByNode.keys());
return Array.from(candidateByNode.values()).filter(function(candidate) {
// A light-DOM ancestor scan cannot cross into this node's shadow root.
// Keep the host as its own scan root so shadow-media detection is unchanged.
if (candidate.node.shadowRoot) return true;
var ancestor = candidate.node.parentNode;
while (ancestor) {
if (candidateNodes.has(ancestor)) return false;
ancestor = ancestor.parentNode;
}
return true;
});
}
function getScanNodeForRoot(root) {
@@ -1860,52 +2042,208 @@ function rescanOpenShadowRoots(root, visited, rescanObserved) {
hosts.forEach(function(host) {
var shadowRoot = host.shadowRoot;
if (!shadowRoot || seen.has(shadowRoot)) return;
if (
!shadowRoot ||
seen.has(shadowRoot) ||
isSpeederControllerShadowRoot(shadowRoot)
) {
return;
}
seen.add(shadowRoot);
if (rescanObserved !== false || !vscObservedRoots.has(shadowRoot)) {
if (
rescanObserved !== false ||
!vscObservedRoots.has(shadowRoot) ||
!shadowRoot.vscMutationObserverAttached
) {
observeRoot(shadowRoot);
}
rescanOpenShadowRoots(shadowRoot, seen, rescanObserved);
});
}
function rescanObservedMediaRoots(doc) {
var retainedRoots = [];
vscObservedRootList.forEach(function(root) {
if (!root || root.nodeType === Node.DOCUMENT_NODE) return;
var connected = Boolean(root.host ? root.host.isConnected : root.isConnected);
if (!connected) return;
retainedRoots.push(root);
if (
!doc ||
root.ownerDocument === doc ||
(root.host && root.host.ownerDocument === doc)
) {
scanRootForMedia(root);
function isSpeederControllerHost(node) {
return Boolean(node && node.vscControllerHost === true);
}
function isSpeederControllerShadowRoot(root) {
return Boolean(
root &&
root.host &&
isSpeederControllerHost(root.host)
);
}
function isObservedRootConnected(root) {
if (!root) return false;
try {
return Boolean(
root.nodeType === Node.DOCUMENT_NODE ||
root.isConnected ||
(root.host &&
(root.host.isConnected ||
(root.host.ownerDocument &&
root.host.ownerDocument.contains(root.host)))) ||
(root.ownerDocument && root.ownerDocument.contains(root))
);
} catch (_error) {
return true;
}
}
function trackObservedRoot(root) {
if (
!root ||
root.nodeType === Node.DOCUMENT_NODE ||
root.vscObservedRootTracked
) {
return;
}
root.vscObservedRootTracked = true;
vscObservedRootList.push(createObservedRootReference(root));
}
function forEachTrackedObservedRoot(callback) {
var retainedRootReferences = [];
vscObservedRootList.forEach(function(rootReference) {
var root = dereferenceObservedRoot(rootReference);
if (!root) return;
if (isSpeederControllerShadowRoot(root)) {
root.vscObservedRootTracked = false;
return;
}
if (callback(root) !== false) {
retainedRootReferences.push(rootReference);
}
});
vscObservedRootList = retainedRoots;
vscObservedRootList = retainedRootReferences;
}
function createObservedRootReference(root) {
return vscSupportsWeakRootReferences ? new WeakRef(root) : root;
}
function dereferenceObservedRoot(rootReference) {
return vscSupportsWeakRootReferences ? rootReference.deref() : rootReference;
}
function trackSuspendedObservedRoot(root) {
if (!root || vscSuspendedObservedRoots.has(root)) return;
vscSuspendedObservedRoots.add(root);
vscSuspendedObservedRootList.push(createObservedRootReference(root));
}
function forgetSuspendedObservedRoot(root) {
if (!root) return;
vscSuspendedObservedRoots.delete(root);
vscSuspendedObservedRootList = vscSuspendedObservedRootList.filter(
function(rootReference) {
var suspendedRoot = dereferenceObservedRoot(rootReference);
return suspendedRoot && suspendedRoot !== root;
}
);
}
function suspendRootMutationObserver(root, retainForReconnect) {
if (retainForReconnect !== false) trackSuspendedObservedRoot(root);
var observer = root && root.vscMutationObserver;
if (!observer) return;
observer.disconnect();
if (typeof observer.takeRecords === "function") observer.takeRecords();
root.vscMutationObserver = null;
root.vscMutationObserverAttached = false;
}
function reconcileObservedRootConnections(doc, scanAllConnected) {
forEachTrackedObservedRoot(function(root) {
var connected = isObservedRootConnected(root);
if (!connected) {
// Weak references preserve detached/reinserted custom elements without
// retaining them. On older engines, release the strong fallback entry;
// an insertion scan will rediscover the open root if it returns.
suspendRootMutationObserver(root, vscSupportsWeakRootReferences);
if (!vscSupportsWeakRootReferences) {
forgetSuspendedObservedRoot(root);
root.vscObservedRootTracked = false;
return false;
}
return true;
}
if (
doc &&
root.ownerDocument !== doc &&
(!root.host || root.host.ownerDocument !== doc)
) {
return;
}
setupListener(root);
attachMediaDetectionListeners(root);
vscSuspendedObservedRoots.delete(root);
var wasObservingMutations = root.vscMutationObserverAttached === true;
if (!wasObservingMutations) attachMutationObserver(root);
if (scanAllConnected || !wasObservingMutations) {
scanRootForMedia(root);
}
return true;
});
}
function resumeSuspendedObservedRoots(doc) {
var retainedSuspendedRootReferences = [];
vscSuspendedObservedRootList.forEach(function(rootReference) {
var root = dereferenceObservedRoot(rootReference);
if (!root || isSpeederControllerShadowRoot(root)) return;
if (!vscSuspendedObservedRoots.has(root)) return;
if (!isObservedRootConnected(root)) {
retainedSuspendedRootReferences.push(rootReference);
return;
}
if (
doc &&
root.ownerDocument !== doc &&
(!root.host || root.host.ownerDocument !== doc)
) {
retainedSuspendedRootReferences.push(rootReference);
return;
}
vscSuspendedObservedRoots.delete(root);
setupListener(root);
attachMediaDetectionListeners(root);
if (!root.vscMutationObserverAttached) attachMutationObserver(root);
scanRootForMedia(root);
});
vscSuspendedObservedRootList = retainedSuspendedRootReferences;
}
function getObservedRootDocument(root) {
if (!root) return null;
if (root.nodeType === Node.DOCUMENT_NODE) return root;
return root.ownerDocument || (root.host && root.host.ownerDocument) || null;
}
function rescanObservedMediaRoots(doc) {
reconcileObservedRootConnections(doc, true);
}
function observeRoot(root) {
if (!root) return;
if (!root || isSpeederControllerShadowRoot(root)) return;
var isConnected = false;
try {
isConnected = root.nodeType === Node.DOCUMENT_NODE ||
root.isConnected ||
(root.host && (root.host.isConnected || (root.host.ownerDocument && root.host.ownerDocument.contains(root.host)))) ||
(root.ownerDocument && root.ownerDocument.contains(root));
} catch (e) {
isConnected = true;
}
var isConnected = isObservedRootConnected(root);
if (!vscObservedRoots.has(root)) {
vscObservedRoots.add(root);
vscObservedRootList.push(root);
}
trackObservedRoot(root);
if (isConnected) {
setupListener(root);
attachMutationObserver(root);
attachMediaDetectionListeners(root);
if (!root.vscMutationObserverAttached) attachMutationObserver(root);
vscSuspendedObservedRoots.delete(root);
} else {
trackSuspendedObservedRoot(root);
}
if (
@@ -1943,6 +2281,23 @@ function patchAttachShadow() {
window.vscAttachShadowPatched = true;
}
function markPageShadowBridgeReady() {
// Close the small interval between the pre-injection discovery pass and the
// page-world attachShadow patch becoming active. This runs at document_start
// while the tree is still small and replaces the old delayed whole-page scan.
rescanOpenShadowRoots(document, undefined, false);
window.vscPageShadowBridgeLoaded = true;
window.vscPageShadowBridgeRetries = 0;
clearTimeout(window.vscPageShadowBridgeRetryTimer);
if (window.vscPageNavigationApiBridgeLoaded) stopLocationWatch();
if (Array.isArray(window.vscBoundedShadowFallbackTimers)) {
window.vscBoundedShadowFallbackTimers.forEach(function(timer) {
clearTimeout(timer);
});
window.vscBoundedShadowFallbackTimers.length = 0;
}
}
function installPageShadowBridge() {
if (window.vscPageShadowBridgeRequested) return;
@@ -1963,11 +2318,24 @@ function installPageShadowBridge() {
);
document.addEventListener(
"speeder-location-changed",
function() {
scheduleNavigationRescan();
function(event) {
scheduleNavigationRescan(event);
},
true
);
document.addEventListener(
"speeder-page-navigation-api-ready",
function() {
window.vscPageNavigationApiBridgeLoaded = true;
if (window.vscPageShadowBridgeLoaded) stopLocationWatch();
},
true
);
document.addEventListener(
"speeder-page-bridge-ready",
markPageShadowBridgeReady,
true
);
}
function startBoundedShadowFallback() {
@@ -1980,7 +2348,7 @@ function installPageShadowBridge() {
function() {
if (
!document.body ||
(delay !== 3000 && window.vscPageShadowBridgeLoaded)
window.vscPageShadowBridgeLoaded
) {
return;
}
@@ -2022,14 +2390,19 @@ function installPageShadowBridge() {
return;
}
try {
// Discover roots that genuinely predate document_start now, while the DOM
// is still small. Once the page-world bridge loads it reports every future
// root, so a delayed full-document traversal is unnecessary.
if (!window.vscInitialShadowDiscoveryComplete) {
window.vscInitialShadowDiscoveryComplete = true;
rescanOpenShadowRoots(document, undefined, false);
}
var bridge = document.createElement("script");
bridge.src = chrome.runtime.getURL("content/shadow-bridge.js");
bridge.async = false;
bridge.addEventListener("load", function() {
bridge.remove();
window.vscPageShadowBridgeLoaded = true;
window.vscPageShadowBridgeRetries = 0;
clearTimeout(window.vscPageShadowBridgeRetryTimer);
if (!window.vscPageShadowBridgeLoaded) handleBridgeFailure();
});
bridge.addEventListener("error", function() {
bridge.remove();
@@ -2063,8 +2436,31 @@ function log(message, level) {
}
}
function cloneRuntimeStorageSnapshot(storage) {
if (typeof vscClonePlainData === "function") {
return vscClonePlainData(storage || {});
}
try {
return JSON.parse(JSON.stringify(storage || {}));
} catch (_error) {
return Object.assign({}, storage || {});
}
}
function runtimeStorageSnapshotsEqual(left, right) {
if (typeof vscAreComparableValuesEqual === "function") {
return vscAreComparableValuesEqual(left || {}, right || {});
}
try {
return JSON.stringify(left || {}) === JSON.stringify(right || {});
} catch (_error) {
return false;
}
}
function hydrateRuntimeSettings(rawStorage, options) {
var config = options || {};
tc.rawSyncSettingsSnapshot = cloneRuntimeStorageSnapshot(rawStorage || {});
var storage =
typeof vscExpandStoredSettings === "function"
? vscExpandStoredSettings(rawStorage || {})
@@ -2374,6 +2770,7 @@ function scheduleRuntimeSettingsReload() {
clearTimeout(window.vscSettingsReloadTimer);
clearTimeout(window.vscSettingsReloadRetryTimer);
window.vscSettingsReloadTimer = setTimeout(function() {
window.vscSettingsReloadTimer = null;
chrome.storage.sync.get(null, function(rawStorage) {
if (reloadGeneration !== window.vscSettingsReloadGeneration) return;
if (chrome.runtime.lastError) {
@@ -2388,6 +2785,14 @@ function scheduleRuntimeSettingsReload() {
return;
}
tc.settingsReloadRetries = 0;
if (
runtimeStorageSnapshotsEqual(
rawStorage || {},
tc.rawSyncSettingsSnapshot || {}
)
) {
return;
}
var wasForceEnabled = tc.settings.forceLastSavedSpeed === true;
var wasRememberEnabled = tc.settings.rememberSpeed === true;
hydrateRuntimeSettings(rawStorage || {}, {
@@ -2410,17 +2815,57 @@ function loadInitialLocalSettings(attempt, callback) {
chrome.storage.local.get(
["customButtonIcons", "rememberedSpeeds", "rememberedSpeedsResetAt"],
function(localStorage) {
if (chrome.runtime.lastError && attempt < 3) {
var readFailed = Boolean(chrome.runtime.lastError);
if (readFailed && attempt < 3) {
setTimeout(function() {
loadInitialLocalSettings(attempt + 1, callback);
}, 100 * (attempt + 1));
return;
}
callback(chrome.runtime.lastError ? {} : localStorage || {});
callback(readFailed ? {} : localStorage || {}, readFailed);
}
);
}
function beginStartupStorageWatch() {
if (
window.vscStartupStorageListener ||
!chrome.storage ||
!chrome.storage.onChanged
) {
return;
}
window.vscStartupStorageChanges = { sync: false, local: false };
window.vscStartupStorageListener = function(_changes, area) {
if (
window.vscStartupStorageChanges &&
(area === "sync" || area === "local")
) {
window.vscStartupStorageChanges[area] = true;
}
};
chrome.storage.onChanged.addListener(window.vscStartupStorageListener);
}
function finishStartupStorageWatch() {
var changes = window.vscStartupStorageChanges || {
sync: true,
local: true
};
if (
window.vscStartupStorageListener &&
chrome.storage &&
chrome.storage.onChanged &&
typeof chrome.storage.onChanged.removeListener === "function"
) {
chrome.storage.onChanged.removeListener(window.vscStartupStorageListener);
}
window.vscStartupStorageListener = null;
window.vscStartupStorageChanges = null;
return changes;
}
// Patch attachShadow immediately — before any async operations — so we
// catch shadow roots created while chrome.storage.sync.get is pending.
// Sites like archive.org create Lit/LitElement shadow DOMs during page load;
@@ -2430,7 +2875,8 @@ installPageShadowBridge();
function loadInitialRuntimeSettings(attempt) {
chrome.storage.sync.get(null, function(rawStorage) {
if (chrome.runtime.lastError && attempt < 3) {
var initialSyncReadFailed = Boolean(chrome.runtime.lastError);
if (initialSyncReadFailed && attempt < 3) {
setTimeout(function() {
loadInitialRuntimeSettings(attempt + 1);
}, 100 * (attempt + 1));
@@ -2536,7 +2982,7 @@ function loadInitialRuntimeSettings(attempt) {
// Set the flag to prevent adding the listener again.
window.vscMessageListener = true;
}
loadInitialLocalSettings(0, function(loc) {
loadInitialLocalSettings(0, function(loc, initialLocalReadFailed) {
applyCustomButtonIcons(loc && loc.customButtonIcons);
applyRememberedSpeedsResetMarker(
@@ -2554,6 +3000,9 @@ function loadInitialRuntimeSettings(attempt) {
if (isLastSpeedOnly) {
var changedLastSpeed = Number(changes.lastSpeed.newValue);
if (isValidSpeed(changedLastSpeed)) {
if (tc.rawSyncSettingsSnapshot) {
tc.rawSyncSettingsSnapshot.lastSpeed = changedLastSpeed;
}
tc.persistedLastSpeed = changedLastSpeed;
var newerLocalLastSpeed = isValidSpeed(tc.pendingLastSpeedValue)
? Number(tc.pendingLastSpeedValue)
@@ -2656,48 +3105,65 @@ function loadInitialRuntimeSettings(attempt) {
applyCustomButtonIcons(nv);
});
}
// Close the small gap between the initial sync/local reads and listener
// registration. A final read also converges if Options saved while this
// content script was starting.
scheduleRuntimeSettingsReload();
var customIconGeneration =
Number(window.vscCustomIconGeneration) || 0;
var rememberedSpeedsGeneration =
Number(window.vscRememberedSpeedsGeneration) || 0;
chrome.storage.local.get(
["customButtonIcons", "rememberedSpeeds", "rememberedSpeedsResetAt"],
function(latestLocal) {
if (chrome.runtime.lastError) return;
if (
customIconGeneration ===
(Number(window.vscCustomIconGeneration) || 0)
) {
applyCustomButtonIcons(latestLocal && latestLocal.customButtonIcons);
}
if (
rememberedSpeedsGeneration ===
(Number(window.vscRememberedSpeedsGeneration) || 0)
) {
applyRememberedSpeedsResetMarker(
latestLocal && latestLocal.rememberedSpeedsResetAt
);
mergeRememberedSpeeds(latestLocal && latestLocal.rememberedSpeeds);
var latestRememberedSpeeds =
latestLocal && latestLocal.rememberedSpeeds;
// A temporary listener covered the gap before the durable listener above
// was registered. Verify storage only when it actually changed during
// startup instead of issuing two extra reads in every frame.
var startupStorageChanges = finishStartupStorageWatch();
if (initialSyncReadFailed) startupStorageChanges.sync = true;
if (initialLocalReadFailed) startupStorageChanges.local = true;
if (startupStorageChanges.sync) scheduleRuntimeSettingsReload();
if (startupStorageChanges.local) {
var customIconGeneration =
Number(window.vscCustomIconGeneration) || 0;
var rememberedSpeedsGeneration =
Number(window.vscRememberedSpeedsGeneration) || 0;
loadInitialLocalSettings(
0,
function(latestLocal, latestLocalReadFailed) {
if (latestLocalReadFailed) return;
if (
!rememberedSpeedsPayloadMatches(
latestRememberedSpeeds,
buildRememberedSpeedsPayload()
)
customIconGeneration ===
(Number(window.vscCustomIconGeneration) || 0)
) {
schedulePersistRememberedSpeeds(true);
var latestCustomButtonIcons =
(latestLocal && latestLocal.customButtonIcons) || {};
if (
!runtimeStorageSnapshotsEqual(
latestCustomButtonIcons,
tc.settings.customButtonIcons || {}
)
) {
applyCustomButtonIcons(latestCustomButtonIcons);
}
}
if (tc.settings.rememberSpeed || tc.settings.forceLastSavedSpeed) {
tc.mediaElements.slice().forEach(applyRememberedSpeedPolicy);
if (
rememberedSpeedsGeneration ===
(Number(window.vscRememberedSpeedsGeneration) || 0)
) {
var resetMarkerChanged = applyRememberedSpeedsResetMarker(
latestLocal && latestLocal.rememberedSpeedsResetAt
);
var latestRememberedSpeeds =
latestLocal && latestLocal.rememberedSpeeds;
var currentRememberedSpeeds = buildRememberedSpeedsPayload();
var rememberedSpeedsChanged = !rememberedSpeedsPayloadMatches(
latestRememberedSpeeds,
currentRememberedSpeeds
);
if (rememberedSpeedsChanged) {
mergeRememberedSpeeds(latestRememberedSpeeds);
schedulePersistRememberedSpeeds(true);
}
if (
(resetMarkerChanged || rememberedSpeedsChanged) &&
(tc.settings.rememberSpeed || tc.settings.forceLastSavedSpeed)
) {
tc.mediaElements.slice().forEach(applyRememberedSpeedPolicy);
}
}
}
}
);
);
}
tc.runtimeSettingsHydrated = true;
initializeWhenReady(document);
});
@@ -2707,6 +3173,7 @@ function loadInitialRuntimeSettings(attempt) {
// Install before async settings hydration so SPA-owned window capture handlers
// cannot hide later key events from Speeder.
attachKeydownListeners(document);
beginStartupStorageWatch();
loadInitialRuntimeSettings(0);
function getKeyBindings(action, what = "value") {
@@ -2976,14 +3443,7 @@ function positionControllerHost(wrapper, video, mount) {
return;
}
var videoRect = video.getBoundingClientRect();
wrapper.vscVideoRect = {
left: Number(videoRect.left) || 0,
top: Number(videoRect.top) || 0,
right: Number(videoRect.right) || 0,
bottom: Number(videoRect.bottom) || 0,
width: Number(videoRect.width) || 0,
height: Number(videoRect.height) || 0
};
cacheVideoRect(wrapper, video, videoRect);
if (wrapper.classList.contains("vsc-fullscreen-popover")) {
if (videoRect.width <= 0 || videoRect.height <= 0) {
wrapper.classList.add("vsc-geometry-hidden");
@@ -3078,6 +3538,57 @@ function configureControllerAutoHide(videoController, wrapper, force) {
}
}
function registerControllerMount(videoController, mount) {
if (!videoController || !mount) return;
var controllers = vscControllersByMount.get(mount);
if (!controllers) {
controllers = new Set();
vscControllersByMount.set(mount, controllers);
}
controllers.add(videoController);
}
function unregisterControllerMount(videoController, mount) {
if (!videoController || !mount) return;
var controllers = vscControllersByMount.get(mount);
if (!controllers) return;
controllers.delete(videoController);
if (controllers.size === 0) vscControllersByMount.delete(mount);
}
function attachSharedViewportScrollTracking(win) {
if (!win || win.vscViewportScrollTrackingAttached) return;
win.vscViewportScrollEpoch = getViewportScrollEpoch(win);
win.addEventListener(
"scroll",
function() {
win.vscViewportScrollEpoch = getViewportScrollEpoch(win) + 1;
},
{ capture: true, passive: true }
);
win.vscViewportScrollTrackingAttached = true;
}
function attachSharedControllerResizeListener(win) {
if (!win || win.vscControllerResizeListenerAttached) return;
win.addEventListener(
"resize",
function() {
tc.mediaElements.forEach(function(media) {
var controller = media && media.vsc;
if (
controller &&
typeof controller.controllerHostSchedule === "function"
) {
controller.controllerHostSchedule();
}
});
},
{ passive: true }
);
win.vscControllerResizeListenerAttached = true;
}
function setupControllerHostTracking(videoController, wrapper, mount) {
if (!videoController || !wrapper || !mount) return;
@@ -3158,28 +3669,35 @@ function setupControllerHostTracking(videoController, wrapper, mount) {
passive: true
});
});
win.addEventListener("resize", schedule, { passive: true });
doc.addEventListener("fullscreenchange", schedule, { passive: true });
attachSharedViewportScrollTracking(win);
attachSharedControllerResizeListener(win);
geometryMount.addEventListener("scroll", schedule, { passive: true });
update();
videoController.controllerHostMount = mount;
registerControllerMount(videoController, mount);
videoController.controllerHostSchedule = schedule;
videoController.controllerHostCleanup = function(forceRestore) {
unregisterControllerMount(videoController, mount);
if (resizeObserver) resizeObserver.disconnect();
if (frameId !== null) win.cancelAnimationFrame(frameId);
if (geometryRetryTimer !== null) win.clearTimeout(geometryRetryTimer);
mediaGeometryEvents.forEach(function(eventName) {
videoController.video.removeEventListener(eventName, schedule);
});
win.removeEventListener("resize", schedule);
doc.removeEventListener("fullscreenchange", schedule);
geometryMount.removeEventListener("scroll", schedule);
if (videoController.controllerHostSchedule === schedule) {
videoController.controllerHostSchedule = null;
}
var hasOtherController = false;
try {
hasOtherController = Array.from(
mount.querySelectorAll(".vsc-controller")
).some(function(controllerHost) {
return controllerHost !== wrapper;
return (
controllerHost !== wrapper &&
isSpeederControllerHost(controllerHost)
);
});
} catch (e) {}
if (
@@ -3252,16 +3770,33 @@ function remountControllerHost(videoController, mount) {
videoController.div.isConnected &&
videoController.div.parentNode === mount
) {
// Keep the max-z-index host last among equal-z-index player overlays.
if (videoController.div.nextSibling) {
// Keep controller hosts after page-owned overlays, but do not make multiple
// Speeder controllers in one mount endlessly append past one another.
var followingSibling = videoController.div.nextSibling;
var hasPageOwnedFollowingSibling = false;
while (followingSibling) {
if (
followingSibling.nodeType === Node.ELEMENT_NODE &&
!isSpeederControllerHost(followingSibling)
) {
hasPageOwnedFollowingSibling = true;
break;
}
followingSibling = followingSibling.nextSibling;
}
if (hasPageOwnedFollowingSibling) {
mount.appendChild(videoController.div);
return true;
}
positionControllerHost(
videoController.div,
videoController.video,
mount
);
if (typeof videoController.controllerHostSchedule === "function") {
videoController.controllerHostSchedule();
} else {
positionControllerHost(
videoController.div,
videoController.video,
mount
);
}
return false;
}
@@ -3392,12 +3927,15 @@ function defineVideoController() {
this.suppressedRateChangeCount = 0;
this.suppressedRateChangeUntil = 0;
this.visibilityResumeHandler = null;
this.dragCleanup = null;
this.lifecycleReconcileFrame = null;
this.resetToggleArmed = false;
this.resetButtonEl = null;
this.controllerLocation = normalizeControllerLocation(
tc.settings.controllerLocation
);
this.structureSignature = getControllerStructureSignature();
attachFullscreenListeners(target.ownerDocument);
log(`Creating video controller for ${target.tagName} with src: ${target.src || target.currentSrc || "none"}`, 4);
@@ -3554,23 +4092,21 @@ function defineVideoController() {
var srcObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (
mutation.type === "attributes" &&
(mutation.attributeName === "src" ||
mutation.attributeName === "currentSrc")
) {
if (isMediaSourceAttributeMutation(mutation, this.video)) {
log("mutation of A/V element", 5);
if (this.div) {
this.stopSubtitleNudge();
if (!mutation.target.src && !mutation.target.currentSrc) {
this.div.classList.toggle(
"vsc-nosource",
!hasUsableMediaSource(mutation.target)
);
var sourceAvailable = hasUsableMediaSource(this.video);
this.div.classList.toggle("vsc-nosource", !sourceAvailable);
if (!sourceAvailable) {
this.mediaSourceKey = "unknown_src";
this.targetSpeedSourceKey = "unknown_src";
} else {
this.div.classList.remove("vsc-nosource");
applySourceTransitionPolicy(this.video, true);
if (!mutation.target.paused) this.startSubtitleNudge();
applySourceTransitionPolicy(
this.video,
mutation.target === this.video
);
if (!this.video.paused) this.startSubtitleNudge();
}
updateSubtitleNudgeIndicator(this.video);
}
@@ -3578,7 +4114,11 @@ function defineVideoController() {
});
});
this.srcObserver = srcObserver;
srcObserver.observe(target, { attributeFilter: ["src", "currentSrc"] });
srcObserver.observe(target, {
attributes: true,
subtree: true,
attributeFilter: ["src", "currentSrc"]
});
if (!target.paused && target.playbackRate !== 1.0)
this.startSubtitleNudge();
};
@@ -3586,6 +4126,18 @@ function defineVideoController() {
tc.videoController.prototype.remove = function() {
this.stopSubtitleNudge();
disableDirectFullscreenPopover(this);
if (this.lifecycleReconcileFrame !== null) {
var lifecycleWindow =
this.video && this.video.ownerDocument
? this.video.ownerDocument.defaultView || window
: window;
lifecycleWindow.cancelAnimationFrame(this.lifecycleReconcileFrame);
this.lifecycleReconcileFrame = null;
}
if (this.dragCleanup) {
this.dragCleanup();
this.dragCleanup = null;
}
if (
this.visibilityResumeHandler &&
this.video &&
@@ -3610,7 +4162,22 @@ function defineVideoController() {
this.genericAutoHideCleanup();
this.genericAutoHideCleanup = null;
}
if (this.div) this.div.remove();
if (this.div) {
if (this.div.showTimeOut !== undefined) {
clearTimeout(this.div.showTimeOut);
this.div.showTimeOut = undefined;
}
if (this.div.blinkTimeOut !== undefined) {
clearTimeout(this.div.blinkTimeOut);
this.div.blinkTimeOut = undefined;
}
var nudgeFlashIndicator = this.nudgeFlashIndicator;
if (nudgeFlashIndicator && nudgeFlashIndicator._flashTimer) {
clearTimeout(nudgeFlashIndicator._flashTimer);
nudgeFlashIndicator._flashTimer = null;
}
this.div.remove();
}
if (this.controllerHostCleanup) {
this.controllerHostCleanup();
this.controllerHostCleanup = null;
@@ -3966,6 +4533,7 @@ function defineVideoController() {
const speed = this.video.playbackRate.toFixed(2);
var wrapper = doc.createElement("div");
wrapper.classList.add("vsc-controller");
wrapper.vscControllerHost = true;
// Keep the host out of player layout while its shadow stylesheet loads.
wrapper.style.position = "absolute";
wrapper.style.pointerEvents = "none";
@@ -4166,10 +4734,12 @@ function defineVideoController() {
}
function applySiteRuleOverrides() {
var currentUrl = location.href;
var wasForceEnabled = tc.settings.forceLastSavedSpeed === true;
var wasRememberEnabled = tc.settings.rememberSpeed === true;
function finishSiteRuleApplication(result) {
tc.siteRuleAppliedHref = currentUrl;
if (
(wasForceEnabled && !tc.settings.forceLastSavedSpeed) ||
(wasRememberEnabled && !tc.settings.rememberSpeed)
@@ -4186,7 +4756,6 @@ function applySiteRuleOverrides() {
return finishSiteRuleApplication(false);
}
var currentUrl = location.href;
var matchedRule = siteRuleUtils.matchSiteRule(currentUrl, tc.settings.siteRules);
if (!matchedRule) {
@@ -4262,12 +4831,14 @@ function applySiteRuleOverrides() {
function removeIneligibleMediaControllers() {
tc.mediaElements.slice().forEach(function(media) {
if (
media &&
((media.nodeName === "AUDIO" && !tc.settings.audioBoolean) ||
isAmbientLoopMedia(media))
) {
if (!media) return;
if (media.nodeName === "AUDIO" && !tc.settings.audioBoolean) {
removeController(media);
return;
}
if (isAmbientLoopMedia(media)) {
removeController(media);
vscDeferredMediaCandidates.add(media);
}
});
}
@@ -4285,8 +4856,12 @@ function refreshAllControllerGeometry() {
});
}
/** Re-match site rules for current URL and refresh controller position/opacity on every video. */
/** Re-match site rules and refresh existing controllers once per URL. */
function reapplySiteRulesAndControllerGeometry() {
if (tc.siteRuleAppliedHref === location.href) {
return isSpeederActiveForCurrentPage();
}
applySiteRuleOverrides();
if (!isSpeederActiveForCurrentPage()) {
tc.mediaElements.slice().forEach(function(video) {
@@ -4531,6 +5106,7 @@ function attachKeydownListeners(doc) {
keyTarget.addEventListener(
"keydown",
function(event) {
if (!tc.runtimeSettingsHydrated || !tc.mediaElements.length) return;
if (
!event.getModifierState ||
event.getModifierState("Alt") ||
@@ -4551,8 +5127,6 @@ function attachKeydownListeners(doc) {
return;
}
if (!tc.mediaElements.length) return;
var item = tc.settings.keyBindings.find(function(binding) {
return matchesKeyBinding(binding, event);
});
@@ -4583,11 +5157,14 @@ function attachMediaTargetTracking(doc) {
if (!Number.isFinite(event.clientX) || !Number.isFinite(event.clientY)) {
return;
}
tc.lastPointerPosition = {
document: doc,
x: event.clientX,
y: event.clientY
};
if (
!tc.lastPointerPosition ||
tc.lastPointerPosition.document !== doc
) {
tc.lastPointerPosition = { document: doc, x: 0, y: 0 };
}
tc.lastPointerPosition.x = event.clientX;
tc.lastPointerPosition.y = event.clientY;
},
{ capture: true, passive: true }
);
@@ -4597,109 +5174,226 @@ function attachMediaTargetTracking(doc) {
function attachMutationObserver(root) {
if (root.vscMutationObserverAttached) return;
var pendingMutations = [];
var pendingControllerMountTargets = new Set();
var pendingAddedCandidates = new Map();
var pendingRemovedCandidates = new Map();
var pendingAriaHiddenTargets = new Set();
var mutationProcessingScheduled = false;
var observer = new MutationObserver(function(mutations) {
pendingMutations.push(...mutations);
mutations.forEach(function(mutation) {
if (mutation.type === "childList") {
// Text-only updates can still change a player's geometry. Preserve the
// former mount reconciliation while keeping the lookup mount-scoped.
pendingControllerMountTargets.add(mutation.target);
mutation.addedNodes.forEach(function(node) {
if (node.nodeType !== Node.ELEMENT_NODE) return;
if (isSpeederControllerHost(node)) return;
pendingAddedCandidates.set(node, {
node: node,
parent: mutation.target
});
});
mutation.removedNodes.forEach(function(node) {
if (node.nodeType !== Node.ELEMENT_NODE) return;
if (isSpeederControllerHost(node)) return;
pendingRemovedCandidates.set(node, {
node: node,
parent: mutation.target
});
});
return;
}
if (
mutation.type === "attributes" &&
mutation.attributeName === "aria-hidden"
) {
pendingAriaHiddenTargets.add(mutation.target);
}
});
if (mutationProcessingScheduled) return;
mutationProcessingScheduled = true;
requestIdle(
function() {
var mutationsToProcess = pendingMutations.splice(0);
mutationProcessingScheduled = false;
var controllerMountTargets = new Set();
var controllerMountTargets = pendingControllerMountTargets;
var addedCandidates = Array.from(pendingAddedCandidates.values());
var removedCandidates = Array.from(pendingRemovedCandidates.values());
var ariaHiddenTargets = pendingAriaHiddenTargets;
pendingControllerMountTargets = new Set();
pendingAddedCandidates = new Map();
pendingRemovedCandidates = new Map();
pendingAriaHiddenTargets = new Set();
mutationsToProcess.forEach(function(mutation) {
if (mutation.type === "childList") {
controllerMountTargets.add(mutation.target);
mutation.addedNodes.forEach(function(node) {
// Skip text nodes, comments, etc. — only elements can contain media
if (node.nodeType !== Node.ELEMENT_NODE) return;
scanNodeForMedia(node, node.parentNode || mutation.target, true);
});
mutation.removedNodes.forEach(function(node) {
if (node.nodeType !== Node.ELEMENT_NODE) return;
scanNodeForMedia(node, node.parentNode || mutation.target, false);
});
// A detached shadow root can be suspended while this idle task is
// queued. Ignore its stale summary; reconnecting the root performs a
// fresh media scan.
if (root.vscMutationObserver !== observer) return;
return;
compactMutationScanCandidates(removedCandidates).forEach(
function(candidate) {
scanNodeForMedia(
candidate.node,
candidate.node.parentNode || candidate.parent,
false
);
}
if (mutation.type !== "attributes") return;
var target = mutation.target;
if (
isMediaElement(target) &&
(mutation.attributeName === "src" ||
mutation.attributeName === "currentSrc")
) {
ensureController(target, target.parentElement || target.parentNode);
return;
);
compactMutationScanCandidates(addedCandidates).forEach(
function(candidate) {
scanNodeForMedia(
candidate.node,
candidate.node.parentNode || candidate.parent,
true
);
}
);
ariaHiddenTargets.forEach(function(target) {
if (
target.nodeName === "SOURCE" &&
mutation.attributeName === "src"
) {
ensureControllerForMediaChild(target);
return;
}
if (
mutation.attributeName === "aria-hidden" &&
target.attributes["aria-hidden"] &&
target.attributes["aria-hidden"].value === "false"
target &&
target.getAttribute &&
target.getAttribute("aria-hidden") === "false"
) {
scanNodeForMedia(
target,
target.parentNode || root.host || mutation.target,
target.parentNode || root.host || target,
true
);
}
});
if (addedCandidates.length > 0) {
// Reconnect shadow roots that were created while detached, including
// roots nested below a larger subtree added in one DOM operation.
resumeSuspendedObservedRoots(
getObservedRootDocument(root)
);
}
// Reconcile once per batch, not once per mutation. Large news/social
// pages can emit hundreds of childList records in one render pass.
if (controllerMountTargets.size > 0) {
tc.mediaElements.slice().forEach(function(video) {
if (!video || !video.vsc || !video.vsc.div) return;
var mount = video.vsc.controllerHostMount;
if (!controllerMountTargets.has(mount)) return;
remountControllerHost(video.vsc, mount);
controllerMountTargets.forEach(function(mount) {
var controllers = vscControllersByMount.get(mount);
if (!controllers) return;
Array.from(controllers).forEach(function(videoController) {
if (!videoController || !videoController.div) return;
remountControllerHost(videoController, mount);
});
});
}
// Document selectors do not cross shadow boundaries. A detached
// custom-element host can therefore hide connected-looking media from
// the removed subtree scan; prune by composed connectivity as a final
// reconciliation step for every mutation batch.
tc.mediaElements.slice().forEach(function(video) {
if (!video || video.isConnected) return;
removeController(video);
});
if (removedCandidates.length > 0) {
// Document selectors do not cross shadow boundaries. A detached
// custom-element host can therefore hide media from the removed
// subtree scan; prune once, then suspend detached-root observers.
tc.mediaElements.slice().forEach(function(video) {
if (!video || video.isConnected) return;
removeController(video);
});
Array.from(vscDeferredMediaCandidates).forEach(function(media) {
if (!media || media.isConnected) return;
vscDeferredMediaCandidates.delete(media);
disconnectBootstrapMediaSourceObserver(media);
});
reconcileObservedRootConnections(
getObservedRootDocument(root),
false
);
}
},
{ timeout: 1000 }
);
});
observer.observe(root, {
attributeFilter: ["aria-hidden", "src", "currentSrc"],
attributeFilter: ["aria-hidden"],
childList: true,
subtree: true,
attributes: true
});
root.vscMutationObserver = observer;
root.vscMutationObserverAttached = true;
}
function scheduleMediaLifecycleReconcile(media, videoController) {
if (
!media ||
!videoController ||
videoController.lifecycleReconcileFrame !== null
) {
return;
}
var doc = media.ownerDocument;
var win = (doc && doc.defaultView) || window;
videoController.lifecycleReconcileFrame = win.requestAnimationFrame(
function() {
videoController.lifecycleReconcileFrame = null;
if (
!media.isConnected ||
media.vsc !== videoController ||
!videoController.div
) {
return;
}
var nextMount = getControllerMount(media);
if (
nextMount &&
(nextMount !== videoController.controllerHostMount ||
videoController.div.parentNode !== nextMount ||
!videoController.div.isConnected)
) {
remountControllerHost(videoController, nextMount);
return;
}
if (typeof videoController.controllerHostSchedule === "function") {
videoController.controllerHostSchedule();
}
}
);
}
function attachMediaDetectionListeners(root) {
if (root.vscMediaEventListenersAttached) return;
var handleDetectedMedia = function(event) {
var target = event.target;
if (!isMediaElement(target)) return;
var existingController = target.vsc;
var geometryMount =
existingController &&
getControllerGeometryMount(existingController.controllerHostMount);
var ambientSignatureMayNowApply = Boolean(
target.nodeName === "VIDEO" &&
target.autoplay === true &&
(target.muted === true || target.defaultMuted === true) &&
target.loop === true &&
target.playsInline === true &&
target.controls !== true &&
!tc.settings.showAmbientLoopControls
);
if (
tc.siteRuleAppliedHref === location.href &&
!ambientSignatureMayNowApply &&
existingController &&
existingController.div &&
existingController.div.isConnected &&
existingController.structureSignature === getControllerStructureSignature() &&
geometryMount &&
isComposedDescendant(target, geometryMount)
) {
// The controller's target-level lifecycle listeners own source and speed
// updates. Defer the mount/geometry repair to one animation frame so a
// lifecycle burst cannot force repeated synchronous layout.
scheduleMediaLifecycleReconcile(target, existingController);
return;
}
ensureController(target, target.parentElement || target.parentNode);
};
@@ -4716,23 +5410,133 @@ function attachMediaDetectionListeners(root) {
root.vscMediaEventListenersAttached = true;
}
function scheduleNavigationRescan() {
function scheduleNavigationRescan(event) {
var nextHref = location.href;
if (window.vscLastObservedHref !== nextHref) {
// Pointer and controller interaction state belongs to the previous route.
// YouTube keeps hover-preview and Shorts players connected across SPA
// navigations, so retaining either target can send shortcuts to an
// off-screen/stale player even though the new watch controller works.
tc.lastPointerPosition = null;
var urlChanged = window.vscLastObservedHref !== nextHref;
var navigationSignaled = Boolean(event && event.type);
var forceYouTubeReconcile = Boolean(
event && event.type === "yt-navigate-finish"
);
if (!urlChanged && !navigationSignaled) return;
if (urlChanged) {
// A media identity belongs to the previous route, but viewport pointer
// coordinates remain valid while the cursor is stationary. Keep them so
// shortcuts can still choose the closest controller after an SPA swap.
tc.lastInteractedMedia = null;
}
window.vscLastObservedHref = nextHref;
window.vscNavigationForceDiscovery =
window.vscNavigationForceDiscovery === true || forceYouTubeReconcile;
if (!urlChanged && window.vscNavigationRescanTimer) return;
clearTimeout(window.vscNavigationRescanTimer);
window.vscNavigationRescanTimer = setTimeout(function() {
initializeWhenReady(document, true);
window.vscNavigationRescanTimer = null;
var forceDiscovery = window.vscNavigationForceDiscovery === true;
window.vscNavigationForceDiscovery = false;
reconcileAfterNavigation(document, forceDiscovery);
}, 300);
}
function stopLocationWatch() {
if (!window.vscLocationWatchTimer) return;
clearInterval(window.vscLocationWatchTimer);
window.vscLocationWatchTimer = null;
}
function startLocationWatchFallback() {
if (
window.vscLocationWatchTimer ||
(window.vscPageShadowBridgeLoaded &&
window.vscPageNavigationApiBridgeLoaded)
) {
return;
}
window.vscLocationWatchTimer = setInterval(function() {
if (window.vscLastObservedHref !== location.href) {
scheduleNavigationRescan();
}
}, 1000);
}
function reconcileAfterNavigation(doc, forceDiscovery) {
if (!tc.runtimeSettingsHydrated || !doc || !doc.body) {
initializeWhenReady(doc, true);
return;
}
var wasActive = isSpeederActiveForCurrentPage();
var previouslyIncludedAudio = tc.settings.audioBoolean === true;
var previouslyIncludedAmbient = tc.settings.showAmbientLoopControls === true;
var previousStructureSignature = getControllerStructureSignature();
if (tc.siteRuleAppliedHref !== location.href) applySiteRuleOverrides();
var isActive = isSpeederActiveForCurrentPage();
observeRoot(doc);
resumeSuspendedObservedRoots(doc);
if (!isActive) {
clearDeferredMediaCandidates();
tc.mediaElements.slice().forEach(function(media) {
removeController(media);
});
vscInitializedDocuments.add(doc);
return;
}
removeIneligibleMediaControllers();
var discoveryScopeExpanded = Boolean(
!wasActive ||
(!previouslyIncludedAudio && tc.settings.audioBoolean === true) ||
(!previouslyIncludedAmbient &&
tc.settings.showAmbientLoopControls === true)
);
var needsFullDiscovery = Boolean(
forceDiscovery ||
discoveryScopeExpanded ||
!vscInitializedDocuments.has(doc) ||
!window.vscPageShadowBridgeLoaded
);
if (needsFullDiscovery) {
scanRootForMedia(doc);
rescanObservedMediaRoots(doc);
} else {
Array.from(vscDeferredMediaCandidates).forEach(function(media) {
if (!media || !media.isConnected) {
vscDeferredMediaCandidates.delete(media);
return;
}
ensureController(media, media.parentElement || media.parentNode);
});
var structureChanged =
previousStructureSignature !== getControllerStructureSignature();
tc.mediaElements.slice().forEach(function(media) {
if (!media || !media.isConnected) {
removeController(media);
return;
}
if (
structureChanged ||
!media.vsc ||
!media.vsc.div ||
!media.vsc.div.isConnected
) {
ensureController(media, media.parentElement || media.parentNode);
return;
}
scheduleMediaLifecycleReconcile(media, media.vsc);
});
}
refreshAllControllerGeometry();
if (tc.settings.rememberSpeed || tc.settings.forceLastSavedSpeed) {
tc.mediaElements.slice().forEach(applyRememberedSpeedPolicy);
}
vscInitializedDocuments.add(doc);
flushPendingMediaCandidates();
}
function attachNavigationListeners() {
if (window.vscNavigationListenersAttached) return;
@@ -4741,7 +5545,7 @@ function attachNavigationListeners() {
var original = history[method];
history[method] = function() {
var result = original.apply(this, arguments);
scheduleNavigationRescan();
scheduleNavigationRescan({ type: "history-state" });
return result;
};
});
@@ -4753,13 +5557,7 @@ function attachNavigationListeners() {
document.addEventListener("yt-navigate-finish", scheduleNavigationRescan);
}
window.vscLastObservedHref = location.href;
if (!window.vscLocationWatchTimer) {
window.vscLocationWatchTimer = setInterval(function() {
if (window.vscLastObservedHref !== location.href) {
scheduleNavigationRescan();
}
}, 1000);
}
startLocationWatchFallback();
window.vscNavigationListenersAttached = true;
}
@@ -4770,13 +5568,15 @@ function initializeNow(doc, forceReinit = false) {
// later whitelisted SPA route can never reactivate the extension.
attachNavigationListeners();
if (typeof tc.videoController === "undefined") defineVideoController();
applySiteRuleOverrides();
if (tc.siteRuleAppliedHref !== location.href) applySiteRuleOverrides();
var isActive = isSpeederActiveForCurrentPage();
// Keep observing while inactive so dynamically-created media/shadow roots
// are available to the next forced SPA rescan, but remove stale controls.
observeRoot(doc);
resumeSuspendedObservedRoots(doc);
if (!isActive) {
clearDeferredMediaCandidates();
tc.mediaElements.slice().forEach(function(video) {
removeController(video);
});
@@ -5195,7 +5995,7 @@ function resetSpeed(v, target, isFastKey = false) {
setSpeed(v, lastToggle, false, true);
} else {
// Not at preferred speed, save current as toggle speed and go to preferred
lastToggleSpeed[videoId] = currentSpeed;
rememberToggleSpeed(videoId, currentSpeed);
setSpeed(v, preferredSpeed, false, true);
}
} else {
@@ -5212,7 +6012,7 @@ function resetSpeed(v, target, isFastKey = false) {
setSpeed(v, speedToRestore, false, true, true);
} else {
// Not at 1.0, save current as toggle speed and go to 1.0
lastToggleSpeed[videoId] = currentSpeed;
rememberToggleSpeed(videoId, currentSpeed);
v.vsc.resetToggleArmed = true;
setSpeed(v, resetSpeedValue, false, true, true);
}
@@ -5278,6 +6078,8 @@ function jumpToMark(v) {
function handleDrag(video, e) {
const c = video.vsc.div;
const dragController = video.vsc;
if (dragController.dragCleanup) dragController.dragCleanup();
const sC = convertControllerToManualPosition(video.vsc);
if (!sC) return;
var pE =
@@ -5316,7 +6118,11 @@ function handleDrag(video, e) {
pE.removeEventListener("mouseleave", eD);
sC.classList.remove("dragging");
video.classList.remove("vcs-dragging");
if (dragController.dragCleanup === eD) {
dragController.dragCleanup = null;
}
};
dragController.dragCleanup = eD;
pE.addEventListener("mouseup", eD);
pE.addEventListener("mouseleave", eD);
pE.addEventListener("mousemove", sD);
@@ -5361,11 +6167,15 @@ function handleFullscreenControllerTransition() {
});
}
[
"fullscreenchange",
"webkitfullscreenchange",
"mozfullscreenchange",
"MSFullscreenChange"
].forEach(function(eventName) {
document.addEventListener(eventName, handleFullscreenControllerTransition);
});
function attachFullscreenListeners(doc) {
if (!doc || doc.vscFullscreenListenersAttached) return;
[
"fullscreenchange",
"webkitfullscreenchange",
"mozfullscreenchange",
"MSFullscreenChange"
].forEach(function(eventName) {
doc.addEventListener(eventName, handleFullscreenControllerTransition);
});
doc.vscFullscreenListenersAttached = true;
}