@editframe/elements 0.20.3-beta.0 → 0.21.0-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/DelayedLoadingState.js +0 -27
- package/dist/EF_FRAMEGEN.d.ts +5 -3
- package/dist/EF_FRAMEGEN.js +51 -29
- package/dist/_virtual/_@oxc-project_runtime@0.93.0/helpers/decorate.js +7 -0
- package/dist/elements/ContextProxiesController.js +2 -22
- package/dist/elements/EFAudio.js +4 -8
- package/dist/elements/EFCaptions.js +59 -84
- package/dist/elements/EFImage.js +5 -6
- package/dist/elements/EFMedia/AssetIdMediaEngine.js +2 -4
- package/dist/elements/EFMedia/AssetMediaEngine.d.ts +4 -4
- package/dist/elements/EFMedia/AssetMediaEngine.js +41 -32
- package/dist/elements/EFMedia/BaseMediaEngine.d.ts +10 -2
- package/dist/elements/EFMedia/BaseMediaEngine.js +57 -67
- package/dist/elements/EFMedia/BufferedSeekingInput.js +134 -76
- package/dist/elements/EFMedia/JitMediaEngine.js +22 -23
- package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js +4 -7
- package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js +1 -3
- package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.js +2 -2
- package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.js +9 -7
- package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js +1 -3
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js +2 -12
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js +2 -2
- package/dist/elements/EFMedia/audioTasks/makeAudioTasksVideoOnly.browsertest.d.ts +1 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js +6 -3
- package/dist/elements/EFMedia/shared/AudioSpanUtils.d.ts +1 -1
- package/dist/elements/EFMedia/shared/AudioSpanUtils.js +5 -17
- package/dist/elements/EFMedia/shared/BufferUtils.d.ts +1 -1
- package/dist/elements/EFMedia/shared/BufferUtils.js +2 -13
- package/dist/elements/EFMedia/shared/GlobalInputCache.js +0 -24
- package/dist/elements/EFMedia/shared/MediaTaskUtils.d.ts +1 -1
- package/dist/elements/EFMedia/shared/PrecisionUtils.js +0 -21
- package/dist/elements/EFMedia/shared/RenditionHelpers.d.ts +1 -9
- package/dist/elements/EFMedia/shared/ThumbnailExtractor.js +0 -17
- package/dist/elements/EFMedia/tasks/makeMediaEngineTask.d.ts +1 -2
- package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js +2 -16
- package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.d.ts +29 -0
- package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js +32 -0
- package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js +1 -15
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.js +3 -8
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.js +0 -2
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoInputTask.js +8 -7
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.js +12 -13
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.js +0 -2
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.js +1 -3
- package/dist/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.js +134 -71
- package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js +8 -12
- package/dist/elements/EFMedia.d.ts +2 -1
- package/dist/elements/EFMedia.js +26 -23
- package/dist/elements/EFSourceMixin.js +5 -7
- package/dist/elements/EFSurface.js +6 -9
- package/dist/elements/EFTemporal.js +19 -37
- package/dist/elements/EFThumbnailStrip.js +16 -59
- package/dist/elements/EFTimegroup.js +96 -91
- package/dist/elements/EFVideo.d.ts +6 -2
- package/dist/elements/EFVideo.js +142 -107
- package/dist/elements/EFWaveform.js +18 -27
- package/dist/elements/SampleBuffer.js +2 -5
- package/dist/elements/TargetController.js +3 -3
- package/dist/elements/durationConverter.js +4 -4
- package/dist/elements/updateAnimations.js +14 -35
- package/dist/gui/ContextMixin.js +23 -52
- package/dist/gui/EFConfiguration.js +7 -7
- package/dist/gui/EFControls.js +5 -5
- package/dist/gui/EFFilmstrip.js +77 -98
- package/dist/gui/EFFitScale.js +5 -6
- package/dist/gui/EFFocusOverlay.js +4 -4
- package/dist/gui/EFPreview.js +4 -4
- package/dist/gui/EFScrubber.js +9 -9
- package/dist/gui/EFTimeDisplay.js +5 -5
- package/dist/gui/EFToggleLoop.js +4 -4
- package/dist/gui/EFTogglePlay.js +5 -5
- package/dist/gui/EFWorkbench.js +5 -5
- package/dist/gui/TWMixin2.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/otel/BridgeSpanExporter.d.ts +13 -0
- package/dist/otel/BridgeSpanExporter.js +87 -0
- package/dist/otel/setupBrowserTracing.d.ts +12 -0
- package/dist/otel/setupBrowserTracing.js +30 -0
- package/dist/otel/tracingHelpers.d.ts +34 -0
- package/dist/otel/tracingHelpers.js +113 -0
- package/dist/transcoding/cache/RequestDeduplicator.js +0 -21
- package/dist/transcoding/cache/URLTokenDeduplicator.js +1 -21
- package/dist/transcoding/types/index.d.ts +6 -4
- package/dist/transcoding/utils/UrlGenerator.js +2 -19
- package/dist/utils/LRUCache.js +6 -53
- package/package.json +10 -2
- package/src/elements/EFCaptions.browsertest.ts +2 -0
- package/src/elements/EFMedia/AssetIdMediaEngine.test.ts +6 -4
- package/src/elements/EFMedia/AssetMediaEngine.browsertest.ts +25 -23
- package/src/elements/EFMedia/AssetMediaEngine.ts +81 -43
- package/src/elements/EFMedia/BaseMediaEngine.browsertest.ts +94 -0
- package/src/elements/EFMedia/BaseMediaEngine.ts +120 -60
- package/src/elements/EFMedia/BufferedSeekingInput.ts +218 -101
- package/src/elements/EFMedia/JitMediaEngine.ts +20 -6
- package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.ts +5 -2
- package/src/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.ts +0 -5
- package/src/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.ts +2 -1
- package/src/elements/EFMedia/audioTasks/makeAudioInputTask.ts +18 -8
- package/src/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.ts +4 -16
- package/src/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.ts +4 -2
- package/src/elements/EFMedia/audioTasks/makeAudioTasksVideoOnly.browsertest.ts +95 -0
- package/src/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.ts +5 -6
- package/src/elements/EFMedia/shared/AudioSpanUtils.ts +5 -4
- package/src/elements/EFMedia/shared/BufferUtils.ts +7 -3
- package/src/elements/EFMedia/shared/MediaTaskUtils.ts +1 -1
- package/src/elements/EFMedia/shared/RenditionHelpers.browsertest.ts +41 -42
- package/src/elements/EFMedia/shared/RenditionHelpers.ts +0 -23
- package/src/elements/EFMedia/tasks/makeMediaEngineTask.ts +1 -9
- package/src/elements/EFMedia/videoTasks/MainVideoInputCache.ts +76 -0
- package/src/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.ts +3 -2
- package/src/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.ts +0 -5
- package/src/elements/EFMedia/videoTasks/makeScrubVideoInputTask.ts +17 -15
- package/src/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.ts +7 -1
- package/src/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.ts +0 -5
- package/src/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.ts +0 -5
- package/src/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.ts +222 -125
- package/src/elements/EFMedia/videoTasks/makeVideoBufferTask.ts +2 -5
- package/src/elements/EFMedia.ts +18 -2
- package/src/elements/EFThumbnailStrip.media-engine.browsertest.ts +2 -1
- package/src/elements/EFTimegroup.browsertest.ts +10 -8
- package/src/elements/EFTimegroup.ts +165 -77
- package/src/elements/EFVideo.browsertest.ts +19 -27
- package/src/elements/EFVideo.ts +203 -101
- package/src/otel/BridgeSpanExporter.ts +150 -0
- package/src/otel/setupBrowserTracing.ts +68 -0
- package/src/otel/tracingHelpers.ts +251 -0
- package/src/transcoding/types/index.ts +6 -4
- package/types.json +1 -1
|
@@ -16,7 +16,7 @@ const fetchAudioSegmentData = async (
|
|
|
16
16
|
): Promise<Map<number, ArrayBuffer>> => {
|
|
17
17
|
const audioRendition = mediaEngine.audioRendition;
|
|
18
18
|
if (!audioRendition) {
|
|
19
|
-
throw new Error("
|
|
19
|
+
throw new Error("Audio rendition not available");
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
const segmentData = new Map<number, ArrayBuffer>();
|
|
@@ -62,7 +62,7 @@ export const fetchAudioSpanningTime = async (
|
|
|
62
62
|
fromMs: number,
|
|
63
63
|
toMs: number,
|
|
64
64
|
signal: AbortSignal,
|
|
65
|
-
): Promise<AudioSpan> => {
|
|
65
|
+
): Promise<AudioSpan | undefined> => {
|
|
66
66
|
// Validate inputs
|
|
67
67
|
if (fromMs >= toMs || fromMs < 0) {
|
|
68
68
|
throw new Error(`Invalid time range: fromMs=${fromMs}, toMs=${toMs}`);
|
|
@@ -72,12 +72,13 @@ export const fetchAudioSpanningTime = async (
|
|
|
72
72
|
const mediaEngine = await host.mediaEngineTask.taskComplete;
|
|
73
73
|
const initSegment = await host.audioInitSegmentFetchTask.taskComplete;
|
|
74
74
|
|
|
75
|
+
// Return undefined if no audio rendition available
|
|
75
76
|
if (!mediaEngine?.audioRendition) {
|
|
76
|
-
|
|
77
|
+
return undefined;
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
if (!initSegment) {
|
|
80
|
-
|
|
81
|
+
return undefined;
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
// Calculate segments needed using the media engine's method
|
|
@@ -35,7 +35,7 @@ export interface MediaBufferDependencies<
|
|
|
35
35
|
) => Promise<number | undefined>;
|
|
36
36
|
prefetchSegment: (segmentId: number, rendition: T) => Promise<void>; // Just trigger prefetch, don't return data
|
|
37
37
|
isSegmentCached: (segmentId: number, rendition: T) => boolean; // Check BaseMediaEngine cache
|
|
38
|
-
getRendition: () => Promise<T>;
|
|
38
|
+
getRendition: () => Promise<T | undefined>;
|
|
39
39
|
logError: (message: string, error: any) => void;
|
|
40
40
|
}
|
|
41
41
|
|
|
@@ -208,6 +208,10 @@ export const manageMediaBuffer = async <
|
|
|
208
208
|
}
|
|
209
209
|
|
|
210
210
|
const rendition = await deps.getRendition();
|
|
211
|
+
if (!rendition) {
|
|
212
|
+
// Cannot buffer without a rendition
|
|
213
|
+
return currentState;
|
|
214
|
+
}
|
|
211
215
|
const endTimeMs = seekTimeMs + config.bufferDurationMs;
|
|
212
216
|
|
|
213
217
|
const desiredSegments = await computeSegmentRangeAsync(
|
|
@@ -217,7 +221,6 @@ export const manageMediaBuffer = async <
|
|
|
217
221
|
rendition,
|
|
218
222
|
deps.computeSegmentId,
|
|
219
223
|
);
|
|
220
|
-
|
|
221
224
|
// Filter out segments already cached by BaseMediaEngine
|
|
222
225
|
const uncachedSegments = desiredSegments.filter(
|
|
223
226
|
(segmentId) => !deps.isSegmentCached(segmentId, rendition),
|
|
@@ -287,10 +290,11 @@ export const manageMediaBuffer = async <
|
|
|
287
290
|
startNextSegment();
|
|
288
291
|
}
|
|
289
292
|
|
|
290
|
-
|
|
293
|
+
const result = {
|
|
291
294
|
currentSeekTimeMs: seekTimeMs,
|
|
292
295
|
requestedSegments: newRequestedSegments,
|
|
293
296
|
activeRequests: newActiveRequests,
|
|
294
297
|
requestQueue: remainingQueue, // What's left in the queue
|
|
295
298
|
};
|
|
299
|
+
return result;
|
|
296
300
|
};
|
|
@@ -4,12 +4,7 @@ import type {
|
|
|
4
4
|
MediaEngine,
|
|
5
5
|
VideoRendition,
|
|
6
6
|
} from "../../../transcoding/types";
|
|
7
|
-
import {
|
|
8
|
-
calculateSegmentRange,
|
|
9
|
-
computeSegmentId,
|
|
10
|
-
getAudioRendition,
|
|
11
|
-
getVideoRendition,
|
|
12
|
-
} from "./RenditionHelpers";
|
|
7
|
+
import { calculateSegmentRange, computeSegmentId } from "./RenditionHelpers";
|
|
13
8
|
|
|
14
9
|
const test = baseTest.extend<{
|
|
15
10
|
mockMediaEngine: MediaEngine;
|
|
@@ -36,6 +31,8 @@ const test = baseTest.extend<{
|
|
|
36
31
|
src: "https://example.com/media.mp4",
|
|
37
32
|
videoRendition: mockVideoRendition,
|
|
38
33
|
audioRendition: mockAudioRendition,
|
|
34
|
+
getVideoRendition: () => mockVideoRendition,
|
|
35
|
+
getAudioRendition: () => mockAudioRendition,
|
|
39
36
|
fetchMediaSegment: vi.fn(),
|
|
40
37
|
} as unknown as MediaEngine;
|
|
41
38
|
await use(mockMediaEngine);
|
|
@@ -60,30 +57,38 @@ const test = baseTest.extend<{
|
|
|
60
57
|
},
|
|
61
58
|
|
|
62
59
|
mockMediaEngineWithoutAudio: async ({}, use) => {
|
|
60
|
+
const videoRendition = {
|
|
61
|
+
trackId: 1,
|
|
62
|
+
src: "video-track.mp4",
|
|
63
|
+
segmentDurationMs: 1000,
|
|
64
|
+
} as VideoRendition;
|
|
65
|
+
|
|
63
66
|
const mockMediaEngine = {
|
|
64
67
|
durationMs: 10000,
|
|
65
68
|
src: "https://example.com/media.mp4",
|
|
66
|
-
videoRendition
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
} as VideoRendition,
|
|
71
|
-
audioRendition: null,
|
|
69
|
+
videoRendition,
|
|
70
|
+
audioRendition: undefined,
|
|
71
|
+
getVideoRendition: () => videoRendition,
|
|
72
|
+
getAudioRendition: () => undefined,
|
|
72
73
|
fetchMediaSegment: vi.fn(),
|
|
73
74
|
} as unknown as MediaEngine;
|
|
74
75
|
await use(mockMediaEngine);
|
|
75
76
|
},
|
|
76
77
|
|
|
77
78
|
mockMediaEngineWithoutVideo: async ({}, use) => {
|
|
79
|
+
const audioRendition = {
|
|
80
|
+
trackId: 2,
|
|
81
|
+
src: "audio-track.mp4",
|
|
82
|
+
segmentDurationMs: 1000,
|
|
83
|
+
} as AudioRendition;
|
|
84
|
+
|
|
78
85
|
const mockMediaEngine = {
|
|
79
86
|
durationMs: 10000,
|
|
80
87
|
src: "https://example.com/media.mp4",
|
|
81
|
-
videoRendition:
|
|
82
|
-
audioRendition
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
segmentDurationMs: 1000,
|
|
86
|
-
} as AudioRendition,
|
|
88
|
+
videoRendition: undefined,
|
|
89
|
+
audioRendition,
|
|
90
|
+
getVideoRendition: () => undefined,
|
|
91
|
+
getAudioRendition: () => audioRendition,
|
|
87
92
|
fetchMediaSegment: vi.fn(),
|
|
88
93
|
} as unknown as MediaEngine;
|
|
89
94
|
await use(mockMediaEngine);
|
|
@@ -91,45 +96,39 @@ const test = baseTest.extend<{
|
|
|
91
96
|
});
|
|
92
97
|
|
|
93
98
|
describe("RenditionHelpers", () => {
|
|
94
|
-
describe("
|
|
95
|
-
test("returns
|
|
96
|
-
|
|
99
|
+
describe("MediaEngine Rendition Access", () => {
|
|
100
|
+
test("mediaEngine.getAudioRendition() returns undefined for video-only assets", ({
|
|
101
|
+
mockMediaEngineWithoutAudio,
|
|
97
102
|
expect,
|
|
98
103
|
}) => {
|
|
99
|
-
const result = getAudioRendition(
|
|
100
|
-
expect(result).
|
|
101
|
-
expect(result.trackId).toBe(2);
|
|
102
|
-
expect(result.src).toBe("audio-track.mp4");
|
|
104
|
+
const result = mockMediaEngineWithoutAudio.getAudioRendition();
|
|
105
|
+
expect(result).toBeUndefined();
|
|
103
106
|
});
|
|
104
107
|
|
|
105
|
-
test("
|
|
106
|
-
|
|
108
|
+
test("mediaEngine.getVideoRendition() returns undefined for audio-only assets", ({
|
|
109
|
+
mockMediaEngineWithoutVideo,
|
|
107
110
|
expect,
|
|
108
111
|
}) => {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
);
|
|
112
|
+
const result = mockMediaEngineWithoutVideo.getVideoRendition();
|
|
113
|
+
expect(result).toBeUndefined();
|
|
112
114
|
});
|
|
113
|
-
});
|
|
114
115
|
|
|
115
|
-
|
|
116
|
-
test("returns video rendition when available", ({
|
|
116
|
+
test("mediaEngine.getAudioRendition() returns rendition when available", ({
|
|
117
117
|
mockMediaEngine,
|
|
118
118
|
expect,
|
|
119
119
|
}) => {
|
|
120
|
-
const result =
|
|
121
|
-
expect(result).
|
|
122
|
-
expect(result
|
|
123
|
-
expect(result.src).toBe("video-track.mp4");
|
|
120
|
+
const result = mockMediaEngine.getAudioRendition();
|
|
121
|
+
expect(result).toBeDefined();
|
|
122
|
+
expect(result?.trackId).toBe(2);
|
|
124
123
|
});
|
|
125
124
|
|
|
126
|
-
test("
|
|
127
|
-
|
|
125
|
+
test("mediaEngine.getVideoRendition() returns rendition when available", ({
|
|
126
|
+
mockMediaEngine,
|
|
128
127
|
expect,
|
|
129
128
|
}) => {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
);
|
|
129
|
+
const result = mockMediaEngine.getVideoRendition();
|
|
130
|
+
expect(result).toBeDefined();
|
|
131
|
+
expect(result?.trackId).toBe(1);
|
|
133
132
|
});
|
|
134
133
|
});
|
|
135
134
|
|
|
@@ -1,31 +1,8 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
AudioRendition,
|
|
3
|
-
MediaEngine,
|
|
4
3
|
VideoRendition,
|
|
5
4
|
} from "../../../transcoding/types";
|
|
6
5
|
|
|
7
|
-
/**
|
|
8
|
-
* Get audio rendition from media engine, throwing if not available
|
|
9
|
-
*/
|
|
10
|
-
export const getAudioRendition = (mediaEngine: MediaEngine): AudioRendition => {
|
|
11
|
-
const audioRendition = mediaEngine.audioRendition;
|
|
12
|
-
if (!audioRendition) {
|
|
13
|
-
throw new Error("No audio track available in source");
|
|
14
|
-
}
|
|
15
|
-
return audioRendition;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Get video rendition from media engine, throwing if not available
|
|
20
|
-
*/
|
|
21
|
-
export const getVideoRendition = (mediaEngine: MediaEngine): VideoRendition => {
|
|
22
|
-
const videoRendition = mediaEngine.videoRendition;
|
|
23
|
-
if (!videoRendition) {
|
|
24
|
-
throw new Error("No video track available in source");
|
|
25
|
-
}
|
|
26
|
-
return videoRendition;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
6
|
/**
|
|
30
7
|
* Calculate which segment contains a given timestamp
|
|
31
8
|
* Returns 1-based segment ID, or undefined if segmentDurationMs is not available
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Task } from "@lit/task";
|
|
2
2
|
import { EF_INTERACTIVE } from "../../../EF_INTERACTIVE";
|
|
3
|
-
import type { MediaEngine
|
|
3
|
+
import type { MediaEngine } from "../../../transcoding/types";
|
|
4
4
|
import type { EFMedia } from "../../EFMedia";
|
|
5
5
|
import { AssetIdMediaEngine } from "../AssetIdMediaEngine";
|
|
6
6
|
import { AssetMediaEngine } from "../AssetMediaEngine";
|
|
@@ -18,14 +18,6 @@ export const getLatestMediaEngine = async (
|
|
|
18
18
|
return mediaEngine;
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
-
export const getVideoRendition = (mediaEngine: MediaEngine): VideoRendition => {
|
|
22
|
-
const videoRendition = mediaEngine.videoRendition;
|
|
23
|
-
if (!videoRendition) {
|
|
24
|
-
throw new Error("No video track available in source");
|
|
25
|
-
}
|
|
26
|
-
return videoRendition;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
21
|
/**
|
|
30
22
|
* Core logic for creating a MediaEngine with explicit dependencies.
|
|
31
23
|
* Pure function that requires all dependencies to be provided.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { BufferedSeekingInput } from "../BufferedSeekingInput";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Cache for main video BufferedSeekingInput instances
|
|
5
|
+
* Main video segments are typically 2s long, so we can reuse the same input
|
|
6
|
+
* for multiple frames within that segment (e.g., 60 frames at 30fps)
|
|
7
|
+
*/
|
|
8
|
+
export class MainVideoInputCache {
|
|
9
|
+
private cache = new Map<string, BufferedSeekingInput>();
|
|
10
|
+
private maxCacheSize = 10; // Keep last 10 main inputs (covers 20 seconds at 2s/segment)
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create a cache key that uniquely identifies a segment
|
|
14
|
+
*/
|
|
15
|
+
private getCacheKey(
|
|
16
|
+
src: string,
|
|
17
|
+
segmentId: number,
|
|
18
|
+
renditionId: string | undefined,
|
|
19
|
+
): string {
|
|
20
|
+
return `${src}:${renditionId || "default"}:${segmentId}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get or create BufferedSeekingInput for a main video segment
|
|
25
|
+
*/
|
|
26
|
+
async getOrCreateInput(
|
|
27
|
+
src: string,
|
|
28
|
+
segmentId: number,
|
|
29
|
+
renditionId: string | undefined,
|
|
30
|
+
createInputFn: () => Promise<BufferedSeekingInput | undefined>,
|
|
31
|
+
): Promise<BufferedSeekingInput | undefined> {
|
|
32
|
+
const cacheKey = this.getCacheKey(src, segmentId, renditionId);
|
|
33
|
+
|
|
34
|
+
// Check if we already have this segment cached
|
|
35
|
+
const cached = this.cache.get(cacheKey);
|
|
36
|
+
if (cached) {
|
|
37
|
+
return cached;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Create new input
|
|
41
|
+
const input = await createInputFn();
|
|
42
|
+
if (!input) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Add to cache and maintain size limit
|
|
47
|
+
this.cache.set(cacheKey, input);
|
|
48
|
+
|
|
49
|
+
// Evict oldest entries if cache is too large (LRU-like behavior)
|
|
50
|
+
if (this.cache.size > this.maxCacheSize) {
|
|
51
|
+
const oldestKey = this.cache.keys().next().value;
|
|
52
|
+
if (oldestKey !== undefined) {
|
|
53
|
+
this.cache.delete(oldestKey);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return input;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Clear the entire cache (called when video changes)
|
|
62
|
+
*/
|
|
63
|
+
clear() {
|
|
64
|
+
this.cache.clear();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get cache statistics
|
|
69
|
+
*/
|
|
70
|
+
getStats() {
|
|
71
|
+
return {
|
|
72
|
+
size: this.cache.size,
|
|
73
|
+
cacheKeys: Array.from(this.cache.keys()),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { Task } from "@lit/task";
|
|
2
|
+
|
|
3
|
+
import { EF_INTERACTIVE } from "../../../EF_INTERACTIVE.js";
|
|
2
4
|
import { EF_RENDERING } from "../../../EF_RENDERING.js";
|
|
3
5
|
import type { VideoRendition } from "../../../transcoding/types";
|
|
4
6
|
import type { EFVideo } from "../../EFVideo";
|
|
@@ -19,8 +21,7 @@ export const makeScrubVideoBufferTask = (host: EFVideo) => {
|
|
|
19
21
|
};
|
|
20
22
|
|
|
21
23
|
return new Task(host, {
|
|
22
|
-
|
|
23
|
-
autoRun: false,
|
|
24
|
+
autoRun: EF_INTERACTIVE,
|
|
24
25
|
args: () => [host.mediaEngineTask.value] as const,
|
|
25
26
|
onError: (error) => {
|
|
26
27
|
console.error("scrubVideoBufferTask error", error);
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Task } from "@lit/task";
|
|
2
|
-
import { EF_RENDERING } from "../../../EF_RENDERING";
|
|
3
2
|
import type { MediaEngine } from "../../../transcoding/types";
|
|
4
3
|
import type { EFVideo } from "../../EFVideo";
|
|
5
4
|
import { getLatestMediaEngine } from "../tasks/makeMediaEngineTask";
|
|
@@ -14,10 +13,6 @@ export const makeScrubVideoInitSegmentFetchTask = (
|
|
|
14
13
|
},
|
|
15
14
|
onComplete: (_value) => {},
|
|
16
15
|
task: async ([_mediaEngine], { signal }) => {
|
|
17
|
-
if (EF_RENDERING()) {
|
|
18
|
-
return new ArrayBuffer(0);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
16
|
const mediaEngine = await getLatestMediaEngine(host, signal);
|
|
22
17
|
|
|
23
18
|
// Get scrub rendition using the proper interface method
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Task } from "@lit/task";
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
import { EFMedia } from "../../EFMedia";
|
|
4
4
|
import type { EFVideo } from "../../EFVideo";
|
|
5
5
|
import { BufferedSeekingInput } from "../BufferedSeekingInput";
|
|
@@ -8,7 +8,7 @@ import type { InputTask } from "../shared/MediaTaskUtils";
|
|
|
8
8
|
export const makeScrubVideoInputTask = (host: EFVideo): InputTask => {
|
|
9
9
|
return new Task<
|
|
10
10
|
readonly [ArrayBuffer | undefined, ArrayBuffer | undefined],
|
|
11
|
-
BufferedSeekingInput
|
|
11
|
+
BufferedSeekingInput | undefined
|
|
12
12
|
>(host, {
|
|
13
13
|
args: () =>
|
|
14
14
|
[
|
|
@@ -19,31 +19,33 @@ export const makeScrubVideoInputTask = (host: EFVideo): InputTask => {
|
|
|
19
19
|
console.error("scrubVideoInputTask error", error);
|
|
20
20
|
},
|
|
21
21
|
onComplete: (_value) => {},
|
|
22
|
-
task: async () => {
|
|
23
|
-
if (EF_RENDERING()) {
|
|
24
|
-
console.info("Scrub not available in rendering mode");
|
|
25
|
-
}
|
|
26
|
-
|
|
22
|
+
task: async (_, { signal }) => {
|
|
27
23
|
const initSegment =
|
|
28
24
|
await host.scrubVideoInitSegmentFetchTask.taskComplete;
|
|
25
|
+
if (signal.aborted) return undefined;
|
|
26
|
+
|
|
29
27
|
const segment = await host.scrubVideoSegmentFetchTask.taskComplete;
|
|
28
|
+
if (signal.aborted) return undefined;
|
|
29
|
+
|
|
30
30
|
if (!initSegment || !segment) {
|
|
31
31
|
throw new Error("Scrub init segment or segment is not available");
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
// Get startTimeOffsetMs from the scrub rendition if available
|
|
35
35
|
const mediaEngine = await host.mediaEngineTask.taskComplete;
|
|
36
|
+
if (signal.aborted) return undefined;
|
|
37
|
+
|
|
36
38
|
const scrubRendition = mediaEngine.getScrubVideoRendition();
|
|
37
39
|
const startTimeOffsetMs = scrubRendition?.startTimeOffsetMs;
|
|
38
40
|
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
);
|
|
41
|
+
const arrayBuffer = await new Blob([initSegment, segment]).arrayBuffer();
|
|
42
|
+
if (signal.aborted) return undefined;
|
|
43
|
+
|
|
44
|
+
const input = new BufferedSeekingInput(arrayBuffer, {
|
|
45
|
+
videoBufferSize: EFMedia.VIDEO_SAMPLE_BUFFER_SIZE,
|
|
46
|
+
audioBufferSize: EFMedia.AUDIO_SAMPLE_BUFFER_SIZE,
|
|
47
|
+
startTimeOffsetMs,
|
|
48
|
+
});
|
|
47
49
|
return input;
|
|
48
50
|
},
|
|
49
51
|
});
|
|
@@ -94,13 +94,19 @@ export const makeScrubVideoSeekTask = (host: EFVideo): ScrubVideoSeekTask => {
|
|
|
94
94
|
return undefined;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
if (signal.aborted) {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
97
101
|
// Get video track and seek to precise time within the 30s scrub segment
|
|
98
102
|
const videoTrack = await scrubInput.getFirstVideoTrack();
|
|
99
103
|
if (!videoTrack) {
|
|
100
104
|
return undefined;
|
|
101
105
|
}
|
|
102
106
|
|
|
103
|
-
signal.
|
|
107
|
+
if (signal.aborted) {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
104
110
|
|
|
105
111
|
const sample = (await scrubInput.seek(
|
|
106
112
|
videoTrack.id,
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Task } from "@lit/task";
|
|
2
|
-
import { EF_RENDERING } from "../../../EF_RENDERING";
|
|
3
2
|
import type { MediaEngine } from "../../../transcoding/types";
|
|
4
3
|
import type { EFVideo } from "../../EFVideo";
|
|
5
4
|
import { getLatestMediaEngine } from "../tasks/makeMediaEngineTask";
|
|
@@ -18,10 +17,6 @@ export const makeScrubVideoSegmentFetchTask = (
|
|
|
18
17
|
},
|
|
19
18
|
onComplete: (_value) => {},
|
|
20
19
|
task: async (_, { signal }) => {
|
|
21
|
-
if (EF_RENDERING()) {
|
|
22
|
-
return new ArrayBuffer(0);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
20
|
const mediaEngine = await getLatestMediaEngine(host, signal);
|
|
26
21
|
const segmentId = await host.scrubVideoSegmentIdTask.taskComplete;
|
|
27
22
|
if (segmentId === undefined) {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Task } from "@lit/task";
|
|
2
|
-
import { EF_RENDERING } from "../../../EF_RENDERING";
|
|
3
2
|
import type { MediaEngine } from "../../../transcoding/types";
|
|
4
3
|
import type { EFVideo } from "../../EFVideo";
|
|
5
4
|
import { getLatestMediaEngine } from "../tasks/makeMediaEngineTask";
|
|
@@ -14,10 +13,6 @@ export const makeScrubVideoSegmentIdTask = (
|
|
|
14
13
|
},
|
|
15
14
|
onComplete: (_value) => {},
|
|
16
15
|
task: async ([, targetSeekTimeMs], { signal }) => {
|
|
17
|
-
if (EF_RENDERING()) {
|
|
18
|
-
return undefined;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
16
|
const mediaEngine = await getLatestMediaEngine(host, signal);
|
|
22
17
|
signal.throwIfAborted(); // Abort if a new seek started
|
|
23
18
|
|