@editframe/elements 0.26.3-beta.0 โ 0.26.4-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/scripts/build-css.js +3 -3
- package/tsdown.config.ts +1 -1
- package/src/elements/ContextProxiesController.ts +0 -124
- package/src/elements/CrossUpdateController.ts +0 -22
- package/src/elements/EFAudio.browsertest.ts +0 -706
- package/src/elements/EFAudio.ts +0 -56
- package/src/elements/EFCaptions.browsertest.ts +0 -1960
- package/src/elements/EFCaptions.ts +0 -823
- package/src/elements/EFImage.browsertest.ts +0 -120
- package/src/elements/EFImage.ts +0 -113
- package/src/elements/EFMedia/AssetIdMediaEngine.test.ts +0 -224
- package/src/elements/EFMedia/AssetIdMediaEngine.ts +0 -110
- package/src/elements/EFMedia/AssetMediaEngine.browsertest.ts +0 -140
- package/src/elements/EFMedia/AssetMediaEngine.ts +0 -385
- package/src/elements/EFMedia/BaseMediaEngine.browsertest.ts +0 -400
- package/src/elements/EFMedia/BaseMediaEngine.ts +0 -505
- package/src/elements/EFMedia/BufferedSeekingInput.browsertest.ts +0 -386
- package/src/elements/EFMedia/BufferedSeekingInput.ts +0 -430
- package/src/elements/EFMedia/JitMediaEngine.browsertest.ts +0 -226
- package/src/elements/EFMedia/JitMediaEngine.ts +0 -256
- package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.browsertest.ts +0 -679
- package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.ts +0 -117
- package/src/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.ts +0 -246
- package/src/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.browsertest.ts +0 -59
- package/src/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.ts +0 -27
- package/src/elements/EFMedia/audioTasks/makeAudioInputTask.browsertest.ts +0 -55
- package/src/elements/EFMedia/audioTasks/makeAudioInputTask.ts +0 -53
- package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.chunkboundary.regression.browsertest.ts +0 -207
- package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.ts +0 -72
- package/src/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.ts +0 -32
- package/src/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.ts +0 -29
- package/src/elements/EFMedia/audioTasks/makeAudioTasksVideoOnly.browsertest.ts +0 -95
- package/src/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.ts +0 -184
- package/src/elements/EFMedia/shared/AudioSpanUtils.ts +0 -129
- package/src/elements/EFMedia/shared/BufferUtils.ts +0 -342
- package/src/elements/EFMedia/shared/GlobalInputCache.ts +0 -77
- package/src/elements/EFMedia/shared/MediaTaskUtils.ts +0 -44
- package/src/elements/EFMedia/shared/PrecisionUtils.ts +0 -46
- package/src/elements/EFMedia/shared/RenditionHelpers.browsertest.ts +0 -246
- package/src/elements/EFMedia/shared/RenditionHelpers.ts +0 -56
- package/src/elements/EFMedia/shared/ThumbnailExtractor.ts +0 -227
- package/src/elements/EFMedia/tasks/makeMediaEngineTask.browsertest.ts +0 -167
- package/src/elements/EFMedia/tasks/makeMediaEngineTask.ts +0 -88
- package/src/elements/EFMedia/videoTasks/MainVideoInputCache.ts +0 -76
- package/src/elements/EFMedia/videoTasks/ScrubInputCache.ts +0 -61
- package/src/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.ts +0 -114
- package/src/elements/EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.ts +0 -35
- package/src/elements/EFMedia/videoTasks/makeScrubVideoInputTask.ts +0 -52
- package/src/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.ts +0 -124
- package/src/elements/EFMedia/videoTasks/makeScrubVideoSegmentFetchTask.ts +0 -44
- package/src/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.ts +0 -32
- package/src/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.ts +0 -370
- package/src/elements/EFMedia/videoTasks/makeVideoBufferTask.ts +0 -109
- package/src/elements/EFMedia.browsertest.ts +0 -872
- package/src/elements/EFMedia.ts +0 -341
- package/src/elements/EFSourceMixin.ts +0 -60
- package/src/elements/EFSurface.browsertest.ts +0 -151
- package/src/elements/EFSurface.ts +0 -142
- package/src/elements/EFTemporal.browsertest.ts +0 -215
- package/src/elements/EFTemporal.ts +0 -800
- package/src/elements/EFThumbnailStrip.browsertest.ts +0 -585
- package/src/elements/EFThumbnailStrip.media-engine.browsertest.ts +0 -714
- package/src/elements/EFThumbnailStrip.ts +0 -906
- package/src/elements/EFTimegroup.browsertest.ts +0 -934
- package/src/elements/EFTimegroup.ts +0 -882
- package/src/elements/EFVideo.browsertest.ts +0 -1482
- package/src/elements/EFVideo.ts +0 -564
- package/src/elements/EFWaveform.ts +0 -547
- package/src/elements/FetchContext.browsertest.ts +0 -401
- package/src/elements/FetchMixin.ts +0 -38
- package/src/elements/SampleBuffer.ts +0 -94
- package/src/elements/TargetController.browsertest.ts +0 -230
- package/src/elements/TargetController.ts +0 -224
- package/src/elements/TimegroupController.ts +0 -26
- package/src/elements/durationConverter.ts +0 -35
- package/src/elements/parseTimeToMs.ts +0 -9
- package/src/elements/printTaskStatus.ts +0 -16
- package/src/elements/renderTemporalAudio.ts +0 -108
- package/src/elements/updateAnimations.browsertest.ts +0 -1884
- package/src/elements/updateAnimations.ts +0 -217
- package/src/elements/util.ts +0 -24
- package/src/gui/ContextMixin.browsertest.ts +0 -860
- package/src/gui/ContextMixin.ts +0 -562
- package/src/gui/Controllable.browsertest.ts +0 -258
- package/src/gui/Controllable.ts +0 -41
- package/src/gui/EFConfiguration.ts +0 -40
- package/src/gui/EFControls.browsertest.ts +0 -389
- package/src/gui/EFControls.ts +0 -195
- package/src/gui/EFDial.browsertest.ts +0 -84
- package/src/gui/EFDial.ts +0 -172
- package/src/gui/EFFilmstrip.browsertest.ts +0 -712
- package/src/gui/EFFilmstrip.ts +0 -1349
- package/src/gui/EFFitScale.ts +0 -152
- package/src/gui/EFFocusOverlay.ts +0 -79
- package/src/gui/EFPause.browsertest.ts +0 -202
- package/src/gui/EFPause.ts +0 -73
- package/src/gui/EFPlay.browsertest.ts +0 -202
- package/src/gui/EFPlay.ts +0 -73
- package/src/gui/EFPreview.ts +0 -74
- package/src/gui/EFResizableBox.browsertest.ts +0 -79
- package/src/gui/EFResizableBox.ts +0 -898
- package/src/gui/EFScrubber.ts +0 -151
- package/src/gui/EFTimeDisplay.browsertest.ts +0 -237
- package/src/gui/EFTimeDisplay.ts +0 -55
- package/src/gui/EFToggleLoop.ts +0 -35
- package/src/gui/EFTogglePlay.ts +0 -70
- package/src/gui/EFWorkbench.ts +0 -115
- package/src/gui/PlaybackController.ts +0 -527
- package/src/gui/TWMixin.css +0 -6
- package/src/gui/TWMixin.ts +0 -61
- package/src/gui/TargetOrContextMixin.ts +0 -185
- package/src/gui/currentTimeContext.ts +0 -5
- package/src/gui/durationContext.ts +0 -3
- package/src/gui/efContext.ts +0 -6
- package/src/gui/fetchContext.ts +0 -5
- package/src/gui/focusContext.ts +0 -7
- package/src/gui/focusedElementContext.ts +0 -5
- package/src/gui/playingContext.ts +0 -5
- package/src/otel/BridgeSpanExporter.ts +0 -150
- package/src/otel/setupBrowserTracing.ts +0 -73
- package/src/otel/tracingHelpers.ts +0 -251
- package/src/transcoding/cache/RequestDeduplicator.test.ts +0 -170
- package/src/transcoding/cache/RequestDeduplicator.ts +0 -65
- package/src/transcoding/cache/URLTokenDeduplicator.test.ts +0 -182
- package/src/transcoding/cache/URLTokenDeduplicator.ts +0 -101
- package/src/transcoding/types/index.ts +0 -312
- package/src/transcoding/utils/MediaUtils.ts +0 -63
- package/src/transcoding/utils/UrlGenerator.ts +0 -68
- package/src/transcoding/utils/constants.ts +0 -36
- package/src/utils/LRUCache.test.ts +0 -274
- package/src/utils/LRUCache.ts +0 -696
|
@@ -1,1482 +0,0 @@
|
|
|
1
|
-
import { html, render } from "lit";
|
|
2
|
-
import { beforeAll, beforeEach, describe, expect, vi } from "vitest";
|
|
3
|
-
|
|
4
|
-
import { test as baseTest } from "../../test/useMSW.js";
|
|
5
|
-
import type { EFVideo } from "./EFVideo.js";
|
|
6
|
-
import "./EFVideo.js";
|
|
7
|
-
import "../gui/EFWorkbench.js";
|
|
8
|
-
import "../gui/EFPreview.js";
|
|
9
|
-
import "./EFTimegroup.js";
|
|
10
|
-
|
|
11
|
-
import type { EFTimegroup } from "./EFTimegroup.js";
|
|
12
|
-
|
|
13
|
-
// Helper to wait for task completion but ignore abort errors
|
|
14
|
-
async function waitForTaskIgnoringAborts(taskPromise: Promise<any>) {
|
|
15
|
-
try {
|
|
16
|
-
await taskPromise;
|
|
17
|
-
} catch (error) {
|
|
18
|
-
// Ignore AbortError - this is expected when tasks are cancelled due to new seeks
|
|
19
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
throw error;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
beforeAll(async () => {
|
|
27
|
-
console.clear();
|
|
28
|
-
await fetch("/@ef-clear-cache", {
|
|
29
|
-
method: "DELETE",
|
|
30
|
-
});
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
beforeEach(() => {
|
|
34
|
-
localStorage.clear();
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
// Extend the base test with fixtures following EFMedia.browsertest.ts pattern
|
|
38
|
-
const test = baseTest.extend<{
|
|
39
|
-
timegroup: EFTimegroup;
|
|
40
|
-
configuration: any;
|
|
41
|
-
headMoov480p: EFVideo;
|
|
42
|
-
barsNtone: EFVideo;
|
|
43
|
-
barsNtoneTimegroup: EFTimegroup;
|
|
44
|
-
sequenceTimegroup: EFTimegroup;
|
|
45
|
-
}>({
|
|
46
|
-
timegroup: async ({}, use) => {
|
|
47
|
-
const timegroup = document.createElement("ef-timegroup");
|
|
48
|
-
timegroup.setAttribute("mode", "contain");
|
|
49
|
-
await use(timegroup);
|
|
50
|
-
},
|
|
51
|
-
configuration: async ({}, use) => {
|
|
52
|
-
const configuration = document.createElement("ef-configuration");
|
|
53
|
-
const apiHost = "http://localhost:63315";
|
|
54
|
-
configuration.setAttribute("api-host", apiHost);
|
|
55
|
-
configuration.apiHost = apiHost;
|
|
56
|
-
configuration.signingURL = ""; // Disable URL signing for tests
|
|
57
|
-
document.body.appendChild(configuration);
|
|
58
|
-
await use(configuration);
|
|
59
|
-
},
|
|
60
|
-
headMoov480p: async ({ configuration, timegroup }, use) => {
|
|
61
|
-
localStorage.removeItem("ef-timegroup-root-this");
|
|
62
|
-
const host = document.createElement("ef-video");
|
|
63
|
-
host.src = "http://web:3000/head-moov-480p.mp4";
|
|
64
|
-
timegroup.append(host);
|
|
65
|
-
configuration.append(timegroup);
|
|
66
|
-
await host.mediaEngineTask.run();
|
|
67
|
-
await use(host);
|
|
68
|
-
},
|
|
69
|
-
barsNtone: async ({ barsNtoneTimegroup }, use) => {
|
|
70
|
-
// The timegroup fixture will have already created the structure
|
|
71
|
-
const video = barsNtoneTimegroup.querySelector("ef-video") as EFVideo;
|
|
72
|
-
await video.updateComplete;
|
|
73
|
-
use(video);
|
|
74
|
-
},
|
|
75
|
-
barsNtoneTimegroup: async ({}, use) => {
|
|
76
|
-
// Clear localStorage to prevent test contamination
|
|
77
|
-
localStorage.removeItem("ef-timegroup-barsNtoneTimegroup");
|
|
78
|
-
|
|
79
|
-
const container = document.createElement("div");
|
|
80
|
-
render(
|
|
81
|
-
html`
|
|
82
|
-
<ef-configuration api-host="http://localhost:63315" signing-url="">
|
|
83
|
-
<ef-preview>
|
|
84
|
-
<ef-timegroup mode="sequence" id="barsNtoneTimegroup"
|
|
85
|
-
class="relative h-[500px] w-[1000px] overflow-hidden bg-slate-500">
|
|
86
|
-
<ef-video src="bars-n-tone.mp4" id="barsNtoneVideo"></ef-video>
|
|
87
|
-
</ef-timegroup>
|
|
88
|
-
</ef-preview>
|
|
89
|
-
</ef-configuration>
|
|
90
|
-
`,
|
|
91
|
-
container,
|
|
92
|
-
);
|
|
93
|
-
document.body.appendChild(container);
|
|
94
|
-
const timegroup = container.querySelector("ef-timegroup") as EFTimegroup;
|
|
95
|
-
await timegroup.updateComplete;
|
|
96
|
-
await timegroup.waitForMediaDurations();
|
|
97
|
-
await use(timegroup);
|
|
98
|
-
// Cleanup: remove from DOM
|
|
99
|
-
container.remove();
|
|
100
|
-
},
|
|
101
|
-
sequenceTimegroup: async ({}, use) => {
|
|
102
|
-
const container = document.createElement("div");
|
|
103
|
-
render(
|
|
104
|
-
html`
|
|
105
|
-
<ef-configuration api-host="http://localhost:63315" signing-url="">
|
|
106
|
-
<ef-preview>
|
|
107
|
-
<ef-timegroup mode="sequence"
|
|
108
|
-
class="relative h-[500px] w-[1000px] overflow-hidden bg-slate-500">
|
|
109
|
-
|
|
110
|
-
<ef-timegroup mode="contain" class="absolute w-full h-full">
|
|
111
|
-
<ef-video src="bars-n-tone.mp4" class="size-full object-fit absolute top-0 left-0"></ef-video>
|
|
112
|
-
</ef-timegroup>
|
|
113
|
-
|
|
114
|
-
<ef-timegroup mode="contain" class="absolute w-full h-full">
|
|
115
|
-
<ef-video src="bars-n-tone.mp4" class="size-full object-fit absolute top-0 left-0"></ef-video>
|
|
116
|
-
</ef-timegroup>
|
|
117
|
-
|
|
118
|
-
</ef-timegroup>
|
|
119
|
-
</ef-preview>
|
|
120
|
-
</ef-configuration>
|
|
121
|
-
`,
|
|
122
|
-
container,
|
|
123
|
-
);
|
|
124
|
-
document.body.appendChild(container);
|
|
125
|
-
const timegroup = container.querySelector("ef-timegroup") as EFTimegroup;
|
|
126
|
-
await timegroup.updateComplete;
|
|
127
|
-
await timegroup.waitForMediaDurations();
|
|
128
|
-
await use(timegroup);
|
|
129
|
-
// Cleanup: remove from DOM
|
|
130
|
-
container.remove();
|
|
131
|
-
},
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
describe("EFVideo", () => {
|
|
135
|
-
describe("basic rendering", () => {
|
|
136
|
-
beforeEach(async () => {
|
|
137
|
-
const response = await fetch("/@ef-clear-cache", {
|
|
138
|
-
method: "DELETE",
|
|
139
|
-
});
|
|
140
|
-
await response.text();
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
test("should be defined and render canvas", async ({ expect }) => {
|
|
144
|
-
const element = document.createElement("ef-video");
|
|
145
|
-
document.body.appendChild(element);
|
|
146
|
-
|
|
147
|
-
// Wait for element to render
|
|
148
|
-
await element.updateComplete;
|
|
149
|
-
|
|
150
|
-
expect(element.tagName).toBe("EF-VIDEO");
|
|
151
|
-
expect(element.canvasElement).toBeDefined();
|
|
152
|
-
expect(element.canvasElement?.tagName).toBe("CANVAS");
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
test("canvas has correct default properties", async ({ expect }) => {
|
|
156
|
-
const container = document.createElement("div");
|
|
157
|
-
render(html`<ef-video></ef-video>`, container);
|
|
158
|
-
document.body.appendChild(container);
|
|
159
|
-
|
|
160
|
-
const video = container.querySelector("ef-video") as EFVideo;
|
|
161
|
-
|
|
162
|
-
// Wait for element to render
|
|
163
|
-
await video.updateComplete;
|
|
164
|
-
|
|
165
|
-
const canvas = video.canvasElement;
|
|
166
|
-
|
|
167
|
-
expect(canvas).toBeDefined();
|
|
168
|
-
expect(canvas?.width).toBeGreaterThan(0);
|
|
169
|
-
expect(canvas?.height).toBeGreaterThan(0);
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
test("canvas inherits styling correctly", async ({ expect }) => {
|
|
173
|
-
const container = document.createElement("div");
|
|
174
|
-
render(
|
|
175
|
-
html`
|
|
176
|
-
<ef-video style="width: 640px; height: 360px;"></ef-video>
|
|
177
|
-
`,
|
|
178
|
-
container,
|
|
179
|
-
);
|
|
180
|
-
document.body.appendChild(container);
|
|
181
|
-
|
|
182
|
-
const video = container.querySelector("ef-video") as EFVideo;
|
|
183
|
-
|
|
184
|
-
// Wait for element to render
|
|
185
|
-
await video.updateComplete;
|
|
186
|
-
|
|
187
|
-
const canvas = video.canvasElement;
|
|
188
|
-
|
|
189
|
-
expect(canvas).toBeDefined();
|
|
190
|
-
// Canvas should inherit the styling
|
|
191
|
-
const computedStyle = window.getComputedStyle(canvas!);
|
|
192
|
-
expect(computedStyle.width).toBe("640px");
|
|
193
|
-
expect(computedStyle.height).toBe("360px");
|
|
194
|
-
});
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
describe("video asset integration", () => {
|
|
198
|
-
test("integrates with video asset loading", async ({ expect }) => {
|
|
199
|
-
const container = document.createElement("div");
|
|
200
|
-
render(
|
|
201
|
-
html`
|
|
202
|
-
<ef-preview>
|
|
203
|
-
<ef-video src="bars-n-tone.mp4" mode="asset"></ef-video>
|
|
204
|
-
</ef-preview>
|
|
205
|
-
`,
|
|
206
|
-
container,
|
|
207
|
-
);
|
|
208
|
-
document.body.appendChild(container);
|
|
209
|
-
|
|
210
|
-
const video = container.querySelector("ef-video") as EFVideo;
|
|
211
|
-
await video.updateComplete;
|
|
212
|
-
|
|
213
|
-
// Wait for media to be ready by waiting for the media engine task to complete
|
|
214
|
-
await video.mediaEngineTask.taskComplete;
|
|
215
|
-
|
|
216
|
-
expect(video.src).toBe("bars-n-tone.mp4");
|
|
217
|
-
|
|
218
|
-
// The video should have loaded successfully and have a duration > 0
|
|
219
|
-
expect(video.intrinsicDurationMs).toBeGreaterThan(0);
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
test("handles missing video asset gracefully", async ({ expect }) => {
|
|
223
|
-
const container = document.createElement("div");
|
|
224
|
-
render(
|
|
225
|
-
html`
|
|
226
|
-
<ef-preview>
|
|
227
|
-
<ef-video src="/nonexistent.mp4"></ef-video>
|
|
228
|
-
</ef-preview>
|
|
229
|
-
`,
|
|
230
|
-
container,
|
|
231
|
-
);
|
|
232
|
-
document.body.appendChild(container);
|
|
233
|
-
|
|
234
|
-
const video = container.querySelector("ef-video") as EFVideo;
|
|
235
|
-
|
|
236
|
-
// Should not throw when video asset is missing
|
|
237
|
-
expect(() => {
|
|
238
|
-
video.paint(0);
|
|
239
|
-
}).not.toThrow();
|
|
240
|
-
});
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
describe("frame painting and canvas updates", () => {
|
|
244
|
-
test("canvas dimensions update when frame dimensions change", async ({
|
|
245
|
-
expect,
|
|
246
|
-
}) => {
|
|
247
|
-
const container = document.createElement("div");
|
|
248
|
-
render(html`<ef-video></ef-video>`, container);
|
|
249
|
-
document.body.appendChild(container);
|
|
250
|
-
|
|
251
|
-
const video = container.querySelector("ef-video") as EFVideo;
|
|
252
|
-
|
|
253
|
-
// Wait for element to render
|
|
254
|
-
await video.updateComplete;
|
|
255
|
-
|
|
256
|
-
const canvas = video.canvasElement!;
|
|
257
|
-
|
|
258
|
-
// Mock a video frame with specific dimensions
|
|
259
|
-
const mockFrame = {
|
|
260
|
-
codedWidth: 1920,
|
|
261
|
-
codedHeight: 1080,
|
|
262
|
-
format: "RGBA",
|
|
263
|
-
timestamp: 0,
|
|
264
|
-
close: vi.fn(),
|
|
265
|
-
} as unknown as VideoFrame;
|
|
266
|
-
|
|
267
|
-
// Simulate frame painting (this would normally happen through paint method)
|
|
268
|
-
const ctx = canvas.getContext("2d");
|
|
269
|
-
if (ctx && mockFrame.codedWidth && mockFrame.codedHeight) {
|
|
270
|
-
canvas.width = mockFrame.codedWidth;
|
|
271
|
-
canvas.height = mockFrame.codedHeight;
|
|
272
|
-
// Mock drawing the frame
|
|
273
|
-
ctx.fillStyle = "red";
|
|
274
|
-
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
expect(canvas.width).toBe(1920);
|
|
278
|
-
expect(canvas.height).toBe(1080);
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
test("handles frame painting with null format gracefully", async ({
|
|
282
|
-
expect,
|
|
283
|
-
}) => {
|
|
284
|
-
const container = document.createElement("div");
|
|
285
|
-
render(html`<ef-video></ef-video>`, container);
|
|
286
|
-
document.body.appendChild(container);
|
|
287
|
-
|
|
288
|
-
const video = container.querySelector("ef-video") as EFVideo;
|
|
289
|
-
|
|
290
|
-
// Wait for element to render
|
|
291
|
-
await video.updateComplete;
|
|
292
|
-
|
|
293
|
-
const canvas = video.canvasElement!;
|
|
294
|
-
|
|
295
|
-
// Mock a frame with null format (edge case)
|
|
296
|
-
const mockFrame = {
|
|
297
|
-
codedWidth: 640,
|
|
298
|
-
codedHeight: 480,
|
|
299
|
-
format: null,
|
|
300
|
-
timestamp: 0,
|
|
301
|
-
close: vi.fn(),
|
|
302
|
-
} as unknown as VideoFrame;
|
|
303
|
-
|
|
304
|
-
const ctx = canvas.getContext("2d");
|
|
305
|
-
|
|
306
|
-
// Should handle null format gracefully
|
|
307
|
-
expect(() => {
|
|
308
|
-
if (ctx && mockFrame.format === null) {
|
|
309
|
-
console.warn("Frame format is null", mockFrame);
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
312
|
-
}).not.toThrow();
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
test("canvas context is available for drawing", async ({ expect }) => {
|
|
316
|
-
const container = document.createElement("div");
|
|
317
|
-
render(html`<ef-video></ef-video>`, container);
|
|
318
|
-
document.body.appendChild(container);
|
|
319
|
-
|
|
320
|
-
const video = container.querySelector("ef-video") as EFVideo;
|
|
321
|
-
|
|
322
|
-
// Wait for element to render
|
|
323
|
-
await video.updateComplete;
|
|
324
|
-
|
|
325
|
-
const canvas = video.canvasElement!;
|
|
326
|
-
const ctx = canvas.getContext("2d");
|
|
327
|
-
|
|
328
|
-
expect(ctx).toBeDefined();
|
|
329
|
-
expect(ctx).toBeInstanceOf(CanvasRenderingContext2D);
|
|
330
|
-
|
|
331
|
-
// Test that we can draw on the canvas
|
|
332
|
-
expect(() => {
|
|
333
|
-
ctx!.fillStyle = "blue";
|
|
334
|
-
ctx!.fillRect(0, 0, 100, 100);
|
|
335
|
-
}).not.toThrow();
|
|
336
|
-
});
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
describe("decoder lock scenarios", () => {
|
|
340
|
-
test("handles concurrent paint attempts safely", async ({ expect }) => {
|
|
341
|
-
const container = document.createElement("div");
|
|
342
|
-
render(html`<ef-video></ef-video>`, container);
|
|
343
|
-
document.body.appendChild(container);
|
|
344
|
-
|
|
345
|
-
const video = container.querySelector("ef-video") as EFVideo;
|
|
346
|
-
|
|
347
|
-
// Access the private decoder lock through reflection for testing
|
|
348
|
-
const decoderLockDescriptor = Object.getOwnPropertyDescriptor(
|
|
349
|
-
Object.getPrototypeOf(video),
|
|
350
|
-
"#decoderLock",
|
|
351
|
-
);
|
|
352
|
-
|
|
353
|
-
// Simulate the decoder being in use
|
|
354
|
-
if (decoderLockDescriptor) {
|
|
355
|
-
// We can test that multiple paint calls don't cause issues
|
|
356
|
-
expect(() => {
|
|
357
|
-
video.paint(0);
|
|
358
|
-
video.paint(0);
|
|
359
|
-
video.paint(0);
|
|
360
|
-
}).not.toThrow();
|
|
361
|
-
}
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
test("paint handles missing canvas gracefully", ({ expect }) => {
|
|
365
|
-
const container = document.createElement("div");
|
|
366
|
-
render(html`<ef-video></ef-video>`, container);
|
|
367
|
-
document.body.appendChild(container);
|
|
368
|
-
|
|
369
|
-
const video = container.querySelector("ef-video") as EFVideo;
|
|
370
|
-
|
|
371
|
-
// Remove canvas to test edge case
|
|
372
|
-
const canvas = video.canvasElement;
|
|
373
|
-
canvas?.remove();
|
|
374
|
-
|
|
375
|
-
// Paint should handle missing canvas
|
|
376
|
-
expect(() => video.paint(0)).not.toThrow();
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
test("handles paint with no video asset", ({ expect }) => {
|
|
380
|
-
const container = document.createElement("div");
|
|
381
|
-
render(html`<ef-video></ef-video>`, container);
|
|
382
|
-
document.body.appendChild(container);
|
|
383
|
-
|
|
384
|
-
const video = container.querySelector("ef-video") as EFVideo;
|
|
385
|
-
|
|
386
|
-
// Paint should handle missing video asset gracefully
|
|
387
|
-
expect(() => video.paint(0)).not.toThrow();
|
|
388
|
-
});
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
describe("frame task integration", () => {
|
|
392
|
-
test("frameTask coordinates all required tasks", async ({ expect }) => {
|
|
393
|
-
const container = document.createElement("div");
|
|
394
|
-
render(
|
|
395
|
-
html`
|
|
396
|
-
<ef-preview>
|
|
397
|
-
<ef-video src="/test-video.mp4"></ef-video>
|
|
398
|
-
</ef-preview>
|
|
399
|
-
`,
|
|
400
|
-
container,
|
|
401
|
-
);
|
|
402
|
-
document.body.appendChild(container);
|
|
403
|
-
|
|
404
|
-
const video = container.querySelector("ef-video") as EFVideo;
|
|
405
|
-
|
|
406
|
-
// frameTask should complete without errors even when other tasks fail
|
|
407
|
-
expect(() => {
|
|
408
|
-
video.frameTask.run();
|
|
409
|
-
}).not.toThrow();
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
test("frameTask handles missing dependencies", ({ expect }) => {
|
|
413
|
-
const container = document.createElement("div");
|
|
414
|
-
render(html`<ef-video></ef-video>`, container);
|
|
415
|
-
document.body.appendChild(container);
|
|
416
|
-
|
|
417
|
-
const video = container.querySelector("ef-video") as EFVideo;
|
|
418
|
-
|
|
419
|
-
// Should handle missing dependencies gracefully
|
|
420
|
-
expect(() => {
|
|
421
|
-
video.frameTask.run();
|
|
422
|
-
}).not.toThrow();
|
|
423
|
-
});
|
|
424
|
-
});
|
|
425
|
-
|
|
426
|
-
describe("error handling and edge cases", () => {
|
|
427
|
-
test("handles seek to invalid time", ({ expect }) => {
|
|
428
|
-
const container = document.createElement("div");
|
|
429
|
-
render(html`<ef-video></ef-video>`, container);
|
|
430
|
-
document.body.appendChild(container);
|
|
431
|
-
|
|
432
|
-
const video = container.querySelector("ef-video") as EFVideo;
|
|
433
|
-
|
|
434
|
-
// Should handle invalid seek times gracefully
|
|
435
|
-
expect(() => {
|
|
436
|
-
video.desiredSeekTimeMs = -1000; // Invalid negative time
|
|
437
|
-
video.paint(-1000);
|
|
438
|
-
}).not.toThrow();
|
|
439
|
-
|
|
440
|
-
expect(() => {
|
|
441
|
-
video.desiredSeekTimeMs = Number.POSITIVE_INFINITY;
|
|
442
|
-
video.paint(Number.POSITIVE_INFINITY);
|
|
443
|
-
}).not.toThrow();
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
test("handles video element removal during playback", ({ expect }) => {
|
|
447
|
-
const container = document.createElement("div");
|
|
448
|
-
render(html`<ef-video></ef-video>`, container);
|
|
449
|
-
document.body.appendChild(container);
|
|
450
|
-
|
|
451
|
-
const video = container.querySelector("ef-video") as EFVideo;
|
|
452
|
-
|
|
453
|
-
// Start some operations
|
|
454
|
-
video.paint(0);
|
|
455
|
-
|
|
456
|
-
// Remove element
|
|
457
|
-
video.remove();
|
|
458
|
-
|
|
459
|
-
// Should not cause errors
|
|
460
|
-
expect(() => {
|
|
461
|
-
video.paint(0);
|
|
462
|
-
}).not.toThrow();
|
|
463
|
-
});
|
|
464
|
-
|
|
465
|
-
test("handles canvas context loss gracefully", async ({ expect }) => {
|
|
466
|
-
const container = document.createElement("div");
|
|
467
|
-
render(html`<ef-video></ef-video>`, container);
|
|
468
|
-
document.body.appendChild(container);
|
|
469
|
-
|
|
470
|
-
const video = container.querySelector("ef-video") as EFVideo;
|
|
471
|
-
|
|
472
|
-
// Wait for element to render
|
|
473
|
-
await video.updateComplete;
|
|
474
|
-
|
|
475
|
-
const canvas = video.canvasElement!;
|
|
476
|
-
|
|
477
|
-
// Simulate context loss by making getContext return null
|
|
478
|
-
const originalGetContext = canvas.getContext;
|
|
479
|
-
canvas.getContext = vi.fn().mockReturnValue(null);
|
|
480
|
-
|
|
481
|
-
// Should handle context loss gracefully
|
|
482
|
-
expect(() => {
|
|
483
|
-
video.paint(0);
|
|
484
|
-
}).not.toThrow();
|
|
485
|
-
|
|
486
|
-
// Restore original method
|
|
487
|
-
canvas.getContext = originalGetContext;
|
|
488
|
-
});
|
|
489
|
-
});
|
|
490
|
-
|
|
491
|
-
describe("assetId property", () => {
|
|
492
|
-
test("reads assetId from html source", async ({ expect }) => {
|
|
493
|
-
const container = document.createElement("div");
|
|
494
|
-
container.innerHTML = `<ef-video asset-id="test-video-asset-123"></ef-video>`;
|
|
495
|
-
document.body.appendChild(container);
|
|
496
|
-
|
|
497
|
-
const video = container.querySelector("ef-video") as EFVideo;
|
|
498
|
-
await video.updateComplete;
|
|
499
|
-
|
|
500
|
-
expect(video).toBeDefined();
|
|
501
|
-
expect(video.assetId).toBe("test-video-asset-123");
|
|
502
|
-
|
|
503
|
-
container.remove();
|
|
504
|
-
});
|
|
505
|
-
|
|
506
|
-
test("reads from js property", ({ expect }) => {
|
|
507
|
-
const container = document.createElement("div");
|
|
508
|
-
render(html`<ef-video></ef-video>`, container);
|
|
509
|
-
const video = container.querySelector("ef-video") as EFVideo;
|
|
510
|
-
|
|
511
|
-
video.assetId = "test-video-456";
|
|
512
|
-
expect(video.assetId).toBe("test-video-456");
|
|
513
|
-
});
|
|
514
|
-
|
|
515
|
-
test("reflects property changes to attribute", async ({ expect }) => {
|
|
516
|
-
const container = document.createElement("div");
|
|
517
|
-
render(html`<ef-video></ef-video>`, container);
|
|
518
|
-
document.body.appendChild(container);
|
|
519
|
-
|
|
520
|
-
const video = container.querySelector("ef-video") as EFVideo;
|
|
521
|
-
await video.updateComplete;
|
|
522
|
-
|
|
523
|
-
video.assetId = "test-video-789";
|
|
524
|
-
await video.updateComplete;
|
|
525
|
-
expect(video.getAttribute("asset-id")).toBe("test-video-789");
|
|
526
|
-
|
|
527
|
-
video.assetId = null;
|
|
528
|
-
await video.updateComplete;
|
|
529
|
-
expect(video.hasAttribute("asset-id")).toBe(false);
|
|
530
|
-
|
|
531
|
-
container.remove();
|
|
532
|
-
});
|
|
533
|
-
});
|
|
534
|
-
|
|
535
|
-
describe("integration with timegroups", () => {
|
|
536
|
-
test("integrates correctly within timegroup structure", async ({
|
|
537
|
-
expect,
|
|
538
|
-
}) => {
|
|
539
|
-
const container = document.createElement("div");
|
|
540
|
-
render(
|
|
541
|
-
html`
|
|
542
|
-
<ef-preview>
|
|
543
|
-
<ef-timegroup mode="sequence">
|
|
544
|
-
<ef-video src="bars-n-tone.mp4" mode="asset"></ef-video>
|
|
545
|
-
</ef-timegroup>
|
|
546
|
-
</ef-preview>
|
|
547
|
-
`,
|
|
548
|
-
container,
|
|
549
|
-
);
|
|
550
|
-
document.body.appendChild(container);
|
|
551
|
-
|
|
552
|
-
const video = container.querySelector("ef-video") as EFVideo;
|
|
553
|
-
const timegroup = container.querySelector("ef-timegroup");
|
|
554
|
-
await video.updateComplete;
|
|
555
|
-
|
|
556
|
-
// Wait for media to be ready by waiting for the media engine task to complete
|
|
557
|
-
await video.mediaEngineTask.taskComplete;
|
|
558
|
-
|
|
559
|
-
expect(timegroup).toBeDefined();
|
|
560
|
-
|
|
561
|
-
// The video should have loaded successfully within the timegroup
|
|
562
|
-
expect(video.intrinsicDurationMs).toBeGreaterThan(0);
|
|
563
|
-
});
|
|
564
|
-
|
|
565
|
-
test("works as standalone root temporal in ef-preview", async ({
|
|
566
|
-
expect,
|
|
567
|
-
}) => {
|
|
568
|
-
const container = document.createElement("div");
|
|
569
|
-
render(
|
|
570
|
-
html`
|
|
571
|
-
<ef-configuration api-host="http://localhost:63315" signing-url="">
|
|
572
|
-
<ef-preview id="test-preview">
|
|
573
|
-
<ef-video src="bars-n-tone.mp4" mode="asset" id="standalone-video"></ef-video>
|
|
574
|
-
</ef-preview>
|
|
575
|
-
</ef-configuration>
|
|
576
|
-
`,
|
|
577
|
-
container,
|
|
578
|
-
);
|
|
579
|
-
document.body.appendChild(container);
|
|
580
|
-
|
|
581
|
-
const preview = container.querySelector("ef-preview") as any;
|
|
582
|
-
const video = container.querySelector("ef-video") as EFVideo;
|
|
583
|
-
|
|
584
|
-
await preview.updateComplete;
|
|
585
|
-
await video.updateComplete;
|
|
586
|
-
|
|
587
|
-
// Wait for media to be ready
|
|
588
|
-
await video.mediaEngineTask.taskComplete;
|
|
589
|
-
|
|
590
|
-
// Video should have loaded successfully
|
|
591
|
-
expect(video.intrinsicDurationMs).toBeGreaterThan(0);
|
|
592
|
-
|
|
593
|
-
// Preview should recognize the video as its root temporal
|
|
594
|
-
expect(preview.targetTemporal).toBe(video);
|
|
595
|
-
|
|
596
|
-
// Video should have a playback controller as a root element
|
|
597
|
-
expect(video.playbackController).toBeDefined();
|
|
598
|
-
|
|
599
|
-
// Preview should be able to control playback
|
|
600
|
-
expect(preview.playing).toBe(false);
|
|
601
|
-
|
|
602
|
-
// Seek the video through the preview
|
|
603
|
-
preview.currentTimeMs = 1000;
|
|
604
|
-
await video.frameTask.taskComplete;
|
|
605
|
-
|
|
606
|
-
// Video should have seeked
|
|
607
|
-
expect(video.ownCurrentTimeMs).toBeCloseTo(1000, 0);
|
|
608
|
-
|
|
609
|
-
// Cleanup
|
|
610
|
-
container.remove();
|
|
611
|
-
});
|
|
612
|
-
});
|
|
613
|
-
|
|
614
|
-
describe.skip("loading indicator", () => {
|
|
615
|
-
test("should not show loading indicator for operations completing under 250ms", async ({
|
|
616
|
-
expect,
|
|
617
|
-
}) => {
|
|
618
|
-
const container = document.createElement("div");
|
|
619
|
-
render(html`<ef-video></ef-video>`, container);
|
|
620
|
-
document.body.appendChild(container);
|
|
621
|
-
|
|
622
|
-
const video = container.querySelector("ef-video") as EFVideo;
|
|
623
|
-
await video.updateComplete;
|
|
624
|
-
|
|
625
|
-
// Start a fast operation
|
|
626
|
-
video.startDelayedLoading("test-fast", "Fast operation");
|
|
627
|
-
|
|
628
|
-
// Clear it quickly (under 250ms)
|
|
629
|
-
setTimeout(() => {
|
|
630
|
-
video.clearDelayedLoading("test-fast");
|
|
631
|
-
}, 100);
|
|
632
|
-
|
|
633
|
-
expect(video.loadingState.isLoading).toBe(false);
|
|
634
|
-
});
|
|
635
|
-
|
|
636
|
-
test("should show loading indicator only after 250ms for slow operations", async ({
|
|
637
|
-
expect,
|
|
638
|
-
}) => {
|
|
639
|
-
const container = document.createElement("div");
|
|
640
|
-
render(html`<ef-video></ef-video>`, container);
|
|
641
|
-
document.body.appendChild(container);
|
|
642
|
-
|
|
643
|
-
const video = container.querySelector("ef-video") as EFVideo;
|
|
644
|
-
await video.updateComplete;
|
|
645
|
-
|
|
646
|
-
// Start a slow operation
|
|
647
|
-
video.startDelayedLoading("test-slow", "Slow operation");
|
|
648
|
-
|
|
649
|
-
// Should not be loading immediately
|
|
650
|
-
expect(video.loadingState.isLoading).toBe(false);
|
|
651
|
-
|
|
652
|
-
// Should now be loading
|
|
653
|
-
expect(video.loadingState.isLoading).toBe(true);
|
|
654
|
-
expect(video.loadingState.message).toBe("Slow operation");
|
|
655
|
-
|
|
656
|
-
// Clear the loading
|
|
657
|
-
video.clearDelayedLoading("test-slow");
|
|
658
|
-
|
|
659
|
-
// Should stop loading
|
|
660
|
-
expect(video.loadingState.isLoading).toBe(false);
|
|
661
|
-
});
|
|
662
|
-
|
|
663
|
-
test("should handle multiple concurrent loading operations", async ({
|
|
664
|
-
expect,
|
|
665
|
-
}) => {
|
|
666
|
-
const container = document.createElement("div");
|
|
667
|
-
render(html`<ef-video></ef-video>`, container);
|
|
668
|
-
document.body.appendChild(container);
|
|
669
|
-
|
|
670
|
-
const video = container.querySelector("ef-video") as EFVideo;
|
|
671
|
-
await video.updateComplete;
|
|
672
|
-
|
|
673
|
-
// Start multiple operations
|
|
674
|
-
video.startDelayedLoading("op1", "Operation 1");
|
|
675
|
-
video.startDelayedLoading("op2", "Operation 2");
|
|
676
|
-
|
|
677
|
-
// Should be loading
|
|
678
|
-
expect(video.loadingState.isLoading).toBe(true);
|
|
679
|
-
|
|
680
|
-
// Clear one operation
|
|
681
|
-
video.clearDelayedLoading("op1");
|
|
682
|
-
|
|
683
|
-
// Should still be loading (op2 still active)
|
|
684
|
-
expect(video.loadingState.isLoading).toBe(true);
|
|
685
|
-
|
|
686
|
-
// Clear second operation
|
|
687
|
-
video.clearDelayedLoading("op2");
|
|
688
|
-
|
|
689
|
-
// Should stop loading
|
|
690
|
-
expect(video.loadingState.isLoading).toBe(false);
|
|
691
|
-
});
|
|
692
|
-
|
|
693
|
-
test("should not show loading for background operations", async ({
|
|
694
|
-
expect,
|
|
695
|
-
}) => {
|
|
696
|
-
const container = document.createElement("div");
|
|
697
|
-
render(html`<ef-video></ef-video>`, container);
|
|
698
|
-
document.body.appendChild(container);
|
|
699
|
-
|
|
700
|
-
const video = container.querySelector("ef-video") as EFVideo;
|
|
701
|
-
await video.updateComplete;
|
|
702
|
-
|
|
703
|
-
// Start a background operation
|
|
704
|
-
video.startDelayedLoading("bg-op", "Background operation", {
|
|
705
|
-
background: true,
|
|
706
|
-
});
|
|
707
|
-
|
|
708
|
-
// Should not show loading UI for background operations
|
|
709
|
-
expect(video.loadingState.isLoading).toBe(false);
|
|
710
|
-
|
|
711
|
-
// Clear the operation
|
|
712
|
-
video.clearDelayedLoading("bg-op");
|
|
713
|
-
});
|
|
714
|
-
|
|
715
|
-
test("should properly clean up loading state on disconnect", async ({
|
|
716
|
-
expect,
|
|
717
|
-
}) => {
|
|
718
|
-
const container = document.createElement("div");
|
|
719
|
-
render(html`<ef-video></ef-video>`, container);
|
|
720
|
-
document.body.appendChild(container);
|
|
721
|
-
|
|
722
|
-
const video = container.querySelector("ef-video") as EFVideo;
|
|
723
|
-
await video.updateComplete;
|
|
724
|
-
|
|
725
|
-
// Start an operation
|
|
726
|
-
video.startDelayedLoading("cleanup-test", "Test operation");
|
|
727
|
-
|
|
728
|
-
// Disconnect the element
|
|
729
|
-
video.remove();
|
|
730
|
-
|
|
731
|
-
// Loading should be cleared
|
|
732
|
-
expect(video.loadingState.isLoading).toBe(false);
|
|
733
|
-
});
|
|
734
|
-
});
|
|
735
|
-
|
|
736
|
-
describe("AssetMediaEngine", () => {
|
|
737
|
-
test("seeks to 8074ms", async ({ barsNtone, barsNtoneTimegroup }) => {
|
|
738
|
-
// Wait for any initial loading to complete
|
|
739
|
-
await waitForTaskIgnoringAborts(
|
|
740
|
-
barsNtone.unifiedVideoSeekTask.taskComplete,
|
|
741
|
-
);
|
|
742
|
-
await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
|
|
743
|
-
|
|
744
|
-
// Use timegroup for seeking to ensure audio and video are synchronized
|
|
745
|
-
barsNtoneTimegroup.currentTimeMs = 8074;
|
|
746
|
-
await barsNtone.updateComplete;
|
|
747
|
-
|
|
748
|
-
// Wait for the new seek tasks to complete (ignoring any aborts from previous operations)
|
|
749
|
-
await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
|
|
750
|
-
await waitForTaskIgnoringAborts(
|
|
751
|
-
barsNtone.unifiedVideoSeekTask.taskComplete,
|
|
752
|
-
);
|
|
753
|
-
});
|
|
754
|
-
|
|
755
|
-
test("seeks to beginning of video (0ms)", async ({
|
|
756
|
-
barsNtone,
|
|
757
|
-
barsNtoneTimegroup,
|
|
758
|
-
}) => {
|
|
759
|
-
await waitForTaskIgnoringAborts(
|
|
760
|
-
barsNtone.unifiedVideoSeekTask.taskComplete,
|
|
761
|
-
);
|
|
762
|
-
await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
|
|
763
|
-
barsNtoneTimegroup.currentTimeMs = 0;
|
|
764
|
-
await barsNtone.updateComplete;
|
|
765
|
-
|
|
766
|
-
// Wait for the new seek tasks to complete
|
|
767
|
-
await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
|
|
768
|
-
await waitForTaskIgnoringAborts(
|
|
769
|
-
barsNtone.unifiedVideoSeekTask.taskComplete,
|
|
770
|
-
);
|
|
771
|
-
});
|
|
772
|
-
|
|
773
|
-
test("seeks to exact segment boundary at 2066ms", async ({
|
|
774
|
-
barsNtone,
|
|
775
|
-
barsNtoneTimegroup,
|
|
776
|
-
}) => {
|
|
777
|
-
await waitForTaskIgnoringAborts(
|
|
778
|
-
barsNtone.unifiedVideoSeekTask.taskComplete,
|
|
779
|
-
);
|
|
780
|
-
await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
|
|
781
|
-
// This is approximately where segment 0 ends and segment 1 begins
|
|
782
|
-
barsNtoneTimegroup.currentTimeMs = 2066;
|
|
783
|
-
await barsNtone.updateComplete;
|
|
784
|
-
|
|
785
|
-
// Wait for the new seek tasks to complete
|
|
786
|
-
await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
|
|
787
|
-
await waitForTaskIgnoringAborts(
|
|
788
|
-
barsNtone.unifiedVideoSeekTask.taskComplete,
|
|
789
|
-
);
|
|
790
|
-
});
|
|
791
|
-
|
|
792
|
-
test("seeks to exact segment boundary at 4033ms", async ({
|
|
793
|
-
barsNtone,
|
|
794
|
-
barsNtoneTimegroup,
|
|
795
|
-
}) => {
|
|
796
|
-
await waitForTaskIgnoringAborts(
|
|
797
|
-
barsNtone.unifiedVideoSeekTask.taskComplete,
|
|
798
|
-
);
|
|
799
|
-
await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
|
|
800
|
-
// This is approximately where segment 1 ends and segment 2 begins
|
|
801
|
-
barsNtoneTimegroup.currentTimeMs = 4033;
|
|
802
|
-
await barsNtone.updateComplete;
|
|
803
|
-
|
|
804
|
-
// Wait for the new seek tasks to complete
|
|
805
|
-
await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
|
|
806
|
-
await waitForTaskIgnoringAborts(
|
|
807
|
-
barsNtone.unifiedVideoSeekTask.taskComplete,
|
|
808
|
-
);
|
|
809
|
-
});
|
|
810
|
-
|
|
811
|
-
test("seeks to exact segment boundary at 6066ms", async ({
|
|
812
|
-
barsNtone,
|
|
813
|
-
barsNtoneTimegroup,
|
|
814
|
-
}) => {
|
|
815
|
-
await waitForTaskIgnoringAborts(
|
|
816
|
-
barsNtone.unifiedVideoSeekTask.taskComplete,
|
|
817
|
-
);
|
|
818
|
-
await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
|
|
819
|
-
// Reset to 0 first to ensure clean state
|
|
820
|
-
barsNtoneTimegroup.currentTimeMs = 0;
|
|
821
|
-
await barsNtone.updateComplete;
|
|
822
|
-
// Wait for both audio and video to complete the reset
|
|
823
|
-
await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
|
|
824
|
-
await waitForTaskIgnoringAborts(
|
|
825
|
-
barsNtone.unifiedVideoSeekTask.taskComplete,
|
|
826
|
-
);
|
|
827
|
-
|
|
828
|
-
// Updated: Use time safely within segment boundaries (6000ms instead of 6066ms)
|
|
829
|
-
// The actual boundary is at 6066.67ms, so 6000ms should be in segment 2
|
|
830
|
-
barsNtoneTimegroup.currentTimeMs = 6000;
|
|
831
|
-
await barsNtone.updateComplete;
|
|
832
|
-
await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
|
|
833
|
-
await waitForTaskIgnoringAborts(
|
|
834
|
-
barsNtone.unifiedVideoSeekTask.taskComplete,
|
|
835
|
-
);
|
|
836
|
-
});
|
|
837
|
-
|
|
838
|
-
test("seeks to exact segment boundary at 8033ms", async ({
|
|
839
|
-
barsNtone,
|
|
840
|
-
barsNtoneTimegroup,
|
|
841
|
-
}) => {
|
|
842
|
-
await waitForTaskIgnoringAborts(
|
|
843
|
-
barsNtone.unifiedVideoSeekTask.taskComplete,
|
|
844
|
-
);
|
|
845
|
-
await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
|
|
846
|
-
// This is approximately where segment 2 ends and segment 3 begins
|
|
847
|
-
barsNtoneTimegroup.currentTimeMs = 8033;
|
|
848
|
-
await barsNtone.updateComplete;
|
|
849
|
-
await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
|
|
850
|
-
await waitForTaskIgnoringAborts(
|
|
851
|
-
barsNtone.unifiedVideoSeekTask.taskComplete,
|
|
852
|
-
);
|
|
853
|
-
});
|
|
854
|
-
|
|
855
|
-
test("seeks to near end of video at 9900ms", async ({
|
|
856
|
-
barsNtone,
|
|
857
|
-
barsNtoneTimegroup,
|
|
858
|
-
}) => {
|
|
859
|
-
await waitForTaskIgnoringAborts(
|
|
860
|
-
barsNtone.unifiedVideoSeekTask.taskComplete,
|
|
861
|
-
);
|
|
862
|
-
await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
|
|
863
|
-
// Seek to near the end of the video
|
|
864
|
-
barsNtoneTimegroup.currentTimeMs = 9900;
|
|
865
|
-
await barsNtone.updateComplete;
|
|
866
|
-
await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
|
|
867
|
-
await waitForTaskIgnoringAborts(
|
|
868
|
-
barsNtone.unifiedVideoSeekTask.taskComplete,
|
|
869
|
-
);
|
|
870
|
-
});
|
|
871
|
-
|
|
872
|
-
test("seeks backward from 8000ms to 2000ms", async ({
|
|
873
|
-
barsNtone,
|
|
874
|
-
barsNtoneTimegroup,
|
|
875
|
-
expect,
|
|
876
|
-
}) => {
|
|
877
|
-
await barsNtoneTimegroup.seek(8000);
|
|
878
|
-
expect(barsNtone.unifiedVideoSeekTask.value?.timestamp).toBeCloseTo(
|
|
879
|
-
8.066,
|
|
880
|
-
);
|
|
881
|
-
|
|
882
|
-
// Then seek backward
|
|
883
|
-
await barsNtoneTimegroup.seek(2000);
|
|
884
|
-
expect(barsNtone.unifiedVideoSeekTask.value?.timestamp).toBeCloseTo(
|
|
885
|
-
2.066,
|
|
886
|
-
);
|
|
887
|
-
});
|
|
888
|
-
|
|
889
|
-
test("seeks to multiple points across segments", async ({
|
|
890
|
-
barsNtone,
|
|
891
|
-
barsNtoneTimegroup,
|
|
892
|
-
}) => {
|
|
893
|
-
await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
|
|
894
|
-
|
|
895
|
-
// Use seek points that are within the actual media duration
|
|
896
|
-
// Based on the fragment index, the media is about 9.8 seconds long
|
|
897
|
-
const seekPoints = [1000, 3000, 5000, 7000, 9000];
|
|
898
|
-
|
|
899
|
-
for (const seekPoint of seekPoints) {
|
|
900
|
-
barsNtoneTimegroup.currentTimeMs = seekPoint;
|
|
901
|
-
await barsNtone.updateComplete;
|
|
902
|
-
await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
|
|
903
|
-
await waitForTaskIgnoringAborts(
|
|
904
|
-
barsNtone.unifiedVideoSeekTask.taskComplete,
|
|
905
|
-
);
|
|
906
|
-
}
|
|
907
|
-
});
|
|
908
|
-
|
|
909
|
-
test("seeks just before segment boundary at 8030ms", async ({
|
|
910
|
-
barsNtone,
|
|
911
|
-
barsNtoneTimegroup,
|
|
912
|
-
}) => {
|
|
913
|
-
await waitForTaskIgnoringAborts(
|
|
914
|
-
barsNtone.unifiedVideoSeekTask.taskComplete,
|
|
915
|
-
);
|
|
916
|
-
await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
|
|
917
|
-
// Use a safe seek time within the media duration
|
|
918
|
-
barsNtoneTimegroup.currentTimeMs = 8030;
|
|
919
|
-
await barsNtone.updateComplete;
|
|
920
|
-
await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
|
|
921
|
-
await waitForTaskIgnoringAborts(
|
|
922
|
-
barsNtone.unifiedVideoSeekTask.taskComplete,
|
|
923
|
-
);
|
|
924
|
-
});
|
|
925
|
-
|
|
926
|
-
test("seeks just after segment boundary at 8070ms", async ({
|
|
927
|
-
barsNtone,
|
|
928
|
-
barsNtoneTimegroup,
|
|
929
|
-
}) => {
|
|
930
|
-
await waitForTaskIgnoringAborts(
|
|
931
|
-
barsNtone.unifiedVideoSeekTask.taskComplete,
|
|
932
|
-
);
|
|
933
|
-
await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
|
|
934
|
-
// Use a safe seek time within the media duration
|
|
935
|
-
barsNtoneTimegroup.currentTimeMs = 8070;
|
|
936
|
-
await barsNtone.updateComplete;
|
|
937
|
-
await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
|
|
938
|
-
await waitForTaskIgnoringAborts(
|
|
939
|
-
barsNtone.unifiedVideoSeekTask.taskComplete,
|
|
940
|
-
);
|
|
941
|
-
});
|
|
942
|
-
|
|
943
|
-
test("handles rapid scrubbing between segments", async ({
|
|
944
|
-
barsNtone,
|
|
945
|
-
barsNtoneTimegroup,
|
|
946
|
-
}) => {
|
|
947
|
-
await waitForTaskIgnoringAborts(
|
|
948
|
-
barsNtone.unifiedVideoSeekTask.taskComplete,
|
|
949
|
-
);
|
|
950
|
-
await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
|
|
951
|
-
|
|
952
|
-
// Simulate rapid scrubbing back and forth across segments
|
|
953
|
-
// Use times that are within the actual media duration
|
|
954
|
-
const scrubSequence = [
|
|
955
|
-
0, // Start
|
|
956
|
-
1000, // Jump to segment 1
|
|
957
|
-
3000, // Back to segment 0
|
|
958
|
-
5000, // Forward to segment 2
|
|
959
|
-
7000, // Back to segment 1
|
|
960
|
-
9000, // Forward to segment 3
|
|
961
|
-
0, // Back to segment 0
|
|
962
|
-
1000, // Jump to segment 1
|
|
963
|
-
];
|
|
964
|
-
|
|
965
|
-
for (const timeMs of scrubSequence) {
|
|
966
|
-
// Don't wait for completion between rapid scrubs to simulate the race condition
|
|
967
|
-
barsNtoneTimegroup.currentTimeMs = timeMs;
|
|
968
|
-
await barsNtone.updateComplete;
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
// Final seek operations should complete without errors
|
|
972
|
-
await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
|
|
973
|
-
await waitForTaskIgnoringAborts(
|
|
974
|
-
barsNtone.unifiedVideoSeekTask.taskComplete,
|
|
975
|
-
);
|
|
976
|
-
});
|
|
977
|
-
|
|
978
|
-
test("handles concurrent seeks to different segments", async ({
|
|
979
|
-
expect,
|
|
980
|
-
barsNtone,
|
|
981
|
-
barsNtoneTimegroup,
|
|
982
|
-
}) => {
|
|
983
|
-
await waitForTaskIgnoringAborts(
|
|
984
|
-
barsNtone.unifiedVideoSeekTask.taskComplete,
|
|
985
|
-
);
|
|
986
|
-
await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
|
|
987
|
-
|
|
988
|
-
// Start multiple seeks without waiting for completion
|
|
989
|
-
const seekPromises = [];
|
|
990
|
-
|
|
991
|
-
// Seek to beginning of segment 0
|
|
992
|
-
barsNtoneTimegroup.currentTimeMs = 100;
|
|
993
|
-
await barsNtone.updateComplete;
|
|
994
|
-
seekPromises.push(
|
|
995
|
-
Promise.allSettled([
|
|
996
|
-
barsNtone.audioSeekTask.taskComplete,
|
|
997
|
-
barsNtone.unifiedVideoSeekTask.taskComplete,
|
|
998
|
-
]),
|
|
999
|
-
);
|
|
1000
|
-
|
|
1001
|
-
// Immediately seek to middle of video (different segment)
|
|
1002
|
-
barsNtoneTimegroup.currentTimeMs = 5000;
|
|
1003
|
-
await barsNtone.updateComplete;
|
|
1004
|
-
seekPromises.push(
|
|
1005
|
-
Promise.allSettled([
|
|
1006
|
-
barsNtone.audioSeekTask.taskComplete,
|
|
1007
|
-
barsNtone.unifiedVideoSeekTask.taskComplete,
|
|
1008
|
-
]),
|
|
1009
|
-
);
|
|
1010
|
-
|
|
1011
|
-
// Immediately seek to end (within valid range)
|
|
1012
|
-
barsNtoneTimegroup.currentTimeMs = 9000;
|
|
1013
|
-
await barsNtone.updateComplete;
|
|
1014
|
-
seekPromises.push(
|
|
1015
|
-
Promise.allSettled([
|
|
1016
|
-
barsNtone.audioSeekTask.taskComplete,
|
|
1017
|
-
barsNtone.unifiedVideoSeekTask.taskComplete,
|
|
1018
|
-
]),
|
|
1019
|
-
);
|
|
1020
|
-
|
|
1021
|
-
// Wait for all seeks to complete
|
|
1022
|
-
const results = await Promise.all(seekPromises);
|
|
1023
|
-
|
|
1024
|
-
// At least the final seek should succeed
|
|
1025
|
-
const finalResults = results[results.length - 1];
|
|
1026
|
-
expect(finalResults).toBeDefined();
|
|
1027
|
-
expect(finalResults?.some((r) => r.status === "fulfilled")).toBe(true);
|
|
1028
|
-
});
|
|
1029
|
-
|
|
1030
|
-
test("recovers from segment range errors during scrubbing", async ({
|
|
1031
|
-
barsNtone,
|
|
1032
|
-
barsNtoneTimegroup,
|
|
1033
|
-
}) => {
|
|
1034
|
-
await waitForTaskIgnoringAborts(
|
|
1035
|
-
barsNtone.unifiedVideoSeekTask.taskComplete,
|
|
1036
|
-
);
|
|
1037
|
-
await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
|
|
1038
|
-
|
|
1039
|
-
// Try to reproduce the exact error scenario
|
|
1040
|
-
// First seek to segment 0
|
|
1041
|
-
barsNtoneTimegroup.currentTimeMs = 1000;
|
|
1042
|
-
await barsNtone.updateComplete;
|
|
1043
|
-
|
|
1044
|
-
// Then immediately seek to a time that would be in segment 2
|
|
1045
|
-
// This might cause the range error if segment 0 is still loaded
|
|
1046
|
-
barsNtoneTimegroup.currentTimeMs = 5000; // Safe time within media duration
|
|
1047
|
-
await barsNtone.updateComplete;
|
|
1048
|
-
|
|
1049
|
-
// The system should recover and eventually succeed
|
|
1050
|
-
await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
|
|
1051
|
-
await waitForTaskIgnoringAborts(
|
|
1052
|
-
barsNtone.unifiedVideoSeekTask.taskComplete,
|
|
1053
|
-
);
|
|
1054
|
-
});
|
|
1055
|
-
|
|
1056
|
-
test("seeks to 7975ms", async ({
|
|
1057
|
-
barsNtone,
|
|
1058
|
-
barsNtoneTimegroup,
|
|
1059
|
-
expect,
|
|
1060
|
-
}) => {
|
|
1061
|
-
await barsNtoneTimegroup.seek(7975);
|
|
1062
|
-
|
|
1063
|
-
expect(barsNtone.unifiedVideoSeekTask.value?.timestamp).toBeCloseTo(
|
|
1064
|
-
8.033,
|
|
1065
|
-
);
|
|
1066
|
-
});
|
|
1067
|
-
|
|
1068
|
-
test("seeks to 8041.667ms in video track 1", async ({
|
|
1069
|
-
barsNtone,
|
|
1070
|
-
barsNtoneTimegroup,
|
|
1071
|
-
expect,
|
|
1072
|
-
}) => {
|
|
1073
|
-
await barsNtoneTimegroup.seekTask.taskComplete;
|
|
1074
|
-
await barsNtoneTimegroup.seek(8041.667);
|
|
1075
|
-
expect(barsNtone.unifiedVideoSeekTask.value?.timestamp).toBe(8.1);
|
|
1076
|
-
});
|
|
1077
|
-
|
|
1078
|
-
test("seeks to 10000ms near end of file", async ({
|
|
1079
|
-
barsNtone,
|
|
1080
|
-
barsNtoneTimegroup,
|
|
1081
|
-
}) => {
|
|
1082
|
-
await waitForTaskIgnoringAborts(
|
|
1083
|
-
barsNtone.unifiedVideoSeekTask.taskComplete,
|
|
1084
|
-
);
|
|
1085
|
-
await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
|
|
1086
|
-
|
|
1087
|
-
// Use a safe seek time within the media duration
|
|
1088
|
-
// The original test was trying to seek to 10000ms which is outside the valid range
|
|
1089
|
-
barsNtoneTimegroup.currentTimeMs = 10000;
|
|
1090
|
-
await barsNtone.updateComplete;
|
|
1091
|
-
|
|
1092
|
-
// Should not throw "Sample not found" errors
|
|
1093
|
-
await waitForTaskIgnoringAborts(
|
|
1094
|
-
barsNtone.unifiedVideoSeekTask.taskComplete,
|
|
1095
|
-
);
|
|
1096
|
-
await waitForTaskIgnoringAborts(barsNtone.audioSeekTask.taskComplete);
|
|
1097
|
-
});
|
|
1098
|
-
});
|
|
1099
|
-
|
|
1100
|
-
describe("JIT Transcoder", () => {
|
|
1101
|
-
test("seeks to start at 0ms", async ({
|
|
1102
|
-
timegroup,
|
|
1103
|
-
headMoov480p,
|
|
1104
|
-
expect,
|
|
1105
|
-
}) => {
|
|
1106
|
-
await waitForTaskIgnoringAborts(
|
|
1107
|
-
headMoov480p.unifiedVideoSeekTask.taskComplete,
|
|
1108
|
-
);
|
|
1109
|
-
await waitForTaskIgnoringAborts(headMoov480p.audioSeekTask.taskComplete);
|
|
1110
|
-
|
|
1111
|
-
timegroup.currentTimeMs = 0;
|
|
1112
|
-
await timegroup.seekTask.taskComplete;
|
|
1113
|
-
|
|
1114
|
-
expect(headMoov480p.unifiedVideoSeekTask.value?.timestamp).toBe(0);
|
|
1115
|
-
});
|
|
1116
|
-
|
|
1117
|
-
test("seeks to 1000ms", async ({ timegroup, headMoov480p, expect }) => {
|
|
1118
|
-
await timegroup.seek(1000);
|
|
1119
|
-
expect(headMoov480p.unifiedVideoSeekTask.value?.timestamp).toBe(1);
|
|
1120
|
-
});
|
|
1121
|
-
|
|
1122
|
-
test("seeks to 3000ms", async ({ timegroup, headMoov480p, expect }) => {
|
|
1123
|
-
await timegroup.seek(3000);
|
|
1124
|
-
expect(headMoov480p.unifiedVideoSeekTask.value?.timestamp).toBe(3);
|
|
1125
|
-
});
|
|
1126
|
-
|
|
1127
|
-
test("seeks to 5000ms", async ({ timegroup, headMoov480p, expect }) => {
|
|
1128
|
-
await timegroup.seek(5000);
|
|
1129
|
-
expect(headMoov480p.unifiedVideoSeekTask.value?.timestamp).toBe(5);
|
|
1130
|
-
});
|
|
1131
|
-
|
|
1132
|
-
test("seeks to 7500ms", async ({ timegroup, headMoov480p, expect }) => {
|
|
1133
|
-
await timegroup.seek(7500);
|
|
1134
|
-
|
|
1135
|
-
// JIT transcoding returns actual video frame timestamps, not idealized segment boundaries
|
|
1136
|
-
expect(headMoov480p.unifiedVideoSeekTask.value?.timestamp).toBeCloseTo(
|
|
1137
|
-
7.5,
|
|
1138
|
-
1,
|
|
1139
|
-
);
|
|
1140
|
-
});
|
|
1141
|
-
|
|
1142
|
-
test("seeks to 8500ms", async ({ timegroup, headMoov480p, expect }) => {
|
|
1143
|
-
await timegroup.seek(8500);
|
|
1144
|
-
|
|
1145
|
-
expect(headMoov480p.unifiedVideoSeekTask.value?.timestamp).toBeCloseTo(
|
|
1146
|
-
8.5,
|
|
1147
|
-
1,
|
|
1148
|
-
);
|
|
1149
|
-
});
|
|
1150
|
-
|
|
1151
|
-
test("seeks to near end at 9000ms", async ({
|
|
1152
|
-
timegroup,
|
|
1153
|
-
headMoov480p,
|
|
1154
|
-
expect,
|
|
1155
|
-
}) => {
|
|
1156
|
-
await timegroup.seek(9000);
|
|
1157
|
-
|
|
1158
|
-
expect(headMoov480p.unifiedVideoSeekTask.value?.timestamp).toBe(9);
|
|
1159
|
-
});
|
|
1160
|
-
|
|
1161
|
-
test("seeks backward from 7000ms to 2000ms", async ({
|
|
1162
|
-
timegroup,
|
|
1163
|
-
headMoov480p,
|
|
1164
|
-
expect,
|
|
1165
|
-
}) => {
|
|
1166
|
-
await timegroup.seek(7000);
|
|
1167
|
-
expect(headMoov480p.unifiedVideoSeekTask.value?.timestamp).toBe(7);
|
|
1168
|
-
|
|
1169
|
-
await timegroup.seek(2000);
|
|
1170
|
-
expect(headMoov480p.unifiedVideoSeekTask.value?.timestamp).toBe(2);
|
|
1171
|
-
});
|
|
1172
|
-
|
|
1173
|
-
test("seeks to multiple points in sequence", async ({
|
|
1174
|
-
timegroup,
|
|
1175
|
-
headMoov480p,
|
|
1176
|
-
expect,
|
|
1177
|
-
}) => {
|
|
1178
|
-
const seekPoints = [1000, 3000, 5000, 2000, 6000, 0];
|
|
1179
|
-
const expectedTimestamps = [1, 3, 5, 2, 6, 0];
|
|
1180
|
-
|
|
1181
|
-
for (let i = 0; i < seekPoints.length; i++) {
|
|
1182
|
-
await timegroup.seek(seekPoints[i]!);
|
|
1183
|
-
expect(headMoov480p.unifiedVideoSeekTask.value?.timestamp).toBeCloseTo(
|
|
1184
|
-
expectedTimestamps[i]!,
|
|
1185
|
-
1,
|
|
1186
|
-
);
|
|
1187
|
-
}
|
|
1188
|
-
});
|
|
1189
|
-
|
|
1190
|
-
test("seeks to fractional timestamps", async ({
|
|
1191
|
-
timegroup,
|
|
1192
|
-
headMoov480p,
|
|
1193
|
-
expect,
|
|
1194
|
-
}) => {
|
|
1195
|
-
const fractionalTimes = [1234.567, 3456.789, 5678.901];
|
|
1196
|
-
const expectedTimestamps = [1.234567, 3.456789, 5.678901];
|
|
1197
|
-
|
|
1198
|
-
for (let i = 0; i < fractionalTimes.length; i++) {
|
|
1199
|
-
await timegroup.seek(fractionalTimes[i]!);
|
|
1200
|
-
expect(headMoov480p.unifiedVideoSeekTask.value?.timestamp).toBeCloseTo(
|
|
1201
|
-
expectedTimestamps[i]!,
|
|
1202
|
-
1,
|
|
1203
|
-
);
|
|
1204
|
-
}
|
|
1205
|
-
});
|
|
1206
|
-
|
|
1207
|
-
test("frame tasks are not complete until internal video seek is complete", async ({
|
|
1208
|
-
timegroup,
|
|
1209
|
-
headMoov480p,
|
|
1210
|
-
expect,
|
|
1211
|
-
}) => {
|
|
1212
|
-
await timegroup.seek(0);
|
|
1213
|
-
expect(headMoov480p.unifiedVideoSeekTask.value?.timestamp).toBeCloseTo(
|
|
1214
|
-
0,
|
|
1215
|
-
1,
|
|
1216
|
-
);
|
|
1217
|
-
|
|
1218
|
-
await timegroup.seek(1000);
|
|
1219
|
-
expect(headMoov480p.unifiedVideoSeekTask.value?.timestamp).toBeCloseTo(
|
|
1220
|
-
1,
|
|
1221
|
-
1,
|
|
1222
|
-
);
|
|
1223
|
-
|
|
1224
|
-
await timegroup.seek(4000);
|
|
1225
|
-
expect(headMoov480p.unifiedVideoSeekTask.value?.timestamp).toBeCloseTo(
|
|
1226
|
-
4,
|
|
1227
|
-
1,
|
|
1228
|
-
);
|
|
1229
|
-
});
|
|
1230
|
-
|
|
1231
|
-
test("rapid succession seeks cause intermediate seeks to be skipped", async ({
|
|
1232
|
-
timegroup,
|
|
1233
|
-
headMoov480p,
|
|
1234
|
-
expect,
|
|
1235
|
-
}) => {
|
|
1236
|
-
await timegroup.seek(1000);
|
|
1237
|
-
expect(headMoov480p.unifiedVideoSeekTask.value?.timestamp).toBe(1);
|
|
1238
|
-
|
|
1239
|
-
// // Track frameTask executions using a spy on the run method
|
|
1240
|
-
// const runSpy = vi.spyOn(timegroup.frameTask, 'run');
|
|
1241
|
-
|
|
1242
|
-
// // Rapid succession of seeks - intermediate ones should be skipped
|
|
1243
|
-
// timegroup.currentTimeMs = 1000;
|
|
1244
|
-
// timegroup.currentTimeMs = 2000;
|
|
1245
|
-
// timegroup.currentTimeMs = 3000;
|
|
1246
|
-
// timegroup.currentTimeMs = 4000;
|
|
1247
|
-
// timegroup.currentTimeMs = 1000;
|
|
1248
|
-
// timegroup.currentTimeMs = 2000;
|
|
1249
|
-
// timegroup.currentTimeMs = 3000;
|
|
1250
|
-
// timegroup.currentTimeMs = 8000;
|
|
1251
|
-
|
|
1252
|
-
// await timegroup.seekTask.taskComplete;
|
|
1253
|
-
// expect(headMoov480p.unifiedVideoSeekTask.value?.timestamp).toBe(8);
|
|
1254
|
-
// expect(runSpy).toHaveBeenCalledTimes(3);
|
|
1255
|
-
});
|
|
1256
|
-
});
|
|
1257
|
-
|
|
1258
|
-
describe("audio analysis tasks with timeline sequences", () => {
|
|
1259
|
-
test("should handle audio analysis when seeking into second video in sequence", async ({
|
|
1260
|
-
sequenceTimegroup,
|
|
1261
|
-
}) => {
|
|
1262
|
-
// Use the sequence fixture which creates two videos in sequence
|
|
1263
|
-
const videos = sequenceTimegroup.querySelectorAll(
|
|
1264
|
-
"ef-video",
|
|
1265
|
-
) as NodeListOf<EFVideo>;
|
|
1266
|
-
const video1 = videos[0]!;
|
|
1267
|
-
const video2 = videos[1]!;
|
|
1268
|
-
|
|
1269
|
-
// Wait for initial loading
|
|
1270
|
-
await waitForTaskIgnoringAborts(video1.unifiedVideoSeekTask.taskComplete);
|
|
1271
|
-
await waitForTaskIgnoringAborts(video1.audioSeekTask.taskComplete);
|
|
1272
|
-
await waitForTaskIgnoringAborts(video2.unifiedVideoSeekTask.taskComplete);
|
|
1273
|
-
await waitForTaskIgnoringAborts(video2.audioSeekTask.taskComplete);
|
|
1274
|
-
|
|
1275
|
-
// Get the duration of the first video to know where the second video starts
|
|
1276
|
-
const firstVideoDuration = video1.intrinsicDurationMs || 10000;
|
|
1277
|
-
|
|
1278
|
-
// Seek into the second video (after the first one ends)
|
|
1279
|
-
const secondVideoSeekTime = firstVideoDuration + 1000;
|
|
1280
|
-
sequenceTimegroup.currentTimeMs = secondVideoSeekTime;
|
|
1281
|
-
await sequenceTimegroup.updateComplete;
|
|
1282
|
-
|
|
1283
|
-
// Both videos should handle the timeline positioning correctly
|
|
1284
|
-
await waitForTaskIgnoringAborts(video1.audioSeekTask.taskComplete);
|
|
1285
|
-
await waitForTaskIgnoringAborts(video2.audioSeekTask.taskComplete);
|
|
1286
|
-
});
|
|
1287
|
-
|
|
1288
|
-
test("fixed: JIT transcoding off-by-one bug for exact duration seeks", async ({
|
|
1289
|
-
headMoov480p, // This uses JIT transcoding, not asset transcoding
|
|
1290
|
-
}) => {
|
|
1291
|
-
// This test verifies the fix for the off-by-one bug in JitMediaEngine.computeSegmentId
|
|
1292
|
-
const timegroup = headMoov480p.closest("ef-timegroup") as EFTimegroup;
|
|
1293
|
-
|
|
1294
|
-
// Wait for initial loading to complete
|
|
1295
|
-
await waitForTaskIgnoringAborts(
|
|
1296
|
-
headMoov480p.unifiedVideoSeekTask.taskComplete,
|
|
1297
|
-
);
|
|
1298
|
-
await waitForTaskIgnoringAborts(headMoov480p.audioSeekTask.taskComplete);
|
|
1299
|
-
|
|
1300
|
-
// The fix: JitMediaEngine.computeSegmentId should handle seeking to exact duration
|
|
1301
|
-
// Before fix: if (desiredSeekTimeMs >= this.durationMs) { return undefined; } โ
|
|
1302
|
-
// After fix: if (desiredSeekTimeMs > this.durationMs) { return undefined; } โ
|
|
1303
|
-
|
|
1304
|
-
// Get the media engine to verify it's JIT transcoding
|
|
1305
|
-
const mediaEngine = headMoov480p.mediaEngineTask.value;
|
|
1306
|
-
|
|
1307
|
-
if (mediaEngine?.constructor.name === "JitMediaEngine") {
|
|
1308
|
-
// Test seeking to exact duration - this should NOT fail with "Segment ID is not available"
|
|
1309
|
-
const exactDuration = headMoov480p.intrinsicDurationMs;
|
|
1310
|
-
|
|
1311
|
-
timegroup.currentTimeMs = exactDuration;
|
|
1312
|
-
await headMoov480p.updateComplete;
|
|
1313
|
-
|
|
1314
|
-
// This should now work without throwing "Segment ID is not available"
|
|
1315
|
-
await waitForTaskIgnoringAborts(
|
|
1316
|
-
headMoov480p.unifiedVideoSeekTask.taskComplete,
|
|
1317
|
-
);
|
|
1318
|
-
await waitForTaskIgnoringAborts(
|
|
1319
|
-
headMoov480p.audioSeekTask.taskComplete,
|
|
1320
|
-
);
|
|
1321
|
-
}
|
|
1322
|
-
});
|
|
1323
|
-
|
|
1324
|
-
test("FIXED: audio analysis tasks handle out-of-bounds time ranges gracefully", async ({
|
|
1325
|
-
headMoov480p,
|
|
1326
|
-
}) => {
|
|
1327
|
-
// This test verifies the fix for "No segments found for time range 10000-15000ms" error
|
|
1328
|
-
const timegroup = headMoov480p.closest("ef-timegroup") as EFTimegroup;
|
|
1329
|
-
|
|
1330
|
-
// Wait for initial loading to complete
|
|
1331
|
-
await waitForTaskIgnoringAborts(
|
|
1332
|
-
headMoov480p.unifiedVideoSeekTask.taskComplete,
|
|
1333
|
-
);
|
|
1334
|
-
await waitForTaskIgnoringAborts(headMoov480p.audioSeekTask.taskComplete);
|
|
1335
|
-
|
|
1336
|
-
console.log("๐งช TESTING: Audio analysis out-of-bounds time range fix");
|
|
1337
|
-
console.log(`๐ Video duration: ${headMoov480p.intrinsicDurationMs}ms`);
|
|
1338
|
-
|
|
1339
|
-
// Seek to exactly the end of the video to trigger the audio analysis tasks
|
|
1340
|
-
const exactDuration = headMoov480p.intrinsicDurationMs; // Should be 10000ms
|
|
1341
|
-
timegroup.currentTimeMs = exactDuration;
|
|
1342
|
-
await headMoov480p.updateComplete;
|
|
1343
|
-
|
|
1344
|
-
// The fix: audio analysis tasks should now clamp their time ranges to video duration
|
|
1345
|
-
// Before fix: requested "10000-15000ms" โ "No segments found" error
|
|
1346
|
-
// After fix: requested "10000-10000ms" โ gracefully skipped or clamped to available range
|
|
1347
|
-
|
|
1348
|
-
console.log(
|
|
1349
|
-
`๐ฏ EXPECTED FIX: Audio analysis tasks should clamp range to ${exactDuration}-${exactDuration}ms`,
|
|
1350
|
-
);
|
|
1351
|
-
console.log("๐ฏ Or gracefully skip analysis when seeking beyond end");
|
|
1352
|
-
|
|
1353
|
-
// Let the audio analysis tasks run - they should now handle this gracefully
|
|
1354
|
-
|
|
1355
|
-
// The basic seek should complete without errors
|
|
1356
|
-
await waitForTaskIgnoringAborts(
|
|
1357
|
-
headMoov480p.unifiedVideoSeekTask.taskComplete,
|
|
1358
|
-
);
|
|
1359
|
-
|
|
1360
|
-
// Audio tasks may still throw their own errors, but not the "No segments found" error
|
|
1361
|
-
// We don't explicitly test the audio analysis tasks here since they might legitimately
|
|
1362
|
-
// return null when seeking beyond the end, which is the expected behavior
|
|
1363
|
-
});
|
|
1364
|
-
|
|
1365
|
-
test("FIXED: rapid seeking race condition handled gracefully", async ({
|
|
1366
|
-
expect,
|
|
1367
|
-
headMoov480p,
|
|
1368
|
-
}) => {
|
|
1369
|
-
// This test verifies the fix for race condition where rapid seeks cause
|
|
1370
|
-
// "Seek time Xms is before track start Yms" errors
|
|
1371
|
-
const timegroup = headMoov480p.closest("ef-timegroup") as EFTimegroup;
|
|
1372
|
-
|
|
1373
|
-
// Wait for initial loading to complete
|
|
1374
|
-
await waitForTaskIgnoringAborts(
|
|
1375
|
-
headMoov480p.unifiedVideoSeekTask.taskComplete,
|
|
1376
|
-
);
|
|
1377
|
-
await waitForTaskIgnoringAborts(headMoov480p.audioSeekTask.taskComplete);
|
|
1378
|
-
|
|
1379
|
-
console.log("๐งช TESTING: Rapid seeking race condition fix");
|
|
1380
|
-
console.log(`๐ Video duration: ${headMoov480p.intrinsicDurationMs}ms`);
|
|
1381
|
-
|
|
1382
|
-
// Simulate rapid seeking that previously caused race conditions
|
|
1383
|
-
// Now should be handled gracefully with warnings instead of errors
|
|
1384
|
-
const rapidSeekSequence = [
|
|
1385
|
-
2000, // Start at 2s
|
|
1386
|
-
7000, // Jump to 7s
|
|
1387
|
-
1000, // Back to 1s (previously caused race condition)
|
|
1388
|
-
8000, // Jump to 8s
|
|
1389
|
-
500, // Back to 0.5s (previously caused race condition)
|
|
1390
|
-
5000, // Jump to 5s
|
|
1391
|
-
];
|
|
1392
|
-
|
|
1393
|
-
for (const seekTime of rapidSeekSequence) {
|
|
1394
|
-
timegroup.currentTimeMs = seekTime;
|
|
1395
|
-
await headMoov480p.updateComplete;
|
|
1396
|
-
}
|
|
1397
|
-
|
|
1398
|
-
// The fix should prevent errors - both video and audio tasks should complete
|
|
1399
|
-
await waitForTaskIgnoringAborts(
|
|
1400
|
-
headMoov480p.unifiedVideoSeekTask.taskComplete,
|
|
1401
|
-
);
|
|
1402
|
-
|
|
1403
|
-
// Audio tasks should also complete without throwing, though they may log warnings
|
|
1404
|
-
await waitForTaskIgnoringAborts(headMoov480p.audioSeekTask.taskComplete);
|
|
1405
|
-
|
|
1406
|
-
// Test passes if we reach here without unhandled errors
|
|
1407
|
-
expect(true).toBe(true);
|
|
1408
|
-
});
|
|
1409
|
-
});
|
|
1410
|
-
|
|
1411
|
-
describe("loop attribute", () => {
|
|
1412
|
-
test(
|
|
1413
|
-
"standalone ef-video respects loop attribute",
|
|
1414
|
-
{ timeout: 1000 },
|
|
1415
|
-
async () => {
|
|
1416
|
-
const container = document.createElement("div");
|
|
1417
|
-
render(
|
|
1418
|
-
html`
|
|
1419
|
-
<ef-video
|
|
1420
|
-
loop
|
|
1421
|
-
id="loop-video"
|
|
1422
|
-
src="bars-n-tone.mp4"
|
|
1423
|
-
sourceout="2s"
|
|
1424
|
-
></ef-video>
|
|
1425
|
-
`,
|
|
1426
|
-
container,
|
|
1427
|
-
);
|
|
1428
|
-
document.body.appendChild(container);
|
|
1429
|
-
|
|
1430
|
-
const video = container.querySelector("#loop-video") as EFVideo;
|
|
1431
|
-
await video.updateComplete;
|
|
1432
|
-
|
|
1433
|
-
expect(video.loop).toBe(true);
|
|
1434
|
-
expect(video.playbackController).toBeDefined();
|
|
1435
|
-
expect(video.playbackController?.loop).toBe(true);
|
|
1436
|
-
|
|
1437
|
-
container.remove();
|
|
1438
|
-
},
|
|
1439
|
-
);
|
|
1440
|
-
|
|
1441
|
-
test(
|
|
1442
|
-
"loop property is reactive after initialization",
|
|
1443
|
-
{ timeout: 1000 },
|
|
1444
|
-
async () => {
|
|
1445
|
-
const container = document.createElement("div");
|
|
1446
|
-
render(
|
|
1447
|
-
html`
|
|
1448
|
-
<ef-video
|
|
1449
|
-
id="reactive-loop-video"
|
|
1450
|
-
src="bars-n-tone.mp4"
|
|
1451
|
-
sourceout="2s"
|
|
1452
|
-
></ef-video>
|
|
1453
|
-
`,
|
|
1454
|
-
container,
|
|
1455
|
-
);
|
|
1456
|
-
document.body.appendChild(container);
|
|
1457
|
-
|
|
1458
|
-
const video = container.querySelector(
|
|
1459
|
-
"#reactive-loop-video",
|
|
1460
|
-
) as EFVideo;
|
|
1461
|
-
await video.updateComplete;
|
|
1462
|
-
|
|
1463
|
-
expect(video.loop).toBe(false);
|
|
1464
|
-
expect(video.playbackController?.loop).toBe(false);
|
|
1465
|
-
|
|
1466
|
-
video.loop = true;
|
|
1467
|
-
await video.updateComplete;
|
|
1468
|
-
|
|
1469
|
-
expect(video.loop).toBe(true);
|
|
1470
|
-
expect(video.playbackController?.loop).toBe(true);
|
|
1471
|
-
|
|
1472
|
-
video.loop = false;
|
|
1473
|
-
await video.updateComplete;
|
|
1474
|
-
|
|
1475
|
-
expect(video.loop).toBe(false);
|
|
1476
|
-
expect(video.playbackController?.loop).toBe(false);
|
|
1477
|
-
|
|
1478
|
-
container.remove();
|
|
1479
|
-
},
|
|
1480
|
-
);
|
|
1481
|
-
});
|
|
1482
|
-
});
|