mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
feat(spf): honor a pre-applied anchor when first-resolving a media playlist
parseMediaPlaylist now places a first-resolved window relative to a `startDate` set on the unresolved shell (each segment at PDT − anchor) instead of always anchoring at the local base 0. This lets the shared presentation anchor pre-position not-yet-resolved tracks so they resolve already on the shared timeline — no separate positioning pass needed. Inert until a caller sets `startDate` on a shell; a playlist with no PDT falls back to the local base, and the recomputed track `startDate` reads back as the anchor. 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
2b42ff48ef
commit
2007a173b3
@@ -132,6 +132,31 @@ function placeOnPreviousTimeline(
|
||||
return { segments: placed, startTime: anchor };
|
||||
}
|
||||
|
||||
/**
|
||||
* Place a first-resolved window on a *pre-applied* anchor — the track's
|
||||
* `startDate` (wall clock at media-time 0) set on the unresolved shell before any
|
||||
* segments exist. Each PDT-bearing segment lands at `segment.startDate − anchor`,
|
||||
* so the window resolves already on the shared presentation timeline rather than
|
||||
* a local-from-zero one (the `startDate` recomputed afterwards then reads back as
|
||||
* the anchor). Falls back to the local base when no segment carries PDT — there's
|
||||
* nothing to anchor against.
|
||||
*/
|
||||
function placeOnAnchor(segments: Segment[], anchor: number): { segments: Segment[]; startTime: number } {
|
||||
const localBase = segments[0]?.startTime ?? 0;
|
||||
const anchorSegment = segments.find((segment) => !isUndefined(segment.startDate));
|
||||
if (!anchorSegment || isUndefined(anchorSegment.startDate)) {
|
||||
return { segments, startTime: localBase };
|
||||
}
|
||||
|
||||
const shift = anchorSegment.startDate - anchor - anchorSegment.startTime;
|
||||
if (shift === 0) {
|
||||
return { segments, startTime: localBase };
|
||||
}
|
||||
|
||||
const placed = segments.map((segment) => ({ ...segment, startTime: segment.startTime + shift }));
|
||||
return { segments: placed, startTime: localBase + shift };
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an HLS media playlist into a resolved track with segments.
|
||||
*
|
||||
@@ -285,12 +310,17 @@ export function parseMediaPlaylist<T extends PartiallyResolvedTrack>(
|
||||
const complete = endList || playlistType === 'VOD';
|
||||
const trackDuration = complete ? totalDuration : Number.POSITIVE_INFINITY;
|
||||
|
||||
// Position this window on the timeline. First resolve (previous is the
|
||||
// unresolved shell, no segments) anchors at 0; a live reload (previous is the
|
||||
// prior resolved snapshot) carries the timeline forward from the overlap.
|
||||
// Position this window on the timeline. A live reload (previous is the prior
|
||||
// resolved snapshot) carries the timeline forward from the overlap. First
|
||||
// resolve places relative to a pre-applied anchor (`startDate` set on the
|
||||
// shell) when present — so a track resolves already on the shared presentation
|
||||
// timeline — else anchors at the local base 0.
|
||||
const presetAnchor = previous.startDate;
|
||||
const placed = isResolvedTrack(previous)
|
||||
? placeOnPreviousTimeline(previous, segments, mediaSequence, targetDuration)
|
||||
: { segments, startTime: 0 };
|
||||
: isUndefined(presetAnchor)
|
||||
? { segments, startTime: 0 }
|
||||
: placeOnAnchor(segments, presetAnchor);
|
||||
|
||||
// Wall-clock anchor: `startDate − startTime` for the first PDT-bearing
|
||||
// segment (constant along a linear timeline). Maps this track's origin to
|
||||
|
||||
@@ -808,4 +808,54 @@ s0.ts`;
|
||||
expect((audio.startDate ?? 0) - (video.startDate ?? 0)).toBeCloseTo(2, 3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pre-applied anchor (startDate on the unresolved shell)', () => {
|
||||
const shell: PartiallyResolvedVideoTrack = {
|
||||
type: 'video',
|
||||
id: 'video-0',
|
||||
url: 'https://example.com/video/playlist.m3u8',
|
||||
bandwidth: 1400000,
|
||||
codecs: ['avc1.4d401f'],
|
||||
mimeType: 'video/mp4',
|
||||
};
|
||||
const epoch = (iso: string) => Date.parse(iso) / 1000;
|
||||
const anchor = epoch('2026-01-01T00:00:00.000Z');
|
||||
|
||||
const withPdt = `#EXTM3U
|
||||
#EXT-X-TARGETDURATION:2
|
||||
#EXT-X-MEDIA-SEQUENCE:5
|
||||
#EXT-X-PROGRAM-DATE-TIME:2026-01-01T00:00:10.000Z
|
||||
#EXTINF:2,
|
||||
s5.m4s
|
||||
#EXTINF:2,
|
||||
s6.m4s`;
|
||||
|
||||
it('places first-resolve segments by PDT relative to the pre-applied startDate', () => {
|
||||
const r = parseMediaPlaylist(withPdt, { ...shell, startDate: anchor });
|
||||
// segment.startTime = segment PDT − anchor (10s and 12s past media-time 0).
|
||||
expect(r.segments.map((s) => s.startTime)).toEqual([10, 12]);
|
||||
expect(r.startTime).toBe(10);
|
||||
// The recomputed track startDate reads back as the anchor.
|
||||
expect(r.startDate).toBe(anchor);
|
||||
});
|
||||
|
||||
it('anchors at the local base 0 when the shell carries no startDate (unchanged)', () => {
|
||||
const r = parseMediaPlaylist(withPdt, shell);
|
||||
expect(r.segments.map((s) => s.startTime)).toEqual([0, 2]);
|
||||
expect(r.startTime).toBe(0);
|
||||
});
|
||||
|
||||
it('falls back to the local base when the shell has a startDate but no segment carries PDT', () => {
|
||||
const noPdt = `#EXTM3U
|
||||
#EXT-X-TARGETDURATION:2
|
||||
#EXT-X-MEDIA-SEQUENCE:5
|
||||
#EXTINF:2,
|
||||
s5.m4s
|
||||
#EXTINF:2,
|
||||
s6.m4s`;
|
||||
const r = parseMediaPlaylist(noPdt, { ...shell, startDate: anchor });
|
||||
expect(r.segments.map((s) => s.startTime)).toEqual([0, 2]);
|
||||
expect(r.startDate).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user