Skip to main content

max / makenotwork

3.3 KB · 90 lines History Blame Raw
1 // Pure media-player logic (no DOM) — the virtual-timeline math for gapless
2 // "segment" playback, extracted from static/media-player.js where it was
3 // inlined seven times. Imported by the element and unit-tested directly.
4 //
5 // A track can be split into ordered segments: `main` segments (slices of the
6 // creator's media file) interleaved with insertions (ads/intros). A single
7 // main media URL may appear across several `main` segments (split around
8 // insertions), so seeking within one needs its offset *inside that URL*.
9
10 export interface Segment {
11 url: string;
12 duration_ms: number;
13 /** 'main' for the creator's media; anything else is an insertion. */
14 segment_type: string;
15 title?: string;
16 }
17
18 /** Format seconds as `m:ss` (or `h:mm:ss`). Ported from media-player.js:31. */
19 export function formatTime(seconds: number): string {
20 if (Number.isNaN(seconds)) return '0:00';
21 const h = Math.floor(seconds / 3600);
22 const m = Math.floor((seconds % 3600) / 60);
23 const s = Math.floor(seconds % 60);
24 const ss = s.toString().padStart(2, '0');
25 if (h > 0) return `${h}:${m.toString().padStart(2, '0')}:${ss}`;
26 return `${m}:${ss}`;
27 }
28
29 /** Cumulative virtual start (ms) of each segment, and the total duration. */
30 export function buildTimeline(segments: Segment[]): { segStarts: number[]; totalMs: number } {
31 const segStarts: number[] = [];
32 let totalMs = 0;
33 for (const seg of segments) {
34 segStarts.push(totalMs);
35 totalMs += seg.duration_ms;
36 }
37 return { segStarts, totalMs };
38 }
39
40 /**
41 * Offset (ms) of segment `idx` within its own main media URL: the summed
42 * duration of every earlier `main` segment sharing the same URL. Zero for
43 * insertions or a URL's first appearance. This is the calculation that was
44 * copy-pasted seven times in the original — the single source now.
45 */
46 export function mainOffsetMs(segments: Segment[], idx: number): number {
47 const seg = segments[idx];
48 if (!seg || seg.segment_type !== 'main') return 0;
49 let offset = 0;
50 for (let j = 0; j < idx; j++) {
51 if (segments[j].segment_type === 'main' && segments[j].url === seg.url) {
52 offset += segments[j].duration_ms;
53 }
54 }
55 return offset;
56 }
57
58 /** The segment index containing virtual time `targetMs`; clamps to the last
59 * segment past the end. Ported from the three identical search loops. */
60 export function segmentIndexAtVirtualMs(segStarts: number[], segments: Segment[], targetMs: number): number {
61 for (let k = 0; k < segments.length; k++) {
62 if (targetMs < segStarts[k] + segments[k].duration_ms) return k;
63 }
64 return segments.length - 1;
65 }
66
67 /**
68 * Map a chapter time (measured against the *main* media only, ignoring
69 * insertions) to a virtual timeline position in ms. Ported from
70 * media-player.js:381 — a chapter at main-time T lands after all insertions
71 * that precede it.
72 */
73 export function chapterToVirtualMs(segments: Segment[], chapterMs: number): number {
74 let virtualMs = 0;
75 let mainElapsedMs = 0;
76 for (const seg of segments) {
77 if (seg.segment_type === 'main') {
78 if (mainElapsedMs + seg.duration_ms > chapterMs) {
79 virtualMs += chapterMs - mainElapsedMs;
80 break;
81 }
82 mainElapsedMs += seg.duration_ms;
83 virtualMs += seg.duration_ms;
84 } else if (mainElapsedMs <= chapterMs) {
85 virtualMs += seg.duration_ms;
86 }
87 }
88 return virtualMs;
89 }
90