PageSourceSearch

https://15half.com/assets/mobile-broadcast-transport.js?v=20260909…boundaries-v3-20260912-replay-hold-speed-v1

js 15half.com collected 2026-10-09 22:39:39 UTC 6,409 bytes, 163 lines download raw bytes

1import { setReplayMediaRate } from "./replay-speed.js";
2// Used only behind the mobile-device gate. Preference controls output, never
3// transport lifetime. Every retry reads today's segment/clock, not a stale
4// play() promise or the position where a suspended browser stopped.
5export function createMobileBroadcastTransport(media, read, {
6  now = () => performance.now(),
7  schedule = (fn) => window.setInterval(fn, 250),
8  cancel = (id) => window.clearInterval(id),
9} = {}) {
10  let disposed = false, key = null, generation = 0, pending = false;
11  let lastTime = NaN, lastProgress = now(), lastReload = -Infinity;
12  let blocked = false, completed = false, needsAlignment = true;
13  let initialAlignment = true;
14  let lastMuted = null;
15  let frameRequest = 0, lastFrameAt = now();
16  const observesFrames = typeof media.requestVideoFrameCallback === "function";
17  const observeFrame = () => {
18    if (!observesFrames || disposed) return;
19    frameRequest = media.requestVideoFrameCallback(() => {
20      lastFrameAt = now();
21      observeFrame();
22    });
23  };
24  observeFrame();
25  const valid = (version) => !disposed && generation === version &&
26    read()?.key === key && read()?.active;
27  const align = (segment) => {
28    if (media.readyState < 1) return;
29    // Narration may protect its first sample from ordinary boundary/loading
30    // latency. This applies once per segment, never to stall/foreground recovery.
31    const requestedTime = initialAlignment && Number.isFinite(segment.initialTime)
32      ? segment.initialTime : segment.time;
33    let target = Math.max(0, Number(requestedTime) || 0);
34    if (Number.isFinite(media.duration) && media.duration > 0) {
35      if (segment.loop) target %= media.duration;
36      else if (target >= media.duration - 0.01) {
37        completed = true;
38        media.pause();
39        return;
40      }
41    }
42    if (Math.abs(media.currentTime - target) > 0.08) {
43      try { media.currentTime = target; } catch { return; }
44    }
45    needsAlignment = false;
46    initialAlignment = false;
47    lastTime = media.currentTime;
48    lastProgress = now();
49  };
50  const play = () => {
51    if (pending || completed || disposed) return;
52    const version = generation;
53    pending = true;
54    let result;
55    try { result = media.play(); } catch (error) { result = Promise.reject(error); }
56    Promise.resolve(result).then(() => {
57      if (!valid(version)) return;
58      pending = false;
59    }, (error) => {
60      if (!valid(version)) return;
61      pending = false;
62      if (error?.name === "NotAllowedError" && !media.muted) {
63        // Keep the viewer's sound preference; retry only this element silently.
64        blocked = true;
65        media.muted = true;
66        media.dataset.mobileBroadcastSound = "gesture-required";
67        play();
68      }
69    });
70  };
71  function tick({ fromGesture = false } = {}) {
72    if (disposed) return;
73    const segment = read();
74    if (!segment?.active) {
75      if (!media.paused) media.pause();
76      if (pending) { generation += 1; pending = false; }
77      needsAlignment = true;
78      if (key !== null) initialAlignment = false;
79      lastProgress = now();
80      lastFrameAt = now();
81      return;
82    }
83    if (key !== segment.key) {
84      key = segment.key;
85      generation += 1;
86      pending = false;
87      blocked = false;
88      completed = false;
89      needsAlignment = true;
90      initialAlignment = true;
91      lastTime = NaN;
92      lastProgress = now();
93      lastFrameAt = now();
94      lastReload = -Infinity;
95      if (segment.src && media.src !== new URL(segment.src, window.location.href).href) {
96        media.src = segment.src;
97        media.load();
98      }
99    }
100    if (fromGesture || (lastMuted === true && !segment.muted)) blocked = false;
101    lastMuted = Boolean(segment.muted);
102    media.muted = lastMuted || blocked;
103    media.dataset.mobileBroadcastSound = blocked && !lastMuted
104      ? "gesture-required" : lastMuted ? "muted" : "on";
105    setReplayMediaRate(media, segment.rate || 1);
106    media.loop = Boolean(segment.loop);
107    if (!needsAlignment && media.ended && !segment.loop) completed = true;
108    if (completed) return;
109    const progressed = !media.paused && Number.isFinite(lastTime) &&
110      Math.abs(media.currentTime - lastTime) > 0.015;
111    if (progressed) lastProgress = now();
112    lastTime = media.currentTime;
113    if (needsAlignment) align(segment);
114    if (completed) return;
115    // An initial metadata request is not a frozen playing clock. Quiz may give
116    // that first request a little longer without changing recovery after start.
117    const progressTimeout = initialAlignment && media.readyState < 1
118      ? Math.max(1250, Math.min(3000, Number(segment.initialLoadGraceMs) || 0))
119      : 1250;
120    const stalled = now() - lastProgress > progressTimeout ||
121      (observesFrames && now() - lastFrameAt > 1500);
122    if (stalled && now() - lastReload > 4000) {
123      // Covers unresolved play(), buffering and frozen clocks. A bounded reload
124      // invalidates old promises and resumes at the current broadcast position.
125      generation += 1;
126      pending = false;
127      lastReload = now();
128      lastProgress = now();
129      lastFrameAt = now();
130      needsAlignment = true;
131      initialAlignment = false;
132      media.load();
133      align(segment);
134    }
135    if (needsAlignment) return;
136    if (media.paused) play();
137  }
138  const onReady = () => tick();
139  const onGesture = () => tick({ fromGesture: true });
140  const onMute = () => tick({ fromGesture: read()?.muted === false });
141  media.addEventListener("loadedmetadata", onReady);
142  media.addEventListener("canplay", onReady);
143  window.addEventListener("channel155:mobile-playback-gesture", onGesture);
144  window.addEventListener("channel:mute-changed", onMute);
145  window.addEventListener("channel155:mobile-playback-state-ready", onReady);
146  const timer = schedule(tick);
147  tick();
148  return {
149    tick,
150    dispose() {
151      disposed = true;
152      generation += 1;
153      cancel(timer);
154      if (observesFrames) media.cancelVideoFrameCallback?.(frameRequest);
155      media.removeEventListener("loadedmetadata", onReady);
156      media.removeEventListener("canplay", onReady);
157      window.removeEventListener("channel155:mobile-playback-gesture", onGesture);
158      window.removeEventListener("channel:mute-changed", onMute);
159      window.removeEventListener("channel155:mobile-playback-state-ready", onReady);
160      media.pause();
161    },
162  };
163}

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.