mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
refactor(spf): rename anchorLiveTracks to anchorPresentationTimeline
The behavior's mechanism — pin one track from buffer ground truth, derive one shared offset, stamp every track onto it — is format-neutral; only the offset source (PDT) is live-specific. Rename it and its published signal (liveAnchor → presentationAnchor, matching the PresentationAnchor type it holds) so the name reflects the general role, and note the non-zero-PTS reuse path through the existing resolveBufferedAnchor seam. No behavior change. 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
6bbbc23961
commit
dbddbf67bb
+33
-25
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Establish the live presentation's shared timeline anchor — the wall clock
|
||||
* (PDT) at media-time 0 — once per source, and stamp it onto every track so the
|
||||
* model's coordinates coincide with the SourceBuffer's native-PTS coordinates
|
||||
* Establish the presentation's shared timeline anchor — for live HLS, the wall
|
||||
* clock (PDT) at media-time 0 — once per source, and stamp it onto every track so
|
||||
* the model's coordinates coincide with the SourceBuffer's native-PTS coordinates
|
||||
* (the loader matches `currentTime`, a native-PTS value since segments append
|
||||
* unmodified, against each segment's `startTime`).
|
||||
*
|
||||
@@ -34,6 +34,14 @@
|
||||
* touches `HTMLMediaElement`. Cross-track A/V skew is intentionally not corrected
|
||||
* here — under the native-PTS default all tracks share the encoder's PTS clock,
|
||||
* so one anchor describes them all (see the decision doc).
|
||||
*
|
||||
* Format-neutral by design (hence not named for live): the mechanism — pin one
|
||||
* track from buffer ground truth, derive one shared offset, stamp every track
|
||||
* onto it — is independent of how that offset is *sourced*. Live HLS sources it
|
||||
* from PDT (`presentationAnchorFromBuffer`); a non-zero-PTS VOD source would
|
||||
* derive it from the observed first PTS instead (no PDT), reusing this behavior
|
||||
* through the same `resolveBufferedAnchor` seam. See
|
||||
* [non-zero-pts-support](../../../../internal/design/spf/features/non-zero-pts-support.md).
|
||||
*/
|
||||
|
||||
import { isUndefined } from '@videojs/utils/predicate';
|
||||
@@ -49,7 +57,7 @@ import {
|
||||
import { isResolvedPresentation, isResolvedTrack, type MaybeResolvedPresentation } from '../../media/types';
|
||||
import { findTrackById } from '../../media/utils/tracks';
|
||||
|
||||
export interface AnchorLiveTracksState {
|
||||
export interface AnchorPresentationTimelineState {
|
||||
presentation?: MaybeResolvedPresentation;
|
||||
/**
|
||||
* The established shared anchor (wall clock at media-time 0), published once
|
||||
@@ -58,7 +66,7 @@ export interface AnchorLiveTracksState {
|
||||
* seeking on the pre-anchor (raw) timeline would strand the playhead when the
|
||||
* pin later shifts the window.
|
||||
*/
|
||||
liveAnchor?: number;
|
||||
presentationAnchor?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -76,13 +84,13 @@ export interface BufferedTrackAnchor extends BufferedAnchor {
|
||||
* behavior stays DOM-free — the engine (DOM boundary) names the concrete buffer
|
||||
* actors; here `Context` is opaque.
|
||||
*/
|
||||
export type AnchorLiveTracksDeps<Context extends object> = BehaviorDeps<
|
||||
{ presentation: Signal<AnchorLiveTracksState['presentation']> },
|
||||
export type AnchorPresentationTimelineDeps<Context extends object> = BehaviorDeps<
|
||||
{ presentation: Signal<AnchorPresentationTimelineState['presentation']> },
|
||||
ContextSignals<Context>,
|
||||
AnchorLiveTracksConfig<Context>
|
||||
AnchorPresentationTimelineConfig<Context>
|
||||
>;
|
||||
|
||||
export interface AnchorLiveTracksConfig<Context extends object = object> {
|
||||
export interface AnchorPresentationTimelineConfig<Context extends object = object> {
|
||||
/**
|
||||
* Buffered-ground-truth resolver, injected by the engine (the DOM boundary).
|
||||
* Reads the first A/V buffer actor with ground truth and reports where a
|
||||
@@ -91,26 +99,26 @@ export interface AnchorLiveTracksConfig<Context extends object = object> {
|
||||
* (rather than closing over engine scope) so the engine reads its buffer actors
|
||||
* from `context`. Absent → never anchors (e.g. non-DOM tests with no buffer).
|
||||
*/
|
||||
resolveBufferedAnchor?: (deps: AnchorLiveTracksDeps<Context>) => BufferedTrackAnchor | undefined;
|
||||
resolveBufferedAnchor?: (deps: AnchorPresentationTimelineDeps<Context>) => BufferedTrackAnchor | undefined;
|
||||
}
|
||||
|
||||
type AnchorFsmState = 'unanchored' | 'anchored';
|
||||
|
||||
function anchorLiveTracksSetup<Context extends object>({
|
||||
function anchorPresentationTimelineSetup<Context extends object>({
|
||||
state,
|
||||
context,
|
||||
config = {},
|
||||
}: {
|
||||
state: {
|
||||
presentation: Signal<AnchorLiveTracksState['presentation']>;
|
||||
liveAnchor: Signal<AnchorLiveTracksState['liveAnchor']>;
|
||||
presentation: Signal<AnchorPresentationTimelineState['presentation']>;
|
||||
presentationAnchor: Signal<AnchorPresentationTimelineState['presentationAnchor']>;
|
||||
};
|
||||
context: ContextSignals<Context>;
|
||||
config?: AnchorLiveTracksConfig<Context>;
|
||||
config?: AnchorPresentationTimelineConfig<Context>;
|
||||
}): Reactor<AnchorFsmState | 'destroying' | 'destroyed'> {
|
||||
// The deps handed to the injected resolver, so the engine reads its buffer
|
||||
// actors from `context` — no pre-composition closure over engine scope.
|
||||
const deps: AnchorLiveTracksDeps<Context> = { state, context, config };
|
||||
const deps: AnchorPresentationTimelineDeps<Context> = { state, context, config };
|
||||
|
||||
// The shared anchor from the first actually-buffered A/V track: the resolver
|
||||
// reports the buffered segment + its track id; that track's segment carries the
|
||||
@@ -138,15 +146,15 @@ function anchorLiveTracksSetup<Context extends object>({
|
||||
// drop the established anchor: doing so re-opens the seekToLiveEdge gate and
|
||||
// re-fires its one-time live-edge seek, jumping the playhead. "Pin-once"
|
||||
// means pin once per source — see live-presentation-anchor.md.
|
||||
if (state.liveAnchor.get() !== undefined) return 'anchored';
|
||||
if (state.presentationAnchor.get() !== undefined) return 'anchored';
|
||||
return isUndefined(deriveBufferAnchor(presentation)) ? 'unanchored' : 'anchored';
|
||||
},
|
||||
states: {
|
||||
// Reset the published anchor per source so a new source re-gates the seek.
|
||||
unanchored: { entry: () => state.liveAnchor.set(undefined) },
|
||||
unanchored: { entry: () => state.presentationAnchor.set(undefined) },
|
||||
anchored: {
|
||||
// Establish the shared anchor once and stamp it onto every track. The
|
||||
// sticky monitor keeps us `anchored` for the source once `liveAnchor` is
|
||||
// sticky monitor keeps us `anchored` for the source once `presentationAnchor` is
|
||||
// published, so this runs exactly once per source; only a source change
|
||||
// (exit to `unanchored`) re-arms it. (Were it to re-enter, re-deriving the
|
||||
// same buffer anchor is idempotent and `positionAllTracksToAnchor` writes
|
||||
@@ -161,7 +169,7 @@ function anchorLiveTracksSetup<Context extends object>({
|
||||
);
|
||||
// Publish after stamping, so a consumer reacting to the anchor (e.g.
|
||||
// seekToLiveEdge) sees the already-shifted window.
|
||||
state.liveAnchor.set(anchor);
|
||||
state.presentationAnchor.set(anchor);
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -174,17 +182,17 @@ function anchorLiveTracksSetup<Context extends object>({
|
||||
* `resolveBufferedAnchor` seam reads — supplies the context type while this
|
||||
* behavior stays DOM-free.
|
||||
*/
|
||||
export function makeAnchorLiveTracks<Context extends object = object>(): Behavior<
|
||||
export function makeAnchorPresentationTimeline<Context extends object = object>(): Behavior<
|
||||
{
|
||||
presentation: Signal<AnchorLiveTracksState['presentation']>;
|
||||
liveAnchor: Signal<AnchorLiveTracksState['liveAnchor']>;
|
||||
presentation: Signal<AnchorPresentationTimelineState['presentation']>;
|
||||
presentationAnchor: Signal<AnchorPresentationTimelineState['presentationAnchor']>;
|
||||
},
|
||||
ContextSignals<Context>,
|
||||
AnchorLiveTracksConfig<Context>
|
||||
AnchorPresentationTimelineConfig<Context>
|
||||
> {
|
||||
return {
|
||||
stateKeys: ['presentation', 'liveAnchor'],
|
||||
stateKeys: ['presentation', 'presentationAnchor'],
|
||||
contextKeys: [],
|
||||
setup: anchorLiveTracksSetup,
|
||||
setup: anchorPresentationTimelineSetup,
|
||||
};
|
||||
}
|
||||
@@ -54,12 +54,12 @@ export interface SeekToLiveEdgeState {
|
||||
selectedVideoTrackId?: string;
|
||||
selectedAudioTrackId?: string;
|
||||
/**
|
||||
* The shared live anchor, published by `anchorLiveTracks` once the buffer pin
|
||||
* The shared presentation anchor, published by `anchorPresentationTimeline` once the buffer pin
|
||||
* lands (`undefined` until then). Gates the live-edge seek: seeking before the
|
||||
* timeline is anchored would target the raw (pre-anchor) window, and the pin's
|
||||
* later shift would strand the playhead off-window.
|
||||
*/
|
||||
liveAnchor?: number;
|
||||
presentationAnchor?: number;
|
||||
}
|
||||
|
||||
export interface SeekToLiveEdgeContext {
|
||||
@@ -82,8 +82,8 @@ type SeekToLiveEdgeFsmState = 'inactive' | 'live';
|
||||
|
||||
/**
|
||||
* `'live'` once the seek preconditions hold: a media element, a (published →
|
||||
* open) MediaSource, a derivable live edge, and an established live anchor
|
||||
* (`anchorLiveTracks` has buffer-pinned the timeline — so the edge we seek to is
|
||||
* open) MediaSource, a derivable live edge, and an established presentation anchor
|
||||
* (`anchorPresentationTimeline` has buffer-pinned the timeline — so the edge we seek to is
|
||||
* the final native-PTS one, not the raw pre-anchor window). `'inactive'`
|
||||
* otherwise.
|
||||
*/
|
||||
@@ -105,7 +105,7 @@ function seekToLiveEdgeSetup({
|
||||
presentation: ReadonlySignal<SeekToLiveEdgeState['presentation']>;
|
||||
selectedVideoTrackId?: ReadonlySignal<SeekToLiveEdgeState['selectedVideoTrackId']>;
|
||||
selectedAudioTrackId?: ReadonlySignal<SeekToLiveEdgeState['selectedAudioTrackId']>;
|
||||
liveAnchor?: ReadonlySignal<SeekToLiveEdgeState['liveAnchor']>;
|
||||
presentationAnchor?: ReadonlySignal<SeekToLiveEdgeState['presentationAnchor']>;
|
||||
};
|
||||
context: {
|
||||
mediaElement: ReadonlySignal<SeekToLiveEdgeContext['mediaElement']>;
|
||||
@@ -118,7 +118,7 @@ function seekToLiveEdgeSetup({
|
||||
context.mediaElement.get(),
|
||||
context.mediaSource.get(),
|
||||
getLiveEdge({ state, config }),
|
||||
state.liveAnchor?.get() !== undefined
|
||||
state.presentationAnchor?.get() !== undefined
|
||||
)
|
||||
);
|
||||
|
||||
@@ -175,7 +175,7 @@ function seekToLiveEdgeSetup({
|
||||
}
|
||||
|
||||
/**
|
||||
* Manual `Behavior<>` literal (like `anchorLiveTracks` /
|
||||
* Manual `Behavior<>` literal (like `anchorPresentationTimeline` /
|
||||
* `calculatePresentationDuration`): declares only `presentation` in stateKeys
|
||||
* while reading `selectedVideoTrackId` defensively (contributed by
|
||||
* `switchVideoTrack`), so it composes without a stateKeys/type conflict.
|
||||
|
||||
@@ -78,17 +78,17 @@ function run(opts: {
|
||||
mediaElement?: HTMLMediaElement;
|
||||
mediaSource?: MediaSource;
|
||||
config?: SeekToLiveEdgeConfig;
|
||||
liveAnchor?: number;
|
||||
presentationAnchor?: number;
|
||||
}) {
|
||||
// Built as vars (not inline literals) so the defensively-read
|
||||
// `selectedVideoTrackId` isn't rejected by the excess-property check against
|
||||
// the behavior's declared `{ presentation }` state slice. `liveAnchor` defaults
|
||||
// the behavior's declared `{ presentation }` state slice. `presentationAnchor` defaults
|
||||
// to a defined value (the timeline is anchored) so the seek gate is open;
|
||||
// pass `liveAnchor: undefined` to exercise the pre-anchor gate.
|
||||
// pass `presentationAnchor: undefined` to exercise the pre-anchor gate.
|
||||
const state = {
|
||||
presentation: signal<MaybeResolvedPresentation | undefined>(opts.presentation),
|
||||
selectedVideoTrackId: signal<string | undefined>(opts.trackId),
|
||||
liveAnchor: signal<number | undefined>('liveAnchor' in opts ? opts.liveAnchor : 1000),
|
||||
presentationAnchor: signal<number | undefined>('presentationAnchor' in opts ? opts.presentationAnchor : 1000),
|
||||
};
|
||||
const context = {
|
||||
mediaElement: signal<HTMLMediaElement | undefined>(opts.mediaElement),
|
||||
@@ -136,19 +136,19 @@ describe('seekToLiveEdge', () => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it('does not seek until the timeline is anchored (liveAnchor published)', () => {
|
||||
it('does not seek until the timeline is anchored (presentationAnchor published)', () => {
|
||||
const ms = fakeMediaSource();
|
||||
const el = fakeMediaElement();
|
||||
|
||||
// Pre-anchor: anchorLiveTracks hasn't buffer-pinned yet, so the window is the
|
||||
// Pre-anchor: anchorPresentationTimeline hasn't buffer-pinned yet, so the window is the
|
||||
// raw (pre-shift) one. Seeking now would strand the playhead when the pin
|
||||
// later shifts the window; the gate holds the seek until `liveAnchor` lands.
|
||||
// later shifts the window; the gate holds the seek until `presentationAnchor` lands.
|
||||
const { cleanup } = run({
|
||||
presentation: makePresentation(),
|
||||
trackId: 'v-1',
|
||||
mediaElement: el,
|
||||
mediaSource: ms,
|
||||
liveAnchor: undefined,
|
||||
presentationAnchor: undefined,
|
||||
});
|
||||
|
||||
expect(el.currentTime).toBe(0);
|
||||
|
||||
@@ -137,8 +137,8 @@ function setupTrackResolution<K extends SelectedTrackKey>({
|
||||
const text = await fetchResolvableText(track, { signal });
|
||||
|
||||
// Re-read `previous` *after* the fetch: a concurrent write during
|
||||
// the await — notably anchor-live-tracks shifting this track onto
|
||||
// the shared live anchor — must be carried forward, not clobbered.
|
||||
// the await — notably anchor-presentation-timeline shifting this track onto
|
||||
// the shared presentation anchor — must be carried forward, not clobbered.
|
||||
// Parsing against the pre-fetch snapshot would strand the track
|
||||
// off the anchor for good (anchoring is pin-once). Correctness
|
||||
// rests on a run-to-completion invariant: NOTHING may yield
|
||||
|
||||
+12
-12
@@ -11,7 +11,7 @@ import {
|
||||
type VideoTrack,
|
||||
} from '../../../media/types';
|
||||
import { findTrack } from '../../../media/utils/tracks';
|
||||
import { type AnchorLiveTracksConfig, makeAnchorLiveTracks } from '../anchor-live-tracks';
|
||||
import { type AnchorPresentationTimelineConfig, makeAnchorPresentationTimeline } from '../anchor-presentation-timeline';
|
||||
|
||||
const META = { [MEDIA_PLAYLIST_METADATA_KEY]: { mediaSequence: 85, targetDuration: 5, endList: false } };
|
||||
|
||||
@@ -80,14 +80,14 @@ function makePresentation(tracks: (ResolvedTrack | PartiallyResolvedTextTrack)[]
|
||||
return { id: 'pres-1', url: 'https://example.com/master.m3u8', startTime: 0, selectionSets } as Presentation;
|
||||
}
|
||||
|
||||
function run(opts: { presentation?: MaybeResolvedPresentation; config?: AnchorLiveTracksConfig }) {
|
||||
function run(opts: { presentation?: MaybeResolvedPresentation; config?: AnchorPresentationTimelineConfig }) {
|
||||
const state = {
|
||||
presentation: signal<MaybeResolvedPresentation | undefined>(opts.presentation),
|
||||
liveAnchor: signal<number | undefined>(undefined),
|
||||
presentationAnchor: signal<number | undefined>(undefined),
|
||||
};
|
||||
// The manual `Behavior<>` literal widens the setup return to `BehaviorCleanup`;
|
||||
// narrow back to the reactor's destroy handle for teardown.
|
||||
const reactor = makeAnchorLiveTracks().setup({ state, context: {}, config: opts.config ?? {} }) as {
|
||||
const reactor = makeAnchorPresentationTimeline().setup({ state, context: {}, config: opts.config ?? {} }) as {
|
||||
destroy: () => void;
|
||||
};
|
||||
return { cleanup: () => reactor.destroy(), state };
|
||||
@@ -102,13 +102,13 @@ function resolved(presentation: MaybeResolvedPresentation, type: ResolvedTrack['
|
||||
return track as ResolvedTrack;
|
||||
}
|
||||
|
||||
describe('anchorLiveTracks', () => {
|
||||
describe('anchorPresentationTimeline', () => {
|
||||
it('does nothing until a track has buffer ground truth', () => {
|
||||
// No resolveBufferedAnchor → never anchors; the track keeps its raw timeline.
|
||||
const { cleanup, state } = run({ presentation: makePresentation([makeVideoTrack()]) });
|
||||
|
||||
expect(resolved(state.presentation.get()!, 'video', 'v-1').startTime).toBe(0);
|
||||
expect(state.liveAnchor.get()).toBeUndefined();
|
||||
expect(state.presentationAnchor.get()).toBeUndefined();
|
||||
|
||||
cleanup();
|
||||
});
|
||||
@@ -132,7 +132,7 @@ describe('anchorLiveTracks', () => {
|
||||
expect(text?.startDate).toBe(500);
|
||||
expect(isResolvedTrack(text!)).toBe(false);
|
||||
// The anchor is published for seekToLiveEdge to gate on.
|
||||
expect(state.liveAnchor.get()).toBe(500);
|
||||
expect(state.presentationAnchor.get()).toBe(500);
|
||||
|
||||
cleanup();
|
||||
});
|
||||
@@ -158,7 +158,7 @@ describe('anchorLiveTracks', () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(state.liveAnchor.get()).toBe(500);
|
||||
expect(state.presentationAnchor.get()).toBe(500);
|
||||
|
||||
// Buffer ground truth momentarily vanishes (underrun / flush / seek), then a
|
||||
// reload fires. The established anchor must persist — dropping it re-opens the
|
||||
@@ -173,7 +173,7 @@ describe('anchorLiveTracks', () => {
|
||||
await flush();
|
||||
await flush();
|
||||
|
||||
expect(state.liveAnchor.get()).toBe(500);
|
||||
expect(state.presentationAnchor.get()).toBe(500);
|
||||
|
||||
cleanup();
|
||||
});
|
||||
@@ -185,21 +185,21 @@ describe('anchorLiveTracks', () => {
|
||||
config: { resolveBufferedAnchor: () => ({ trackId: 'v-1', segmentId: 'segment-85', actualStart }) },
|
||||
});
|
||||
|
||||
expect(state.liveAnchor.get()).toBe(500);
|
||||
expect(state.presentationAnchor.get()).toBe(500);
|
||||
|
||||
// Source change → presentation reset to an unresolved value: the anchor clears
|
||||
// so the new source re-gates the seek.
|
||||
state.presentation.set({ url: 'https://example.com/new.m3u8' });
|
||||
await flush();
|
||||
await flush();
|
||||
expect(state.liveAnchor.get()).toBeUndefined();
|
||||
expect(state.presentationAnchor.get()).toBeUndefined();
|
||||
|
||||
// New source resolves with its own buffer truth → re-establishes.
|
||||
actualStart = 700;
|
||||
state.presentation.set(makePresentation([makeVideoTrack()]));
|
||||
await flush();
|
||||
await flush();
|
||||
expect(state.liveAnchor.get()).toBe(300); // video seg PDT 1000 − actualStart 700
|
||||
expect(state.presentationAnchor.get()).toBe(300); // video seg PDT 1000 − actualStart 700
|
||||
|
||||
cleanup();
|
||||
});
|
||||
@@ -534,7 +534,7 @@ http://example.com/seg0.m4s`;
|
||||
});
|
||||
|
||||
describe('resolveVideoTrack — concurrent anchor stamp during fetch', () => {
|
||||
// Regression: anchor-live-tracks establishes the shared live anchor and stamps
|
||||
// Regression: anchor-presentation-timeline establishes the shared presentation anchor and stamps
|
||||
// every track's timeline while a track resolution's playlist fetch is in
|
||||
// flight. The resolution must parse against the track as stamped — not the
|
||||
// pre-fetch snapshot — or it clobbers the stamp and strands the track off the
|
||||
@@ -591,7 +591,7 @@ http://example.com/seg0.m4s`;
|
||||
const reactor = resolveVideoTrack.setup({ state });
|
||||
|
||||
await started;
|
||||
// Establish + stamp the anchor mid-fetch, exactly as anchor-live-tracks does.
|
||||
// Establish + stamp the anchor mid-fetch, exactly as anchor-presentation-timeline does.
|
||||
state.presentation.set(positionAllTracksToAnchor(state.presentation.get() as Presentation, ANCHOR));
|
||||
releaseFetch();
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ import type { SegmentLoaderActor } from '../../actors/dom/segment-loader';
|
||||
import type { SourceBufferActor } from '../../actors/dom/source-buffer';
|
||||
import type { TextTracksActor } from '../../actors/dom/text-tracks';
|
||||
import type { TextTrackSegmentLoaderActor, TextTrackSegmentResolver } from '../../actors/text-track-segment-loader';
|
||||
import { makeAnchorLiveTracks } from '../../behaviors/anchor-live-tracks';
|
||||
import { makeAnchorPresentationTimeline } from '../../behaviors/anchor-presentation-timeline';
|
||||
import {
|
||||
calculatePresentationDuration,
|
||||
type PresentationDurationResolver,
|
||||
@@ -118,11 +118,11 @@ export interface SimpleHlsEngineState {
|
||||
currentTime?: number;
|
||||
loadActivated?: boolean;
|
||||
/**
|
||||
* The shared live timeline anchor (wall clock at media-time 0), published by
|
||||
* `anchorLiveTracks` once the buffer pin lands. `seekToLiveEdge` gates its
|
||||
* The shared presentation timeline anchor (wall clock at media-time 0), published by
|
||||
* `anchorPresentationTimeline` once the buffer pin lands. `seekToLiveEdge` gates its
|
||||
* live-edge seek on it. Absent for VoD / until the first segment buffers.
|
||||
*/
|
||||
liveAnchor?: number;
|
||||
presentationAnchor?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -194,7 +194,7 @@ export interface SimpleHlsEngineConfig extends ShareSignalsConfig<SimpleHlsEngin
|
||||
resolveDuration?: PresentationDurationResolver;
|
||||
/**
|
||||
* Sequence number assumed to be the stream origin (time 0) for the live
|
||||
* timeline anchor (`anchorLiveTracks`). Default 0. Only meaningful for live
|
||||
* timeline anchor (`anchorPresentationTimeline`). Default 0. Only meaningful for live
|
||||
* sources; ignored for VoD (the anchor is a no-op without `#EXT-X-PROGRAM-DATE-TIME`).
|
||||
*/
|
||||
presumedStartSequence?: number;
|
||||
@@ -383,7 +383,7 @@ export function createSimpleHlsEngine(
|
||||
|
||||
// Re-base selected live tracks' timelines onto the shared presentation
|
||||
// anchor (estimate, then buffer ground truth). No-op for VoD (no PDT).
|
||||
makeAnchorLiveTracks<SimpleHlsEngineContext>(),
|
||||
makeAnchorPresentationTimeline<SimpleHlsEngineContext>(),
|
||||
|
||||
// Presentation duration (finite for complete playlists, Infinity for live)
|
||||
calculatePresentationDuration,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { untrack } from '../../../core/signals/primitives';
|
||||
import { bufferedAnchorFor } from '../../../media/buffered-anchor';
|
||||
import type { SourceBufferActor } from '../../actors/dom/source-buffer';
|
||||
import type { AnchorLiveTracksDeps, BufferedTrackAnchor } from '../../behaviors/anchor-live-tracks';
|
||||
import type { AnchorPresentationTimelineDeps, BufferedTrackAnchor } from '../../behaviors/anchor-presentation-timeline';
|
||||
|
||||
/**
|
||||
* The engine context this resolver reads — the per-type SourceBuffer actors.
|
||||
@@ -15,7 +15,7 @@ export interface BufferActorContext {
|
||||
}
|
||||
|
||||
/**
|
||||
* An engine's implementation of `anchorLiveTracks`' `resolveBufferedAnchor` seam.
|
||||
* An engine's implementation of `anchorPresentationTimeline`' `resolveBufferedAnchor` seam.
|
||||
* Reads the first A/V buffer actor with ground truth (video preferred) from the
|
||||
* behavior's `context` deps — the actor knows which track it's buffering
|
||||
* (`initTrackId`) and exposes DOM-free snapshot data (appended segments +
|
||||
@@ -28,7 +28,7 @@ export interface BufferActorContext {
|
||||
*/
|
||||
export function resolveBufferedAnchor<Context extends BufferActorContext>({
|
||||
context,
|
||||
}: AnchorLiveTracksDeps<Context>): BufferedTrackAnchor | undefined {
|
||||
}: AnchorPresentationTimelineDeps<Context>): BufferedTrackAnchor | undefined {
|
||||
return untrack(() => {
|
||||
// Video preferred; under the no-skew assumption both agree, so this is just
|
||||
// a tiebreak for which actor supplies the (shared) anchor.
|
||||
|
||||
Reference in New Issue
Block a user