diff --git a/packages/spf/src/media/align-track-timelines.ts b/packages/spf/src/media/align-track-timelines.ts index 217c2140..d37b7717 100644 --- a/packages/spf/src/media/align-track-timelines.ts +++ b/packages/spf/src/media/align-track-timelines.ts @@ -13,9 +13,9 @@ import type { Track } from './types'; * difference in their `startDate`s. This shifts each track's `startTime`s (and * its origin `startTime`/`startDate`) by that difference, re-basing all tracks * to the earliest origin. After alignment, segments with equal - * `programDateTime` have equal `startTime`. + * `startDate` have equal `startTime`. * - * Tracks without a `startDate` (no `programDateTime` in the source) can't be + * Tracks without a `startDate` (no segment carried a wall-clock date) can't be * aligned and pass through unchanged. The common origin is the earliest * `startDate`, so no `startTime` goes negative. */ diff --git a/packages/spf/src/media/anchor-track-to-sequence-origin.ts b/packages/spf/src/media/anchor-track-to-sequence-origin.ts index 537693e7..312c41ba 100644 --- a/packages/spf/src/media/anchor-track-to-sequence-origin.ts +++ b/packages/spf/src/media/anchor-track-to-sequence-origin.ts @@ -34,16 +34,16 @@ export interface AnchorToSequenceOriginOptions { * video). Exact cross-track A/V alignment comes from `alignTrackTimelines` * (PDT) and ultimately the buffer, not from these estimates. * - * No-op when there are no segments or none carries `programDateTime`. + * No-op when there are no segments or none carries `startDate`. */ export function anchorTrackToSequenceOrigin( track: Tracks, { startSequence = 0 }: AnchorToSequenceOriginOptions = {} ): Tracks { const { segments } = track; - const anchorIndex = segments.findIndex((segment) => !isUndefined(segment.programDateTime)); + const anchorIndex = segments.findIndex((segment) => !isUndefined(segment.startDate)); const anchor = segments[anchorIndex]; - if (!anchor || isUndefined(anchor.programDateTime)) { + if (!anchor || isUndefined(anchor.startDate)) { return track; } @@ -59,7 +59,7 @@ export function anchorTrackToSequenceOrigin( return { ...track, startTime: track.startTime + shift, - startDate: anchor.programDateTime - originOffset, + startDate: anchor.startDate - originOffset, segments: segments.map((segment) => ({ ...segment, startTime: segment.startTime + shift })), }; } diff --git a/packages/spf/src/media/hls/parse-media-playlist.ts b/packages/spf/src/media/hls/parse-media-playlist.ts index 87ecb526..f9ff6250 100644 --- a/packages/spf/src/media/hls/parse-media-playlist.ts +++ b/packages/spf/src/media/hls/parse-media-playlist.ts @@ -159,7 +159,7 @@ export function parseMediaPlaylist( // Seeded by an explicit `#EXT-X-PROGRAM-DATE-TIME` (which re-anchors, e.g. // across a discontinuity) and advanced by each segment's duration so segments // without their own tag are interpolated forward (per RFC 8216). - let currentProgramDateTime: number | undefined; + let currentStartDate: number | undefined; // Playlist-level metadata (surfaced for live reload pacing / merge / termination). let targetDuration = 0; @@ -186,7 +186,7 @@ export function parseMediaPlaylist( if (trimmed.startsWith('#EXT-X-PROGRAM-DATE-TIME:')) { const parsed = Date.parse(trimmed.slice('#EXT-X-PROGRAM-DATE-TIME:'.length).trim()); - currentProgramDateTime = Number.isNaN(parsed) ? currentProgramDateTime : parsed / 1000; + currentStartDate = Number.isNaN(parsed) ? currentStartDate : parsed / 1000; continue; } @@ -244,11 +244,11 @@ export function parseMediaPlaylist( startTime: currentTime, }; - if (!isUndefined(currentProgramDateTime)) { - segment.programDateTime = currentProgramDateTime; + if (!isUndefined(currentStartDate)) { + segment.startDate = currentStartDate; // Interpolate forward: the next segment without an explicit tag inherits // this anchor plus this segment's duration. - currentProgramDateTime += currentDuration; + currentStartDate += currentDuration; } if (currentByteRange) { @@ -282,14 +282,14 @@ export function parseMediaPlaylist( ? placeOnPreviousTimeline(previous, segments, mediaSequence, targetDuration) : { segments, startTime: 0 }; - // Wall-clock anchor: `programDateTime − startTime` for the first PDT-bearing + // Wall-clock anchor: `startDate − startTime` for the first PDT-bearing // segment (constant along a linear timeline). Maps this track's origin to // wall clock; recomputed each parse, so it stays stable as the window slides // and is comparable across tracks for A/V alignment. - const anchorSegment = placed.segments.find((segment) => !isUndefined(segment.programDateTime)); + const anchorSegment = placed.segments.find((segment) => !isUndefined(segment.startDate)); const startDate = - anchorSegment && !isUndefined(anchorSegment.programDateTime) - ? anchorSegment.programDateTime - anchorSegment.startTime + anchorSegment && !isUndefined(anchorSegment.startDate) + ? anchorSegment.startDate - anchorSegment.startTime : undefined; // Build initialization (VTT may not have init segment) diff --git a/packages/spf/src/media/hls/tests/parse-media-playlist.test.ts b/packages/spf/src/media/hls/tests/parse-media-playlist.test.ts index ba21ad8e..d31821ee 100644 --- a/packages/spf/src/media/hls/tests/parse-media-playlist.test.ts +++ b/packages/spf/src/media/hls/tests/parse-media-playlist.test.ts @@ -573,7 +573,7 @@ s0.ts #EXTINF:4, s1.ts`; const r = parseMediaPlaylist(text, videoShell); - expect(r.segments.map((s) => s.programDateTime)).toEqual([ + expect(r.segments.map((s) => s.startDate)).toEqual([ epoch('2026-01-01T00:00:00.000Z'), epoch('2026-01-01T00:00:04.000Z'), ]); @@ -590,7 +590,7 @@ s1.ts #EXTINF:4, s2.ts`; const r = parseMediaPlaylist(text, videoShell); - expect(r.segments.map((s) => s.programDateTime)).toEqual([ + expect(r.segments.map((s) => s.startDate)).toEqual([ epoch('2026-01-01T00:00:00.000Z'), epoch('2026-01-01T00:00:04.000Z'), epoch('2026-01-01T00:00:08.000Z'), @@ -609,7 +609,7 @@ s0.ts s1.ts`; const r = parseMediaPlaylist(text, videoShell); // s1 takes the jumped absolute time, not s0 + 4s. - expect(r.segments.map((s) => s.programDateTime)).toEqual([ + expect(r.segments.map((s) => s.startDate)).toEqual([ epoch('2026-01-01T00:00:00.000Z'), epoch('2026-01-01T01:00:00.000Z'), ]); @@ -627,9 +627,9 @@ s1.ts s2.ts`; const r = parseMediaPlaylist(text, videoShell); const t0 = epoch('2026-01-01T00:00:00.000Z'); - expect(r.segments[0]?.programDateTime).toBeCloseTo(t0, 6); - expect(r.segments[1]?.programDateTime).toBeCloseTo(t0 + 1.9, 6); - expect(r.segments[2]?.programDateTime).toBeCloseTo(t0 + 1.9 + 2.05, 6); + expect(r.segments[0]?.startDate).toBeCloseTo(t0, 6); + expect(r.segments[1]?.startDate).toBeCloseTo(t0 + 1.9, 6); + expect(r.segments[2]?.startDate).toBeCloseTo(t0 + 1.9 + 2.05, 6); }); it('leaves program date time undefined when the source carries no PDT', () => { @@ -640,10 +640,10 @@ s0.ts #EXTINF:4, s1.ts`; const r = parseMediaPlaylist(text, videoShell); - expect(r.segments.every((s) => s.programDateTime === undefined)).toBe(true); + expect(r.segments.every((s) => s.startDate === undefined)).toBe(true); }); - it('exposes Track.startDate as the wall-clock at the origin (programDateTime − startTime)', () => { + it('exposes Track.startDate as the wall-clock at the origin (startDate − startTime)', () => { const text = `#EXTM3U #EXT-X-MEDIA-SEQUENCE:0 #EXT-X-PROGRAM-DATE-TIME:2026-01-01T00:00:10.000Z @@ -727,9 +727,9 @@ s0.ts`; expect(s3.segments[0]?.id).toBe('segment-88'); expect(s3.mimeType).toBe('video/mp2t'); // TS container detected // PDT rides through carry-forward unchanged (absolute, not re-based). - expect(s3.segments[0]?.programDateTime).toBeDefined(); - expect(s3.segments.map((seg) => seg.programDateTime ?? 0)).toEqual( - [...s3.segments.map((seg) => seg.programDateTime ?? 0)].sort((a, b) => a - b) + expect(s3.segments[0]?.startDate).toBeDefined(); + expect(s3.segments.map((seg) => seg.startDate ?? 0)).toEqual( + [...s3.segments.map((seg) => seg.startDate ?? 0)].sort((a, b) => a - b) ); }); @@ -755,7 +755,7 @@ s0.ts`; expect(a82).toBeDefined(); // Same real instant → identical absolute PDT (the cross-track sync anchor)… - expect(v82?.programDateTime).toBe(a82?.programDateTime); + expect(v82?.startDate).toBe(a82?.startDate); // …even though per-track relative startTime disagrees by a full segment // (video's window starts one segment earlier). This 2s gap is exactly the // A/V misalignment that PDT-based alignment resolves and sequence-number diff --git a/packages/spf/src/media/tests/align-track-timelines.test.ts b/packages/spf/src/media/tests/align-track-timelines.test.ts index a392f524..a996cfea 100644 --- a/packages/spf/src/media/tests/align-track-timelines.test.ts +++ b/packages/spf/src/media/tests/align-track-timelines.test.ts @@ -21,7 +21,7 @@ function makeTrack(startDate: number | undefined, segments: Array<{ startTime: n url: `s${i}.m4s`, duration: 2, startTime: s.startTime, - ...(s.pdt === undefined ? {} : { programDateTime: s.pdt }), + ...(s.pdt === undefined ? {} : { startDate: s.pdt }), }) ), }; @@ -50,8 +50,8 @@ describe('alignTrackTimelines', () => { expect(alignedAudio?.segments.map((s) => s.startTime)).toEqual([2, 4]); // The same instant (PDT 1004) now has the same startTime in both tracks. - const vAt1004 = alignedVideo?.segments.find((s) => s.programDateTime === 1004); - const aAt1004 = alignedAudio?.segments.find((s) => s.programDateTime === 1004); + const vAt1004 = alignedVideo?.segments.find((s) => s.startDate === 1004); + const aAt1004 = alignedAudio?.segments.find((s) => s.startDate === 1004); expect(vAt1004?.startTime).toBe(aAt1004?.startTime); }); diff --git a/packages/spf/src/media/tests/anchor-track-to-sequence-origin.test.ts b/packages/spf/src/media/tests/anchor-track-to-sequence-origin.test.ts index d3eaa58c..26fd8230 100644 --- a/packages/spf/src/media/tests/anchor-track-to-sequence-origin.test.ts +++ b/packages/spf/src/media/tests/anchor-track-to-sequence-origin.test.ts @@ -20,7 +20,7 @@ function makeTrack( url: `${mediaSequence + i}.m4s`, duration: s.duration, startTime: s.startTime, - ...(s.pdt === undefined ? {} : { programDateTime: s.pdt }), + ...(s.pdt === undefined ? {} : { startDate: s.pdt }), }) ), metadata: { @@ -77,7 +77,7 @@ describe('anchorTrackToSequenceOrigin', () => { expect((b?.startTime ?? 0) - (a?.startTime ?? 0)).toBeCloseTo(1.9, 6); }); - it('is a no-op when no segment carries programDateTime', () => { + it('is a no-op when no segment carries startDate', () => { const track = makeTrack(85, [ { startTime: 0, duration: 4 }, { startTime: 4, duration: 4 }, diff --git a/packages/spf/src/media/types/index.ts b/packages/spf/src/media/types/index.ts index 230012f8..e12432e0 100644 --- a/packages/spf/src/media/types/index.ts +++ b/packages/spf/src/media/types/index.ts @@ -140,15 +140,15 @@ export type Track = Ham & segments: Segment[]; /** * Wall-clock time (epoch seconds) corresponding to the track's timeline - * origin (`startTime`) — i.e. `programDateTime − startTime`, the single + * origin (`startTime`) — i.e. `startDate − startTime`, the single * rolling anchor that maps this track's media timeline to wall clock. - * Optional: absent when no segment carries `programDateTime`. + * Optional: absent when no segment carries `startDate`. * * Provisional from the manifest, where the origin is the first fetched * segment; later refined from the buffer (`buffered`/`tfdt`) to pin the * origin to encoded-media zero. Comparable across tracks: the difference in * `startDate` between demuxed audio and video is their relative skew — the - * offset a cross-track aligner removes — and equal `programDateTime` across + * offset a cross-track aligner removes — and equal `startDate` across * tracks marks the same presentation instant. */ startDate?: number; @@ -338,7 +338,7 @@ export type SelectionSet = VideoSelectionSet | AudioSelectionSet | TextSelection * Media segment with timing information. * Follows CMAF-HAM composition pattern. * - * `programDateTime` is the absolute wall-clock time of the segment's first + * `startDate` is the absolute wall-clock time of the segment's first * sample, in **epoch seconds** (unit-consistent with `startTime`/`duration`), * derived from `#EXT-X-PROGRAM-DATE-TIME` (explicit or interpolated forward via * `EXTINF`). Unlike the per-track-relative `startTime`, it is comparable across @@ -347,7 +347,7 @@ export type SelectionSet = VideoSelectionSet | AudioSelectionSet | TextSelection * source carries no PDT (allowed by RFC 8216, required by Apple's HLS authoring * spec — so present on conformant content). */ -export type Segment = Ham & AddressableObject & TimeSpan & { programDateTime?: number }; +export type Segment = Ham & AddressableObject & TimeSpan & { startDate?: number }; /** * Floating-point tolerance for matching segments by `startTime`. Two