PageSourceSearch

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

js 15half.com collected 2026-10-09 22:39:34 UTC 3,932 bytes, 91 lines download raw bytes

1import { createMobileBroadcastTransport } from "./mobile-broadcast-transport.js?v=20260909-mobile-quiz-boundaries-v3-20260912-replay-hold-speed-v1";
2
3// Caption alignment is approximate; leave a little more of the recorded attack
4// on mobile instead of treating the first caption timestamp as a sample cut.
5export function mobileQuizVoiceTrimSeconds(cues) {
6  const starts = (Array.isArray(cues) ? cues : [])
7    .map(cue => Number(cue?.startMs))
8    .filter(value => Number.isFinite(value) && value >= 0);
9  return starts.length ? Math.max(0, Math.min(...starts) - 180) / 1000 : 0;
10}
11
12// One player for the lifetime of the mobile quiz, not one per React effect.
13// Question timers and show state are never delayed by this short audio handoff.
14export function createMobileQuizNarration(media, {
15  isActive,
16  isMuted,
17  now = () => performance.now(),
18  schedule = fn => window.setInterval(fn, 50),
19  cancel = id => window.clearInterval(id),
20  createTransport = createMobileBroadcastTransport,
21} = {}) {
22  let requested = null, current = null, drainUntil = null;
23  let started = false, captionTime = null, disposed = false;
24  const sameSource = () => current && media.currentSrc ===
25    new URL(current.src, window.location.href).href;
26  const remainingMs = () => (media.duration - media.currentTime) /
27    (media.playbackRate || current?.rate || 1) * 1000;
28  const read = () => {
29    const active = !disposed && isActive();
30    if (current?.key !== requested?.key) {
31      const remaining = remainingMs();
32      const canFinish = active && current &&
33        (!requested || requested.runId === current.runId) && sameSource() &&
34        started && !media.paused && !media.ended && media.readyState >= 2 &&
35        Number.isFinite(remaining) && remaining > 0 && remaining <= 900;
36      if (canFinish && drainUntil === null) {
37        drainUntil = now() + Math.min(900, remaining + 100);
38      }
39      if (!canFinish || drainUntil !== null && now() >= drainUntil) {
40        current = requested;
41        drainUntil = null;
42        started = false;
43        captionTime = null;
44      }
45    } else if (current && requested) {
46      // A same-segment render may refresh clock data; it must not restart audio.
47      current = requested;
48      drainUntil = null;
49    }
50    return current ? {
51      key: current.key, src: current.src,
52      active: active && !(current.startsInSeconds?.() > 0),
53      time: current.readTime(), initialTime: current.initialTime,
54      initialLoadGraceMs: 3000,
55      rate: current.rate, muted: isMuted(),
56    } : { active: false };
57  };
58  const transport = createTransport(media, read, { now, schedule, cancel });
59  const onPlaying = () => { if (sameSource()) started = true; };
60  const onEnded = () => transport.tick();
61  media.addEventListener("playing", onPlaying);
62  media.addEventListener("ended", onEnded);
63  return {
64    update(segment) {
65      if (disposed) return;
66      const sameKey = segment && requested?.key === segment.key;
67      requested = segment ? {
68        ...segment,
69        // Freeze the boundary decision before metadata loading / tail draining.
70        initialTime: sameKey ? requested.initialTime : segment.initialTime,
71      } : null;
72      transport.tick();
73    },
74    timeFor(segmentKey) {
75      if (!isActive() || !segmentKey || requested?.key !== segmentKey ||
76          current?.key !== segmentKey || !started) return null;
77      if (sameSource() && media.readyState >= 1 && Number.isFinite(media.currentTime)) {
78        captionTime = Math.max(captionTime ?? 0, media.currentTime * 1000);
79      }
80      // Hold through buffering/reload; never flash ahead on the broadcast clock
81      // or borrow the previous clip's currentTime during React's render/commit.
82      return captionTime;
83    },
84    dispose() {
85      disposed = true;
86      media.removeEventListener("playing", onPlaying);
87      media.removeEventListener("ended", onEnded);
88      transport.dispose();
89    },
90  };
91}

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.