refactor(spf): extract resolveLiveLatency out of the engine closure

The HLS live-latency bridge was defined inside createSimpleHlsEngine's closure
despite capturing nothing — a fresh allocation per engine, and pure HLS logic
stranded where it can't be tested. Move it to media/hls/reload-policy.ts as a
pure resolveLiveLatency(presentation, trackId) next to liveLatencyFor; the
engine injects the import. Adds direct coverage for the bridge (previously only
stubbed via seek-to-live-edge's injected resolver).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Christian Pillsbury
2026-06-25 10:00:45 -07:00
co-authored by Claude Opus 4.8
parent de2e1f823f
commit bfe12eedf9
3 changed files with 61 additions and 24 deletions
+23 -1
View File
@@ -1,4 +1,11 @@
import { getMediaPlaylistMetadata, type ResolvedTrack } from '../types';
import {
getMediaPlaylistMetadata,
isResolvedPresentation,
isResolvedTrack,
type MaybeResolvedPresentation,
type ResolvedTrack,
} from '../types';
import { findTrackById } from '../utils/tracks';
/** Reload cadence when a playlist carries no usable target duration. */
const FALLBACK_TARGET_DURATION = 6;
@@ -56,3 +63,18 @@ export function mediaPlaylistReloadDelay(current: ResolvedTrack, previous: Resol
export function liveLatencyFor(track: ResolvedTrack): number {
return HOLD_BACK_TARGET_MULTIPLIER * targetDurationOf(track);
}
/**
* Resolve the target live latency for a presentation's timeline-bearing track —
* the HLS engine injects this as `seekToLiveEdge`'s format-neutral
* `resolveLiveLatency` seam. `0` when there is no resolved track to read (the
* behavior then seeks straight to the edge).
*/
export function resolveLiveLatency(
presentation: MaybeResolvedPresentation | undefined,
trackId: string | undefined
): number {
if (!isResolvedPresentation(presentation) || !trackId) return 0;
const track = findTrackById(presentation, trackId);
return track && isResolvedTrack(track) ? liveLatencyFor(track) : 0;
}
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';
import { MEDIA_PLAYLIST_METADATA_KEY, type ResolvedTrack } from '../../types';
import { liveLatencyFor, mediaPlaylistReloadDelay } from '../reload-policy';
import { type MaybeResolvedPresentation, MEDIA_PLAYLIST_METADATA_KEY, type ResolvedTrack } from '../../types';
import { liveLatencyFor, mediaPlaylistReloadDelay, resolveLiveLatency } from '../reload-policy';
/** Minimal resolved-track stand-in carrying only what the policy reads. */
function track(opts: {
@@ -58,3 +58,29 @@ describe('liveLatencyFor', () => {
expect(liveLatencyFor(track({ targetDuration: 0 }))).toBe(18);
});
});
describe('resolveLiveLatency', () => {
/** Minimal presentation wrapping one video track, enough for findTrackById + liveLatencyFor. */
function presentation(targetDuration: number): MaybeResolvedPresentation {
const video = {
id: 'v-1',
segments: [{ id: 's0', url: 's0.m4s', duration: 2, startTime: 0 }],
metadata: { [MEDIA_PLAYLIST_METADATA_KEY]: { targetDuration, mediaSequence: 0, endList: false } },
};
return {
id: 'p-1',
url: 'master.m3u8',
selectionSets: [{ switchingSets: [{ tracks: [video] }] }],
} as unknown as MaybeResolvedPresentation;
}
it('returns the timeline-bearing track latency (3× target duration)', () => {
expect(resolveLiveLatency(presentation(2), 'v-1')).toBe(6);
});
it('returns 0 when the presentation is unresolved, or the track id is absent / unknown', () => {
expect(resolveLiveLatency(undefined, 'v-1')).toBe(0);
expect(resolveLiveLatency(presentation(2), undefined)).toBe(0);
expect(resolveLiveLatency(presentation(2), 'missing')).toBe(0);
});
});
+10 -21
View File
@@ -20,20 +20,17 @@ import {
removeAllSubtitlesTracksFromMedia,
} from '../../../media/dom/text/text-track-slots';
import { parseMultivariantPlaylist } from '../../../media/hls/parse-multivariant';
import { liveLatencyFor, mediaPlaylistReloadDelay } from '../../../media/hls/reload-policy';
import {
type AudioTrack,
type CanPlayTrack,
isResolvedPresentation,
isResolvedTrack,
type MaybeResolvedPresentation,
type ResolvedTrack,
type TextTrack,
type VideoTrack,
import { mediaPlaylistReloadDelay, resolveLiveLatency } from '../../../media/hls/reload-policy';
import type {
AudioTrack,
CanPlayTrack,
MaybeResolvedPresentation,
ResolvedTrack,
TextTrack,
VideoTrack,
} from '../../../media/types';
import type { GetCdnId } from '../../../media/utils/cdn';
import { getResolvedSelectedTrackDuration } from '../../../media/utils/track-selection';
import { findTrackById } from '../../../media/utils/tracks';
import type { BandwidthConfig, BandwidthState } from '../../../network/bandwidth-estimator';
import type { SegmentLoaderActor } from '../../actors/dom/segment-loader';
import type { SourceBufferActor } from '../../actors/dom/source-buffer';
@@ -341,19 +338,11 @@ export function createSimpleHlsEngine(
return bufferedAnchorFor(context.segments, context.bufferedRanges);
});
// Format-specific live latency for `seekToLiveEdge`: look up the
// timeline-bearing track and apply the HLS HOLD-BACK rule. The behavior
// consumes this as a neutral `resolveLiveLatency` seam (a DASH engine would
// inject `suggestedPresentationDelay`).
const resolveLiveLatency = (presentation: MaybeResolvedPresentation | undefined, trackId: string | undefined) => {
if (!isResolvedPresentation(presentation) || !trackId) return 0;
const track = findTrackById(presentation, trackId);
return track && isResolvedTrack(track) ? liveLatencyFor(track) : 0;
};
const finalConfig = {
...config,
resolveBufferedAnchor,
// Format-neutral live-latency seam for `seekToLiveEdge` — the HLS resolver
// (HOLD-BACK); a DASH engine would inject `suggestedPresentationDelay`.
resolveLiveLatency,
canPlayTrack: config.canPlayTrack ?? canPlayTrack,
// The resolve* loaders' RecurringRunner re-runs on this `reschedule`: the pure