@editframe/elements 0.26.2-beta.0 → 0.26.4-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/elements/EFTimegroup.js +7 -2
- package/dist/elements/EFTimegroup.js.map +1 -1
- package/package.json +2 -2
- package/scripts/build-css.js +3 -3
- package/tsdown.config.ts +1 -1
- package/types.json +1 -1
- package/src/elements/ContextProxiesController.ts +0 -124
- package/src/elements/CrossUpdateController.ts +0 -22
- package/src/elements/EFAudio.browsertest.ts +0 -706
- package/src/elements/EFAudio.ts +0 -56
- package/src/elements/EFCaptions.browsertest.ts +0 -1960
- package/src/elements/EFCaptions.ts +0 -823
- package/src/elements/EFImage.browsertest.ts +0 -120
- package/src/elements/EFImage.ts +0 -113
- package/src/elements/EFMedia/AssetIdMediaEngine.test.ts +0 -224
- package/src/elements/EFMedia/AssetIdMediaEngine.ts +0 -110
- package/src/elements/EFMedia/AssetMediaEngine.browsertest.ts +0 -140
- package/src/elements/EFMedia/AssetMediaEngine.ts +0 -385
- package/src/elements/EFMedia/BaseMediaEngine.browsertest.ts +0 -400
- package/src/elements/EFMedia/BaseMediaEngine.ts +0 -505
- package/src/elements/EFMedia/BufferedSeekingInput.browsertest.ts +0 -386
- package/src/elements/EFMedia/BufferedSeekingInput.ts +0 -430
- package/src/elements/EFMedia/JitMediaEngine.browsertest.ts +0 -226
- package/src/elements/EFMedia/JitMediaEngine.ts +0 -256
- package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.browsertest.ts +0 -679
- package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.ts +0 -117
- package/src/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.ts +0 -246
- package/src/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.browsertest.ts +0 -59
- package/src/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.ts +0 -27
- package/src/elements/EFMedia/audioTasks/makeAudioInputTask.browsertest.ts +0 -55
- package/src/elements/EFMedia/audioTasks/makeAudioInputTask.ts +0 -53
- package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.chunkboundary.regression.browsertest.ts +0 -207
- package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.ts +0 -72
- package/src/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.ts +0 -32
- package/src/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.ts +0 -29
- package/src/elements/EFMedia/audioTasks/makeAudioTasksVideoOnly.browsertest.ts +0 -95
- package/src/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.ts +0 -184
- package/src/elements/EFMedia/shared/AudioSpanUtils.ts +0 -129
- package/src/elements/EFMedia/shared/BufferUtils.ts +0 -342
- package/src/elements/EFMedia/shared/GlobalInputCache.ts +0 -77
- package/src/elements/EFMedia/shared/MediaTaskUtils.ts +0 -44
- package/src/elements/EFMedia/shared/PrecisionUtils.ts +0 -46
- package/src/elements/EFMedia/shared/RenditionHelpers.browsertest.ts +0 -246
- package/src/elements/EFMedia/shared/RenditionHelpers.ts +0 -56
- package/src/elements/EFMedia/shared/ThumbnailExtractor.ts +0 -227
- package/src/elements/EFMedia/tasks/makeMediaEngineTask.browsertest.ts +0 -167
- package/src/elements/EFMedia/tasks/makeMediaEngineTask.ts +0 -88
- package/src/elements/EFMedia/videoTasks/MainVideoInputCache.ts +0 -76
- package/src/elements/EFMedia/videoTasks/ScrubInputCache.ts +0 -61
- package/src/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.ts +0 -114
- package/src/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.ts +0 -35
- package/src/elements/EFMedia/videoTasks/makeScrubVideoInputTask.ts +0 -52
- package/src/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.ts +0 -124
- package/src/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.ts +0 -44
- package/src/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.ts +0 -32
- package/src/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.ts +0 -370
- package/src/elements/EFMedia/videoTasks/makeVideoBufferTask.ts +0 -109
- package/src/elements/EFMedia.browsertest.ts +0 -872
- package/src/elements/EFMedia.ts +0 -341
- package/src/elements/EFSourceMixin.ts +0 -60
- package/src/elements/EFSurface.browsertest.ts +0 -151
- package/src/elements/EFSurface.ts +0 -142
- package/src/elements/EFTemporal.browsertest.ts +0 -215
- package/src/elements/EFTemporal.ts +0 -800
- package/src/elements/EFThumbnailStrip.browsertest.ts +0 -585
- package/src/elements/EFThumbnailStrip.media-engine.browsertest.ts +0 -714
- package/src/elements/EFThumbnailStrip.ts +0 -906
- package/src/elements/EFTimegroup.browsertest.ts +0 -870
- package/src/elements/EFTimegroup.ts +0 -878
- package/src/elements/EFVideo.browsertest.ts +0 -1482
- package/src/elements/EFVideo.ts +0 -564
- package/src/elements/EFWaveform.ts +0 -547
- package/src/elements/FetchContext.browsertest.ts +0 -401
- package/src/elements/FetchMixin.ts +0 -38
- package/src/elements/SampleBuffer.ts +0 -94
- package/src/elements/TargetController.browsertest.ts +0 -230
- package/src/elements/TargetController.ts +0 -224
- package/src/elements/TimegroupController.ts +0 -26
- package/src/elements/durationConverter.ts +0 -35
- package/src/elements/parseTimeToMs.ts +0 -9
- package/src/elements/printTaskStatus.ts +0 -16
- package/src/elements/renderTemporalAudio.ts +0 -108
- package/src/elements/updateAnimations.browsertest.ts +0 -1884
- package/src/elements/updateAnimations.ts +0 -217
- package/src/elements/util.ts +0 -24
- package/src/gui/ContextMixin.browsertest.ts +0 -860
- package/src/gui/ContextMixin.ts +0 -562
- package/src/gui/Controllable.browsertest.ts +0 -258
- package/src/gui/Controllable.ts +0 -41
- package/src/gui/EFConfiguration.ts +0 -40
- package/src/gui/EFControls.browsertest.ts +0 -389
- package/src/gui/EFControls.ts +0 -195
- package/src/gui/EFDial.browsertest.ts +0 -84
- package/src/gui/EFDial.ts +0 -172
- package/src/gui/EFFilmstrip.browsertest.ts +0 -712
- package/src/gui/EFFilmstrip.ts +0 -1349
- package/src/gui/EFFitScale.ts +0 -152
- package/src/gui/EFFocusOverlay.ts +0 -79
- package/src/gui/EFPause.browsertest.ts +0 -202
- package/src/gui/EFPause.ts +0 -73
- package/src/gui/EFPlay.browsertest.ts +0 -202
- package/src/gui/EFPlay.ts +0 -73
- package/src/gui/EFPreview.ts +0 -74
- package/src/gui/EFResizableBox.browsertest.ts +0 -79
- package/src/gui/EFResizableBox.ts +0 -898
- package/src/gui/EFScrubber.ts +0 -151
- package/src/gui/EFTimeDisplay.browsertest.ts +0 -237
- package/src/gui/EFTimeDisplay.ts +0 -55
- package/src/gui/EFToggleLoop.ts +0 -35
- package/src/gui/EFTogglePlay.ts +0 -70
- package/src/gui/EFWorkbench.ts +0 -115
- package/src/gui/PlaybackController.ts +0 -527
- package/src/gui/TWMixin.css +0 -6
- package/src/gui/TWMixin.ts +0 -61
- package/src/gui/TargetOrContextMixin.ts +0 -185
- package/src/gui/currentTimeContext.ts +0 -5
- package/src/gui/durationContext.ts +0 -3
- package/src/gui/efContext.ts +0 -6
- package/src/gui/fetchContext.ts +0 -5
- package/src/gui/focusContext.ts +0 -7
- package/src/gui/focusedElementContext.ts +0 -5
- package/src/gui/playingContext.ts +0 -5
- package/src/otel/BridgeSpanExporter.ts +0 -150
- package/src/otel/setupBrowserTracing.ts +0 -73
- package/src/otel/tracingHelpers.ts +0 -251
- package/src/transcoding/cache/RequestDeduplicator.test.ts +0 -170
- package/src/transcoding/cache/RequestDeduplicator.ts +0 -65
- package/src/transcoding/cache/URLTokenDeduplicator.test.ts +0 -182
- package/src/transcoding/cache/URLTokenDeduplicator.ts +0 -101
- package/src/transcoding/types/index.ts +0 -312
- package/src/transcoding/utils/MediaUtils.ts +0 -63
- package/src/transcoding/utils/UrlGenerator.ts +0 -68
- package/src/transcoding/utils/constants.ts +0 -36
- package/src/utils/LRUCache.test.ts +0 -274
- package/src/utils/LRUCache.ts +0 -696
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
AudioSpan,
|
|
3
|
-
MediaEngine,
|
|
4
|
-
SegmentTimeRange,
|
|
5
|
-
} from "../../../transcoding/types";
|
|
6
|
-
import type { EFMedia } from "../../EFMedia";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Fetch audio segment data using MediaEngine
|
|
10
|
-
* Pure function with explicit dependencies
|
|
11
|
-
*/
|
|
12
|
-
const fetchAudioSegmentData = async (
|
|
13
|
-
segmentIds: number[],
|
|
14
|
-
mediaEngine: MediaEngine,
|
|
15
|
-
signal: AbortSignal,
|
|
16
|
-
): Promise<Map<number, ArrayBuffer>> => {
|
|
17
|
-
const audioRendition = mediaEngine.audioRendition;
|
|
18
|
-
if (!audioRendition) {
|
|
19
|
-
throw new Error("Audio rendition not available");
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const segmentData = new Map<number, ArrayBuffer>();
|
|
23
|
-
|
|
24
|
-
// Fetch all segments - MediaEngine handles deduplication internally
|
|
25
|
-
const fetchPromises = segmentIds.map(async (segmentId) => {
|
|
26
|
-
const arrayBuffer = await mediaEngine.fetchMediaSegment(
|
|
27
|
-
segmentId,
|
|
28
|
-
audioRendition,
|
|
29
|
-
signal,
|
|
30
|
-
);
|
|
31
|
-
return [segmentId, arrayBuffer] as [number, ArrayBuffer];
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
const fetchedSegments = await Promise.all(fetchPromises);
|
|
35
|
-
signal.throwIfAborted();
|
|
36
|
-
|
|
37
|
-
for (const [segmentId, arrayBuffer] of fetchedSegments) {
|
|
38
|
-
segmentData.set(segmentId, arrayBuffer);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return segmentData;
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Create audio span blob from init segment and media segments
|
|
46
|
-
* Pure function for blob creation
|
|
47
|
-
*/
|
|
48
|
-
const createAudioSpanBlob = (
|
|
49
|
-
initSegment: ArrayBuffer,
|
|
50
|
-
mediaSegments: ArrayBuffer[],
|
|
51
|
-
): Blob => {
|
|
52
|
-
const chunks = [initSegment, ...mediaSegments];
|
|
53
|
-
return new Blob(chunks, { type: "audio/mp4" });
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Fetch audio spanning a time range
|
|
58
|
-
* Main function that orchestrates segment calculation, fetching, and blob creation
|
|
59
|
-
*/
|
|
60
|
-
export const fetchAudioSpanningTime = async (
|
|
61
|
-
host: EFMedia,
|
|
62
|
-
fromMs: number,
|
|
63
|
-
toMs: number,
|
|
64
|
-
signal: AbortSignal,
|
|
65
|
-
): Promise<AudioSpan | undefined> => {
|
|
66
|
-
// Validate inputs
|
|
67
|
-
if (fromMs >= toMs || fromMs < 0) {
|
|
68
|
-
throw new Error(`Invalid time range: fromMs=${fromMs}, toMs=${toMs}`);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Get dependencies from host
|
|
72
|
-
const mediaEngine = await host.mediaEngineTask.taskComplete;
|
|
73
|
-
const initSegment = await host.audioInitSegmentFetchTask.taskComplete;
|
|
74
|
-
|
|
75
|
-
// Return undefined if no audio rendition available
|
|
76
|
-
if (!mediaEngine?.audioRendition) {
|
|
77
|
-
return undefined;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (!initSegment) {
|
|
81
|
-
return undefined;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Calculate segments needed using the media engine's method
|
|
85
|
-
const segmentRanges = mediaEngine.calculateAudioSegmentRange(
|
|
86
|
-
fromMs,
|
|
87
|
-
toMs,
|
|
88
|
-
mediaEngine.audioRendition,
|
|
89
|
-
host.intrinsicDurationMs || 10000,
|
|
90
|
-
);
|
|
91
|
-
|
|
92
|
-
if (segmentRanges.length === 0) {
|
|
93
|
-
throw new Error(`No segments found for time range ${fromMs}-${toMs}ms`);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Fetch segment data
|
|
97
|
-
const segmentIds = segmentRanges.map((r: SegmentTimeRange) => r.segmentId);
|
|
98
|
-
const segmentData = await fetchAudioSegmentData(
|
|
99
|
-
segmentIds,
|
|
100
|
-
mediaEngine,
|
|
101
|
-
signal,
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
// Create ordered array of segments
|
|
105
|
-
const orderedSegments = segmentIds.map((id: number) => {
|
|
106
|
-
const segment = segmentData.get(id);
|
|
107
|
-
if (!segment) {
|
|
108
|
-
throw new Error(`Missing segment data for segment ID ${id}`);
|
|
109
|
-
}
|
|
110
|
-
return segment;
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
// Create blob
|
|
114
|
-
const blob = createAudioSpanBlob(initSegment, orderedSegments);
|
|
115
|
-
|
|
116
|
-
// Calculate actual time boundaries
|
|
117
|
-
const actualStartMs = Math.min(
|
|
118
|
-
...segmentRanges.map((r: SegmentTimeRange) => r.startMs),
|
|
119
|
-
);
|
|
120
|
-
const actualEndMs = Math.max(
|
|
121
|
-
...segmentRanges.map((r: SegmentTimeRange) => r.endMs),
|
|
122
|
-
);
|
|
123
|
-
|
|
124
|
-
return {
|
|
125
|
-
startMs: actualStartMs,
|
|
126
|
-
endMs: actualEndMs,
|
|
127
|
-
blob,
|
|
128
|
-
};
|
|
129
|
-
};
|
|
@@ -1,342 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
AudioRendition,
|
|
3
|
-
VideoRendition,
|
|
4
|
-
} from "../../../transcoding/types";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* State interface for media buffering - orchestration only, no data storage
|
|
8
|
-
*/
|
|
9
|
-
export interface MediaBufferState {
|
|
10
|
-
currentSeekTimeMs: number;
|
|
11
|
-
requestedSegments: Set<number>; // Segments we've requested for buffering
|
|
12
|
-
activeRequests: Set<number>; // Segments currently being fetched
|
|
13
|
-
requestQueue: number[]; // Segments queued to be requested
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Configuration interface for media buffering - generic for both audio and video
|
|
18
|
-
*/
|
|
19
|
-
export interface MediaBufferConfig {
|
|
20
|
-
bufferDurationMs: number;
|
|
21
|
-
maxParallelFetches: number;
|
|
22
|
-
enableBuffering: boolean;
|
|
23
|
-
enableContinuousBuffering?: boolean;
|
|
24
|
-
bufferThresholdMs?: number; // Timeline-aware buffering threshold (default: 30000ms)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Dependencies interface for media buffering - integrates with BaseMediaEngine
|
|
29
|
-
*/
|
|
30
|
-
export interface MediaBufferDependencies<
|
|
31
|
-
T extends AudioRendition | VideoRendition,
|
|
32
|
-
> {
|
|
33
|
-
computeSegmentId: (
|
|
34
|
-
timeMs: number,
|
|
35
|
-
rendition: T,
|
|
36
|
-
) => Promise<number | undefined>;
|
|
37
|
-
prefetchSegment: (segmentId: number, rendition: T) => Promise<void>; // Just trigger prefetch, don't return data
|
|
38
|
-
isSegmentCached: (segmentId: number, rendition: T) => boolean; // Check BaseMediaEngine cache
|
|
39
|
-
getRendition: () => Promise<T | undefined>;
|
|
40
|
-
logError: (message: string, error: any) => void;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Compute segment range for a time window
|
|
45
|
-
* Pure function - determines which segments are needed for a time range
|
|
46
|
-
*/
|
|
47
|
-
export const computeSegmentRange = <T extends AudioRendition | VideoRendition>(
|
|
48
|
-
startTimeMs: number,
|
|
49
|
-
endTimeMs: number,
|
|
50
|
-
rendition: T,
|
|
51
|
-
computeSegmentId: (timeMs: number, rendition: T) => number | undefined,
|
|
52
|
-
): number[] => {
|
|
53
|
-
const segments: number[] = [];
|
|
54
|
-
const segmentDurationMs = (rendition as any).segmentDurationMs || 1000;
|
|
55
|
-
|
|
56
|
-
// Calculate segment indices that overlap with [startTimeMs, endTimeMs]
|
|
57
|
-
const startSegmentIndex = Math.floor(startTimeMs / segmentDurationMs);
|
|
58
|
-
const endSegmentIndex = Math.floor(endTimeMs / segmentDurationMs);
|
|
59
|
-
|
|
60
|
-
for (let i = startSegmentIndex; i <= endSegmentIndex; i++) {
|
|
61
|
-
const segmentId = computeSegmentId(i * segmentDurationMs, rendition);
|
|
62
|
-
if (segmentId !== undefined) {
|
|
63
|
-
segments.push(segmentId);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return segments.filter((id, index, arr) => arr.indexOf(id) === index); // Remove duplicates
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Async version of computeSegmentRange for when computeSegmentId is async
|
|
72
|
-
*/
|
|
73
|
-
export const computeSegmentRangeAsync = async <
|
|
74
|
-
T extends AudioRendition | VideoRendition,
|
|
75
|
-
>(
|
|
76
|
-
startTimeMs: number,
|
|
77
|
-
endTimeMs: number,
|
|
78
|
-
durationMs: number,
|
|
79
|
-
rendition: T,
|
|
80
|
-
computeSegmentId: (
|
|
81
|
-
timeMs: number,
|
|
82
|
-
rendition: T,
|
|
83
|
-
) => Promise<number | undefined>,
|
|
84
|
-
): Promise<number[]> => {
|
|
85
|
-
const segments: number[] = [];
|
|
86
|
-
const segmentDurationMs = (rendition as any).segmentDurationMs || 1000;
|
|
87
|
-
|
|
88
|
-
// Calculate segment indices that overlap with [startTimeMs, endTimeMs]
|
|
89
|
-
const startSegmentIndex = Math.floor(startTimeMs / segmentDurationMs);
|
|
90
|
-
const endSegmentIndex = Math.floor(
|
|
91
|
-
Math.min(endTimeMs, durationMs) / segmentDurationMs,
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
for (let i = startSegmentIndex; i <= endSegmentIndex; i++) {
|
|
95
|
-
const timeMs = i * segmentDurationMs;
|
|
96
|
-
if (timeMs < durationMs) {
|
|
97
|
-
const segmentId = await computeSegmentId(timeMs, rendition);
|
|
98
|
-
if (segmentId !== undefined) {
|
|
99
|
-
segments.push(segmentId);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return segments.filter((id, index, arr) => arr.indexOf(id) === index); // Remove duplicates
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Compute buffer queue based on desired segments and what we've already requested
|
|
109
|
-
* Pure function - determines what new segments should be prefetched
|
|
110
|
-
*/
|
|
111
|
-
export const computeBufferQueue = (
|
|
112
|
-
desiredSegments: number[],
|
|
113
|
-
requestedSegments: Set<number>,
|
|
114
|
-
): number[] => {
|
|
115
|
-
return desiredSegments.filter(
|
|
116
|
-
(segmentId) => !requestedSegments.has(segmentId),
|
|
117
|
-
);
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Handle seek time change and recompute buffer queue
|
|
122
|
-
* Pure function - computes new queue when seek time changes
|
|
123
|
-
*/
|
|
124
|
-
export const handleSeekTimeChange = <T extends AudioRendition | VideoRendition>(
|
|
125
|
-
newSeekTimeMs: number,
|
|
126
|
-
bufferDurationMs: number,
|
|
127
|
-
rendition: T,
|
|
128
|
-
currentState: MediaBufferState,
|
|
129
|
-
computeSegmentId: (timeMs: number, rendition: T) => number | undefined,
|
|
130
|
-
): { newQueue: number[]; overlappingRequests: number[] } => {
|
|
131
|
-
const endTimeMs = newSeekTimeMs + bufferDurationMs;
|
|
132
|
-
const desiredSegments = computeSegmentRange(
|
|
133
|
-
newSeekTimeMs,
|
|
134
|
-
endTimeMs,
|
|
135
|
-
rendition,
|
|
136
|
-
computeSegmentId,
|
|
137
|
-
);
|
|
138
|
-
|
|
139
|
-
// Find segments that are already being requested
|
|
140
|
-
const overlappingRequests = desiredSegments.filter((segmentId) =>
|
|
141
|
-
currentState.requestedSegments.has(segmentId),
|
|
142
|
-
);
|
|
143
|
-
|
|
144
|
-
const newQueue = computeBufferQueue(
|
|
145
|
-
desiredSegments,
|
|
146
|
-
currentState.requestedSegments,
|
|
147
|
-
);
|
|
148
|
-
|
|
149
|
-
return { newQueue, overlappingRequests };
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Check if a segment has been requested for buffering
|
|
154
|
-
* Pure function for checking buffer orchestration state
|
|
155
|
-
*/
|
|
156
|
-
export const isSegmentRequested = (
|
|
157
|
-
segmentId: number,
|
|
158
|
-
bufferState: MediaBufferState | undefined,
|
|
159
|
-
): boolean => {
|
|
160
|
-
return bufferState?.requestedSegments.has(segmentId) ?? false;
|
|
161
|
-
};
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Get requested segments from a list of segment IDs
|
|
165
|
-
* Pure function that returns which segments have been requested for buffering
|
|
166
|
-
*/
|
|
167
|
-
export const getRequestedSegments = (
|
|
168
|
-
segmentIds: number[],
|
|
169
|
-
bufferState: MediaBufferState | undefined,
|
|
170
|
-
): Set<number> => {
|
|
171
|
-
if (!bufferState) {
|
|
172
|
-
return new Set();
|
|
173
|
-
}
|
|
174
|
-
return new Set(
|
|
175
|
-
segmentIds.filter((id) => bufferState.requestedSegments.has(id)),
|
|
176
|
-
);
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Get unrequested segments from a list of segment IDs
|
|
181
|
-
* Pure function that returns which segments haven't been requested yet
|
|
182
|
-
*/
|
|
183
|
-
export const getUnrequestedSegments = (
|
|
184
|
-
segmentIds: number[],
|
|
185
|
-
bufferState: MediaBufferState | undefined,
|
|
186
|
-
): number[] => {
|
|
187
|
-
if (!bufferState) {
|
|
188
|
-
return segmentIds;
|
|
189
|
-
}
|
|
190
|
-
return segmentIds.filter((id) => !bufferState.requestedSegments.has(id));
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Calculate distance from element to playhead position
|
|
195
|
-
* Returns 0 if element is currently active, otherwise returns distance in milliseconds
|
|
196
|
-
*/
|
|
197
|
-
export const calculatePlayheadDistance = (
|
|
198
|
-
element: { startTimeMs: number; endTimeMs: number },
|
|
199
|
-
playheadMs: number,
|
|
200
|
-
): number => {
|
|
201
|
-
// Element hasn't started yet
|
|
202
|
-
if (playheadMs < element.startTimeMs) {
|
|
203
|
-
return element.startTimeMs - playheadMs;
|
|
204
|
-
}
|
|
205
|
-
// Element already finished
|
|
206
|
-
if (playheadMs > element.endTimeMs) {
|
|
207
|
-
return playheadMs - element.endTimeMs;
|
|
208
|
-
}
|
|
209
|
-
// Element is currently active
|
|
210
|
-
return 0;
|
|
211
|
-
};
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Core media buffering orchestration logic - prefetch only, no data storage
|
|
215
|
-
* Integrates with BaseMediaEngine's existing caching and request deduplication
|
|
216
|
-
*/
|
|
217
|
-
export const manageMediaBuffer = async <
|
|
218
|
-
T extends AudioRendition | VideoRendition,
|
|
219
|
-
>(
|
|
220
|
-
seekTimeMs: number,
|
|
221
|
-
config: MediaBufferConfig,
|
|
222
|
-
currentState: MediaBufferState,
|
|
223
|
-
durationMs: number,
|
|
224
|
-
signal: AbortSignal,
|
|
225
|
-
deps: MediaBufferDependencies<T>,
|
|
226
|
-
timelineContext?: {
|
|
227
|
-
elementStartMs: number;
|
|
228
|
-
elementEndMs: number;
|
|
229
|
-
playheadMs: number;
|
|
230
|
-
},
|
|
231
|
-
): Promise<MediaBufferState> => {
|
|
232
|
-
if (!config.enableBuffering) {
|
|
233
|
-
return currentState;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// Timeline-aware buffering: skip if element is too far from playhead
|
|
237
|
-
if (timelineContext && config.bufferThresholdMs !== undefined) {
|
|
238
|
-
const distance = calculatePlayheadDistance(
|
|
239
|
-
{
|
|
240
|
-
startTimeMs: timelineContext.elementStartMs,
|
|
241
|
-
endTimeMs: timelineContext.elementEndMs,
|
|
242
|
-
},
|
|
243
|
-
timelineContext.playheadMs,
|
|
244
|
-
);
|
|
245
|
-
|
|
246
|
-
if (distance > config.bufferThresholdMs) {
|
|
247
|
-
// Element is too far from playhead, skip buffering
|
|
248
|
-
return currentState;
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
const rendition = await deps.getRendition();
|
|
253
|
-
if (!rendition) {
|
|
254
|
-
// Cannot buffer without a rendition
|
|
255
|
-
return currentState;
|
|
256
|
-
}
|
|
257
|
-
const endTimeMs = seekTimeMs + config.bufferDurationMs;
|
|
258
|
-
|
|
259
|
-
const desiredSegments = await computeSegmentRangeAsync(
|
|
260
|
-
seekTimeMs,
|
|
261
|
-
endTimeMs,
|
|
262
|
-
durationMs,
|
|
263
|
-
rendition,
|
|
264
|
-
deps.computeSegmentId,
|
|
265
|
-
);
|
|
266
|
-
// Filter out segments already cached by BaseMediaEngine
|
|
267
|
-
const uncachedSegments = desiredSegments.filter(
|
|
268
|
-
(segmentId) => !deps.isSegmentCached(segmentId, rendition),
|
|
269
|
-
);
|
|
270
|
-
|
|
271
|
-
const newQueue = computeBufferQueue(
|
|
272
|
-
uncachedSegments,
|
|
273
|
-
currentState.requestedSegments,
|
|
274
|
-
);
|
|
275
|
-
|
|
276
|
-
// Shared state for concurrency control - prevents race conditions
|
|
277
|
-
const newRequestedSegments = new Set(currentState.requestedSegments);
|
|
278
|
-
const newActiveRequests = new Set(currentState.activeRequests);
|
|
279
|
-
const remainingQueue = [...newQueue];
|
|
280
|
-
|
|
281
|
-
// Thread-safe function to start next segment when slot becomes available
|
|
282
|
-
const startNextSegment = (): void => {
|
|
283
|
-
// Check if we have capacity and segments to fetch
|
|
284
|
-
if (
|
|
285
|
-
newActiveRequests.size >= config.maxParallelFetches ||
|
|
286
|
-
remainingQueue.length === 0 ||
|
|
287
|
-
signal.aborted
|
|
288
|
-
) {
|
|
289
|
-
return;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
const nextSegmentId = remainingQueue.shift();
|
|
293
|
-
if (nextSegmentId === undefined) return;
|
|
294
|
-
|
|
295
|
-
// Skip if already requested or now cached
|
|
296
|
-
if (
|
|
297
|
-
newRequestedSegments.has(nextSegmentId) ||
|
|
298
|
-
deps.isSegmentCached(nextSegmentId, rendition)
|
|
299
|
-
) {
|
|
300
|
-
startNextSegment(); // Try next segment immediately
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
newRequestedSegments.add(nextSegmentId);
|
|
305
|
-
newActiveRequests.add(nextSegmentId);
|
|
306
|
-
|
|
307
|
-
// Start the prefetch request
|
|
308
|
-
deps
|
|
309
|
-
.prefetchSegment(nextSegmentId, rendition)
|
|
310
|
-
.then(() => {
|
|
311
|
-
if (signal.aborted) return;
|
|
312
|
-
newActiveRequests.delete(nextSegmentId);
|
|
313
|
-
// Start next segment if continuous buffering is enabled
|
|
314
|
-
if (config.enableContinuousBuffering ?? true) {
|
|
315
|
-
startNextSegment();
|
|
316
|
-
}
|
|
317
|
-
})
|
|
318
|
-
.catch((error) => {
|
|
319
|
-
if (signal.aborted) return;
|
|
320
|
-
newActiveRequests.delete(nextSegmentId);
|
|
321
|
-
deps.logError(`Failed to prefetch segment ${nextSegmentId}`, error);
|
|
322
|
-
// Continue even after error if continuous buffering is enabled
|
|
323
|
-
if (config.enableContinuousBuffering ?? true) {
|
|
324
|
-
startNextSegment();
|
|
325
|
-
}
|
|
326
|
-
});
|
|
327
|
-
};
|
|
328
|
-
|
|
329
|
-
// Start initial batch of requests up to maxParallelFetches limit
|
|
330
|
-
const initialBatchSize = Math.min(config.maxParallelFetches, newQueue.length);
|
|
331
|
-
for (let i = 0; i < initialBatchSize; i++) {
|
|
332
|
-
startNextSegment();
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
const result = {
|
|
336
|
-
currentSeekTimeMs: seekTimeMs,
|
|
337
|
-
requestedSegments: newRequestedSegments,
|
|
338
|
-
activeRequests: newActiveRequests,
|
|
339
|
-
requestQueue: remainingQueue, // What's left in the queue
|
|
340
|
-
};
|
|
341
|
-
return result;
|
|
342
|
-
};
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import type { Input } from "mediabunny";
|
|
2
|
-
import { LRUCache } from "../../../utils/LRUCache.js";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Global cache for MediaBunny Input instances
|
|
6
|
-
* Shared across all MediaEngine instances to prevent duplicate decoding
|
|
7
|
-
* of the same segment data
|
|
8
|
-
*/
|
|
9
|
-
class GlobalInputCache {
|
|
10
|
-
private cache = new LRUCache<string, Input>(50); // 50 Input instances max
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Generate standardized cache key for Input objects
|
|
14
|
-
* Format: "input:{src}:{segmentId}:{renditionId}"
|
|
15
|
-
*/
|
|
16
|
-
private generateKey(
|
|
17
|
-
src: string,
|
|
18
|
-
segmentId: number,
|
|
19
|
-
renditionId?: string,
|
|
20
|
-
): string {
|
|
21
|
-
return `input:${src}:${segmentId}:${renditionId || "default"}`;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Get cached Input object
|
|
26
|
-
*/
|
|
27
|
-
get(src: string, segmentId: number, renditionId?: string): Input | undefined {
|
|
28
|
-
const key = this.generateKey(src, segmentId, renditionId);
|
|
29
|
-
return this.cache.get(key);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Cache Input object
|
|
34
|
-
*/
|
|
35
|
-
set(
|
|
36
|
-
src: string,
|
|
37
|
-
segmentId: number,
|
|
38
|
-
input: Input,
|
|
39
|
-
renditionId?: string,
|
|
40
|
-
): void {
|
|
41
|
-
const key = this.generateKey(src, segmentId, renditionId);
|
|
42
|
-
this.cache.set(key, input);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Check if Input is cached
|
|
47
|
-
*/
|
|
48
|
-
has(src: string, segmentId: number, renditionId?: string): boolean {
|
|
49
|
-
const key = this.generateKey(src, segmentId, renditionId);
|
|
50
|
-
return this.cache.has(key);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Clear all cached Input objects
|
|
55
|
-
*/
|
|
56
|
-
clear(): void {
|
|
57
|
-
this.cache.clear();
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Get cache statistics for debugging
|
|
62
|
-
*/
|
|
63
|
-
getStats() {
|
|
64
|
-
return {
|
|
65
|
-
size: this.cache.size,
|
|
66
|
-
cachedKeys: Array.from((this.cache as any).cache.keys()),
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Single global instance shared across all MediaEngine instances
|
|
72
|
-
export const globalInputCache = new GlobalInputCache();
|
|
73
|
-
|
|
74
|
-
// Export for debugging (works in both browser and server)
|
|
75
|
-
(
|
|
76
|
-
globalThis as typeof globalThis & { debugInputCache: typeof globalInputCache }
|
|
77
|
-
).debugInputCache = globalInputCache;
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import type { Task } from "@lit/task";
|
|
2
|
-
import type {
|
|
3
|
-
AudioRendition,
|
|
4
|
-
MediaEngine,
|
|
5
|
-
VideoRendition,
|
|
6
|
-
} from "../../../transcoding/types";
|
|
7
|
-
import type { BufferedSeekingInput } from "../BufferedSeekingInput";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Generic rendition type that can be either audio or video
|
|
11
|
-
*/
|
|
12
|
-
export type MediaRendition = AudioRendition | VideoRendition;
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Generic task type for init segment fetch
|
|
16
|
-
*/
|
|
17
|
-
export type InitSegmentFetchTask = Task<
|
|
18
|
-
readonly [MediaEngine | undefined],
|
|
19
|
-
ArrayBuffer
|
|
20
|
-
>;
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Generic task type for segment ID calculation
|
|
24
|
-
*/
|
|
25
|
-
export type SegmentIdTask = Task<
|
|
26
|
-
readonly [MediaEngine | undefined, number],
|
|
27
|
-
number | undefined
|
|
28
|
-
>;
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Generic task type for segment fetch
|
|
32
|
-
*/
|
|
33
|
-
export type SegmentFetchTask = Task<
|
|
34
|
-
readonly [MediaEngine | undefined, number | undefined],
|
|
35
|
-
ArrayBuffer
|
|
36
|
-
>;
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Generic task type for input creation
|
|
40
|
-
*/
|
|
41
|
-
export type InputTask = Task<
|
|
42
|
-
readonly [ArrayBuffer, ArrayBuffer],
|
|
43
|
-
BufferedSeekingInput | undefined
|
|
44
|
-
>;
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Centralized precision utilities for consistent timing calculations across the media pipeline.
|
|
3
|
-
*
|
|
4
|
-
* The key insight is that floating-point precision errors can cause inconsistencies between:
|
|
5
|
-
* 1. Segment selection logic (in AssetMediaEngine.computeSegmentId)
|
|
6
|
-
* 2. Sample finding logic (in SampleBuffer.find)
|
|
7
|
-
* 3. Timeline mapping (in BufferedSeekingInput.seek)
|
|
8
|
-
*
|
|
9
|
-
* All timing calculations must use the same rounding strategy to ensure consistency.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Round time to millisecond precision to handle floating-point precision issues.
|
|
14
|
-
* Uses Math.round for consistent behavior across the entire pipeline.
|
|
15
|
-
*
|
|
16
|
-
* This function should be used for ALL time-related calculations that need to be
|
|
17
|
-
* compared between different parts of the system.
|
|
18
|
-
*/
|
|
19
|
-
export const roundToMilliseconds = (timeMs: number): number => {
|
|
20
|
-
// Round to 3 decimal places (microsecond precision)
|
|
21
|
-
return Math.round(timeMs * 1000) / 1000;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Convert media time (in seconds) to scaled time units using consistent rounding.
|
|
26
|
-
* This is used in segment selection to convert from milliseconds to timescale units.
|
|
27
|
-
*/
|
|
28
|
-
export const convertToScaledTime = (
|
|
29
|
-
timeMs: number,
|
|
30
|
-
timescale: number,
|
|
31
|
-
): number => {
|
|
32
|
-
const scaledTime = (timeMs / 1000) * timescale;
|
|
33
|
-
return Math.round(scaledTime);
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Convert scaled time units back to media time (in milliseconds) using consistent rounding.
|
|
38
|
-
* This is the inverse of convertToScaledTime.
|
|
39
|
-
*/
|
|
40
|
-
export const convertFromScaledTime = (
|
|
41
|
-
scaledTime: number,
|
|
42
|
-
timescale: number,
|
|
43
|
-
): number => {
|
|
44
|
-
const timeMs = (scaledTime / timescale) * 1000;
|
|
45
|
-
return roundToMilliseconds(timeMs);
|
|
46
|
-
};
|