PageSourceSearch

https://15half.com/assets/replay-speed.js

js 15half.com collected 2026-10-09 22:39:23 UTC 5,817 bytes, 136 lines download raw bytes

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.