@editframe/elements 0.18.21-beta.0 → 0.18.23-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/elements/EFAudio.d.ts +1 -12
- package/dist/elements/EFAudio.js +3 -18
- package/dist/elements/EFMedia/AssetMediaEngine.d.ts +1 -1
- package/dist/elements/EFMedia/AssetMediaEngine.js +3 -3
- package/dist/elements/EFMedia/BufferedSeekingInput.d.ts +15 -9
- package/dist/elements/EFMedia/BufferedSeekingInput.js +76 -78
- package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js +12 -10
- package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js +2 -18
- package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js +12 -10
- package/dist/elements/EFTimegroup.d.ts +4 -4
- package/dist/elements/EFTimegroup.js +52 -39
- package/dist/elements/EFVideo.d.ts +1 -32
- package/dist/elements/EFVideo.js +13 -51
- package/dist/elements/SampleBuffer.js +1 -1
- package/package.json +2 -2
- package/src/elements/EFAudio.browsertest.ts +0 -3
- package/src/elements/EFAudio.ts +3 -22
- package/src/elements/EFMedia/AssetMediaEngine.browsertest.ts +39 -1
- package/src/elements/EFMedia/AssetMediaEngine.ts +5 -4
- package/src/elements/EFMedia/BufferedSeekingInput.browsertest.ts +90 -185
- package/src/elements/EFMedia/BufferedSeekingInput.ts +119 -130
- package/src/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.ts +21 -21
- package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.chunkboundary.regression.browsertest.ts +10 -5
- package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.ts +33 -34
- package/src/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.ts +22 -20
- package/src/elements/EFMedia/videoTasks/makeVideoSeekTask.ts +0 -3
- package/src/elements/EFMedia.browsertest.ts +72 -60
- package/src/elements/EFTimegroup.browsertest.ts +9 -4
- package/src/elements/EFTimegroup.ts +79 -55
- package/src/elements/EFVideo.browsertest.ts +172 -160
- package/src/elements/EFVideo.ts +17 -73
- package/src/elements/SampleBuffer.ts +1 -2
- package/test/EFVideo.framegen.browsertest.ts +0 -54
- package/types.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { css } from "lit";
|
|
2
2
|
import { customElement } from "lit/decorators.js";
|
|
3
3
|
import type { VideoSample } from "mediabunny";
|
|
4
|
-
import { describe, vi } from "vitest";
|
|
4
|
+
import { afterEach, beforeEach, describe, vi } from "vitest";
|
|
5
5
|
import { test as baseTest } from "../../test/useMSW.js";
|
|
6
6
|
|
|
7
7
|
import type { EFConfiguration } from "../gui/EFConfiguration.js";
|
|
@@ -97,15 +97,11 @@ describe("JIT Media Engine", () => {
|
|
|
97
97
|
jitVideo,
|
|
98
98
|
expect,
|
|
99
99
|
}) => {
|
|
100
|
-
|
|
100
|
+
await timegroup.waitForMediaDurations();
|
|
101
101
|
timegroup.currentTimeMs = 2200;
|
|
102
|
-
|
|
102
|
+
await timegroup.seekTask.taskComplete;
|
|
103
103
|
const sample = await jitVideo.videoSeekTask.taskComplete;
|
|
104
|
-
|
|
105
|
-
expect(sample).toBeDefined();
|
|
106
|
-
// Based on the pattern: 0ms→0, 3000ms→2.96, 5000ms→4.96
|
|
107
|
-
// For 2200ms, we expect timestamp 2.16
|
|
108
|
-
expect(sample?.timestamp).toEqual(2.16);
|
|
104
|
+
expect(sample?.timestamp).toBeCloseTo(2.2, 1);
|
|
109
105
|
});
|
|
110
106
|
});
|
|
111
107
|
|
|
@@ -115,8 +111,25 @@ describe("JIT Media Engine", () => {
|
|
|
115
111
|
jitVideo,
|
|
116
112
|
expect,
|
|
117
113
|
}) => {
|
|
114
|
+
// Debug: Check what segment should be loaded for 0ms
|
|
115
|
+
const mediaEngine = await (jitVideo as any).mediaEngineTask.taskComplete;
|
|
116
|
+
const videoRendition = mediaEngine?.getVideoRendition();
|
|
117
|
+
const expectedSegmentId = mediaEngine?.computeSegmentId(
|
|
118
|
+
0,
|
|
119
|
+
videoRendition,
|
|
120
|
+
);
|
|
121
|
+
console.log(`MediaEngine.computeSegmentId(0ms) = ${expectedSegmentId}`);
|
|
122
|
+
|
|
118
123
|
timegroup.currentTimeMs = 0;
|
|
124
|
+
await timegroup.seekTask.taskComplete;
|
|
125
|
+
|
|
126
|
+
// Check what segment actually got loaded
|
|
127
|
+
const actualSegmentId = (jitVideo as any).videoSegmentIdTask.value;
|
|
128
|
+
console.log(`videoSegmentIdTask.value = ${actualSegmentId}`);
|
|
129
|
+
|
|
119
130
|
const frame = await (jitVideo as any).videoSeekTask.taskComplete;
|
|
131
|
+
console.log(`Frame timestamp when seeking to 0ms: ${frame?.timestamp}`);
|
|
132
|
+
|
|
120
133
|
expect(frame).toBeDefined();
|
|
121
134
|
expect(frame?.timestamp).toEqual(0);
|
|
122
135
|
});
|
|
@@ -126,11 +139,11 @@ describe("JIT Media Engine", () => {
|
|
|
126
139
|
jitVideo,
|
|
127
140
|
expect,
|
|
128
141
|
}) => {
|
|
142
|
+
await timegroup.waitForMediaDurations();
|
|
129
143
|
timegroup.currentTimeMs = 3_000;
|
|
130
|
-
|
|
131
|
-
const frame = await
|
|
132
|
-
expect(frame).
|
|
133
|
-
expect(frame?.timestamp).toEqual(2.96); // Updated: improved mediabunny processing changed frame timing
|
|
144
|
+
await timegroup.seekTask.taskComplete;
|
|
145
|
+
const frame = await jitVideo.videoSeekTask.taskComplete;
|
|
146
|
+
expect(frame?.timestamp).toBeCloseTo(3, 1);
|
|
134
147
|
});
|
|
135
148
|
|
|
136
149
|
test("seeks to 5 seconds and loads frame", async ({
|
|
@@ -138,11 +151,11 @@ describe("JIT Media Engine", () => {
|
|
|
138
151
|
jitVideo,
|
|
139
152
|
expect,
|
|
140
153
|
}) => {
|
|
154
|
+
await timegroup.waitForMediaDurations();
|
|
141
155
|
timegroup.currentTimeMs = 5_000;
|
|
142
|
-
|
|
143
|
-
const frame = await
|
|
144
|
-
expect(frame).
|
|
145
|
-
expect(frame?.timestamp).toEqual(4.96); // Updated: improved mediabunny processing changed frame timing
|
|
156
|
+
await timegroup.seekTask.taskComplete;
|
|
157
|
+
const frame = await jitVideo.videoSeekTask.taskComplete;
|
|
158
|
+
expect(frame?.timestamp).toBeCloseTo(5, 1);
|
|
146
159
|
});
|
|
147
160
|
|
|
148
161
|
test("seeks ahead in 50ms increments", async ({
|
|
@@ -150,48 +163,47 @@ describe("JIT Media Engine", () => {
|
|
|
150
163
|
jitVideo,
|
|
151
164
|
expect,
|
|
152
165
|
}) => {
|
|
166
|
+
await timegroup.waitForMediaDurations();
|
|
153
167
|
timegroup.currentTimeMs = 0;
|
|
154
168
|
let frame: VideoSample | undefined;
|
|
155
169
|
for (let i = 0; i <= 3000; i += 50) {
|
|
156
170
|
timegroup.currentTimeMs = i;
|
|
157
|
-
|
|
158
|
-
frame = await
|
|
171
|
+
await timegroup.seekTask.taskComplete;
|
|
172
|
+
frame = await jitVideo.videoSeekTask.taskComplete;
|
|
159
173
|
expect(frame).toBeDefined();
|
|
160
174
|
}
|
|
161
|
-
expect(frame?.timestamp).
|
|
175
|
+
expect(frame?.timestamp).toBeCloseTo(3, 1);
|
|
162
176
|
});
|
|
163
177
|
});
|
|
164
178
|
|
|
165
179
|
describe("boundary seeking", () => {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
// const segmentId2 = await jitVideo.audioSegmentIdTask.taskComplete
|
|
176
|
-
// console.log({ segmentId2, start2, end2 })
|
|
177
|
-
|
|
178
|
-
// timegroup.currentTimeMs = 2.0266666666666664 * 1000
|
|
179
|
-
// jitVideo.desiredSeekTimeMs = 2.0266666666666664 * 1000
|
|
180
|
-
// await jitVideo.videoSeekTask.taskComplete
|
|
181
|
-
// const segment3 = await jitVideo.audioInputTask.taskComplete;
|
|
182
|
-
// const segment3Audio = await segment3.getFirstAudioTrack()
|
|
183
|
-
// const start3 = await segment3Audio?.getFirstTimestamp();
|
|
184
|
-
// const end3 = await segment3Audio?.computeDuration();
|
|
185
|
-
// const segmentId3 = await jitVideo.audioSegmentIdTask.taskComplete;
|
|
186
|
-
// console.log({ segmentId3, start3, end3 })
|
|
187
|
-
// await expect(jitVideo.videoSegmentIdTask.taskComplete).resolves.toBe(2);
|
|
188
|
-
// });
|
|
180
|
+
test.skip("segment 2 track range and segment 3 track range have no gap between them", async ({
|
|
181
|
+
expect,
|
|
182
|
+
jitVideo,
|
|
183
|
+
timegroup,
|
|
184
|
+
}) => {
|
|
185
|
+
// SKIP: audioSeekTask is not part of the audio rendering pipeline
|
|
186
|
+
await timegroup.waitForMediaDurations();
|
|
187
|
+
timegroup.currentTimeMs = 1000;
|
|
188
|
+
await timegroup.frameTask.taskComplete;
|
|
189
189
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
190
|
+
timegroup.currentTimeMs = 2026.6666666666663;
|
|
191
|
+
await timegroup.frameTask.taskComplete;
|
|
192
|
+
const sample = await jitVideo.videoSeekTask.taskComplete;
|
|
193
|
+
expect(sample?.timestamp).toBeCloseTo(2, 1);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("Can seek audio to 4025.0000000000005ms in head-moov-480p.mp4", async ({
|
|
197
|
+
expect,
|
|
198
|
+
jitVideo,
|
|
199
|
+
timegroup,
|
|
200
|
+
}) => {
|
|
201
|
+
await timegroup.waitForMediaDurations();
|
|
202
|
+
timegroup.currentTimeMs = 2026.6666666666663;
|
|
203
|
+
await expect(
|
|
204
|
+
jitVideo.audioSeekTask.taskComplete,
|
|
205
|
+
).resolves.to.not.toThrowError();
|
|
206
|
+
});
|
|
195
207
|
|
|
196
208
|
test("can seek audio to 4050ms in head-moov-480p.mp4", async ({
|
|
197
209
|
expect,
|
|
@@ -213,20 +225,20 @@ describe("JIT Media Engine", () => {
|
|
|
213
225
|
});
|
|
214
226
|
|
|
215
227
|
describe("EFMedia", () => {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
228
|
+
beforeEach(() => {
|
|
229
|
+
// Clean up DOM
|
|
230
|
+
while (document.body.children.length) {
|
|
231
|
+
document.body.children[0]?.remove();
|
|
232
|
+
}
|
|
233
|
+
});
|
|
222
234
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
235
|
+
afterEach(() => {
|
|
236
|
+
// Clean up any remaining elements
|
|
237
|
+
const elements = document.querySelectorAll("test-media");
|
|
238
|
+
for (const element of elements) {
|
|
239
|
+
element.remove();
|
|
240
|
+
}
|
|
241
|
+
});
|
|
230
242
|
|
|
231
243
|
const test = baseTest.extend<{
|
|
232
244
|
element: TestMedia;
|
|
@@ -348,12 +348,17 @@ describe("startTimeMs", () => {
|
|
|
348
348
|
describe("setting currentTime", () => {
|
|
349
349
|
test("persists in localStorage if the timegroup has an id and is in the dom", async () => {
|
|
350
350
|
const timegroup = renderTimegroup(
|
|
351
|
-
html`<ef-timegroup id="
|
|
351
|
+
html`<ef-timegroup id="localStorage-test" mode="fixed" duration="10s"></ef-timegroup>`,
|
|
352
352
|
);
|
|
353
|
+
localStorage.removeItem(timegroup.storageKey);
|
|
353
354
|
document.body.appendChild(timegroup);
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
355
|
+
await timegroup.waitForMediaDurations();
|
|
356
|
+
|
|
357
|
+
timegroup.currentTime = 5_000; // 5000 seconds, should clamp to 10s
|
|
358
|
+
await timegroup.seekTask.taskComplete;
|
|
359
|
+
|
|
360
|
+
const storedValue = localStorage.getItem(timegroup.storageKey);
|
|
361
|
+
assert.equal(storedValue, "10"); // Should store 10 (clamped from 5000 to duration)
|
|
357
362
|
timegroup.remove();
|
|
358
363
|
});
|
|
359
364
|
|
|
@@ -52,10 +52,6 @@ export class EFTimegroup extends EFTemporal(LitElement) {
|
|
|
52
52
|
|
|
53
53
|
#currentTime = 0;
|
|
54
54
|
|
|
55
|
-
// Frame update locking mechanism (only for root timegroups)
|
|
56
|
-
private isFrameUpdateInProgress = false;
|
|
57
|
-
private queuedTimeUpdate: number | null = null;
|
|
58
|
-
|
|
59
55
|
@property({
|
|
60
56
|
type: String,
|
|
61
57
|
attribute: "mode",
|
|
@@ -74,24 +70,33 @@ export class EFTimegroup extends EFTemporal(LitElement) {
|
|
|
74
70
|
|
|
75
71
|
#resizeObserver?: ResizeObserver;
|
|
76
72
|
|
|
73
|
+
#seekInProgress = false;
|
|
74
|
+
|
|
75
|
+
#pendingSeekTime: number | undefined;
|
|
76
|
+
|
|
77
77
|
@property({ type: Number, attribute: "currenttime" })
|
|
78
78
|
set currentTime(time: number) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
// Only apply locking mechanism for root timegroups to prevent cascade overload
|
|
82
|
-
if (this.isRootTimegroup && this.isFrameUpdateInProgress) {
|
|
83
|
-
// Queue the latest time update - only keep the most recent
|
|
84
|
-
this.queuedTimeUpdate = newTime;
|
|
79
|
+
if (this.#seekInProgress) {
|
|
80
|
+
this.#pendingSeekTime = time;
|
|
85
81
|
return;
|
|
86
82
|
}
|
|
87
83
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
this.#
|
|
93
|
-
|
|
94
|
-
|
|
84
|
+
this.#seekInProgress = true;
|
|
85
|
+
this.#pendingSeekTime = time;
|
|
86
|
+
|
|
87
|
+
this.seekTask.run().finally(() => {
|
|
88
|
+
this.#seekInProgress = false;
|
|
89
|
+
if (
|
|
90
|
+
this.#pendingSeekTime !== undefined &&
|
|
91
|
+
this.#pendingSeekTime !== time
|
|
92
|
+
) {
|
|
93
|
+
const pendingTime = this.#pendingSeekTime;
|
|
94
|
+
this.#pendingSeekTime = undefined;
|
|
95
|
+
this.currentTime = pendingTime;
|
|
96
|
+
} else {
|
|
97
|
+
this.#pendingSeekTime = undefined;
|
|
98
|
+
}
|
|
99
|
+
});
|
|
95
100
|
}
|
|
96
101
|
|
|
97
102
|
get currentTime() {
|
|
@@ -113,34 +118,6 @@ export class EFTimegroup extends EFTemporal(LitElement) {
|
|
|
113
118
|
return this.closest("ef-timegroup") === this;
|
|
114
119
|
}
|
|
115
120
|
|
|
116
|
-
/**
|
|
117
|
-
* Executes time update with frame locking for root timegroups
|
|
118
|
-
*/
|
|
119
|
-
async #executeTimeUpdate(time: number) {
|
|
120
|
-
this.isFrameUpdateInProgress = true;
|
|
121
|
-
this.#currentTime = time;
|
|
122
|
-
|
|
123
|
-
try {
|
|
124
|
-
// Save to localStorage
|
|
125
|
-
this.#saveTimeToLocalStorage(time);
|
|
126
|
-
|
|
127
|
-
// Wait for any pending frame tasks to complete before allowing next update
|
|
128
|
-
await this.waitForFrameTasks();
|
|
129
|
-
} catch (error) {
|
|
130
|
-
console.error("⚠️ [TIME_UPDATE_ERROR] Error during frame update:", error);
|
|
131
|
-
} finally {
|
|
132
|
-
this.isFrameUpdateInProgress = false;
|
|
133
|
-
|
|
134
|
-
// Process queued update if any (ensures latest scrub position is processed)
|
|
135
|
-
if (this.queuedTimeUpdate !== null && this.queuedTimeUpdate !== time) {
|
|
136
|
-
const nextTime = this.queuedTimeUpdate;
|
|
137
|
-
this.queuedTimeUpdate = null;
|
|
138
|
-
// Schedule on next tick to avoid recursive call stack
|
|
139
|
-
setTimeout(() => this.#executeTimeUpdate(nextTime), 0);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
121
|
/**
|
|
145
122
|
* Saves time to localStorage (extracted for reuse)
|
|
146
123
|
*/
|
|
@@ -260,24 +237,45 @@ export class EFTimegroup extends EFTemporal(LitElement) {
|
|
|
260
237
|
}
|
|
261
238
|
}
|
|
262
239
|
|
|
263
|
-
async getPendingFrameTasks() {
|
|
264
|
-
await this.
|
|
240
|
+
async getPendingFrameTasks(signal?: AbortSignal) {
|
|
241
|
+
await this.waitForNestedUpdates(signal);
|
|
242
|
+
signal?.throwIfAborted();
|
|
265
243
|
const temporals = deepGetElementsWithFrameTasks(this);
|
|
266
244
|
return temporals
|
|
267
245
|
.map((temporal) => temporal.frameTask)
|
|
268
246
|
.filter((task) => task.status < TaskStatus.COMPLETE);
|
|
269
247
|
}
|
|
270
248
|
|
|
271
|
-
async
|
|
249
|
+
async waitForNestedUpdates(signal?: AbortSignal) {
|
|
250
|
+
const limit = 10;
|
|
251
|
+
let steps = 0;
|
|
252
|
+
let isComplete = true;
|
|
253
|
+
while (true) {
|
|
254
|
+
steps++;
|
|
255
|
+
if (steps > limit) {
|
|
256
|
+
throw new Error("Reached update depth limit.");
|
|
257
|
+
}
|
|
258
|
+
isComplete = await this.updateComplete;
|
|
259
|
+
signal?.throwIfAborted();
|
|
260
|
+
if (isComplete) {
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async waitForFrameTasks(signal?: AbortSignal) {
|
|
272
267
|
const limit = 10;
|
|
273
268
|
let step = 0;
|
|
274
|
-
await this.
|
|
269
|
+
await this.waitForNestedUpdates(signal);
|
|
275
270
|
while (step < limit) {
|
|
276
271
|
step++;
|
|
277
|
-
let pendingTasks = await this.getPendingFrameTasks();
|
|
272
|
+
let pendingTasks = await this.getPendingFrameTasks(signal);
|
|
273
|
+
signal?.throwIfAborted();
|
|
278
274
|
await Promise.all(pendingTasks.map((task) => task.taskComplete));
|
|
275
|
+
signal?.throwIfAborted();
|
|
279
276
|
await this.updateComplete;
|
|
280
|
-
|
|
277
|
+
signal?.throwIfAborted();
|
|
278
|
+
pendingTasks = await this.getPendingFrameTasks(signal);
|
|
281
279
|
if (pendingTasks.length === 0) {
|
|
282
280
|
break;
|
|
283
281
|
}
|
|
@@ -549,13 +547,39 @@ export class EFTimegroup extends EFTemporal(LitElement) {
|
|
|
549
547
|
frameTask = new Task(this, {
|
|
550
548
|
autoRun: EF_INTERACTIVE,
|
|
551
549
|
args: () => [this.ownCurrentTimeMs, this.currentTimeMs] as const,
|
|
552
|
-
task: async ([], { signal
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
fullyUpdated = await this.updateComplete;
|
|
550
|
+
task: async ([], { signal }) => {
|
|
551
|
+
if (this.isRootTimegroup) {
|
|
552
|
+
await this.waitForFrameTasks(signal);
|
|
556
553
|
}
|
|
557
554
|
},
|
|
558
555
|
});
|
|
556
|
+
|
|
557
|
+
seekTask = new Task(this, {
|
|
558
|
+
args: () => [this.#pendingSeekTime ?? this.#currentTime] as const,
|
|
559
|
+
task: async ([targetTime], { signal }) => {
|
|
560
|
+
const newTime = Math.max(0, Math.min(targetTime, this.durationMs / 1000));
|
|
561
|
+
this.#currentTime = newTime;
|
|
562
|
+
this.requestUpdate("currentTime");
|
|
563
|
+
|
|
564
|
+
// Wait for update to propagate to child elements
|
|
565
|
+
await this.updateComplete;
|
|
566
|
+
signal.throwIfAborted();
|
|
567
|
+
|
|
568
|
+
// Trigger child video seek tasks since they don't auto-run anymore
|
|
569
|
+
const videoElements = this.querySelectorAll(
|
|
570
|
+
"ef-video",
|
|
571
|
+
) as NodeListOf<any>;
|
|
572
|
+
for (const video of videoElements) {
|
|
573
|
+
if (video.videoSeekTask) {
|
|
574
|
+
video.videoSeekTask.run();
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Run frame task and wait for completion
|
|
579
|
+
await this.frameTask.run();
|
|
580
|
+
this.#saveTimeToLocalStorage(newTime);
|
|
581
|
+
},
|
|
582
|
+
});
|
|
559
583
|
}
|
|
560
584
|
|
|
561
585
|
declare global {
|