@editframe/elements 0.20.4-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 +50 -11
- 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.js +35 -30
- package/dist/elements/EFMedia/BaseMediaEngine.js +57 -73
- package/dist/elements/EFMedia/BufferedSeekingInput.js +134 -76
- package/dist/elements/EFMedia/JitMediaEngine.js +9 -19
- package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js +3 -6
- package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.js +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.js +6 -5
- package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js +1 -3
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js +1 -1
- package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js +1 -1
- package/dist/elements/EFMedia/shared/AudioSpanUtils.js +4 -16
- package/dist/elements/EFMedia/shared/BufferUtils.js +2 -15
- package/dist/elements/EFMedia/shared/GlobalInputCache.js +0 -24
- package/dist/elements/EFMedia/shared/PrecisionUtils.js +0 -21
- package/dist/elements/EFMedia/shared/ThumbnailExtractor.js +0 -17
- package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js +1 -10
- 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 +1 -7
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoInputTask.js +8 -5
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.js +12 -13
- package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.js +1 -1
- package/dist/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.js +134 -70
- package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js +7 -11
- package/dist/elements/EFMedia.js +26 -24
- 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 +95 -90
- 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/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/AssetMediaEngine.ts +65 -37
- package/src/elements/EFMedia/BaseMediaEngine.ts +110 -52
- package/src/elements/EFMedia/BufferedSeekingInput.ts +218 -101
- package/src/elements/EFMedia/audioTasks/makeAudioInputTask.ts +7 -3
- package/src/elements/EFMedia/videoTasks/MainVideoInputCache.ts +76 -0
- package/src/elements/EFMedia/videoTasks/makeScrubVideoInputTask.ts +16 -10
- package/src/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.ts +7 -1
- package/src/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.ts +222 -116
- package/src/elements/EFMedia.ts +16 -1
- package/src/elements/EFTimegroup.browsertest.ts +10 -8
- package/src/elements/EFTimegroup.ts +164 -76
- 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/types.json +1 -1
|
@@ -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
|
+
}
|
|
@@ -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,27 +19,33 @@ export const makeScrubVideoInputTask = (host: EFVideo): InputTask => {
|
|
|
19
19
|
console.error("scrubVideoInputTask error", error);
|
|
20
20
|
},
|
|
21
21
|
onComplete: (_value) => {},
|
|
22
|
-
task: async () => {
|
|
22
|
+
task: async (_, { signal }) => {
|
|
23
23
|
const initSegment =
|
|
24
24
|
await host.scrubVideoInitSegmentFetchTask.taskComplete;
|
|
25
|
+
if (signal.aborted) return undefined;
|
|
26
|
+
|
|
25
27
|
const segment = await host.scrubVideoSegmentFetchTask.taskComplete;
|
|
28
|
+
if (signal.aborted) return undefined;
|
|
29
|
+
|
|
26
30
|
if (!initSegment || !segment) {
|
|
27
31
|
throw new Error("Scrub init segment or segment is not available");
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
// Get startTimeOffsetMs from the scrub rendition if available
|
|
31
35
|
const mediaEngine = await host.mediaEngineTask.taskComplete;
|
|
36
|
+
if (signal.aborted) return undefined;
|
|
37
|
+
|
|
32
38
|
const scrubRendition = mediaEngine.getScrubVideoRendition();
|
|
33
39
|
const startTimeOffsetMs = scrubRendition?.startTimeOffsetMs;
|
|
34
40
|
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
);
|
|
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
|
+
});
|
|
43
49
|
return input;
|
|
44
50
|
},
|
|
45
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,8 +1,12 @@
|
|
|
1
1
|
import { Task } from "@lit/task";
|
|
2
2
|
import type { VideoSample } from "mediabunny";
|
|
3
|
+
import { withSpan } from "../../../otel/tracingHelpers.js";
|
|
3
4
|
import type { VideoRendition } from "../../../transcoding/types";
|
|
5
|
+
import { EFMedia } from "../../EFMedia.js";
|
|
4
6
|
import type { EFVideo } from "../../EFVideo";
|
|
7
|
+
import { BufferedSeekingInput } from "../BufferedSeekingInput.js";
|
|
5
8
|
import { getLatestMediaEngine } from "../tasks/makeMediaEngineTask";
|
|
9
|
+
import { MainVideoInputCache } from "./MainVideoInputCache";
|
|
6
10
|
import { ScrubInputCache } from "./ScrubInputCache";
|
|
7
11
|
|
|
8
12
|
type UnifiedVideoSeekTask = Task<readonly [number], VideoSample | undefined>;
|
|
@@ -10,6 +14,9 @@ type UnifiedVideoSeekTask = Task<readonly [number], VideoSample | undefined>;
|
|
|
10
14
|
// Shared cache for scrub inputs
|
|
11
15
|
const scrubInputCache = new ScrubInputCache();
|
|
12
16
|
|
|
17
|
+
// Shared cache for main video inputs
|
|
18
|
+
const mainVideoInputCache = new MainVideoInputCache();
|
|
19
|
+
|
|
13
20
|
export const makeUnifiedVideoSeekTask = (
|
|
14
21
|
host: EFVideo,
|
|
15
22
|
): UnifiedVideoSeekTask => {
|
|
@@ -22,7 +29,7 @@ export const makeUnifiedVideoSeekTask = (
|
|
|
22
29
|
onComplete: (_value) => {},
|
|
23
30
|
task: async ([desiredSeekTimeMs], { signal }) => {
|
|
24
31
|
const mediaEngine = await getLatestMediaEngine(host, signal);
|
|
25
|
-
if (!mediaEngine) return undefined;
|
|
32
|
+
if (!mediaEngine || signal.aborted) return undefined;
|
|
26
33
|
|
|
27
34
|
// FIRST: Check if main quality content is already cached
|
|
28
35
|
const mainRendition = mediaEngine.videoRendition;
|
|
@@ -35,13 +42,18 @@ export const makeUnifiedVideoSeekTask = (
|
|
|
35
42
|
mainSegmentId !== undefined &&
|
|
36
43
|
mediaEngine.isSegmentCached(mainSegmentId, mainRendition)
|
|
37
44
|
) {
|
|
38
|
-
|
|
39
|
-
return await getMainVideoSample(
|
|
45
|
+
const result = await getMainVideoSample(
|
|
40
46
|
host,
|
|
41
47
|
mediaEngine,
|
|
42
48
|
desiredSeekTimeMs,
|
|
43
49
|
signal,
|
|
44
50
|
);
|
|
51
|
+
|
|
52
|
+
if (signal.aborted) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return result;
|
|
45
57
|
}
|
|
46
58
|
}
|
|
47
59
|
|
|
@@ -51,7 +63,12 @@ export const makeUnifiedVideoSeekTask = (
|
|
|
51
63
|
desiredSeekTimeMs,
|
|
52
64
|
signal,
|
|
53
65
|
);
|
|
66
|
+
|
|
54
67
|
if (scrubSample || signal.aborted) {
|
|
68
|
+
if (signal.aborted) {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
55
72
|
// If scrub succeeded, start background main quality upgrade (non-blocking)
|
|
56
73
|
if (scrubSample) {
|
|
57
74
|
startMainQualityUpgrade(
|
|
@@ -68,12 +85,18 @@ export const makeUnifiedVideoSeekTask = (
|
|
|
68
85
|
}
|
|
69
86
|
|
|
70
87
|
// THIRD: Neither are cached, fetch main video path as final fallback
|
|
71
|
-
|
|
88
|
+
const result = await getMainVideoSample(
|
|
72
89
|
host,
|
|
73
90
|
mediaEngine,
|
|
74
91
|
desiredSeekTimeMs,
|
|
75
92
|
signal,
|
|
76
93
|
);
|
|
94
|
+
|
|
95
|
+
if (signal.aborted) {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return result;
|
|
77
100
|
},
|
|
78
101
|
});
|
|
79
102
|
};
|
|
@@ -86,82 +109,124 @@ async function tryGetScrubSample(
|
|
|
86
109
|
desiredSeekTimeMs: number,
|
|
87
110
|
signal: AbortSignal,
|
|
88
111
|
): Promise<VideoSample | undefined> {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
112
|
+
return withSpan(
|
|
113
|
+
"video.tryGetScrubSample",
|
|
114
|
+
{
|
|
115
|
+
desiredSeekTimeMs,
|
|
116
|
+
src: mediaEngine.src || "unknown",
|
|
117
|
+
},
|
|
118
|
+
undefined,
|
|
119
|
+
async (span) => {
|
|
120
|
+
try {
|
|
121
|
+
// Get scrub rendition
|
|
122
|
+
let scrubRendition: VideoRendition | undefined;
|
|
123
|
+
|
|
124
|
+
// Check if media engine has a getScrubVideoRendition method (AssetMediaEngine, etc.)
|
|
125
|
+
if (typeof mediaEngine.getScrubVideoRendition === "function") {
|
|
126
|
+
scrubRendition = mediaEngine.getScrubVideoRendition();
|
|
127
|
+
} else if ("data" in mediaEngine && mediaEngine.data?.videoRenditions) {
|
|
128
|
+
// Fallback to data structure for other engines
|
|
129
|
+
scrubRendition = mediaEngine.data.videoRenditions.find(
|
|
130
|
+
(r: any) => r.id === "scrub",
|
|
131
|
+
);
|
|
132
|
+
}
|
|
102
133
|
|
|
103
|
-
|
|
134
|
+
if (!scrubRendition) {
|
|
135
|
+
span.setAttribute("result", "no-scrub-rendition");
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
104
138
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
139
|
+
const scrubRenditionWithSrc = {
|
|
140
|
+
...scrubRendition,
|
|
141
|
+
src: mediaEngine.src,
|
|
142
|
+
};
|
|
109
143
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
// Get cached scrub input and seek within it
|
|
124
|
-
const scrubInput = await scrubInputCache.getOrCreateInput(
|
|
125
|
-
segmentId,
|
|
126
|
-
async () => {
|
|
127
|
-
const [initSegment, mediaSegment] = await Promise.all([
|
|
128
|
-
mediaEngine.fetchInitSegment(scrubRenditionWithSrc, signal),
|
|
129
|
-
mediaEngine.fetchMediaSegment(segmentId, scrubRenditionWithSrc),
|
|
130
|
-
]);
|
|
131
|
-
|
|
132
|
-
if (!initSegment || !mediaSegment || signal.aborted) return undefined;
|
|
133
|
-
|
|
134
|
-
const { BufferedSeekingInput } = await import(
|
|
135
|
-
"../BufferedSeekingInput.js"
|
|
144
|
+
// Check if scrub segment is cached
|
|
145
|
+
const segmentId = mediaEngine.computeSegmentId(
|
|
146
|
+
desiredSeekTimeMs,
|
|
147
|
+
scrubRenditionWithSrc,
|
|
148
|
+
);
|
|
149
|
+
if (segmentId === undefined) {
|
|
150
|
+
span.setAttribute("result", "no-segment-id");
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const isCached = mediaEngine.isSegmentCached(
|
|
155
|
+
segmentId,
|
|
156
|
+
scrubRenditionWithSrc,
|
|
136
157
|
);
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
158
|
+
span.setAttribute("isCached", isCached);
|
|
159
|
+
if (!isCached) {
|
|
160
|
+
span.setAttribute("result", "not-cached");
|
|
161
|
+
return undefined; // Not cached - let main video handle it
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Get cached scrub input and seek within it
|
|
165
|
+
const scrubInput = await scrubInputCache.getOrCreateInput(
|
|
166
|
+
segmentId,
|
|
167
|
+
async () => {
|
|
168
|
+
const [initSegment, mediaSegment] = await Promise.all([
|
|
169
|
+
mediaEngine.fetchInitSegment(scrubRenditionWithSrc, signal),
|
|
170
|
+
mediaEngine.fetchMediaSegment(segmentId, scrubRenditionWithSrc),
|
|
171
|
+
]);
|
|
172
|
+
|
|
173
|
+
if (!initSegment || !mediaSegment || signal.aborted)
|
|
174
|
+
return undefined;
|
|
175
|
+
|
|
176
|
+
const { BufferedSeekingInput } = await import(
|
|
177
|
+
"../BufferedSeekingInput.js"
|
|
178
|
+
);
|
|
179
|
+
const { EFMedia } = await import("../../EFMedia.js");
|
|
180
|
+
|
|
181
|
+
return new BufferedSeekingInput(
|
|
182
|
+
await new Blob([initSegment, mediaSegment]).arrayBuffer(),
|
|
183
|
+
{
|
|
184
|
+
videoBufferSize: EFMedia.VIDEO_SAMPLE_BUFFER_SIZE,
|
|
185
|
+
audioBufferSize: EFMedia.AUDIO_SAMPLE_BUFFER_SIZE,
|
|
186
|
+
startTimeOffsetMs: scrubRendition.startTimeOffsetMs,
|
|
187
|
+
},
|
|
188
|
+
);
|
|
145
189
|
},
|
|
146
190
|
);
|
|
147
|
-
},
|
|
148
|
-
);
|
|
149
191
|
|
|
150
|
-
|
|
192
|
+
if (!scrubInput) {
|
|
193
|
+
span.setAttribute("result", "no-scrub-input");
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
151
196
|
|
|
152
|
-
|
|
153
|
-
|
|
197
|
+
if (signal.aborted) {
|
|
198
|
+
span.setAttribute("result", "aborted-after-scrub-input");
|
|
199
|
+
return undefined;
|
|
200
|
+
}
|
|
154
201
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
202
|
+
const videoTrack = await scrubInput.getFirstVideoTrack();
|
|
203
|
+
if (!videoTrack) {
|
|
204
|
+
span.setAttribute("result", "no-video-track");
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
159
207
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
208
|
+
if (signal.aborted) {
|
|
209
|
+
span.setAttribute("result", "aborted-after-scrub-track");
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const sample = (await scrubInput.seek(
|
|
214
|
+
videoTrack.id,
|
|
215
|
+
desiredSeekTimeMs,
|
|
216
|
+
)) as unknown as VideoSample | undefined;
|
|
217
|
+
|
|
218
|
+
span.setAttribute("result", sample ? "success" : "no-sample");
|
|
219
|
+
return sample;
|
|
220
|
+
} catch (_error) {
|
|
221
|
+
if (signal.aborted) {
|
|
222
|
+
span.setAttribute("result", "aborted");
|
|
223
|
+
return undefined;
|
|
224
|
+
}
|
|
225
|
+
span.setAttribute("result", "error");
|
|
226
|
+
return undefined; // Scrub failed - let main video handle it
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
);
|
|
165
230
|
}
|
|
166
231
|
|
|
167
232
|
/**
|
|
@@ -173,60 +238,101 @@ async function getMainVideoSample(
|
|
|
173
238
|
desiredSeekTimeMs: number,
|
|
174
239
|
signal: AbortSignal,
|
|
175
240
|
): Promise<VideoSample | undefined> {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
if (!videoRendition) {
|
|
180
|
-
throw new Error(
|
|
181
|
-
"Video rendition unavailable after checking videoRendition exists",
|
|
182
|
-
);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const segmentId = mediaEngine.computeSegmentId(
|
|
241
|
+
return withSpan(
|
|
242
|
+
"video.getMainVideoSample",
|
|
243
|
+
{
|
|
186
244
|
desiredSeekTimeMs,
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
245
|
+
src: mediaEngine.src || "unknown",
|
|
246
|
+
},
|
|
247
|
+
undefined,
|
|
248
|
+
async (span) => {
|
|
249
|
+
try {
|
|
250
|
+
// Use existing main video task chain
|
|
251
|
+
const videoRendition = mediaEngine.getVideoRendition();
|
|
252
|
+
if (!videoRendition) {
|
|
253
|
+
throw new Error(
|
|
254
|
+
"Video rendition unavailable after checking videoRendition exists",
|
|
255
|
+
);
|
|
256
|
+
}
|
|
199
257
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
258
|
+
const segmentId = mediaEngine.computeSegmentId(
|
|
259
|
+
desiredSeekTimeMs,
|
|
260
|
+
videoRendition,
|
|
261
|
+
);
|
|
262
|
+
if (segmentId === undefined) {
|
|
263
|
+
span.setAttribute("result", "no-segment-id");
|
|
264
|
+
return undefined;
|
|
265
|
+
}
|
|
203
266
|
|
|
204
|
-
|
|
267
|
+
span.setAttribute("segmentId", segmentId);
|
|
268
|
+
|
|
269
|
+
// Get cached main video input or create new one
|
|
270
|
+
const mainInput = await mainVideoInputCache.getOrCreateInput(
|
|
271
|
+
mediaEngine.src,
|
|
272
|
+
segmentId,
|
|
273
|
+
videoRendition.id,
|
|
274
|
+
async () => {
|
|
275
|
+
// Fetch main video segment (will be cached at mediaEngine level)
|
|
276
|
+
const [initSegment, mediaSegment] = await Promise.all([
|
|
277
|
+
mediaEngine.fetchInitSegment(videoRendition, signal),
|
|
278
|
+
mediaEngine.fetchMediaSegment(segmentId, videoRendition, signal),
|
|
279
|
+
]);
|
|
280
|
+
|
|
281
|
+
if (!initSegment || !mediaSegment) {
|
|
282
|
+
return undefined;
|
|
283
|
+
}
|
|
284
|
+
signal.throwIfAborted();
|
|
285
|
+
|
|
286
|
+
const startTimeOffsetMs = videoRendition?.startTimeOffsetMs;
|
|
287
|
+
|
|
288
|
+
return new BufferedSeekingInput(
|
|
289
|
+
await new Blob([initSegment, mediaSegment]).arrayBuffer(),
|
|
290
|
+
{
|
|
291
|
+
videoBufferSize: EFMedia.VIDEO_SAMPLE_BUFFER_SIZE,
|
|
292
|
+
audioBufferSize: EFMedia.AUDIO_SAMPLE_BUFFER_SIZE,
|
|
293
|
+
startTimeOffsetMs,
|
|
294
|
+
},
|
|
295
|
+
);
|
|
296
|
+
},
|
|
297
|
+
);
|
|
205
298
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
audioBufferSize: EFMedia.AUDIO_SAMPLE_BUFFER_SIZE,
|
|
211
|
-
startTimeOffsetMs,
|
|
212
|
-
},
|
|
213
|
-
);
|
|
299
|
+
if (!mainInput) {
|
|
300
|
+
span.setAttribute("result", "no-segments");
|
|
301
|
+
return undefined;
|
|
302
|
+
}
|
|
214
303
|
|
|
215
|
-
|
|
216
|
-
|
|
304
|
+
if (signal.aborted) {
|
|
305
|
+
span.setAttribute("result", "aborted-after-input");
|
|
306
|
+
return undefined;
|
|
307
|
+
}
|
|
217
308
|
|
|
218
|
-
|
|
309
|
+
const videoTrack = await mainInput.getFirstVideoTrack();
|
|
310
|
+
if (!videoTrack) {
|
|
311
|
+
span.setAttribute("result", "no-video-track");
|
|
312
|
+
return undefined;
|
|
313
|
+
}
|
|
219
314
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
315
|
+
if (signal.aborted) {
|
|
316
|
+
span.setAttribute("result", "aborted-after-track");
|
|
317
|
+
return undefined;
|
|
318
|
+
}
|
|
224
319
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
320
|
+
const sample = (await mainInput.seek(
|
|
321
|
+
videoTrack.id,
|
|
322
|
+
desiredSeekTimeMs,
|
|
323
|
+
)) as unknown as VideoSample | undefined;
|
|
324
|
+
|
|
325
|
+
span.setAttribute("result", sample ? "success" : "no-sample");
|
|
326
|
+
return sample;
|
|
327
|
+
} catch (error) {
|
|
328
|
+
if (signal.aborted) {
|
|
329
|
+
span.setAttribute("result", "aborted");
|
|
330
|
+
return undefined;
|
|
331
|
+
}
|
|
332
|
+
throw error;
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
);
|
|
230
336
|
}
|
|
231
337
|
|
|
232
338
|
/**
|
package/src/elements/EFMedia.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { css, LitElement, type PropertyValueMap } from "lit";
|
|
2
2
|
import { property, state } from "lit/decorators.js";
|
|
3
3
|
import { isContextMixin } from "../gui/ContextMixin.js";
|
|
4
|
+
import { withSpan } from "../otel/tracingHelpers.js";
|
|
4
5
|
import type { AudioSpan } from "../transcoding/types/index.ts";
|
|
5
6
|
import { UrlGenerator } from "../transcoding/utils/UrlGenerator.ts";
|
|
6
7
|
import { makeAudioBufferTask } from "./EFMedia/audioTasks/makeAudioBufferTask.ts";
|
|
@@ -290,6 +291,20 @@ export class EFMedia extends EFTargetable(
|
|
|
290
291
|
toMs: number,
|
|
291
292
|
signal: AbortSignal = new AbortController().signal,
|
|
292
293
|
): Promise<AudioSpan | undefined> {
|
|
293
|
-
return
|
|
294
|
+
return withSpan(
|
|
295
|
+
"media.fetchAudioSpanningTime",
|
|
296
|
+
{
|
|
297
|
+
elementId: this.id || "unknown",
|
|
298
|
+
tagName: this.tagName.toLowerCase(),
|
|
299
|
+
fromMs,
|
|
300
|
+
toMs,
|
|
301
|
+
durationMs: toMs - fromMs,
|
|
302
|
+
src: this.src || "none",
|
|
303
|
+
},
|
|
304
|
+
undefined,
|
|
305
|
+
async () => {
|
|
306
|
+
return fetchAudioSpanningTime(this, fromMs, toMs, signal);
|
|
307
|
+
},
|
|
308
|
+
);
|
|
294
309
|
}
|
|
295
310
|
}
|
|
@@ -616,23 +616,25 @@ describe("Dynamic content updates", () => {
|
|
|
616
616
|
const frameTaskB = timegroup.querySelector("test-frame-task-b")!;
|
|
617
617
|
const frameTaskC = timegroup.querySelector("test-frame-task-c")!;
|
|
618
618
|
|
|
619
|
-
//
|
|
619
|
+
// Following the initial update, frame tasks may run during initialization
|
|
620
620
|
await timegroup.updateComplete;
|
|
621
621
|
|
|
622
|
-
|
|
622
|
+
// frameTaskB should never run (not visible at time 0ms in sequence)
|
|
623
623
|
assert.equal(frameTaskB.frameTaskCount, 0);
|
|
624
|
-
assert.equal(frameTaskC.frameTaskCount, 1);
|
|
625
624
|
|
|
626
625
|
// Then we run them manually.
|
|
627
626
|
await timegroup.frameTask.run();
|
|
628
627
|
|
|
629
628
|
// At timeline time 0ms:
|
|
630
|
-
// - frameTaskA (0-1000ms) should run
|
|
631
|
-
// - frameTaskB (1000-2000ms) should NOT run
|
|
632
|
-
// - frameTaskC (0-1000ms) should run (inherits root positioning)
|
|
633
|
-
|
|
629
|
+
// - frameTaskA (0-1000ms) should have run (visible)
|
|
630
|
+
// - frameTaskB (1000-2000ms) should NOT run (not visible at time 0)
|
|
631
|
+
// - frameTaskC (0-1000ms) should have run (inherits root positioning, visible)
|
|
632
|
+
|
|
633
|
+
// Verify visible tasks have run at least once
|
|
634
|
+
assert.ok(frameTaskA.frameTaskCount > 0, "frameTaskA should have run");
|
|
635
|
+
assert.ok(frameTaskC.frameTaskCount > 0, "frameTaskC should have run");
|
|
636
|
+
// Verify non-visible task has never run
|
|
634
637
|
assert.equal(frameTaskB.frameTaskCount, 0); // Not visible at time 0
|
|
635
|
-
assert.equal(frameTaskC.frameTaskCount, 2); // Nested in B but inherits root positioning
|
|
636
638
|
});
|
|
637
639
|
});
|
|
638
640
|
|