@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
|
@@ -122,14 +122,16 @@ describe("AssetIdMediaEngine", () => {
|
|
|
122
122
|
|
|
123
123
|
it("should return correct audio rendition", () => {
|
|
124
124
|
const audioRendition = engine.audioRendition;
|
|
125
|
-
expect(audioRendition
|
|
126
|
-
expect(audioRendition
|
|
125
|
+
expect(audioRendition).toBeDefined();
|
|
126
|
+
expect(audioRendition!.trackId).toBe(1);
|
|
127
|
+
expect(audioRendition!.src).toBe(mockAssetId);
|
|
127
128
|
});
|
|
128
129
|
|
|
129
130
|
it("should return correct video rendition", () => {
|
|
130
131
|
const videoRendition = engine.videoRendition;
|
|
131
|
-
expect(videoRendition
|
|
132
|
-
expect(videoRendition
|
|
132
|
+
expect(videoRendition).toBeDefined();
|
|
133
|
+
expect(videoRendition!.trackId).toBe(2);
|
|
134
|
+
expect(videoRendition!.src).toBe(mockAssetId);
|
|
133
135
|
});
|
|
134
136
|
});
|
|
135
137
|
|
|
@@ -57,8 +57,9 @@ describe("AssetMediaEngine", () => {
|
|
|
57
57
|
expect,
|
|
58
58
|
}) => {
|
|
59
59
|
const audioRendition = mediaEngine.audioRendition;
|
|
60
|
-
expect(audioRendition
|
|
61
|
-
expect(audioRendition
|
|
60
|
+
expect(audioRendition).toBeDefined();
|
|
61
|
+
expect(audioRendition!.trackId).toBe(2);
|
|
62
|
+
expect(audioRendition!.src).toBe(host.src);
|
|
62
63
|
});
|
|
63
64
|
|
|
64
65
|
test("returns video rendition with correct properties", ({
|
|
@@ -67,9 +68,10 @@ describe("AssetMediaEngine", () => {
|
|
|
67
68
|
expect,
|
|
68
69
|
}) => {
|
|
69
70
|
const videoRendition = mediaEngine.videoRendition;
|
|
70
|
-
expect(videoRendition
|
|
71
|
-
expect(videoRendition
|
|
72
|
-
expect(videoRendition
|
|
71
|
+
expect(videoRendition).toBeDefined();
|
|
72
|
+
expect(videoRendition!.trackId).toBe(1);
|
|
73
|
+
expect(videoRendition!.src).toBe(host.src);
|
|
74
|
+
expect(videoRendition!.startTimeOffsetMs).toBeCloseTo(66.6, 0);
|
|
73
75
|
});
|
|
74
76
|
|
|
75
77
|
test("provides templates for asset endpoints", ({ mediaEngine, expect }) => {
|
|
@@ -100,39 +102,39 @@ describe("AssetMediaEngine", () => {
|
|
|
100
102
|
|
|
101
103
|
describe("bars n tone segment id computation", () => {
|
|
102
104
|
test("computes 0ms is 0", ({ expect, mediaEngine }) => {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
).toBe(0);
|
|
105
|
+
const videoRendition = mediaEngine.getVideoRendition();
|
|
106
|
+
expect(videoRendition).toBeDefined();
|
|
107
|
+
expect(mediaEngine.computeSegmentId(0, videoRendition!)).toBe(0);
|
|
106
108
|
});
|
|
107
109
|
|
|
108
110
|
test("computes 2000 is 1", ({ expect, mediaEngine }) => {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
).toBe(1);
|
|
111
|
+
const videoRendition = mediaEngine.getVideoRendition();
|
|
112
|
+
expect(videoRendition).toBeDefined();
|
|
113
|
+
expect(mediaEngine.computeSegmentId(2000, videoRendition!)).toBe(1);
|
|
112
114
|
});
|
|
113
115
|
|
|
114
116
|
test("computes 4000 is 2", ({ expect, mediaEngine }) => {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
).toBe(2);
|
|
117
|
+
const videoRendition = mediaEngine.getVideoRendition();
|
|
118
|
+
expect(videoRendition).toBeDefined();
|
|
119
|
+
expect(mediaEngine.computeSegmentId(4000, videoRendition!)).toBe(2);
|
|
118
120
|
});
|
|
119
121
|
|
|
120
122
|
test("computes 6000 is 3", ({ expect, mediaEngine }) => {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
).toBe(3);
|
|
123
|
+
const videoRendition = mediaEngine.getVideoRendition();
|
|
124
|
+
expect(videoRendition).toBeDefined();
|
|
125
|
+
expect(mediaEngine.computeSegmentId(6000, videoRendition!)).toBe(3);
|
|
124
126
|
});
|
|
125
127
|
|
|
126
128
|
test("computes 8000 is 4", ({ expect, mediaEngine }) => {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
).toBe(4);
|
|
129
|
+
const videoRendition = mediaEngine.getVideoRendition();
|
|
130
|
+
expect(videoRendition).toBeDefined();
|
|
131
|
+
expect(mediaEngine.computeSegmentId(8000, videoRendition!)).toBe(4);
|
|
130
132
|
});
|
|
131
133
|
|
|
132
134
|
test("computes 7975 is 3", ({ expect, mediaEngine }) => {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
).toBe(3);
|
|
135
|
+
const videoRendition = mediaEngine.getVideoRendition();
|
|
136
|
+
expect(videoRendition).toBeDefined();
|
|
137
|
+
expect(mediaEngine.computeSegmentId(7975, videoRendition!)).toBe(3);
|
|
136
138
|
});
|
|
137
139
|
});
|
|
138
140
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { TrackFragmentIndex } from "@editframe/assets";
|
|
2
2
|
|
|
3
|
+
import { withSpan } from "../../otel/tracingHelpers.js";
|
|
3
4
|
import type {
|
|
4
5
|
AudioRendition,
|
|
5
6
|
InitSegmentPaths,
|
|
@@ -54,16 +55,28 @@ export class AssetMediaEngine extends BaseMediaEngine implements MediaEngine {
|
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
get videoRendition() {
|
|
58
|
+
const videoTrack = this.videoTrackIndex;
|
|
59
|
+
|
|
60
|
+
if (!videoTrack || videoTrack.track === undefined) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
57
64
|
return {
|
|
58
|
-
trackId:
|
|
65
|
+
trackId: videoTrack.track,
|
|
59
66
|
src: this.src,
|
|
60
|
-
startTimeOffsetMs:
|
|
67
|
+
startTimeOffsetMs: videoTrack.startTimeOffsetMs,
|
|
61
68
|
};
|
|
62
69
|
}
|
|
63
70
|
|
|
64
71
|
get audioRendition() {
|
|
72
|
+
const audioTrack = this.audioTrackIndex;
|
|
73
|
+
|
|
74
|
+
if (!audioTrack || audioTrack.track === undefined) {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
65
78
|
return {
|
|
66
|
-
trackId:
|
|
79
|
+
trackId: audioTrack.track,
|
|
67
80
|
src: this.src,
|
|
68
81
|
};
|
|
69
82
|
}
|
|
@@ -109,23 +122,36 @@ export class AssetMediaEngine extends BaseMediaEngine implements MediaEngine {
|
|
|
109
122
|
rendition: { trackId: number | undefined; src: string },
|
|
110
123
|
signal: AbortSignal,
|
|
111
124
|
) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
125
|
+
return withSpan(
|
|
126
|
+
"assetEngine.fetchInitSegment",
|
|
127
|
+
{
|
|
128
|
+
trackId: rendition.trackId || -1,
|
|
129
|
+
src: rendition.src,
|
|
130
|
+
},
|
|
131
|
+
undefined,
|
|
132
|
+
async (span) => {
|
|
133
|
+
if (!rendition.trackId) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
"[fetchInitSegment] Track ID is required for asset metadata",
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
const url = this.buildInitSegmentUrl(rendition.trackId);
|
|
139
|
+
const initSegment = this.data[rendition.trackId]?.initSegment;
|
|
140
|
+
if (!initSegment) {
|
|
141
|
+
throw new Error("Init segment not found");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
span.setAttribute("offset", initSegment.offset);
|
|
145
|
+
span.setAttribute("size", initSegment.size);
|
|
146
|
+
|
|
147
|
+
// Use unified fetch method with Range headers
|
|
148
|
+
const headers = {
|
|
149
|
+
Range: `bytes=${initSegment.offset}-${initSegment.offset + initSegment.size - 1}`,
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
return this.fetchMediaWithHeaders(url, headers, signal);
|
|
153
|
+
},
|
|
154
|
+
);
|
|
129
155
|
}
|
|
130
156
|
|
|
131
157
|
async fetchMediaSegment(
|
|
@@ -133,26 +159,40 @@ export class AssetMediaEngine extends BaseMediaEngine implements MediaEngine {
|
|
|
133
159
|
rendition: { trackId: number | undefined; src: string },
|
|
134
160
|
signal?: AbortSignal,
|
|
135
161
|
) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
162
|
+
return withSpan(
|
|
163
|
+
"assetEngine.fetchMediaSegment",
|
|
164
|
+
{
|
|
165
|
+
segmentId,
|
|
166
|
+
trackId: rendition.trackId || -1,
|
|
167
|
+
src: rendition.src,
|
|
168
|
+
},
|
|
169
|
+
undefined,
|
|
170
|
+
async (span) => {
|
|
171
|
+
if (!rendition.trackId) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
"[fetchMediaSegment] Track ID is required for asset metadata",
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
if (segmentId === undefined) {
|
|
177
|
+
throw new Error("Segment ID is not available");
|
|
178
|
+
}
|
|
179
|
+
const url = this.buildMediaSegmentUrl(rendition.trackId, segmentId);
|
|
180
|
+
const mediaSegment = this.data[rendition.trackId]?.segments[segmentId];
|
|
181
|
+
if (!mediaSegment) {
|
|
182
|
+
throw new Error("Media segment not found");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
span.setAttribute("offset", mediaSegment.offset);
|
|
186
|
+
span.setAttribute("size", mediaSegment.size);
|
|
187
|
+
|
|
188
|
+
// Use unified fetch method with Range headers
|
|
189
|
+
const headers = {
|
|
190
|
+
Range: `bytes=${mediaSegment.offset}-${mediaSegment.offset + mediaSegment.size - 1}`,
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
return this.fetchMediaWithHeaders(url, headers, signal);
|
|
194
|
+
},
|
|
195
|
+
);
|
|
156
196
|
}
|
|
157
197
|
|
|
158
198
|
/**
|
|
@@ -322,9 +362,7 @@ export class AssetMediaEngine extends BaseMediaEngine implements MediaEngine {
|
|
|
322
362
|
// This is because Asset segments are independent timeline fragments
|
|
323
363
|
|
|
324
364
|
if (!rendition.trackId) {
|
|
325
|
-
throw new Error(
|
|
326
|
-
"[convertToSegmentRelativeTimestamps] Track ID is required for asset metadata",
|
|
327
|
-
);
|
|
365
|
+
throw new Error("Track ID is required for asset metadata");
|
|
328
366
|
}
|
|
329
367
|
// For AssetMediaEngine, we need to calculate the actual segment start time
|
|
330
368
|
// using the precise segment boundaries from the track fragment index
|
|
@@ -34,6 +34,100 @@ class TestMediaEngine extends BaseMediaEngine {
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
// Test implementation for video-only assets
|
|
38
|
+
// @ts-expect-error missing implementations
|
|
39
|
+
class VideoOnlyMediaEngine extends BaseMediaEngine {
|
|
40
|
+
fetchMediaSegment = vi.fn();
|
|
41
|
+
public host: EFMedia;
|
|
42
|
+
|
|
43
|
+
constructor(host: EFMedia) {
|
|
44
|
+
super(host);
|
|
45
|
+
this.host = host;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
get videoRendition() {
|
|
49
|
+
return {
|
|
50
|
+
trackId: 1,
|
|
51
|
+
src: "test-video.mp4",
|
|
52
|
+
segmentDurationMs: 2000,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
get audioRendition() {
|
|
57
|
+
return undefined; // Video-only asset
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Test implementation for audio-only assets
|
|
62
|
+
// @ts-expect-error missing implementations
|
|
63
|
+
class AudioOnlyMediaEngine extends BaseMediaEngine {
|
|
64
|
+
fetchMediaSegment = vi.fn();
|
|
65
|
+
public host: EFMedia;
|
|
66
|
+
|
|
67
|
+
constructor(host: EFMedia) {
|
|
68
|
+
super(host);
|
|
69
|
+
this.host = host;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
get videoRendition() {
|
|
73
|
+
return undefined; // Audio-only asset
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
get audioRendition() {
|
|
77
|
+
return {
|
|
78
|
+
trackId: 1,
|
|
79
|
+
src: "test-audio.mp4",
|
|
80
|
+
segmentDurationMs: 1000,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
describe("BaseMediaEngine API Contract", () => {
|
|
86
|
+
test("getAudioRendition returns audio rendition when available", ({
|
|
87
|
+
expect,
|
|
88
|
+
}) => {
|
|
89
|
+
const host = document.createElement("ef-video") as EFMedia;
|
|
90
|
+
const engine = new TestMediaEngine(host);
|
|
91
|
+
|
|
92
|
+
const result = engine.getAudioRendition();
|
|
93
|
+
expect(result).toBeDefined();
|
|
94
|
+
expect(result?.trackId).toBe(2);
|
|
95
|
+
expect(result?.src).toBe("test-audio.mp4");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("getAudioRendition returns undefined for video-only assets", ({
|
|
99
|
+
expect,
|
|
100
|
+
}) => {
|
|
101
|
+
const host = document.createElement("ef-video") as EFMedia;
|
|
102
|
+
const engine = new VideoOnlyMediaEngine(host);
|
|
103
|
+
|
|
104
|
+
const result = engine.getAudioRendition();
|
|
105
|
+
expect(result).toBeUndefined();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("getVideoRendition returns video rendition when available", ({
|
|
109
|
+
expect,
|
|
110
|
+
}) => {
|
|
111
|
+
const host = document.createElement("ef-video") as EFMedia;
|
|
112
|
+
const engine = new TestMediaEngine(host);
|
|
113
|
+
|
|
114
|
+
const result = engine.getVideoRendition();
|
|
115
|
+
expect(result).toBeDefined();
|
|
116
|
+
expect(result?.trackId).toBe(1);
|
|
117
|
+
expect(result?.src).toBe("test-video.mp4");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("getVideoRendition returns undefined for audio-only assets", ({
|
|
121
|
+
expect,
|
|
122
|
+
}) => {
|
|
123
|
+
const host = document.createElement("ef-audio") as EFMedia;
|
|
124
|
+
const engine = new AudioOnlyMediaEngine(host);
|
|
125
|
+
|
|
126
|
+
const result = engine.getVideoRendition();
|
|
127
|
+
expect(result).toBeUndefined();
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
37
131
|
describe("BaseMediaEngine deduplication", () => {
|
|
38
132
|
test("should fetch segment successfully", async ({ expect }) => {
|
|
39
133
|
const host = document.createElement("ef-video") as EFMedia;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withSpan } from "../../otel/tracingHelpers.js";
|
|
1
2
|
import { RequestDeduplicator } from "../../transcoding/cache/RequestDeduplicator.js";
|
|
2
3
|
import type {
|
|
3
4
|
AudioRendition,
|
|
@@ -23,17 +24,19 @@ export abstract class BaseMediaEngine {
|
|
|
23
24
|
abstract get videoRendition(): VideoRendition | undefined;
|
|
24
25
|
abstract get audioRendition(): AudioRendition | undefined;
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Get video rendition if available. Returns undefined for audio-only assets.
|
|
29
|
+
* Callers should handle undefined gracefully.
|
|
30
|
+
*/
|
|
31
|
+
getVideoRendition(): VideoRendition | undefined {
|
|
30
32
|
return this.videoRendition;
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Get audio rendition if available. Returns undefined for video-only assets.
|
|
37
|
+
* Callers should handle undefined gracefully.
|
|
38
|
+
*/
|
|
39
|
+
getAudioRendition(): AudioRendition | undefined {
|
|
37
40
|
return this.audioRendition;
|
|
38
41
|
}
|
|
39
42
|
|
|
@@ -59,65 +62,122 @@ export abstract class BaseMediaEngine {
|
|
|
59
62
|
signal?: AbortSignal;
|
|
60
63
|
},
|
|
61
64
|
): Promise<any> {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
65
|
+
return withSpan(
|
|
66
|
+
"mediaEngine.fetchWithCache",
|
|
67
|
+
{
|
|
68
|
+
url: url.length > 100 ? `${url.substring(0, 100)}...` : url,
|
|
69
|
+
responseType: options.responseType,
|
|
70
|
+
hasHeaders: !!options.headers,
|
|
71
|
+
},
|
|
72
|
+
undefined,
|
|
73
|
+
async (span) => {
|
|
74
|
+
const t0 = performance.now();
|
|
75
|
+
const { responseType, headers, signal } = options;
|
|
76
|
+
|
|
77
|
+
// Create cache key that includes URL and headers for proper isolation
|
|
78
|
+
// Note: We don't include signal in cache key as it would prevent proper deduplication
|
|
79
|
+
const cacheKey = headers ? `${url}:${JSON.stringify(headers)}` : url;
|
|
80
|
+
|
|
81
|
+
// Check cache first
|
|
82
|
+
const t1 = performance.now();
|
|
83
|
+
const cached = mediaCache.get(cacheKey);
|
|
84
|
+
const t2 = performance.now();
|
|
85
|
+
span.setAttribute("cacheLookupMs", Math.round((t2 - t1) * 1000) / 1000);
|
|
86
|
+
|
|
87
|
+
if (cached) {
|
|
88
|
+
span.setAttribute("cacheHit", true);
|
|
89
|
+
// If we have a cached promise, we need to handle the caller's abort signal
|
|
90
|
+
// without affecting the underlying request that other instances might be using
|
|
91
|
+
if (signal) {
|
|
92
|
+
const t3 = performance.now();
|
|
93
|
+
const result = await this.handleAbortForCachedRequest(
|
|
94
|
+
cached,
|
|
95
|
+
signal,
|
|
96
|
+
);
|
|
97
|
+
const t4 = performance.now();
|
|
98
|
+
span.setAttribute(
|
|
99
|
+
"handleAbortMs",
|
|
100
|
+
Math.round((t4 - t3) * 100) / 100,
|
|
101
|
+
);
|
|
102
|
+
span.setAttribute(
|
|
103
|
+
"totalCacheHitMs",
|
|
104
|
+
Math.round((t4 - t0) * 100) / 100,
|
|
105
|
+
);
|
|
106
|
+
return result;
|
|
91
107
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
108
|
+
span.setAttribute(
|
|
109
|
+
"totalCacheHitMs",
|
|
110
|
+
Math.round((t2 - t0) * 100) / 100,
|
|
111
|
+
);
|
|
112
|
+
return cached;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
span.setAttribute("cacheHit", false);
|
|
116
|
+
|
|
117
|
+
// Use global deduplicator to prevent concurrent requests for the same resource
|
|
118
|
+
// Note: We do NOT pass the signal to the deduplicator - each caller manages their own abort
|
|
119
|
+
const promise = globalRequestDeduplicator.executeRequest(
|
|
120
|
+
cacheKey,
|
|
121
|
+
async () => {
|
|
122
|
+
const fetchStart = performance.now();
|
|
123
|
+
try {
|
|
124
|
+
// Make the fetch request WITHOUT the signal - let each caller handle their own abort
|
|
125
|
+
// This prevents one instance's abort from affecting other instances using the shared cache
|
|
126
|
+
const response = await this.host.fetch(url, { headers });
|
|
127
|
+
const fetchEnd = performance.now();
|
|
128
|
+
span.setAttribute("fetchMs", fetchEnd - fetchStart);
|
|
129
|
+
|
|
130
|
+
if (responseType === "json") {
|
|
131
|
+
return response.json();
|
|
132
|
+
}
|
|
133
|
+
const buffer = await response.arrayBuffer();
|
|
134
|
+
span.setAttribute("sizeBytes", buffer.byteLength);
|
|
135
|
+
return buffer;
|
|
136
|
+
} catch (error) {
|
|
137
|
+
// If the request was aborted, don't cache the error
|
|
138
|
+
if (
|
|
139
|
+
error instanceof DOMException &&
|
|
140
|
+
error.name === "AbortError"
|
|
141
|
+
) {
|
|
142
|
+
// Remove from cache so other requests can retry
|
|
143
|
+
mediaCache.delete(cacheKey);
|
|
144
|
+
}
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
// Cache the promise (not the result) to handle concurrent requests
|
|
151
|
+
mediaCache.set(cacheKey, promise);
|
|
152
|
+
|
|
153
|
+
// Handle the case where the promise might be aborted
|
|
154
|
+
promise.catch((error) => {
|
|
155
|
+
// If the request was aborted, remove it from cache to prevent corrupted data
|
|
95
156
|
if (error instanceof DOMException && error.name === "AbortError") {
|
|
96
|
-
// Remove from cache so other requests can retry
|
|
97
157
|
mediaCache.delete(cacheKey);
|
|
98
158
|
}
|
|
99
|
-
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// If the caller has a signal, handle abort logic without affecting the underlying request
|
|
162
|
+
if (signal) {
|
|
163
|
+
const result = await this.handleAbortForCachedRequest(
|
|
164
|
+
promise,
|
|
165
|
+
signal,
|
|
166
|
+
);
|
|
167
|
+
const tEnd = performance.now();
|
|
168
|
+
span.setAttribute(
|
|
169
|
+
"totalFetchMs",
|
|
170
|
+
Math.round((tEnd - t0) * 100) / 100,
|
|
171
|
+
);
|
|
172
|
+
return result;
|
|
100
173
|
}
|
|
174
|
+
|
|
175
|
+
const result = await promise;
|
|
176
|
+
const tEnd = performance.now();
|
|
177
|
+
span.setAttribute("totalFetchMs", Math.round((tEnd - t0) * 100) / 100);
|
|
178
|
+
return result;
|
|
101
179
|
},
|
|
102
180
|
);
|
|
103
|
-
|
|
104
|
-
// Cache the promise (not the result) to handle concurrent requests
|
|
105
|
-
mediaCache.set(cacheKey, promise);
|
|
106
|
-
|
|
107
|
-
// Handle the case where the promise might be aborted
|
|
108
|
-
promise.catch((error) => {
|
|
109
|
-
// If the request was aborted, remove it from cache to prevent corrupted data
|
|
110
|
-
if (error instanceof DOMException && error.name === "AbortError") {
|
|
111
|
-
mediaCache.delete(cacheKey);
|
|
112
|
-
}
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
// If the caller has a signal, handle abort logic without affecting the underlying request
|
|
116
|
-
if (signal) {
|
|
117
|
-
return this.handleAbortForCachedRequest(promise, signal);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
return promise;
|
|
121
181
|
}
|
|
122
182
|
|
|
123
183
|
/**
|