wip(spf): per-type relocation keying + non-0th-segment origin fix

- Key mediaContainerData by track *type* (video/audio), not track id: one
  init+media pair per type, ABR rungs share the origin. relocationMessagePipelines
  -> relocationPipelinesFor(type); the engines pass per-type.
- #4 fix: record the discovered segment's 0-based startTime and derive the origin
  as baseMediaDecodeTime/timescale - segmentStartTime, so relocation is correct when
  the first loaded segment isn't the 0th (non-zero initial currentTime, live/DVR).
  Adds segmentStartTime to MediaContainerData.
- Rename derivePerTrackStartMediaTime -> derivePerTypeStartMediaTime; stampTracks
  applies each type's origin to every track of that type.
- Add establish-start-media-time derive unit test.

Unit-verified (segmentStartTime=100 still yields origin 60) and end-to-end
sandbox-verified: start-at-300 buffers around the playhead (0-based); default
start-at-0 A/V + audio-only remain 0-based.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Christian Pillsbury
2026-07-10 07:20:43 -07:00
co-authored by Claude Opus 4.8
parent 0b99687db4
commit b820064b77
9 changed files with 195 additions and 117 deletions
@@ -43,20 +43,24 @@ your detailed pass; **DEFERRED** = later/open.
(config messagePipelines) + DOM split; deleted `relocation.ts`/`origin-discoverer.ts``05a0a6452`
- audio-only relocation wiring (reactor + `audioMessagePipelines` baked into
`engine-audio-only`) — audio 0-based sandbox-verified
- per-type keying (#3) + non-0th-segment origin `bmdt/ts segmentStartTime` (#4) +
`derivePerTypeStartMediaTime` unit test — unit + end-to-end (start-at-300) verified
### Architecture (as landed)
- **Steps** (`behaviors/dom/relocation-steps.ts`, `relocationMessagePipelines`): a
plain config array. `discover` (`readInitTimescale`/`readSegmentOrigin`) writes
`state.mediaContainerData`; `stampStartMediaTime` reads that track's origin back
and sets `timestampOffset`. Steps read composition `state` from call-time `deps`
(no closures/context); one map serves both track types (key by segment trackId).
- **Steps** (`behaviors/dom/relocation-steps.ts`, `relocationPipelinesFor(type)`): a
plain config array, one per track type. `discover` (`readInitTimescale`/
`readSegmentOrigin`) writes `state.mediaContainerData[type]` (`timescale`,
`baseMediaDecodeTime`, `segmentStartTime`); `stampStartMediaTime` reads that type's
origin (`bmdt/ts segmentStartTime`) back and sets `timestampOffset`. Steps read
composition `state` from call-time `deps` (no closures/context). ABR rungs of a
type share the entry.
- **Reactor** (`behaviors/establish-start-media-time.ts`, DOM-free): 3 states —
`inactive` (clears the slot per source) / `monitoring` (derive effect = injected
`deriveStartMediaTime` seam → writes `Track.startMediaTime`, the consume) /
`established` (derive disabled, sticky). Owns `mediaContainerData` on `state`;
selection **optional/defensive** (composes across video-only/audio-only/both).
- `StepDeps` carries the composition `{state,context,config}` (opaque conduit).
Engines bake `*MessagePipelines = relocationMessagePipelines` + the reactor +
Engines bake `*MessagePipelines = relocationPipelinesFor(type)` + the reactor +
`mediaContainerData` slot + `deriveStartMediaTime` config, all comment-marked.
### REVIEW — your detailed pass
@@ -81,9 +85,6 @@ your detailed pass; **DEFERRED** = later/open.
with comment markers; revisit a tree-shakeable opt-in + measure Tier-0 bundle.
### DEFERRED / open (see the doc's Open questions)
- **Per-type keying + non-0th-segment origin (#3/#4)** — next up. `mediaContainerData`
keyed by track *type* (ABR rungs share); `startMediaTime = bmdt/ts segment.startTime`
so non-zero starts (initial currentTime ≠ 0, live/DVR) relocate correctly.
- Tier 2 shared-`min` (a `deriveStartMediaTime` swap) + barrier-liveness bound.
- Honest-`startMediaTime`-everywhere convergence + live-anchor dedup.
- Suspected pre-existing: seek-into-evicted-back-buffer.
@@ -158,34 +158,36 @@ signals are **optional/defensive**, so the one reactor composes across video-onl
audio-only / both. Per-source freshness is structural (the `inactive` transition
clears the slot), not a hand-rolled reset.
- **Discover** (per-track step). A head-peek step reads the fetched byte stream —
the init (`mdhd` timescale) and the first media segment (`tfdt`
baseMediaDecodeTime) — and writes the raw values into `mediaContainerData`, a
`Signal<Record<TrackId, { timescale?; baseMediaDecodeTime? }>>`. It's a
**two-source** read across two appends, but the slot *is* the shared state (init
writes `timescale`, the first segment writes `baseMediaDecodeTime`), so the two
- **Discover** (per-type steps). Head-peek steps read the init (`mdhd` timescale)
and the first media segment (`tfdt` baseMediaDecodeTime, plus that segment's
0-based `startTime`) and write them into `mediaContainerData`, a
`Signal<Record<TrackType, { timescale?; baseMediaDecodeTime?; segmentStartTime? }>>`
**keyed by track type** (`'video'`/`'audio'`) — one init+media pair per type
suffices, and ABR rungs of a type share the entry. It's a **two-source** read
across two appends, but the slot *is* the shared state (init writes `timescale`,
the first media segment writes `baseMediaDecodeTime`+`segmentStartTime`), so the
steps are independent — no shared closure, no self-discrimination. Writes are
**synchronous** RMW of disjoint keys (the sync-merge invariant keeps this clear
of the #1746 hazard; an async RMW of the slot would reintroduce it). Runs on the
*fetched* stream, so transport stays pure `fetchBytes` — discovery is a content
concern. `mediaContainerData` is named for what it holds (raw parsed container
values) rather than the one thing derived from it, so other box data can land
there later without a rename.
of the #1746 hazard). Runs on the *fetched* stream, so transport stays pure
`fetchBytes`. `segmentStartTime` is recorded because the origin is
`baseMediaDecodeTime/timescale segmentStartTime` — the first *loaded* segment
isn't necessarily the 0th (non-zero initial `currentTime`, live/DVR).
- **Derive** (reactor effect). Watches `mediaContainerData` (+ selection for
Tier 2) and, via the injected **`deriveStartMediaTime`** seam, writes the
settled per-track `startMediaTime` onto the `Track`s. The seam is **pure**
`(mediaContainerData, ctx) => Record<TrackId, number | undefined>`, where
`undefined` means "not ready yet" (the *when*) and the number is the *how* — and
the effect is the sole writer of the field. It's the **only tier knob**: Tier 1
writes each track its own (`baseMediaDecodeTime / timescale`); Tier 2 writes the
`min` across the selected A/V origins onto every track. No Presentation-level
field is needed — Tier 2 just denormalizes the min across the per-track slots.
Tier 2) and, via the injected **`deriveStartMediaTime`** seam, writes the settled
per-type `startMediaTime` onto the `Track`s (each type's value stamped on every
track of that type). The seam is **pure**
`(mediaContainerData, ctx) => Record<TrackType, number | undefined>``undefined`
means "not ready yet"; the effect is the sole writer of the field. It's the
**only tier knob**: Tier 1 gives each type its own
(`baseMediaDecodeTime/timescale segmentStartTime`); Tier 2 writes the `min`
across the selected A/V origins to every type. No Presentation-level field is
needed — Tier 2 just denormalizes the min across the per-type entries.
- **Apply** (per-track step). A stamp step relocates via `timestampOffset =
startTime startMediaTime`, which the SourceBufferActor applies to
`SourceBuffer.timestampOffset`. **Tier 1** reads the track's *own* discovered
origin straight from `mediaContainerData` — populated by the discover step
earlier in the same pipeline, so it's synchronous, independent of the
derive/model, and robust to `established` + late tracks. When no complete origin
`SourceBuffer.timestampOffset`. **Tier 1** reads the type's *own* discovered
origin (`bmdt/ts segmentStartTime`) straight from `mediaContainerData` —
populated by the discover step earlier in the same pipeline, so it's synchronous,
independent of the derive/model, and robust to `established` + late tracks. When no complete origin
is found (TS / containerless / 0-PTS) it leaves the append native (offset 0).
Reading the *reduced* `Track.startMediaTime` instead — tier-agnostic apply, needed
for Tier 2's shared `min` — is a deferred follow-up (it's where an abortable
+12 -6
View File
@@ -168,16 +168,22 @@ export type Track = Ham &
};
/**
* Raw per-track values parsed from the media container, accumulated as they're
* discovered across appends (`mdhd` timescale from the init, `tfdt`
* baseMediaDecodeTime from the first media segment) — hence both optional. The
* transient input the `establishStartMediaTime` reactor reduces into
* `Track.startMediaTime`. Named for what it holds (container data), not the one
* thing currently derived from it, so other box values can join later.
* Per-track-type origin-establishment data, accumulated across appends (`mdhd`
* timescale from the init, `tfdt` baseMediaDecodeTime from the first media
* segment) — hence optional. The transient input the `establishStartMediaTime`
* reactor reduces into `Track.startMediaTime`.
*
* `segmentStartTime` is the 0-based presentation start of the segment
* `baseMediaDecodeTime` was read from — *not* a container value (it's the playlist
* position), but co-located because the origin is `baseMediaDecodeTime/timescale
* segmentStartTime`: the first *loaded* segment isn't necessarily the 0th (a
* non-zero initial `currentTime`, or live/DVR), so the decode time alone isn't the
* stream origin.
*/
export interface MediaContainerData {
timescale?: number;
baseMediaDecodeTime?: number;
segmentStartTime?: number;
}
/**
@@ -34,62 +34,78 @@ function containerSlot(deps: StepDeps): ContainerSlot {
return (deps.state as unknown as StateSignals<EstablishStartMediaTimeState>).mediaContainerData;
}
/** Synchronous RMW of the per-track entry — disjoint keys across producers, so no lost update. */
function writeContainer(slot: ContainerSlot, trackId: string, patch: Partial<MediaContainerData>): void {
update(slot, (current) => ({ ...current, [trackId]: { ...current?.[trackId], ...patch } }));
/** Synchronous RMW of the per-type entry — disjoint keys across producers, so no lost update. */
function writeContainer(slot: ContainerSlot, trackType: string, patch: Partial<MediaContainerData>): void {
update(slot, (current) => ({ ...current, [trackType]: { ...current?.[trackType], ...patch } }));
}
/** Init pipeline step: head-peek the `mdhd` timescale into `state.mediaContainerData[trackId]`. */
const readInitTimescale: LoadStep = async (frame, _signal, deps) => {
const { op } = frame;
if (op.type !== 'append-init' || !frame.data) return;
const slot = containerSlot(deps);
const { trackId } = op.meta;
if (peek(slot)?.[trackId]?.timescale !== undefined) return; // already have it
frame.data = await peekHead(frame.data, (bytes) => {
const timescale = readFirstMediaTimescale(bytes);
if (timescale === undefined) return false;
writeContainer(slot, trackId, { timescale });
return true;
});
};
/** Media-segment pipeline step: head-peek the `tfdt` baseMediaDecodeTime into `state.mediaContainerData[trackId]`. */
const readSegmentOrigin: LoadStep = async (frame, _signal, deps) => {
const { op } = frame;
if (op.type !== 'append-segment' || !frame.data) return;
const slot = containerSlot(deps);
const { trackId } = op.meta;
if (peek(slot)?.[trackId]?.baseMediaDecodeTime !== undefined) return; // established
frame.data = await peekHead(frame.data, (bytes) => {
const baseMediaDecodeTime = readFirstBaseMediaDecodeTime(bytes);
if (baseMediaDecodeTime === undefined) return false;
writeContainer(slot, trackId, { baseMediaDecodeTime });
return true;
});
};
/**
* Media-segment stamp step. Tier 1: relocate by the track's *own* discovered origin,
* read straight from `mediaContainerData` (populated by `readSegmentOrigin` earlier
* in this same pipeline, so it's available synchronously). If no complete origin was
* found TS / containerless / a 0-PTS source leave the append native (offset 0).
* Relocation pipelines for one track type a plain config `messagePipelines`.
* Keyed by **track type** (`'video'` / `'audio'`), so ABR rungs of a type share the
* origin (discover skips once the type's value is present). The steps read/write
* `state.mediaContainerData[trackType]` via their call-time `deps`.
*/
const stampStartMediaTime: LoadStep = (frame, _signal, deps) => {
const { op } = frame;
if (op.type !== 'append-segment') return;
const data = peek(containerSlot(deps))?.[op.meta.trackId];
if (data?.timescale === undefined || data.baseMediaDecodeTime === undefined) return;
// startTime is 0-based, so the relocating offset is startMediaTime.
frame.meta = { ...(frame.meta ?? op.meta), timestampOffset: -(data.baseMediaDecodeTime / data.timescale) };
};
export function relocationPipelinesFor(trackType: 'video' | 'audio'): MessagePipelines {
/** Init step: head-peek the `mdhd` timescale into `mediaContainerData[trackType]`. */
const readInitTimescale: LoadStep = async (frame, _signal, deps) => {
const { op } = frame;
if (op.type !== 'append-init' || !frame.data) return;
const slot = containerSlot(deps);
if (peek(slot)?.[trackType]?.timescale !== undefined) return; // already have it (any rung of this type)
frame.data = await peekHead(frame.data, (bytes) => {
const timescale = readFirstMediaTimescale(bytes);
if (timescale === undefined) return false;
writeContainer(slot, trackType, { timescale });
return true;
});
};
/**
* Relocation pipelines a plain config `messagePipelines`. The same map serves
* every track type: the steps key by the segment's own `op.meta.trackId`.
*/
export const relocationMessagePipelines: MessagePipelines = () => ({
remove: [dispatchStep],
'append-init': [fetchStep, readInitTimescale, dispatchStep],
'append-segment': [fetchStep, readSegmentOrigin, stampStartMediaTime, dispatchStep],
});
/**
* Media-segment step: head-peek the `tfdt` baseMediaDecodeTime, recording the
* segment's 0-based `startTime` with it the origin is `bmdt/ts segmentStartTime`,
* so the first *loaded* segment need not be the 0th.
*/
const readSegmentOrigin: LoadStep = async (frame, _signal, deps) => {
const { op } = frame;
if (op.type !== 'append-segment' || !frame.data) return;
const slot = containerSlot(deps);
if (peek(slot)?.[trackType]?.baseMediaDecodeTime !== undefined) return; // established
const segmentStartTime = op.meta.startTime;
frame.data = await peekHead(frame.data, (bytes) => {
const baseMediaDecodeTime = readFirstBaseMediaDecodeTime(bytes);
if (baseMediaDecodeTime === undefined) return false;
writeContainer(slot, trackType, { baseMediaDecodeTime, segmentStartTime });
return true;
});
};
/**
* Stamp step. Tier 1: relocate by this type's *own* discovered origin, read
* straight from `mediaContainerData` (populated by `readSegmentOrigin` earlier in
* this pipeline, so it's synchronous). If no complete origin was found TS /
* containerless / a 0-PTS source leave the append native (offset 0).
*/
const stampStartMediaTime: LoadStep = (frame, _signal, deps) => {
const { op } = frame;
if (op.type !== 'append-segment') return;
const data = peek(containerSlot(deps))?.[trackType];
if (
data?.timescale === undefined ||
data.baseMediaDecodeTime === undefined ||
data.segmentStartTime === undefined
) {
return;
}
// startMediaTime = baseMediaDecodeTime/timescale segmentStartTime; offset = startMediaTime.
frame.meta = {
...(frame.meta ?? op.meta),
timestampOffset: data.segmentStartTime - data.baseMediaDecodeTime / data.timescale,
};
};
return () => ({
remove: [dispatchStep],
'append-init': [fetchStep, readInitTimescale, dispatchStep],
'append-segment': [fetchStep, readSegmentOrigin, stampStartMediaTime, dispatchStep],
});
}
@@ -33,9 +33,11 @@ import { findTrackById } from '../../media/utils/tracks';
export interface EstablishStartMediaTimeState {
presentation?: MaybeResolvedPresentation;
/**
* Transient per-track container data (keyed by track id), accumulated by the
* discover steps across appends. Reset per source. Never the model the churn
* stays here; only the settled `startMediaTime` reaches `Track`.
* Transient origin-establishment data, keyed by **track type** (`'video'` /
* `'audio'`) per spec one init+media pair per type suffices, and ABR rungs of
* a type share the origin. Filled by the discover steps across appends, reset
* per source. Never the model the churn stays here; only the settled
* `startMediaTime` reaches `Track`.
*/
mediaContainerData?: Record<string, MediaContainerData>;
selectedVideoTrackId?: string;
@@ -48,27 +50,35 @@ export interface DeriveStartMediaTimeContext {
}
/**
* Reduce the discovered container data into each track's `startMediaTime`.
* `undefined` for a track means "not ready yet". Pure and injected the single
* point of tier variation: Tier 1 is per-track own; a Tier-2 variant returns the
* shared `min` across the selected A/V origins for every track.
* Reduce the discovered container data (keyed by track type) into each type's
* `startMediaTime`. `undefined` means "not ready yet". Pure and injected the
* single point of tier variation: Tier 1 is per-type own; a Tier-2 variant returns
* the shared `min` across the selected A/V origins for every type.
*/
export type DeriveStartMediaTime = (
containerData: Record<string, MediaContainerData>,
ctx: DeriveStartMediaTimeContext
) => Record<string, number | undefined>;
/** Tier 1 default — each track relocates by its own origin (`baseMediaDecodeTime ÷ timescale`). */
export const derivePerTrackStartMediaTime: DeriveStartMediaTime = (containerData) => {
/**
* Tier 1 default each type relocates by its own origin:
* `startMediaTime = baseMediaDecodeTime/timescale segmentStartTime` (the
* `segmentStartTime` term makes it the stream origin even when the first loaded
* segment isn't the 0th).
*/
export const derivePerTypeStartMediaTime: DeriveStartMediaTime = (containerData) => {
const out: Record<string, number | undefined> = {};
for (const [id, { timescale, baseMediaDecodeTime }] of Object.entries(containerData)) {
out[id] = timescale != null && baseMediaDecodeTime != null ? baseMediaDecodeTime / timescale : undefined;
for (const [type, { timescale, baseMediaDecodeTime, segmentStartTime }] of Object.entries(containerData)) {
out[type] =
timescale != null && baseMediaDecodeTime != null && segmentStartTime != null
? baseMediaDecodeTime / timescale - segmentStartTime
: undefined;
}
return out;
};
export interface EstablishStartMediaTimeConfig {
/** The reduce seam (tier knob). Defaults to {@link derivePerTrackStartMediaTime}. */
/** The reduce seam (tier knob). Defaults to {@link derivePerTypeStartMediaTime}. */
deriveStartMediaTime?: DeriveStartMediaTime;
}
@@ -84,7 +94,7 @@ function stampTracks(presentation: Presentation, startMediaTimes: Record<string,
switchingSets: selectionSet.switchingSets.map((switchingSet) => ({
...switchingSet,
tracks: switchingSet.tracks.map((track) => {
const startMediaTime = startMediaTimes[track.id];
const startMediaTime = startMediaTimes[track.type];
if (startMediaTime === undefined || track.startMediaTime === startMediaTime) return track;
changed = true;
return { ...track, startMediaTime };
@@ -112,7 +122,7 @@ function establishStartMediaTimeSetup({
state,
config = {},
}: EstablishStartMediaTimeDeps): Reactor<EstablishFsmState | 'destroying' | 'destroyed'> {
const derive = config.deriveStartMediaTime ?? derivePerTrackStartMediaTime;
const derive = config.deriveStartMediaTime ?? derivePerTypeStartMediaTime;
const selectionContext = (): DeriveStartMediaTimeContext => ({
selectedVideoTrackId: state.selectedVideoTrackId?.get(),
@@ -0,0 +1,44 @@
import { describe, expect, it } from 'vitest';
import { derivePerTypeStartMediaTime } from '../establish-start-media-time';
describe('derivePerTypeStartMediaTime', () => {
it('derives the origin as baseMediaDecodeTime/timescale segmentStartTime (0th segment)', () => {
// Mux asset_start_time=60: origin ≈ 60s. First loaded segment is the 0th (startTime 0).
expect(
derivePerTypeStartMediaTime(
{ video: { timescale: 90000, baseMediaDecodeTime: 90000 * 60, segmentStartTime: 0 } },
{}
)
).toEqual({ video: 60 });
});
it('subtracts a non-zero segmentStartTime so it yields the stream origin, not the loaded segment', () => {
// Same source, but the first *loaded* segment starts at presentation 100 (native ≈160s) —
// e.g. a non-zero initial currentTime. The origin must still be 60, not 160.
expect(
derivePerTypeStartMediaTime(
{ video: { timescale: 90000, baseMediaDecodeTime: 90000 * 160, segmentStartTime: 100 } },
{}
)
).toEqual({ video: 60 });
});
it('is undefined for a type until timescale + baseMediaDecodeTime + segmentStartTime are all present', () => {
expect(derivePerTypeStartMediaTime({ video: { timescale: 90000 } }, {})).toEqual({ video: undefined });
expect(derivePerTypeStartMediaTime({ audio: { baseMediaDecodeTime: 100, segmentStartTime: 0 } }, {})).toEqual({
audio: undefined,
});
});
it('resolves each track type independently (preserving real A/V skew)', () => {
expect(
derivePerTypeStartMediaTime(
{
video: { timescale: 90000, baseMediaDecodeTime: 90000 * 60, segmentStartTime: 0 },
audio: { timescale: 48000, baseMediaDecodeTime: 48000 * 59.956, segmentStartTime: 0 },
},
{}
)
).toEqual({ video: 60, audio: 59.956 });
});
});
@@ -21,7 +21,7 @@ import {
import { deriveCdnPriority } from '../../behaviors/derive-cdn-priority';
import { endOfStream } from '../../behaviors/dom/end-of-stream';
import { loadAudioSegments } from '../../behaviors/dom/load-segments';
import { relocationMessagePipelines } from '../../behaviors/dom/relocation-steps';
import { relocationPipelinesFor } from '../../behaviors/dom/relocation-steps';
import { setupAudioBufferActors } from '../../behaviors/dom/setup-buffer-actors';
import { setupMediaSource } from '../../behaviors/dom/setup-mediasource';
import { trackCurrentTime } from '../../behaviors/dom/track-current-time';
@@ -181,9 +181,9 @@ export function createHlsAudioOnlyEngine(
canPlayTrack: config.canPlayTrack ?? canPlayTrack,
resolveDuration: config.resolveDuration ?? getResolvedSelectedTrackDuration,
parsePresentation: config.parsePresentation ?? parseMultivariantPlaylist,
// Non-zero-PTS relocation (spike): pair the audio loader with the relocation
// steps `establishStartMediaTime` derives from. Remove with the composed reactor.
audioMessagePipelines: relocationMessagePipelines,
// Non-zero-PTS relocation (spike): pair the audio loader with the per-type
// relocation steps `establishStartMediaTime` derives from. Remove with the reactor.
audioMessagePipelines: relocationPipelinesFor('audio'),
};
return createComposition(
@@ -43,7 +43,7 @@ import {
import { deriveCdnPriority } from '../../behaviors/derive-cdn-priority';
import { endOfStream } from '../../behaviors/dom/end-of-stream';
import { loadAudioSegments, loadTextTrackSegments, loadVideoSegments } from '../../behaviors/dom/load-segments';
import { relocationMessagePipelines } from '../../behaviors/dom/relocation-steps';
import { relocationPipelinesFor } from '../../behaviors/dom/relocation-steps';
import { seekToLiveEdge } from '../../behaviors/dom/seek-to-live-edge';
import { setupAudioBufferActors, setupVideoBufferActors } from '../../behaviors/dom/setup-buffer-actors';
import { setupMediaSource } from '../../behaviors/dom/setup-mediasource';
@@ -360,11 +360,10 @@ export function createSimpleHlsEngine(
addSubtitlesTracksToMedia: config.addSubtitlesTracksToMedia ?? addSubtitlesTracksToMedia,
getShowingSubtitlesTrackFromMedia: config.getShowingSubtitlesTrackFromMedia ?? getShowingSubtitlesTrackFromMedia,
removeAllSubtitlesTracksFromMedia: config.removeAllSubtitlesTracksFromMedia ?? removeAllSubtitlesTracksFromMedia,
// Non-zero-PTS relocation (spike): the discover/stamp steps `establishStartMediaTime`
// pairs with, given to both buffer loaders (the steps key by the segment's own track).
// Remove these two lines with the composed reactor to drop relocation.
videoMessagePipelines: relocationMessagePipelines,
audioMessagePipelines: relocationMessagePipelines,
// Non-zero-PTS relocation (spike): the per-type discover/stamp steps
// `establishStartMediaTime` pairs with. Remove these two lines with the reactor.
videoMessagePipelines: relocationPipelinesFor('video'),
audioMessagePipelines: relocationPipelinesFor('audio'),
};
const composition = createComposition(
@@ -7,7 +7,7 @@ export { getMediaPlaylistMetadata } from '../../../media/types';
// consumer swapping the tier policy via `config.deriveStartMediaTime`.
export {
type DeriveStartMediaTime,
derivePerTrackStartMediaTime,
derivePerTypeStartMediaTime,
} from '../../behaviors/establish-start-media-time';
export type { SimpleHlsMediaAPI, SimpleHlsMediaProps } from './adapter';
export { SimpleHlsMediaElement, SimpleHlsMediaMixin, simpleHlsMediaDefaultProps } from './adapter';