1// Replay-local time only. Never replace the browser clock or the live clock. 2export const REPLAY_RATE_EVENT = "channel155:replay-rate-changed"; 3 4export function createReplayClock(startMs, now = () => Date.now()) { 5 let anchorMs = startMs, position = 0, rate = 1; 6 return { 7 positionMs: () => position + Math.max(0, now() - anchorMs) * rate, 8 get rate() { return rate; }, 9 setRate(next) { 10 position += Math.max(0, now() - anchorMs) * rate; 11 anchorMs = Math.max(anchorMs, now()); 12 rate = next === 2 ? 2 : 1; 13 }, 14 reset(nextPosition = 0, startsAt = now()) { 15 position = nextPosition; 16 anchorMs = startsAt; 17 rate = 1; 18 }, 19 }; 20} 21 22export function replayNow() { 23 return globalThis.window?.__channelReplay?.nowMs?.() ?? Date.now(); 24} 25 26// Keep authored rate separate from the hold multiplier (e.g. 1.3 â 2.6). 27export function setReplayMediaRate(media, baseRate = 1) { 28 if (!media) return; 29 const replay = globalThis.window?.__channelReplay; 30 replay?.mediaRates?.set(media, baseRate); 31 media.playbackRate = baseRate * (replay?.playbackRate ?? 1); 32} 33 34export function replayMediaBaseRate(media) { 35 return globalThis.window?.__channelReplay?.mediaRates?.get(media) ?? media.playbackRate; 36} 37 38// YouTube resets rate when loading/cueing a clip. Call on state changes as well 39// as hold changes, and fall back to normal speed if this clip refuses 2Ã. 40export function syncReplayYouTubeRate(player, isActive = () => true) { 41 const replay = globalThis.window?.__channelReplay; 42 if (!replay || !player?.setPlaybackRate) return; 43 const requested = isActive() ? (replay.playbackRate ?? 1) : 1; 44 const available = player.getAvailablePlaybackRates?.() ?? []; 45 if (requested === 2 && available.length && !available.includes(2)) { 46 replay.setPlaybackRate(1); 47 return; 48 } 49 if (player.getPlaybackRate?.() !== requested) player.setPlaybackRate(requested); 50 if (requested === 2 && !youtubeRateChecks.has(player)) { 51 const timer = window.setTimeout(() => { 52 youtubeRateChecks.delete(player); 53 if (isActive() && replay.playbackRate === 2 && player.getPlaybackRate?.() !== 2) replay.setPlaybackRate(1); 54 }, 800); 55 youtubeRateChecks.set(player, timer); 56 } else if (requested === 1) { 57 window.clearTimeout(youtubeRateChecks.get(player)); 58 youtubeRateChecks.delete(player); 59 } 60} 61const youtubeRateChecks = new WeakMap(); 62 63export function installReplaySpeed(replay) { 64 if (!replay || replay.speedInstalled) return; 65 replay.speedInstalled = true; 66 let pointer = null, timer = 0, active = false, suppressClick = false, suppressUntil = 0; 67 const excluded = 'button,a,input,textarea,select,label,[role="button"],[role="slider"],[contenteditable],.topline,.chat-panel,.chat-sheet,[role="dialog"],.show-share-card'; 68 const canHold = () => !replay.completed && !replay.restartInFlight && 69 Date.now() >= replay.startupNotBeforeMs && document.visibilityState === "visible" && 70 ![...document.querySelectorAll('[role="dialog"],.modal-backdrop,.profile-backdrop')] 71 .some(element => element.getClientRects().length && getComputedStyle(element).visibility !== "hidden"); 72 const cancel = () => { 73 if (active) suppressUntil = Date.now() + 500; 74 window.clearTimeout(timer); 75 timer = 0; 76 pointer = null; 77 active = false; 78 replay.setPlaybackRate(1); 79 }; 80 const onDown = event => { 81 if (pointer || !event.isPrimary) { cancel(); return; } 82 if (event.button !== 0 || !canHold() || 83 !event.target.closest?.('.broadcast') || event.target.closest(excluded)) return; 84 suppressClick = false; 85 pointer = { id: event.pointerId, x: event.clientX, y: event.clientY }; 86 timer = window.setTimeout(() => { 87 if (!pointer || !canHold()) return cancel(); 88 active = true; 89 suppressClick = true; 90 replay.setPlaybackRate(2); 91 }, 350); 92 }; 93 const onMove = event => { 94 if (pointer?.id !== event.pointerId) return; 95 if (Math.hypot(event.clientX - pointer.x, event.clientY - pointer.y) > 24 || 96 !event.target.closest?.('.broadcast') || event.target.closest(excluded)) cancel(); 97 }; 98 const onClick = event => { 99 if (!suppressClick) return; 100 suppressClick = false;
101 if (Date.now() > suppressUntil) return; 102 event.preventDefault(); 103 event.stopImmediatePropagation(); 104 }; 105 const onContext = event => { 106 if (pointer || active) event.preventDefault(); 107 }; 108 const onVisibility = () => { if (document.visibilityState !== "visible") cancel(); }; 109 const onMedia = event => { 110 if (event.target instanceof HTMLMediaElement) { 111 setReplayMediaRate(event.target, replay.mediaRates.get(event.target) ?? event.target.defaultPlaybackRate ?? 1); 112 } 113 }; 114 const onKey = event => { if (event.key === "Escape") cancel(); }; 115 const listeners = [ 116 [document, "pointerdown", onDown], [document, "pointermove", onMove], 117 [document, "pointerup", cancel], [document, "pointercancel", cancel], 118 [document, "lostpointercapture", cancel], [document, "click", onClick], 119 [document, "contextmenu", onContext], [document, "visibilitychange", onVisibility], 120 [document, "play", onMedia], [document, "loadedmetadata", onMedia], 121 [document, "keydown", onKey], [window, "blur", cancel], 122 [window, "channel155:web-replay-restarted", cancel], 123 ]; 124 listeners.forEach(([target, name, handler]) => target.addEventListener(name, handler, true)); 125 replay.cancelSpeedHold = cancel; 126 const check = window.setInterval(() => { if (!canHold()) cancel(); }, 100); 127 window.addEventListener("pagehide", () => { 128 cancel(); 129 // Keep listeners for a page restored from the back/forward cache. 130 }); 131 replay.disposeSpeed = () => { 132 cancel(); 133 window.clearInterval(check); 134 listeners.forEach(([target, name, handler]) => target.removeEventListener(name, handler, true)); 135 }; 136}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.