mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
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:
co-authored by
Claude Opus 4.8
parent
0b99687db4
commit
b820064b77
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user