@editframe/elements 0.18.23-beta.0 → 0.18.27-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/EFMedia/AssetMediaEngine.d.ts +2 -1
- package/dist/elements/EFMedia/AssetMediaEngine.js +3 -0
- package/dist/elements/EFMedia/BaseMediaEngine.d.ts +9 -0
- package/dist/elements/EFMedia/BaseMediaEngine.js +27 -0
- package/dist/elements/EFMedia/JitMediaEngine.d.ts +1 -0
- package/dist/elements/EFMedia/JitMediaEngine.js +12 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js +11 -5
- package/dist/elements/EFMedia/shared/BufferUtils.d.ts +19 -18
- package/dist/elements/EFMedia/shared/BufferUtils.js +24 -44
- package/dist/elements/EFMedia/tasks/makeMediaEngineTask.browsertest.d.ts +8 -0
- package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js +5 -5
- package/dist/elements/EFMedia/videoTasks/ScrubInputCache.d.ts +25 -0
- package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js +42 -0
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.d.ts +8 -0
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.js +70 -0
- package/dist/elements/EFMedia/videoTasks/{makeVideoInitSegmentFetchTask.d.ts → makeScrubVideoInitSegmentFetchTask.d.ts} +1 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.js +21 -0
- package/dist/elements/EFMedia/videoTasks/{makeVideoInputTask.d.ts → makeScrubVideoInputTask.d.ts} +1 -1
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoInputTask.js +27 -0
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.d.ts +6 -0
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.js +52 -0
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.d.ts +4 -0
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.js +23 -0
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.d.ts +4 -0
- package/dist/elements/EFMedia/videoTasks/{makeVideoSegmentIdTask.js → makeScrubVideoSegmentIdTask.js} +9 -4
- package/dist/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.d.ts +6 -0
- package/dist/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.js +112 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js +11 -5
- package/dist/elements/EFMedia.d.ts +0 -10
- package/dist/elements/EFMedia.js +1 -17
- package/dist/elements/EFVideo.d.ts +11 -9
- package/dist/elements/EFVideo.js +31 -23
- package/dist/gui/EFConfiguration.d.ts +1 -0
- package/dist/gui/EFConfiguration.js +5 -0
- package/dist/gui/EFFilmstrip.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/transcoding/types/index.d.ts +11 -0
- package/package.json +2 -2
- package/src/elements/EFCaptions.ts +1 -1
- package/src/elements/EFImage.ts +1 -1
- package/src/elements/EFMedia/AssetMediaEngine.ts +6 -0
- package/src/elements/EFMedia/BaseMediaEngine.ts +54 -0
- package/src/elements/EFMedia/JitMediaEngine.ts +18 -0
- package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.browsertest.ts +185 -59
- package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.ts +19 -6
- package/src/elements/EFMedia/shared/BufferUtils.ts +71 -85
- package/src/elements/EFMedia/tasks/makeMediaEngineTask.browsertest.ts +151 -112
- package/src/elements/EFMedia/tasks/makeMediaEngineTask.ts +12 -5
- package/src/elements/EFMedia/videoTasks/ScrubInputCache.ts +61 -0
- package/src/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.ts +113 -0
- package/src/elements/EFMedia/videoTasks/{makeVideoInitSegmentFetchTask.ts → makeScrubVideoInitSegmentFetchTask.ts} +15 -3
- package/src/elements/EFMedia/videoTasks/{makeVideoInputTask.ts → makeScrubVideoInputTask.ts} +11 -10
- package/src/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.ts +118 -0
- package/src/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.ts +44 -0
- package/src/elements/EFMedia/videoTasks/{makeVideoSegmentIdTask.ts → makeScrubVideoSegmentIdTask.ts} +14 -6
- package/src/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.ts +258 -0
- package/src/elements/EFMedia/videoTasks/makeVideoBufferTask.ts +19 -5
- package/src/elements/EFMedia.browsertest.ts +74 -11
- package/src/elements/EFMedia.ts +1 -23
- package/src/elements/EFVideo.browsertest.ts +204 -80
- package/src/elements/EFVideo.ts +38 -26
- package/src/elements/TargetController.browsertest.ts +1 -1
- package/src/gui/EFConfiguration.ts +4 -1
- package/src/gui/EFFilmstrip.ts +4 -4
- package/src/gui/EFFocusOverlay.ts +1 -1
- package/src/gui/EFPreview.ts +3 -4
- package/src/gui/EFScrubber.ts +1 -1
- package/src/gui/EFTimeDisplay.ts +1 -1
- package/src/gui/EFToggleLoop.ts +1 -1
- package/src/gui/EFTogglePlay.ts +1 -1
- package/src/gui/EFWorkbench.ts +1 -1
- package/src/transcoding/types/index.ts +16 -0
- package/test/__cache__/GET__api_v1_transcode_scrub_1_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__6ff5127ebeda578a679474347fbd6137/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_scrub_1_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__6ff5127ebeda578a679474347fbd6137/metadata.json +16 -0
- package/test/__cache__/GET__api_v1_transcode_scrub_init_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__f6d4793fc9ff854ee9a738917fb64a53/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_scrub_init_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__f6d4793fc9ff854ee9a738917fb64a53/metadata.json +16 -0
- package/test/cache-integration-verification.browsertest.ts +84 -0
- package/types.json +1 -1
- package/dist/elements/EFMedia/tasks/makeMediaEngineTask.test.d.ts +0 -1
- package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.browsertest.d.ts +0 -9
- package/dist/elements/EFMedia/videoTasks/makeVideoInitSegmentFetchTask.browsertest.d.ts +0 -9
- package/dist/elements/EFMedia/videoTasks/makeVideoInitSegmentFetchTask.js +0 -16
- package/dist/elements/EFMedia/videoTasks/makeVideoInputTask.browsertest.d.ts +0 -9
- package/dist/elements/EFMedia/videoTasks/makeVideoInputTask.js +0 -27
- package/dist/elements/EFMedia/videoTasks/makeVideoSeekTask.d.ts +0 -7
- package/dist/elements/EFMedia/videoTasks/makeVideoSeekTask.js +0 -34
- package/dist/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.browsertest.d.ts +0 -9
- package/dist/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.d.ts +0 -4
- package/dist/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.js +0 -28
- package/dist/elements/EFMedia/videoTasks/makeVideoSegmentIdTask.browsertest.d.ts +0 -9
- package/dist/elements/EFMedia/videoTasks/makeVideoSegmentIdTask.d.ts +0 -4
- package/src/elements/EFMedia/tasks/makeMediaEngineTask.test.ts +0 -233
- package/src/elements/EFMedia/videoTasks/makeVideoBufferTask.browsertest.ts +0 -555
- package/src/elements/EFMedia/videoTasks/makeVideoInitSegmentFetchTask.browsertest.ts +0 -59
- package/src/elements/EFMedia/videoTasks/makeVideoInputTask.browsertest.ts +0 -55
- package/src/elements/EFMedia/videoTasks/makeVideoSeekTask.ts +0 -65
- package/src/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.browsertest.ts +0 -57
- package/src/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.ts +0 -43
- package/src/elements/EFMedia/videoTasks/makeVideoSegmentIdTask.browsertest.ts +0 -56
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { TrackFragmentIndex } from '../../../../assets/src/index.ts';
|
|
2
|
-
import { AudioRendition, InitSegmentPaths, MediaEngine, SegmentTimeRange } from '../../transcoding/types';
|
|
2
|
+
import { AudioRendition, InitSegmentPaths, MediaEngine, SegmentTimeRange, VideoRendition } from '../../transcoding/types';
|
|
3
3
|
import { UrlGenerator } from '../../transcoding/utils/UrlGenerator';
|
|
4
4
|
import { EFMedia } from '../EFMedia';
|
|
5
5
|
import { BaseMediaEngine } from './BaseMediaEngine';
|
|
@@ -41,4 +41,5 @@ export declare class AssetMediaEngine extends BaseMediaEngine implements MediaEn
|
|
|
41
41
|
*/
|
|
42
42
|
calculateAudioSegmentRange(fromMs: number, toMs: number, rendition: AudioRendition, _durationMs: number): SegmentTimeRange[];
|
|
43
43
|
computeSegmentId(seekTimeMs: number, rendition: MediaRendition): number;
|
|
44
|
+
getScrubVideoRendition(): VideoRendition | undefined;
|
|
44
45
|
}
|
|
@@ -71,4 +71,13 @@ export declare abstract class BaseMediaEngine {
|
|
|
71
71
|
* Each media engine implements this based on their segment structure
|
|
72
72
|
*/
|
|
73
73
|
calculateAudioSegmentRange(fromMs: number, toMs: number, rendition: AudioRendition, durationMs: number): SegmentTimeRange[];
|
|
74
|
+
/**
|
|
75
|
+
* Check if a segment is cached for a given rendition
|
|
76
|
+
* This needs to check the URL-based cache since that's where segments are actually stored
|
|
77
|
+
*/
|
|
78
|
+
isSegmentCached(segmentId: number, rendition: AudioRendition | VideoRendition): boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Get cached segment IDs from a list for a given rendition
|
|
81
|
+
*/
|
|
82
|
+
getCachedSegments(segmentIds: number[], rendition: AudioRendition | VideoRendition): Set<number>;
|
|
74
83
|
}
|
|
@@ -163,5 +163,32 @@ var BaseMediaEngine = class {
|
|
|
163
163
|
}
|
|
164
164
|
return segments;
|
|
165
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* Check if a segment is cached for a given rendition
|
|
168
|
+
* This needs to check the URL-based cache since that's where segments are actually stored
|
|
169
|
+
*/
|
|
170
|
+
isSegmentCached(segmentId, rendition) {
|
|
171
|
+
try {
|
|
172
|
+
const maybeJitEngine = this;
|
|
173
|
+
if (maybeJitEngine.urlGenerator && typeof maybeJitEngine.urlGenerator.generateSegmentUrl === "function") {
|
|
174
|
+
if (!rendition.id) return false;
|
|
175
|
+
const segmentUrl = maybeJitEngine.urlGenerator.generateSegmentUrl(segmentId, rendition.id, maybeJitEngine);
|
|
176
|
+
const urlIsCached = mediaCache.has(segmentUrl);
|
|
177
|
+
return urlIsCached;
|
|
178
|
+
}
|
|
179
|
+
const cacheKey = `${rendition.src}-${rendition.id || "default"}-${segmentId}-${rendition.trackId}`;
|
|
180
|
+
const isCached = mediaCache.has(cacheKey);
|
|
181
|
+
return isCached;
|
|
182
|
+
} catch (error) {
|
|
183
|
+
console.warn(`🎬 BaseMediaEngine: Error checking if segment ${segmentId} is cached:`, error);
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Get cached segment IDs from a list for a given rendition
|
|
189
|
+
*/
|
|
190
|
+
getCachedSegments(segmentIds, rendition) {
|
|
191
|
+
return new Set(segmentIds.filter((id) => this.isSegmentCached(id, rendition)));
|
|
192
|
+
}
|
|
166
193
|
};
|
|
167
194
|
export { BaseMediaEngine };
|
|
@@ -26,4 +26,5 @@ export declare class JitMediaEngine extends BaseMediaEngine implements MediaEngi
|
|
|
26
26
|
src: string;
|
|
27
27
|
}): Promise<ArrayBuffer>;
|
|
28
28
|
computeSegmentId(desiredSeekTimeMs: number, rendition: VideoRendition | AudioRendition): number | undefined;
|
|
29
|
+
getScrubVideoRendition(): VideoRendition | undefined;
|
|
29
30
|
}
|
|
@@ -77,5 +77,17 @@ var JitMediaEngine = class JitMediaEngine extends BaseMediaEngine {
|
|
|
77
77
|
if (segmentStartMs >= this.durationMs) return void 0;
|
|
78
78
|
return segmentIndex + 1;
|
|
79
79
|
}
|
|
80
|
+
getScrubVideoRendition() {
|
|
81
|
+
if (!this.data.videoRenditions) return void 0;
|
|
82
|
+
const scrubManifestRendition = this.data.videoRenditions.find((r) => r.id === "scrub");
|
|
83
|
+
if (!scrubManifestRendition) return this.videoRendition;
|
|
84
|
+
return {
|
|
85
|
+
id: scrubManifestRendition.id,
|
|
86
|
+
trackId: void 0,
|
|
87
|
+
src: this.src,
|
|
88
|
+
segmentDurationMs: scrubManifestRendition.segmentDurationMs,
|
|
89
|
+
segmentDurationsMs: scrubManifestRendition.segmentDurationsMs
|
|
90
|
+
};
|
|
91
|
+
}
|
|
80
92
|
};
|
|
81
93
|
export { JitMediaEngine };
|
|
@@ -6,12 +6,12 @@ import { Task } from "@lit/task";
|
|
|
6
6
|
const makeAudioBufferTask = (host) => {
|
|
7
7
|
let currentState = {
|
|
8
8
|
currentSeekTimeMs: 0,
|
|
9
|
+
requestedSegments: /* @__PURE__ */ new Set(),
|
|
9
10
|
activeRequests: /* @__PURE__ */ new Set(),
|
|
10
|
-
cachedSegments: /* @__PURE__ */ new Set(),
|
|
11
11
|
requestQueue: []
|
|
12
12
|
};
|
|
13
13
|
return new Task(host, {
|
|
14
|
-
autoRun: EF_INTERACTIVE,
|
|
14
|
+
autoRun: EF_INTERACTIVE && !EF_RENDERING(),
|
|
15
15
|
args: () => [host.desiredSeekTimeMs],
|
|
16
16
|
onError: (error) => {
|
|
17
17
|
console.error("audioBufferTask error", error);
|
|
@@ -20,19 +20,25 @@ const makeAudioBufferTask = (host) => {
|
|
|
20
20
|
currentState = value;
|
|
21
21
|
},
|
|
22
22
|
task: async ([seekTimeMs], { signal }) => {
|
|
23
|
+
if (EF_RENDERING()) return currentState;
|
|
23
24
|
const currentConfig = {
|
|
24
25
|
bufferDurationMs: host.audioBufferDurationMs,
|
|
25
26
|
maxParallelFetches: host.maxAudioBufferFetches,
|
|
26
|
-
enableBuffering: host.enableAudioBuffering
|
|
27
|
+
enableBuffering: host.enableAudioBuffering
|
|
27
28
|
};
|
|
28
29
|
return manageMediaBuffer(seekTimeMs, currentConfig, currentState, host.intrinsicDurationMs || 1e4, signal, {
|
|
29
30
|
computeSegmentId: async (timeMs, rendition) => {
|
|
30
31
|
const mediaEngine = await getLatestMediaEngine(host, signal);
|
|
31
32
|
return mediaEngine.computeSegmentId(timeMs, rendition);
|
|
32
33
|
},
|
|
33
|
-
|
|
34
|
+
prefetchSegment: async (segmentId, rendition) => {
|
|
34
35
|
const mediaEngine = await getLatestMediaEngine(host, signal);
|
|
35
|
-
|
|
36
|
+
await mediaEngine.fetchMediaSegment(segmentId, rendition);
|
|
37
|
+
},
|
|
38
|
+
isSegmentCached: (segmentId, rendition) => {
|
|
39
|
+
const mediaEngine = host.mediaEngineTask.value;
|
|
40
|
+
if (!mediaEngine) return false;
|
|
41
|
+
return mediaEngine.isSegmentCached(segmentId, rendition);
|
|
36
42
|
},
|
|
37
43
|
getRendition: async () => {
|
|
38
44
|
const mediaEngine = await getLatestMediaEngine(host, signal);
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { AudioRendition, VideoRendition } from '../../../transcoding/types';
|
|
2
2
|
/**
|
|
3
|
-
* State interface for media buffering -
|
|
3
|
+
* State interface for media buffering - orchestration only, no data storage
|
|
4
4
|
*/
|
|
5
5
|
export interface MediaBufferState {
|
|
6
6
|
currentSeekTimeMs: number;
|
|
7
|
+
requestedSegments: Set<number>;
|
|
7
8
|
activeRequests: Set<number>;
|
|
8
|
-
cachedSegments: Set<number>;
|
|
9
9
|
requestQueue: number[];
|
|
10
10
|
}
|
|
11
11
|
/**
|
|
@@ -18,11 +18,12 @@ export interface MediaBufferConfig {
|
|
|
18
18
|
enableContinuousBuffering?: boolean;
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
21
|
-
* Dependencies interface for media buffering -
|
|
21
|
+
* Dependencies interface for media buffering - integrates with BaseMediaEngine
|
|
22
22
|
*/
|
|
23
23
|
export interface MediaBufferDependencies<T extends AudioRendition | VideoRendition> {
|
|
24
24
|
computeSegmentId: (timeMs: number, rendition: T) => Promise<number | undefined>;
|
|
25
|
-
|
|
25
|
+
prefetchSegment: (segmentId: number, rendition: T) => Promise<void>;
|
|
26
|
+
isSegmentCached: (segmentId: number, rendition: T) => boolean;
|
|
26
27
|
getRendition: () => Promise<T>;
|
|
27
28
|
logError: (message: string, error: any) => void;
|
|
28
29
|
}
|
|
@@ -36,10 +37,10 @@ export declare const computeSegmentRange: <T extends AudioRendition | VideoRendi
|
|
|
36
37
|
*/
|
|
37
38
|
export declare const computeSegmentRangeAsync: <T extends AudioRendition | VideoRendition>(startTimeMs: number, endTimeMs: number, durationMs: number, rendition: T, computeSegmentId: (timeMs: number, rendition: T) => Promise<number | undefined>) => Promise<number[]>;
|
|
38
39
|
/**
|
|
39
|
-
* Compute buffer queue based on
|
|
40
|
-
* Pure function - determines what segments should be
|
|
40
|
+
* Compute buffer queue based on desired segments and what we've already requested
|
|
41
|
+
* Pure function - determines what new segments should be prefetched
|
|
41
42
|
*/
|
|
42
|
-
export declare const computeBufferQueue: (desiredSegments: number[],
|
|
43
|
+
export declare const computeBufferQueue: (desiredSegments: number[], requestedSegments: Set<number>) => number[];
|
|
43
44
|
/**
|
|
44
45
|
* Handle seek time change and recompute buffer queue
|
|
45
46
|
* Pure function - computes new queue when seek time changes
|
|
@@ -49,22 +50,22 @@ export declare const handleSeekTimeChange: <T extends AudioRendition | VideoRend
|
|
|
49
50
|
overlappingRequests: number[];
|
|
50
51
|
};
|
|
51
52
|
/**
|
|
52
|
-
* Check if a
|
|
53
|
-
* Pure function for
|
|
53
|
+
* Check if a segment has been requested for buffering
|
|
54
|
+
* Pure function for checking buffer orchestration state
|
|
54
55
|
*/
|
|
55
|
-
export declare const
|
|
56
|
+
export declare const isSegmentRequested: (segmentId: number, bufferState: MediaBufferState | undefined) => boolean;
|
|
56
57
|
/**
|
|
57
|
-
* Get
|
|
58
|
-
* Pure function that returns which segments
|
|
58
|
+
* Get requested segments from a list of segment IDs
|
|
59
|
+
* Pure function that returns which segments have been requested for buffering
|
|
59
60
|
*/
|
|
60
|
-
export declare const
|
|
61
|
+
export declare const getRequestedSegments: (segmentIds: number[], bufferState: MediaBufferState | undefined) => Set<number>;
|
|
61
62
|
/**
|
|
62
|
-
* Get
|
|
63
|
-
* Pure function that returns which segments
|
|
63
|
+
* Get unrequested segments from a list of segment IDs
|
|
64
|
+
* Pure function that returns which segments haven't been requested yet
|
|
64
65
|
*/
|
|
65
|
-
export declare const
|
|
66
|
+
export declare const getUnrequestedSegments: (segmentIds: number[], bufferState: MediaBufferState | undefined) => number[];
|
|
66
67
|
/**
|
|
67
|
-
* Core media buffering logic
|
|
68
|
-
*
|
|
68
|
+
* Core media buffering orchestration logic - prefetch only, no data storage
|
|
69
|
+
* Integrates with BaseMediaEngine's existing caching and request deduplication
|
|
69
70
|
*/
|
|
70
71
|
export declare const manageMediaBuffer: <T extends AudioRendition | VideoRendition>(seekTimeMs: number, config: MediaBufferConfig, currentState: MediaBufferState, durationMs: number, signal: AbortSignal, deps: MediaBufferDependencies<T>) => Promise<MediaBufferState>;
|
|
@@ -16,74 +16,54 @@ const computeSegmentRangeAsync = async (startTimeMs, endTimeMs, durationMs, rend
|
|
|
16
16
|
return segments.filter((id, index, arr) => arr.indexOf(id) === index);
|
|
17
17
|
};
|
|
18
18
|
/**
|
|
19
|
-
* Compute buffer queue based on
|
|
20
|
-
* Pure function - determines what segments should be
|
|
19
|
+
* Compute buffer queue based on desired segments and what we've already requested
|
|
20
|
+
* Pure function - determines what new segments should be prefetched
|
|
21
21
|
*/
|
|
22
|
-
const computeBufferQueue = (desiredSegments,
|
|
23
|
-
return desiredSegments.filter((segmentId) => !
|
|
22
|
+
const computeBufferQueue = (desiredSegments, requestedSegments) => {
|
|
23
|
+
return desiredSegments.filter((segmentId) => !requestedSegments.has(segmentId));
|
|
24
24
|
};
|
|
25
25
|
/**
|
|
26
|
-
* Core media buffering logic
|
|
27
|
-
*
|
|
26
|
+
* Core media buffering orchestration logic - prefetch only, no data storage
|
|
27
|
+
* Integrates with BaseMediaEngine's existing caching and request deduplication
|
|
28
28
|
*/
|
|
29
29
|
const manageMediaBuffer = async (seekTimeMs, config, currentState, durationMs, signal, deps) => {
|
|
30
30
|
if (!config.enableBuffering) return currentState;
|
|
31
31
|
const rendition = await deps.getRendition();
|
|
32
32
|
const endTimeMs = seekTimeMs + config.bufferDurationMs;
|
|
33
33
|
const desiredSegments = await computeSegmentRangeAsync(seekTimeMs, endTimeMs, durationMs, rendition, deps.computeSegmentId);
|
|
34
|
-
const
|
|
35
|
-
const
|
|
34
|
+
const uncachedSegments = desiredSegments.filter((segmentId) => !deps.isSegmentCached(segmentId, rendition));
|
|
35
|
+
const newQueue = computeBufferQueue(uncachedSegments, currentState.requestedSegments);
|
|
36
|
+
const newRequestedSegments = new Set(currentState.requestedSegments);
|
|
36
37
|
const newActiveRequests = new Set(currentState.activeRequests);
|
|
37
|
-
const
|
|
38
|
-
const startNextSegment = (
|
|
39
|
-
if (remainingQueue.length === 0 || signal.aborted) return;
|
|
40
|
-
const
|
|
41
|
-
if (availableSlots <= 0) return;
|
|
42
|
-
const nextSegmentId = remainingQueue[0];
|
|
38
|
+
const remainingQueue = [...newQueue];
|
|
39
|
+
const startNextSegment = () => {
|
|
40
|
+
if (newActiveRequests.size >= config.maxParallelFetches || remainingQueue.length === 0 || signal.aborted) return;
|
|
41
|
+
const nextSegmentId = remainingQueue.shift();
|
|
43
42
|
if (nextSegmentId === void 0) return;
|
|
44
|
-
if (
|
|
45
|
-
startNextSegment(
|
|
43
|
+
if (newRequestedSegments.has(nextSegmentId) || deps.isSegmentCached(nextSegmentId, rendition)) {
|
|
44
|
+
startNextSegment();
|
|
46
45
|
return;
|
|
47
46
|
}
|
|
47
|
+
newRequestedSegments.add(nextSegmentId);
|
|
48
48
|
newActiveRequests.add(nextSegmentId);
|
|
49
|
-
deps.
|
|
49
|
+
deps.prefetchSegment(nextSegmentId, rendition).then(() => {
|
|
50
50
|
if (signal.aborted) return;
|
|
51
51
|
newActiveRequests.delete(nextSegmentId);
|
|
52
|
-
|
|
53
|
-
startNextSegment(remainingQueue.slice(1));
|
|
52
|
+
if (config.enableContinuousBuffering ?? true) startNextSegment();
|
|
54
53
|
}).catch((error) => {
|
|
55
54
|
if (signal.aborted) return;
|
|
56
55
|
newActiveRequests.delete(nextSegmentId);
|
|
57
|
-
deps.logError(`Failed to
|
|
58
|
-
|
|
56
|
+
deps.logError(`Failed to prefetch segment ${nextSegmentId}`, error);
|
|
57
|
+
if (config.enableContinuousBuffering ?? true) startNextSegment();
|
|
59
58
|
});
|
|
60
59
|
};
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
newActiveRequests.add(segmentId);
|
|
64
|
-
deps.fetchSegment(segmentId, rendition).then(() => {
|
|
65
|
-
if (signal.aborted) return;
|
|
66
|
-
newActiveRequests.delete(segmentId);
|
|
67
|
-
newCachedSegments.add(segmentId);
|
|
68
|
-
if (config.enableContinuousBuffering ?? true) {
|
|
69
|
-
const remainingQueue = newQueue.slice(segmentsToFetch.length);
|
|
70
|
-
startNextSegment(remainingQueue);
|
|
71
|
-
}
|
|
72
|
-
}).catch((error) => {
|
|
73
|
-
if (signal.aborted) return;
|
|
74
|
-
newActiveRequests.delete(segmentId);
|
|
75
|
-
deps.logError(`Failed to fetch segment ${segmentId}`, error);
|
|
76
|
-
if (config.enableContinuousBuffering ?? true) {
|
|
77
|
-
const remainingQueue = newQueue.slice(segmentsToFetch.length);
|
|
78
|
-
startNextSegment(remainingQueue);
|
|
79
|
-
}
|
|
80
|
-
});
|
|
81
|
-
}
|
|
60
|
+
const initialBatchSize = Math.min(config.maxParallelFetches, newQueue.length);
|
|
61
|
+
for (let i = 0; i < initialBatchSize; i++) startNextSegment();
|
|
82
62
|
return {
|
|
83
63
|
currentSeekTimeMs: seekTimeMs,
|
|
64
|
+
requestedSegments: newRequestedSegments,
|
|
84
65
|
activeRequests: newActiveRequests,
|
|
85
|
-
|
|
86
|
-
requestQueue: newQueue.slice(segmentsToFetch.length)
|
|
66
|
+
requestQueue: remainingQueue
|
|
87
67
|
};
|
|
88
68
|
};
|
|
89
69
|
export { manageMediaBuffer };
|
|
@@ -29,11 +29,11 @@ const createMediaEngine = (host) => {
|
|
|
29
29
|
return Promise.reject(/* @__PURE__ */ new Error("Unsupported media source"));
|
|
30
30
|
}
|
|
31
31
|
const lowerSrc = src.toLowerCase();
|
|
32
|
-
if (lowerSrc.startsWith("http://")
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return
|
|
32
|
+
if (!lowerSrc.startsWith("http://") && !lowerSrc.startsWith("https://")) return AssetMediaEngine.fetch(host, urlGenerator, src);
|
|
33
|
+
const configuration = host.closest("ef-configuration");
|
|
34
|
+
if (configuration?.mediaEngine === "local") return AssetMediaEngine.fetch(host, urlGenerator, src);
|
|
35
|
+
const url = urlGenerator.generateManifestUrl(src);
|
|
36
|
+
return JitMediaEngine.fetch(host, urlGenerator, url);
|
|
37
37
|
};
|
|
38
38
|
/**
|
|
39
39
|
* Handle completion of media engine task - triggers necessary updates.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { BufferedSeekingInput } from '../BufferedSeekingInput';
|
|
2
|
+
/**
|
|
3
|
+
* Cache for scrub BufferedSeekingInput instances
|
|
4
|
+
* Since scrub segments are 30s long, we can reuse the same input for many seeks
|
|
5
|
+
* within that time range, making scrub seeking very efficient
|
|
6
|
+
*/
|
|
7
|
+
export declare class ScrubInputCache {
|
|
8
|
+
private cache;
|
|
9
|
+
private maxCacheSize;
|
|
10
|
+
/**
|
|
11
|
+
* Get or create BufferedSeekingInput for a scrub segment
|
|
12
|
+
*/
|
|
13
|
+
getOrCreateInput(segmentId: number, createInputFn: () => Promise<BufferedSeekingInput | undefined>): Promise<BufferedSeekingInput | undefined>;
|
|
14
|
+
/**
|
|
15
|
+
* Clear the entire cache (called when video changes)
|
|
16
|
+
*/
|
|
17
|
+
clear(): void;
|
|
18
|
+
/**
|
|
19
|
+
* Get cache statistics
|
|
20
|
+
*/
|
|
21
|
+
getStats(): {
|
|
22
|
+
size: number;
|
|
23
|
+
segmentIds: number[];
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache for scrub BufferedSeekingInput instances
|
|
3
|
+
* Since scrub segments are 30s long, we can reuse the same input for many seeks
|
|
4
|
+
* within that time range, making scrub seeking very efficient
|
|
5
|
+
*/
|
|
6
|
+
var ScrubInputCache = class {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
9
|
+
this.maxCacheSize = 5;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Get or create BufferedSeekingInput for a scrub segment
|
|
13
|
+
*/
|
|
14
|
+
async getOrCreateInput(segmentId, createInputFn) {
|
|
15
|
+
const cached = this.cache.get(segmentId);
|
|
16
|
+
if (cached) return cached;
|
|
17
|
+
const input = await createInputFn();
|
|
18
|
+
if (!input) return void 0;
|
|
19
|
+
this.cache.set(segmentId, input);
|
|
20
|
+
if (this.cache.size > this.maxCacheSize) {
|
|
21
|
+
const oldestKey = this.cache.keys().next().value;
|
|
22
|
+
if (oldestKey !== void 0) this.cache.delete(oldestKey);
|
|
23
|
+
}
|
|
24
|
+
return input;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Clear the entire cache (called when video changes)
|
|
28
|
+
*/
|
|
29
|
+
clear() {
|
|
30
|
+
this.cache.clear();
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Get cache statistics
|
|
34
|
+
*/
|
|
35
|
+
getStats() {
|
|
36
|
+
return {
|
|
37
|
+
size: this.cache.size,
|
|
38
|
+
segmentIds: Array.from(this.cache.keys())
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
export { ScrubInputCache };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Task } from '@lit/task';
|
|
2
|
+
import { EFVideo } from '../../EFVideo';
|
|
3
|
+
/**
|
|
4
|
+
* Scrub video buffer task - aggressively preloads the ENTIRE scrub track
|
|
5
|
+
* Unlike main video buffering, this loads the full duration with higher concurrency
|
|
6
|
+
* for instant visual feedback during seeking
|
|
7
|
+
*/
|
|
8
|
+
export declare const makeScrubVideoBufferTask: (host: EFVideo) => Task<readonly [import('../../../transcoding/types').MediaEngine | undefined], unknown>;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { EF_RENDERING } from "../../../EF_RENDERING.js";
|
|
2
|
+
import { manageMediaBuffer } from "../shared/BufferUtils.js";
|
|
3
|
+
import { Task } from "@lit/task";
|
|
4
|
+
/**
|
|
5
|
+
* Scrub video buffer task - aggressively preloads the ENTIRE scrub track
|
|
6
|
+
* Unlike main video buffering, this loads the full duration with higher concurrency
|
|
7
|
+
* for instant visual feedback during seeking
|
|
8
|
+
*/
|
|
9
|
+
const makeScrubVideoBufferTask = (host) => {
|
|
10
|
+
let currentState = {
|
|
11
|
+
currentSeekTimeMs: 0,
|
|
12
|
+
requestedSegments: /* @__PURE__ */ new Set(),
|
|
13
|
+
activeRequests: /* @__PURE__ */ new Set(),
|
|
14
|
+
requestQueue: []
|
|
15
|
+
};
|
|
16
|
+
return new Task(host, {
|
|
17
|
+
autoRun: !EF_RENDERING(),
|
|
18
|
+
args: () => [host.mediaEngineTask.value],
|
|
19
|
+
onError: (error) => {
|
|
20
|
+
console.error("scrubVideoBufferTask error", error);
|
|
21
|
+
},
|
|
22
|
+
onComplete: (value) => {
|
|
23
|
+
currentState = value;
|
|
24
|
+
},
|
|
25
|
+
task: async ([mediaEngine], { signal }) => {
|
|
26
|
+
if (EF_RENDERING()) return currentState;
|
|
27
|
+
if (!host.enableVideoBuffering) return currentState;
|
|
28
|
+
if (!mediaEngine) return currentState;
|
|
29
|
+
const scrubRendition = mediaEngine.getScrubVideoRendition();
|
|
30
|
+
if (!scrubRendition) return currentState;
|
|
31
|
+
const scrubRenditionWithSrc = {
|
|
32
|
+
...scrubRendition,
|
|
33
|
+
src: mediaEngine.src
|
|
34
|
+
};
|
|
35
|
+
try {
|
|
36
|
+
try {
|
|
37
|
+
await mediaEngine.fetchInitSegment(scrubRenditionWithSrc, signal);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.warn("ScrubBuffer: Failed to cache scrub init segment:", error);
|
|
40
|
+
}
|
|
41
|
+
const newState = await manageMediaBuffer(0, {
|
|
42
|
+
bufferDurationMs: mediaEngine.durationMs,
|
|
43
|
+
maxParallelFetches: 10,
|
|
44
|
+
enableBuffering: true,
|
|
45
|
+
enableContinuousBuffering: true
|
|
46
|
+
}, currentState, mediaEngine.durationMs, signal, {
|
|
47
|
+
computeSegmentId: async (timeMs, rendition) => {
|
|
48
|
+
return mediaEngine.computeSegmentId(timeMs, rendition);
|
|
49
|
+
},
|
|
50
|
+
prefetchSegment: async (segmentId, rendition) => {
|
|
51
|
+
await mediaEngine.fetchMediaSegment(segmentId, rendition);
|
|
52
|
+
},
|
|
53
|
+
isSegmentCached: (segmentId, rendition) => {
|
|
54
|
+
return mediaEngine.isSegmentCached(segmentId, rendition);
|
|
55
|
+
},
|
|
56
|
+
getRendition: async () => scrubRenditionWithSrc,
|
|
57
|
+
logError: (message, error) => {
|
|
58
|
+
console.warn(`ScrubBuffer: ${message}`, error);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
return newState;
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (signal.aborted) return currentState;
|
|
64
|
+
console.warn("ScrubBuffer failed:", error);
|
|
65
|
+
return currentState;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
export { makeScrubVideoBufferTask };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { Task } from '@lit/task';
|
|
2
2
|
import { MediaEngine } from '../../../transcoding/types';
|
|
3
3
|
import { EFVideo } from '../../EFVideo';
|
|
4
|
-
export declare const
|
|
4
|
+
export declare const makeScrubVideoInitSegmentFetchTask: (host: EFVideo) => Task<readonly [MediaEngine | undefined], ArrayBuffer>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { getLatestMediaEngine } from "../tasks/makeMediaEngineTask.js";
|
|
2
|
+
import { Task } from "@lit/task";
|
|
3
|
+
const makeScrubVideoInitSegmentFetchTask = (host) => {
|
|
4
|
+
return new Task(host, {
|
|
5
|
+
args: () => [host.mediaEngineTask.value],
|
|
6
|
+
onError: (error) => {
|
|
7
|
+
console.error("scrubVideoInitSegmentFetchTask error", error);
|
|
8
|
+
},
|
|
9
|
+
onComplete: (_value) => {},
|
|
10
|
+
task: async ([_mediaEngine], { signal }) => {
|
|
11
|
+
const mediaEngine = await getLatestMediaEngine(host, signal);
|
|
12
|
+
const scrubRendition = mediaEngine.getScrubVideoRendition();
|
|
13
|
+
if (!scrubRendition) throw new Error("No scrub rendition available");
|
|
14
|
+
return mediaEngine.fetchInitSegment({
|
|
15
|
+
...scrubRendition,
|
|
16
|
+
src: mediaEngine.src
|
|
17
|
+
}, signal);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
export { makeScrubVideoInitSegmentFetchTask };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { BufferedSeekingInput } from "../BufferedSeekingInput.js";
|
|
2
|
+
import { EFMedia } from "../../EFMedia.js";
|
|
3
|
+
import { Task } from "@lit/task";
|
|
4
|
+
const makeScrubVideoInputTask = (host) => {
|
|
5
|
+
return new Task(host, {
|
|
6
|
+
args: () => [host.scrubVideoInitSegmentFetchTask.value, host.scrubVideoSegmentFetchTask.value],
|
|
7
|
+
onError: (error) => {
|
|
8
|
+
console.error("scrubVideoInputTask error", error);
|
|
9
|
+
},
|
|
10
|
+
onComplete: (_value) => {},
|
|
11
|
+
task: async () => {
|
|
12
|
+
const initSegment = await host.scrubVideoInitSegmentFetchTask.taskComplete;
|
|
13
|
+
const segment = await host.scrubVideoSegmentFetchTask.taskComplete;
|
|
14
|
+
if (!initSegment || !segment) throw new Error("Scrub init segment or segment is not available");
|
|
15
|
+
const mediaEngine = await host.mediaEngineTask.taskComplete;
|
|
16
|
+
const scrubRendition = mediaEngine.getScrubVideoRendition();
|
|
17
|
+
const startTimeOffsetMs = scrubRendition?.startTimeOffsetMs;
|
|
18
|
+
const input = new BufferedSeekingInput(await new Blob([initSegment, segment]).arrayBuffer(), {
|
|
19
|
+
videoBufferSize: EFMedia.VIDEO_SAMPLE_BUFFER_SIZE,
|
|
20
|
+
audioBufferSize: EFMedia.AUDIO_SAMPLE_BUFFER_SIZE,
|
|
21
|
+
startTimeOffsetMs
|
|
22
|
+
});
|
|
23
|
+
return input;
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
export { makeScrubVideoInputTask };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Task } from '@lit/task';
|
|
2
|
+
import { VideoSample } from 'mediabunny';
|
|
3
|
+
import { EFVideo } from '../../EFVideo';
|
|
4
|
+
type ScrubVideoSeekTask = Task<readonly [number], VideoSample | undefined>;
|
|
5
|
+
export declare const makeScrubVideoSeekTask: (host: EFVideo) => ScrubVideoSeekTask;
|
|
6
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { ScrubInputCache } from "./ScrubInputCache.js";
|
|
2
|
+
import { Task } from "@lit/task";
|
|
3
|
+
const scrubInputCache = new ScrubInputCache();
|
|
4
|
+
const makeScrubVideoSeekTask = (host) => {
|
|
5
|
+
return new Task(host, {
|
|
6
|
+
args: () => [host.desiredSeekTimeMs],
|
|
7
|
+
onError: (error) => {
|
|
8
|
+
console.error("scrubVideoSeekTask error", error);
|
|
9
|
+
},
|
|
10
|
+
onComplete: (_value) => {},
|
|
11
|
+
task: async ([desiredSeekTimeMs], { signal }) => {
|
|
12
|
+
signal.throwIfAborted();
|
|
13
|
+
const mediaEngine = host.mediaEngineTask.value;
|
|
14
|
+
if (!mediaEngine) return void 0;
|
|
15
|
+
const scrubRendition = mediaEngine.getScrubVideoRendition();
|
|
16
|
+
if (!scrubRendition) return void 0;
|
|
17
|
+
const scrubRenditionWithSrc = {
|
|
18
|
+
...scrubRendition,
|
|
19
|
+
src: mediaEngine.src
|
|
20
|
+
};
|
|
21
|
+
const segmentId = mediaEngine.computeSegmentId(desiredSeekTimeMs, scrubRenditionWithSrc);
|
|
22
|
+
if (segmentId === void 0) return void 0;
|
|
23
|
+
const isCached = mediaEngine.isSegmentCached(segmentId, scrubRenditionWithSrc);
|
|
24
|
+
if (!isCached) return void 0;
|
|
25
|
+
signal.throwIfAborted();
|
|
26
|
+
try {
|
|
27
|
+
const scrubInput = await scrubInputCache.getOrCreateInput(segmentId, async () => {
|
|
28
|
+
const [initSegment, mediaSegment] = await Promise.all([mediaEngine.fetchInitSegment(scrubRenditionWithSrc, signal), mediaEngine.fetchMediaSegment(segmentId, scrubRenditionWithSrc)]);
|
|
29
|
+
if (!initSegment || !mediaSegment || signal.aborted) return void 0;
|
|
30
|
+
const { BufferedSeekingInput } = await import("../BufferedSeekingInput.js");
|
|
31
|
+
const { EFMedia } = await import("../../EFMedia.js");
|
|
32
|
+
return new BufferedSeekingInput(await new Blob([initSegment, mediaSegment]).arrayBuffer(), {
|
|
33
|
+
videoBufferSize: EFMedia.VIDEO_SAMPLE_BUFFER_SIZE,
|
|
34
|
+
audioBufferSize: EFMedia.AUDIO_SAMPLE_BUFFER_SIZE,
|
|
35
|
+
startTimeOffsetMs: scrubRendition.startTimeOffsetMs
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
if (!scrubInput) return void 0;
|
|
39
|
+
const videoTrack = await scrubInput.getFirstVideoTrack();
|
|
40
|
+
if (!videoTrack) return void 0;
|
|
41
|
+
signal.throwIfAborted();
|
|
42
|
+
const sample = await scrubInput.seek(videoTrack.id, desiredSeekTimeMs);
|
|
43
|
+
return sample;
|
|
44
|
+
} catch (error) {
|
|
45
|
+
if (signal.aborted) return void 0;
|
|
46
|
+
console.warn("Failed to get scrub video sample:", error);
|
|
47
|
+
return void 0;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
export { makeScrubVideoSeekTask };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { Task } from '@lit/task';
|
|
2
|
+
import { MediaEngine } from '../../../transcoding/types';
|
|
3
|
+
import { EFVideo } from '../../EFVideo';
|
|
4
|
+
export declare const makeScrubVideoSegmentFetchTask: (host: EFVideo) => Task<readonly [MediaEngine | undefined, number | undefined], ArrayBuffer>;
|