mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
refactor(spf): rename Segment.programDateTime → Segment.startDate
The HLS-tag-shaped name doesn't fit the format-neutral media model. startDate parallels startTime (a segment carries both) and Track.startDate, while staying protocol-neutral. Pure rename — no behavior change. (Open question, deferred: whether a per-segment wall-clock field is needed at all once Track.startDate is the anchor, or only re-emerges for discontinuities.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
13996a654d
commit
0cec3132a9
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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<Tracks extends Track>(
|
||||
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<Tracks extends Track>(
|
||||
return {
|
||||
...track,
|
||||
startTime: track.startTime + shift,
|
||||
startDate: anchor.programDateTime - originOffset,
|
||||
startDate: anchor.startDate - originOffset,
|
||||
segments: segments.map((segment) => ({ ...segment, startTime: segment.startTime + shift })),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ export function parseMediaPlaylist<T extends PartiallyResolvedTrack>(
|
||||
// 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<T extends PartiallyResolvedTrack>(
|
||||
|
||||
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<T extends PartiallyResolvedTrack>(
|
||||
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<T extends PartiallyResolvedTrack>(
|
||||
? 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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user