@editframe/elements 0.38.1 → 0.40.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/EF_FRAMEGEN.js +1 -0
- package/dist/EF_FRAMEGEN.js.map +1 -1
- package/dist/elements/EFCaptions.d.ts +2 -2
- package/dist/elements/EFCaptions.js +1 -1
- package/dist/elements/EFCaptions.js.map +1 -1
- package/dist/elements/EFImage.js +3 -4
- package/dist/elements/EFImage.js.map +1 -1
- package/dist/elements/EFMedia/BufferedSeekingInput.js +1 -1
- package/dist/elements/EFMedia/CachedFetcher.js +99 -0
- package/dist/elements/EFMedia/CachedFetcher.js.map +1 -0
- package/dist/elements/EFMedia/MediaEngine.d.ts +19 -0
- package/dist/elements/EFMedia/MediaEngine.js +129 -0
- package/dist/elements/EFMedia/MediaEngine.js.map +1 -0
- package/dist/elements/EFMedia/SegmentIndex.d.ts +32 -0
- package/dist/elements/EFMedia/SegmentIndex.js +185 -0
- package/dist/elements/EFMedia/SegmentIndex.js.map +1 -0
- package/dist/elements/EFMedia/SegmentTransport.d.ts +12 -0
- package/dist/elements/EFMedia/SegmentTransport.js +69 -0
- package/dist/elements/EFMedia/SegmentTransport.js.map +1 -0
- package/dist/elements/EFMedia/TimingModel.d.ts +10 -0
- package/dist/elements/EFMedia/TimingModel.js +28 -0
- package/dist/elements/EFMedia/TimingModel.js.map +1 -0
- package/dist/elements/EFMedia/shared/AudioSpanUtils.js +7 -6
- package/dist/elements/EFMedia/shared/AudioSpanUtils.js.map +1 -1
- package/dist/elements/EFMedia/shared/ThumbnailExtractor.js +13 -34
- package/dist/elements/EFMedia/shared/ThumbnailExtractor.js.map +1 -1
- package/dist/elements/EFMedia.d.ts +4 -3
- package/dist/elements/EFMedia.js +14 -31
- package/dist/elements/EFMedia.js.map +1 -1
- package/dist/elements/EFSourceMixin.js +1 -1
- package/dist/elements/EFSourceMixin.js.map +1 -1
- package/dist/elements/EFTemporal.js +2 -1
- package/dist/elements/EFTemporal.js.map +1 -1
- package/dist/elements/EFTimegroup.js +2 -1
- package/dist/elements/EFTimegroup.js.map +1 -1
- package/dist/elements/EFVideo.js +204 -187
- package/dist/elements/EFVideo.js.map +1 -1
- package/dist/gui/EFConfiguration.d.ts +0 -7
- package/dist/gui/EFConfiguration.js +0 -5
- package/dist/gui/EFConfiguration.js.map +1 -1
- package/dist/gui/EFWorkbench.d.ts +2 -0
- package/dist/gui/EFWorkbench.js +68 -1
- package/dist/gui/EFWorkbench.js.map +1 -1
- package/dist/gui/PlaybackController.d.ts +2 -0
- package/dist/gui/PlaybackController.js +11 -1
- package/dist/gui/PlaybackController.js.map +1 -1
- package/dist/gui/ef-theme.css +11 -0
- package/dist/gui/timeline/tracks/AudioTrack.js +28 -30
- package/dist/gui/timeline/tracks/AudioTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/EFThumbnailStrip.d.ts +1 -0
- package/dist/gui/timeline/tracks/EFThumbnailStrip.js +41 -8
- package/dist/gui/timeline/tracks/EFThumbnailStrip.js.map +1 -1
- package/dist/gui/timeline/tracks/VideoTrack.js +2 -2
- package/dist/gui/timeline/tracks/VideoTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/waveformUtils.js +19 -19
- package/dist/gui/timeline/tracks/waveformUtils.js.map +1 -1
- package/dist/preview/QualityUpgradeScheduler.d.ts +8 -0
- package/dist/preview/QualityUpgradeScheduler.js +13 -1
- package/dist/preview/QualityUpgradeScheduler.js.map +1 -1
- package/dist/preview/renderTimegroupToVideo.js +3 -3
- package/dist/preview/renderTimegroupToVideo.js.map +1 -1
- package/dist/preview/renderVideoToVideo.js +5 -6
- package/dist/preview/renderVideoToVideo.js.map +1 -1
- package/dist/transcoding/types/index.d.ts +6 -94
- package/dist/transcoding/utils/UrlGenerator.d.ts +3 -12
- package/dist/transcoding/utils/UrlGenerator.js +3 -29
- package/dist/transcoding/utils/UrlGenerator.js.map +1 -1
- package/package.json +2 -2
- package/test/setup.ts +1 -1
- package/test/useAssetMSW.ts +0 -100
- package/dist/elements/EFMedia/AssetMediaEngine.js +0 -284
- package/dist/elements/EFMedia/AssetMediaEngine.js.map +0 -1
- package/dist/elements/EFMedia/BaseMediaEngine.js +0 -200
- package/dist/elements/EFMedia/BaseMediaEngine.js.map +0 -1
- package/dist/elements/EFMedia/FileMediaEngine.js +0 -122
- package/dist/elements/EFMedia/FileMediaEngine.js.map +0 -1
- package/dist/elements/EFMedia/JitMediaEngine.js +0 -157
- package/dist/elements/EFMedia/JitMediaEngine.js.map +0 -1
package/dist/elements/EFVideo.js
CHANGED
|
@@ -64,40 +64,23 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
|
|
|
64
64
|
top: 0;
|
|
65
65
|
left: 0;
|
|
66
66
|
right: 0;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
display: flex;
|
|
70
|
-
align-items: center;
|
|
71
|
-
justify-content: center;
|
|
67
|
+
height: 2px;
|
|
68
|
+
overflow: hidden;
|
|
72
69
|
z-index: 10;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
.loading-content {
|
|
76
|
-
background: rgba(0, 0, 0, 0.8);
|
|
77
|
-
border-radius: 8px;
|
|
78
|
-
padding: 16px 24px;
|
|
79
|
-
display: flex;
|
|
80
|
-
align-items: center;
|
|
81
|
-
gap: 12px;
|
|
82
|
-
color: white;
|
|
83
|
-
font-size: 14px;
|
|
84
|
-
font-weight: 500;
|
|
85
|
-
}
|
|
86
|
-
.loading-spinner {
|
|
87
|
-
width: 20px;
|
|
88
|
-
height: 20px;
|
|
89
|
-
border: 2px solid rgba(255, 255, 255, 0.2);
|
|
90
|
-
border-left: 2px solid #fff;
|
|
91
|
-
border-radius: 50%;
|
|
92
|
-
animation: spin 1s linear infinite;
|
|
70
|
+
pointer-events: none;
|
|
71
|
+
background: var(--ef-color-loading-spinner-track, rgba(255, 255, 255, 0.1));
|
|
93
72
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
73
|
+
.loading-bar {
|
|
74
|
+
position: absolute;
|
|
75
|
+
top: 0;
|
|
76
|
+
height: 100%;
|
|
77
|
+
width: 40%;
|
|
78
|
+
background: var(--ef-color-loading-spinner-fill, rgba(255, 255, 255, 0.8));
|
|
79
|
+
animation: loading-sweep 1.4s ease-in-out infinite;
|
|
97
80
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
81
|
+
@keyframes loading-sweep {
|
|
82
|
+
0% { left: -40%; }
|
|
83
|
+
100% { left: 140%; }
|
|
101
84
|
}
|
|
102
85
|
`];
|
|
103
86
|
}
|
|
@@ -117,6 +100,17 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
|
|
|
117
100
|
*/
|
|
118
101
|
#standaloneUpgradeController = null;
|
|
119
102
|
/**
|
|
103
|
+
* Set to true while renderToVideo is executing to suppress background
|
|
104
|
+
* quality upgrade tasks that would race with the render pipeline.
|
|
105
|
+
*/
|
|
106
|
+
#renderingToVideo = false;
|
|
107
|
+
/**
|
|
108
|
+
* Stable per-instance identifier for the quality upgrade scheduler.
|
|
109
|
+
* Uses this.id when available; falls back to a generated unique string so
|
|
110
|
+
* elements without an id attribute never collide with each other.
|
|
111
|
+
*/
|
|
112
|
+
#upgradeOwnerId = crypto.randomUUID();
|
|
113
|
+
/**
|
|
120
114
|
* Current rendition being displayed (for observability).
|
|
121
115
|
*/
|
|
122
116
|
#currentRenditionId = void 0;
|
|
@@ -155,30 +149,35 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
|
|
|
155
149
|
async prepareFrame(_timeMs, signal) {
|
|
156
150
|
signal.throwIfAborted();
|
|
157
151
|
this.unifiedVideoSeekTask.begin();
|
|
158
|
-
|
|
159
|
-
const mediaEngine = await this.getMediaEngine(signal);
|
|
160
|
-
if (!mediaEngine) {
|
|
161
|
-
this.#cachedVideoSample = void 0;
|
|
162
|
-
this.#cachedVideoSampleTimeMs = sourceTimeMs;
|
|
163
|
-
this.unifiedVideoSeekTask.complete(void 0);
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
signal.throwIfAborted();
|
|
152
|
+
this.#delayedLoadingState.startLoading("prepare-frame", "");
|
|
167
153
|
try {
|
|
168
|
-
const
|
|
154
|
+
const sourceTimeMs = this.currentSourceTimeMs;
|
|
155
|
+
const mediaEngine = await this.getMediaEngine(signal);
|
|
156
|
+
if (!mediaEngine) {
|
|
157
|
+
this.#cachedVideoSample = void 0;
|
|
158
|
+
this.#cachedVideoSampleTimeMs = sourceTimeMs;
|
|
159
|
+
this.unifiedVideoSeekTask.complete(void 0);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
169
162
|
signal.throwIfAborted();
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
this.unifiedVideoSeekTask.
|
|
176
|
-
|
|
163
|
+
try {
|
|
164
|
+
const videoSample = await this.#fetchVideoSampleForFrame(mediaEngine, sourceTimeMs, signal);
|
|
165
|
+
signal.throwIfAborted();
|
|
166
|
+
this.#cachedVideoSample = videoSample;
|
|
167
|
+
this.#cachedVideoSampleTimeMs = sourceTimeMs;
|
|
168
|
+
this.unifiedVideoSeekTask.complete(videoSample);
|
|
169
|
+
} catch (error) {
|
|
170
|
+
if (error instanceof DOMException && error.name === "AbortError") {
|
|
171
|
+
this.unifiedVideoSeekTask.abort();
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
console.warn(`Video seek error at ${sourceTimeMs}ms:`, error);
|
|
175
|
+
this.#cachedVideoSample = void 0;
|
|
176
|
+
this.#cachedVideoSampleTimeMs = sourceTimeMs;
|
|
177
|
+
this.unifiedVideoSeekTask.complete(void 0);
|
|
177
178
|
}
|
|
178
|
-
|
|
179
|
-
this.#
|
|
180
|
-
this.#cachedVideoSampleTimeMs = sourceTimeMs;
|
|
181
|
-
this.unifiedVideoSeekTask.complete(void 0);
|
|
179
|
+
} finally {
|
|
180
|
+
this.#delayedLoadingState.clearLoading("prepare-frame");
|
|
182
181
|
}
|
|
183
182
|
}
|
|
184
183
|
/**
|
|
@@ -209,10 +208,10 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
|
|
|
209
208
|
* - If main track segment is already cached: use it (avoid redundant lower-quality fetch)
|
|
210
209
|
*/
|
|
211
210
|
async #fetchVideoSampleForFrame(mediaEngine, desiredSeekTimeMs, signal) {
|
|
212
|
-
const
|
|
213
|
-
if (
|
|
214
|
-
const mainSegmentId = mediaEngine.
|
|
215
|
-
if (mainSegmentId !== void 0 && mediaEngine.
|
|
211
|
+
const mainTrack = mediaEngine.tracks.video;
|
|
212
|
+
if (mainTrack) {
|
|
213
|
+
const mainSegmentId = mediaEngine.index.segmentAt(desiredSeekTimeMs, mainTrack);
|
|
214
|
+
if (mainSegmentId !== void 0 && mediaEngine.transport.isCached(mainSegmentId, mainTrack)) {
|
|
216
215
|
this.#currentRenditionId = "main";
|
|
217
216
|
return this.#getMainVideoSampleForFrame(mediaEngine, desiredSeekTimeMs, signal);
|
|
218
217
|
}
|
|
@@ -221,7 +220,7 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
|
|
|
221
220
|
this.#currentRenditionId = "main";
|
|
222
221
|
return this.#getMainVideoSampleForFrame(mediaEngine, desiredSeekTimeMs, signal);
|
|
223
222
|
}
|
|
224
|
-
if (mediaEngine.
|
|
223
|
+
if (mediaEngine.tracks.scrub) {
|
|
225
224
|
const scrubSample = await this.#getScrubVideoSampleForFrame(mediaEngine, desiredSeekTimeMs, signal);
|
|
226
225
|
if (scrubSample) {
|
|
227
226
|
this.#currentRenditionId = "scrub";
|
|
@@ -237,20 +236,16 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
|
|
|
237
236
|
* Used in preview mode for faster response during timeline scrubbing.
|
|
238
237
|
*/
|
|
239
238
|
async #getScrubVideoSampleForFrame(mediaEngine, desiredSeekTimeMs, signal) {
|
|
240
|
-
const
|
|
241
|
-
if (!
|
|
242
|
-
const
|
|
243
|
-
...scrubRendition,
|
|
244
|
-
src: mediaEngine.src
|
|
245
|
-
};
|
|
246
|
-
const segmentId = mediaEngine.computeSegmentId(desiredSeekTimeMs, scrubRenditionWithSrc);
|
|
239
|
+
const scrubTrack = mediaEngine.tracks.scrub;
|
|
240
|
+
if (!scrubTrack) return;
|
|
241
|
+
const segmentId = mediaEngine.index.segmentAt(desiredSeekTimeMs, scrubTrack);
|
|
247
242
|
if (segmentId === void 0) return;
|
|
248
243
|
const scrubInput = await scrubInputCache.getOrCreateInput(mediaEngine.src, segmentId, async () => {
|
|
249
244
|
let initSegment;
|
|
250
245
|
let mediaSegment;
|
|
251
246
|
try {
|
|
252
|
-
const initP = mediaEngine.fetchInitSegment(
|
|
253
|
-
const mediaP = mediaEngine.fetchMediaSegment(segmentId,
|
|
247
|
+
const initP = mediaEngine.transport.fetchInitSegment(scrubTrack, signal);
|
|
248
|
+
const mediaP = mediaEngine.transport.fetchMediaSegment(segmentId, scrubTrack, signal);
|
|
254
249
|
initP.catch(() => {});
|
|
255
250
|
mediaP.catch(() => {});
|
|
256
251
|
[initSegment, mediaSegment] = await Promise.all([initP, mediaP]);
|
|
@@ -268,7 +263,7 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
|
|
|
268
263
|
return new BufferedSeekingInput(arrayBuffer, {
|
|
269
264
|
videoBufferSize: EFMedia.VIDEO_SAMPLE_BUFFER_SIZE,
|
|
270
265
|
audioBufferSize: EFMedia.AUDIO_SAMPLE_BUFFER_SIZE,
|
|
271
|
-
startTimeOffsetMs:
|
|
266
|
+
startTimeOffsetMs: scrubTrack.startTimeOffsetMs
|
|
272
267
|
});
|
|
273
268
|
});
|
|
274
269
|
if (!scrubInput) return;
|
|
@@ -282,16 +277,16 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
|
|
|
282
277
|
* Get main video sample for a given time.
|
|
283
278
|
*/
|
|
284
279
|
async #getMainVideoSampleForFrame(mediaEngine, desiredSeekTimeMs, signal) {
|
|
285
|
-
const
|
|
286
|
-
if (!
|
|
287
|
-
const segmentId = mediaEngine.
|
|
280
|
+
const videoTrack = mediaEngine.tracks.video;
|
|
281
|
+
if (!videoTrack) return;
|
|
282
|
+
const segmentId = mediaEngine.index.segmentAt(desiredSeekTimeMs, videoTrack);
|
|
288
283
|
if (segmentId === void 0) return;
|
|
289
|
-
const mainInput = await mainVideoInputCache.getOrCreateInput(mediaEngine.src, segmentId,
|
|
284
|
+
const mainInput = await mainVideoInputCache.getOrCreateInput(mediaEngine.src, segmentId, String(videoTrack.id), async () => {
|
|
290
285
|
let initSegment;
|
|
291
286
|
let mediaSegment;
|
|
292
287
|
try {
|
|
293
|
-
const initP = mediaEngine.fetchInitSegment(
|
|
294
|
-
const mediaP = mediaEngine.fetchMediaSegment(segmentId,
|
|
288
|
+
const initP = mediaEngine.transport.fetchInitSegment(videoTrack, signal);
|
|
289
|
+
const mediaP = mediaEngine.transport.fetchMediaSegment(segmentId, videoTrack, signal);
|
|
295
290
|
initP.catch(() => {});
|
|
296
291
|
mediaP.catch(() => {});
|
|
297
292
|
[initSegment, mediaSegment] = await Promise.all([initP, mediaP]);
|
|
@@ -310,15 +305,15 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
|
|
|
310
305
|
return new BufferedSeekingInput(arrayBuffer, {
|
|
311
306
|
videoBufferSize: EFMedia.VIDEO_SAMPLE_BUFFER_SIZE,
|
|
312
307
|
audioBufferSize: EFMedia.AUDIO_SAMPLE_BUFFER_SIZE,
|
|
313
|
-
startTimeOffsetMs:
|
|
308
|
+
startTimeOffsetMs: videoTrack.startTimeOffsetMs
|
|
314
309
|
});
|
|
315
310
|
});
|
|
316
311
|
if (!mainInput) return;
|
|
317
312
|
signal.throwIfAborted();
|
|
318
|
-
const
|
|
319
|
-
if (!
|
|
313
|
+
const videoTrackInfo = await mainInput.getFirstVideoTrack();
|
|
314
|
+
if (!videoTrackInfo) return;
|
|
320
315
|
signal.throwIfAborted();
|
|
321
|
-
return await mainInput.seek(
|
|
316
|
+
return await mainInput.seek(videoTrackInfo.id, desiredSeekTimeMs);
|
|
322
317
|
}
|
|
323
318
|
/**
|
|
324
319
|
* Delayed loading state manager for user feedback
|
|
@@ -333,34 +328,47 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
|
|
|
333
328
|
operation: null,
|
|
334
329
|
message: ""
|
|
335
330
|
};
|
|
336
|
-
this.#delayedLoadingState = new DelayedLoadingState(
|
|
331
|
+
this.#delayedLoadingState = new DelayedLoadingState(100, (isLoading, message) => {
|
|
337
332
|
this.setLoadingState(isLoading, null, message);
|
|
338
333
|
});
|
|
339
334
|
}
|
|
340
335
|
updated(changedProperties) {
|
|
341
336
|
super.updated(changedProperties);
|
|
342
|
-
if (changedProperties.has("src") || changedProperties.has("fileId"))
|
|
337
|
+
if (changedProperties.has("src") || changedProperties.has("fileId")) {
|
|
338
|
+
this.#invalidateUpgradeState("src-change");
|
|
339
|
+
this.#prewarmQualityUpgrade();
|
|
340
|
+
}
|
|
343
341
|
if ([
|
|
344
342
|
"_trimStartMs",
|
|
345
343
|
"_trimEndMs",
|
|
346
344
|
"_sourceInMs",
|
|
347
345
|
"_sourceOutMs"
|
|
348
|
-
].some((prop) => changedProperties.has(prop)))
|
|
346
|
+
].some((prop) => changedProperties.has(prop))) {
|
|
347
|
+
this.#invalidateUpgradeState("bounds-change");
|
|
348
|
+
this.#prewarmQualityUpgrade();
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Eagerly load the media engine and pre-warm main-quality segments for the
|
|
353
|
+
* start of this clip. Called when src/fileId or source bounds change so that
|
|
354
|
+
* segments are already in cache by the time the element first becomes visible.
|
|
355
|
+
*
|
|
356
|
+
* Without pre-warming, quality upgrade only begins after the first scrub frame
|
|
357
|
+
* is displayed, causing ~12 frames of blur at the cold-start of every clip.
|
|
358
|
+
*/
|
|
359
|
+
#prewarmQualityUpgrade() {
|
|
360
|
+
if (this.isInProductionRenderingMode()) return;
|
|
361
|
+
if (!this.src && !this.fileId) return;
|
|
362
|
+
this.getMediaEngine().then((engine) => {
|
|
363
|
+
if (!engine) return;
|
|
364
|
+
const sourceInMs = this.sourceInMs ?? 0;
|
|
365
|
+
this.#maybeScheduleQualityUpgrade(engine, sourceInMs);
|
|
366
|
+
}).catch(() => {});
|
|
349
367
|
}
|
|
350
368
|
render() {
|
|
351
369
|
return html`
|
|
352
370
|
<canvas ${ref(this.canvasRef)}></canvas>
|
|
353
|
-
${this.loadingState.isLoading ? html
|
|
354
|
-
<div class="loading-overlay">
|
|
355
|
-
<div class="loading-content">
|
|
356
|
-
<div class="loading-spinner"></div>
|
|
357
|
-
<div>
|
|
358
|
-
<div>Loading Video...</div>
|
|
359
|
-
<div class="loading-message">${this.loadingState.message}</div>
|
|
360
|
-
</div>
|
|
361
|
-
</div>
|
|
362
|
-
</div>
|
|
363
|
-
` : ""}
|
|
371
|
+
${this.loadingState.isLoading ? html`<div class="loading-overlay"><div class="loading-bar"></div></div>` : ""}
|
|
364
372
|
`;
|
|
365
373
|
}
|
|
366
374
|
get canvasElement() {
|
|
@@ -548,79 +556,80 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
|
|
|
548
556
|
const { quality = "auto", signal: providedSignal } = options;
|
|
549
557
|
const signal = providedSignal ?? new AbortController().signal;
|
|
550
558
|
signal.throwIfAborted();
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
const useMainTrack = quality === "main" || quality === "auto" && this.isInProductionRenderingMode();
|
|
555
|
-
let videoSample;
|
|
556
|
-
const { BufferedSeekingInput } = await import("./EFMedia/BufferedSeekingInput.js");
|
|
557
|
-
signal.throwIfAborted();
|
|
558
|
-
if (useMainTrack) {
|
|
559
|
-
const videoRendition = mediaEngine.getVideoRendition?.() || mediaEngine.videoRendition;
|
|
560
|
-
if (!videoRendition) throw new Error("No video rendition available");
|
|
561
|
-
const segmentId = mediaEngine.computeSegmentId(sourceTimeMs, videoRendition);
|
|
562
|
-
if (segmentId === void 0) throw new Error(`Cannot compute segment ID for time ${sourceTimeMs}ms`);
|
|
563
|
-
const seekingInput = await mainVideoInputCache.getOrCreateInput(mediaEngine.src, segmentId, videoRendition.id, async () => {
|
|
564
|
-
const initP = mediaEngine.fetchInitSegment(videoRendition, signal);
|
|
565
|
-
const mediaP = mediaEngine.fetchMediaSegment(segmentId, videoRendition, signal);
|
|
566
|
-
initP.catch(() => {});
|
|
567
|
-
mediaP.catch(() => {});
|
|
568
|
-
const [initSegment, mediaSegment] = await Promise.all([initP, mediaP]);
|
|
569
|
-
if (!initSegment || !mediaSegment) return;
|
|
570
|
-
return new BufferedSeekingInput(await new Blob([initSegment, mediaSegment]).arrayBuffer(), {
|
|
571
|
-
videoBufferSize: EFMedia.VIDEO_SAMPLE_BUFFER_SIZE,
|
|
572
|
-
audioBufferSize: EFMedia.AUDIO_SAMPLE_BUFFER_SIZE,
|
|
573
|
-
startTimeOffsetMs: videoRendition.startTimeOffsetMs
|
|
574
|
-
});
|
|
575
|
-
});
|
|
576
|
-
signal.throwIfAborted();
|
|
577
|
-
if (!seekingInput) throw new Error(`Failed to fetch video segments for time ${sourceTimeMs}ms`);
|
|
578
|
-
const videoTrack = await seekingInput.getFirstVideoTrack();
|
|
559
|
+
this.playbackController?.suspendSelfRender();
|
|
560
|
+
try {
|
|
561
|
+
const mediaEngine = await this.getMediaEngine(signal);
|
|
579
562
|
signal.throwIfAborted();
|
|
580
|
-
if (!
|
|
581
|
-
|
|
563
|
+
if (!mediaEngine) throw new Error("No media engine available for frame capture");
|
|
564
|
+
const useMainTrack = quality === "main" || quality === "auto" && this.isInProductionRenderingMode();
|
|
565
|
+
let videoSample;
|
|
566
|
+
const { BufferedSeekingInput } = await import("./EFMedia/BufferedSeekingInput.js");
|
|
582
567
|
signal.throwIfAborted();
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
const [initSegment, mediaSegment] = await Promise.all([initP, mediaP]);
|
|
601
|
-
if (!initSegment || !mediaSegment) return;
|
|
602
|
-
return new BufferedSeekingInput(await new Blob([initSegment, mediaSegment]).arrayBuffer(), {
|
|
603
|
-
videoBufferSize: EFMedia.VIDEO_SAMPLE_BUFFER_SIZE,
|
|
604
|
-
audioBufferSize: EFMedia.AUDIO_SAMPLE_BUFFER_SIZE,
|
|
605
|
-
startTimeOffsetMs: scrubRendition.startTimeOffsetMs
|
|
568
|
+
if (useMainTrack) {
|
|
569
|
+
const videoTrack = mediaEngine.tracks.video;
|
|
570
|
+
if (!videoTrack) throw new Error("No video rendition available");
|
|
571
|
+
const segmentId = mediaEngine.index.segmentAt(sourceTimeMs, videoTrack);
|
|
572
|
+
if (segmentId === void 0) throw new Error(`Cannot compute segment ID for time ${sourceTimeMs}ms`);
|
|
573
|
+
const seekingInput = await mainVideoInputCache.getOrCreateInput(mediaEngine.src, segmentId, String(videoTrack.id), async () => {
|
|
574
|
+
const initP = mediaEngine.transport.fetchInitSegment(videoTrack, signal);
|
|
575
|
+
const mediaP = mediaEngine.transport.fetchMediaSegment(segmentId, videoTrack, signal);
|
|
576
|
+
initP.catch(() => {});
|
|
577
|
+
mediaP.catch(() => {});
|
|
578
|
+
const [initSegment, mediaSegment] = await Promise.all([initP, mediaP]);
|
|
579
|
+
if (!initSegment || !mediaSegment) return;
|
|
580
|
+
return new BufferedSeekingInput(await new Blob([initSegment, mediaSegment]).arrayBuffer(), {
|
|
581
|
+
videoBufferSize: EFMedia.VIDEO_SAMPLE_BUFFER_SIZE,
|
|
582
|
+
audioBufferSize: EFMedia.AUDIO_SAMPLE_BUFFER_SIZE,
|
|
583
|
+
startTimeOffsetMs: videoTrack.startTimeOffsetMs
|
|
584
|
+
});
|
|
606
585
|
});
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
586
|
+
signal.throwIfAborted();
|
|
587
|
+
if (!seekingInput) throw new Error(`Failed to fetch video segments for time ${sourceTimeMs}ms`);
|
|
588
|
+
const seekingVideoTrack = await seekingInput.getFirstVideoTrack();
|
|
589
|
+
signal.throwIfAborted();
|
|
590
|
+
if (!seekingVideoTrack) throw new Error("No video track found in segment");
|
|
591
|
+
videoSample = await seekingInput.seek(seekingVideoTrack.id, sourceTimeMs);
|
|
592
|
+
signal.throwIfAborted();
|
|
593
|
+
} else {
|
|
594
|
+
const scrubTrack = mediaEngine.tracks.scrub;
|
|
595
|
+
if (!scrubTrack) return this.getVideoFrameAtSourceTime(sourceTimeMs, {
|
|
596
|
+
quality: "main",
|
|
597
|
+
signal
|
|
598
|
+
});
|
|
599
|
+
const segmentId = mediaEngine.index.segmentAt(sourceTimeMs, scrubTrack);
|
|
600
|
+
if (segmentId === void 0) throw new Error(`Cannot compute scrub segment ID for time ${sourceTimeMs}ms`);
|
|
601
|
+
const seekingInput = await scrubInputCache.getOrCreateInput(mediaEngine.src, segmentId, async () => {
|
|
602
|
+
const initP = mediaEngine.transport.fetchInitSegment(scrubTrack, signal);
|
|
603
|
+
const mediaP = mediaEngine.transport.fetchMediaSegment(segmentId, scrubTrack, signal);
|
|
604
|
+
initP.catch(() => {});
|
|
605
|
+
mediaP.catch(() => {});
|
|
606
|
+
const [initSegment, mediaSegment] = await Promise.all([initP, mediaP]);
|
|
607
|
+
if (!initSegment || !mediaSegment) return;
|
|
608
|
+
return new BufferedSeekingInput(await new Blob([initSegment, mediaSegment]).arrayBuffer(), {
|
|
609
|
+
videoBufferSize: EFMedia.VIDEO_SAMPLE_BUFFER_SIZE,
|
|
610
|
+
audioBufferSize: EFMedia.AUDIO_SAMPLE_BUFFER_SIZE,
|
|
611
|
+
startTimeOffsetMs: scrubTrack.startTimeOffsetMs
|
|
612
|
+
});
|
|
613
|
+
});
|
|
614
|
+
signal.throwIfAborted();
|
|
615
|
+
if (!seekingInput) return this.getVideoFrameAtSourceTime(sourceTimeMs, {
|
|
616
|
+
quality: "main",
|
|
617
|
+
signal
|
|
618
|
+
});
|
|
619
|
+
const seekingVideoTrack = await seekingInput.getFirstVideoTrack();
|
|
620
|
+
signal.throwIfAborted();
|
|
621
|
+
if (!seekingVideoTrack) return this.getVideoFrameAtSourceTime(sourceTimeMs, {
|
|
622
|
+
quality: "main",
|
|
623
|
+
signal
|
|
624
|
+
});
|
|
625
|
+
videoSample = await seekingInput.seek(seekingVideoTrack.id, sourceTimeMs);
|
|
626
|
+
signal.throwIfAborted();
|
|
627
|
+
}
|
|
628
|
+
if (!videoSample) throw new Error(`No video sample found at ${sourceTimeMs}ms`);
|
|
629
|
+
return videoSample.toVideoFrame();
|
|
630
|
+
} finally {
|
|
631
|
+
this.playbackController?.resumeSelfRender();
|
|
621
632
|
}
|
|
622
|
-
if (!videoSample) throw new Error(`No video sample found at ${sourceTimeMs}ms`);
|
|
623
|
-
return videoSample.toVideoFrame();
|
|
624
633
|
}
|
|
625
634
|
/**
|
|
626
635
|
* Capture a video frame directly at a source media timestamp.
|
|
@@ -684,18 +693,14 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
|
|
|
684
693
|
log("prefetchScrubSegments: no media engine available");
|
|
685
694
|
return;
|
|
686
695
|
}
|
|
687
|
-
const
|
|
688
|
-
if (!
|
|
696
|
+
const scrubTrack = mediaEngine.tracks.scrub;
|
|
697
|
+
if (!scrubTrack) {
|
|
689
698
|
log("prefetchScrubSegments: no scrub rendition available");
|
|
690
699
|
return;
|
|
691
700
|
}
|
|
692
|
-
const scrubRenditionWithSrc = {
|
|
693
|
-
...scrubRendition,
|
|
694
|
-
src: mediaEngine.src
|
|
695
|
-
};
|
|
696
701
|
const segmentIds = /* @__PURE__ */ new Set();
|
|
697
702
|
for (const ts of timestamps) {
|
|
698
|
-
const segmentId = mediaEngine.
|
|
703
|
+
const segmentId = mediaEngine.index.segmentAt(ts, scrubTrack);
|
|
699
704
|
if (segmentId !== void 0) segmentIds.add(segmentId);
|
|
700
705
|
}
|
|
701
706
|
if (segmentIds.size === 0) {
|
|
@@ -703,7 +708,7 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
|
|
|
703
708
|
return;
|
|
704
709
|
}
|
|
705
710
|
const firstSegmentId = Array.from(segmentIds)[0];
|
|
706
|
-
if (mediaEngine.
|
|
711
|
+
if (mediaEngine.transport.isCached(firstSegmentId, scrubTrack)) {
|
|
707
712
|
log("prefetchScrubSegments: scrub track already cached");
|
|
708
713
|
return;
|
|
709
714
|
}
|
|
@@ -722,7 +727,7 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
|
|
|
722
727
|
}));
|
|
723
728
|
const fetchSignal = signal ?? new AbortController().signal;
|
|
724
729
|
try {
|
|
725
|
-
await mediaEngine.fetchMediaSegment(firstSegmentId,
|
|
730
|
+
await mediaEngine.transport.fetchMediaSegment(firstSegmentId, scrubTrack, fetchSignal);
|
|
726
731
|
log(`prefetchScrubSegments: scrub track loaded`);
|
|
727
732
|
} catch (error) {
|
|
728
733
|
log(`prefetchScrubSegments: failed to load scrub track`, error);
|
|
@@ -746,25 +751,32 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
|
|
|
746
751
|
* Called when returning a scrub sample - checks if state has changed and submits tasks.
|
|
747
752
|
*/
|
|
748
753
|
#maybeScheduleQualityUpgrade(mediaEngine, sourceTimeMs) {
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
754
|
+
if (this.#renderingToVideo) return;
|
|
755
|
+
const mainTrack = mediaEngine.tracks.video;
|
|
756
|
+
if (!mainTrack) return;
|
|
757
|
+
const segmentId = mediaEngine.index.segmentAt(sourceTimeMs, mainTrack);
|
|
752
758
|
if (segmentId === void 0) return;
|
|
753
759
|
const startTimeMs = this.startTimeMs;
|
|
754
|
-
if (!(this.#upgradeState === null || this.#upgradeState.segmentId !== segmentId || this.#upgradeState.startTimeMs !== startTimeMs))
|
|
755
|
-
|
|
760
|
+
if (!(this.#upgradeState === null || this.#upgradeState.segmentId !== segmentId || this.#upgradeState.startTimeMs !== startTimeMs)) {
|
|
761
|
+
const currentTaskKey = `${this.#upgradeOwnerId}:${segmentId}:${mainTrack.id}`;
|
|
762
|
+
const scheduler$1 = this.rootTimegroup?.qualityUpgradeScheduler;
|
|
763
|
+
if (scheduler$1?.isActive(currentTaskKey) || scheduler$1?.isPending(currentTaskKey)) return;
|
|
764
|
+
this.rootTimegroup?.qualityUpgradeScheduler?.cancelForOwner(this.#upgradeOwnerId);
|
|
765
|
+
this.#upgradeState = null;
|
|
766
|
+
}
|
|
767
|
+
const segments = this.#computeLookaheadSegments(mediaEngine, sourceTimeMs, mainTrack);
|
|
756
768
|
if (segments.length === 0) return;
|
|
757
769
|
const tasks = segments.map((seg) => ({
|
|
758
|
-
key: `${this
|
|
770
|
+
key: `${this.#upgradeOwnerId}:${seg.segmentId}:${mainTrack.id}`,
|
|
759
771
|
fetch: async (signal) => {
|
|
760
|
-
await mediaEngine.fetchInitSegment(
|
|
761
|
-
await mediaEngine.fetchMediaSegment(seg.segmentId,
|
|
772
|
+
await mediaEngine.transport.fetchInitSegment(mainTrack, signal);
|
|
773
|
+
await mediaEngine.transport.fetchMediaSegment(seg.segmentId, mainTrack, signal);
|
|
762
774
|
},
|
|
763
775
|
deadlineMs: seg.deadlineMs,
|
|
764
|
-
owner: this
|
|
776
|
+
owner: this.#upgradeOwnerId
|
|
765
777
|
}));
|
|
766
778
|
const scheduler = this.rootTimegroup?.qualityUpgradeScheduler;
|
|
767
|
-
if (scheduler) scheduler.replaceForOwner(this
|
|
779
|
+
if (scheduler) scheduler.replaceForOwner(this.#upgradeOwnerId, tasks);
|
|
768
780
|
else this.#fetchStandalone(tasks);
|
|
769
781
|
this.#upgradeState = {
|
|
770
782
|
sourceTimeMs,
|
|
@@ -776,24 +788,24 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
|
|
|
776
788
|
/**
|
|
777
789
|
* Compute lookahead segments with deadlines in timeline space.
|
|
778
790
|
*/
|
|
779
|
-
#computeLookaheadSegments(mediaEngine, currentSourceTimeMs,
|
|
791
|
+
#computeLookaheadSegments(mediaEngine, currentSourceTimeMs, track, maxLookahead = 5) {
|
|
780
792
|
const results = [];
|
|
781
793
|
const playheadMs = this.rootTimegroup?.currentTimeMs ?? 0;
|
|
782
794
|
const seen = /* @__PURE__ */ new Set();
|
|
783
795
|
let probeTimeMs = currentSourceTimeMs;
|
|
784
796
|
while (seen.size < maxLookahead) {
|
|
785
|
-
const segmentId = mediaEngine.
|
|
797
|
+
const segmentId = mediaEngine.index.segmentAt(probeTimeMs, track);
|
|
786
798
|
if (segmentId === void 0) break;
|
|
787
799
|
if (seen.has(segmentId)) break;
|
|
788
800
|
seen.add(segmentId);
|
|
789
|
-
if (!mediaEngine.
|
|
801
|
+
if (!mediaEngine.transport.isCached(segmentId, track)) {
|
|
790
802
|
const deadlineMs = playheadMs + (probeTimeMs - currentSourceTimeMs);
|
|
791
803
|
results.push({
|
|
792
804
|
segmentId,
|
|
793
805
|
deadlineMs
|
|
794
806
|
});
|
|
795
807
|
}
|
|
796
|
-
const thisDuration =
|
|
808
|
+
const thisDuration = track.segmentDurationsMs?.[segmentId - 1] ?? track.segmentDurationMs ?? 2e3;
|
|
797
809
|
probeTimeMs += thisDuration;
|
|
798
810
|
}
|
|
799
811
|
return results;
|
|
@@ -812,14 +824,14 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
|
|
|
812
824
|
await task.fetch(signal);
|
|
813
825
|
} catch {}
|
|
814
826
|
}
|
|
815
|
-
if (!signal.aborted) this.playbackController?.runThrottledFrameTask();
|
|
827
|
+
if (!signal.aborted) this.playbackController?.runThrottledFrameTask().catch(() => {});
|
|
816
828
|
})().catch(() => {});
|
|
817
829
|
}
|
|
818
830
|
/**
|
|
819
831
|
* Invalidate upgrade state and optionally cancel queued tasks.
|
|
820
832
|
*/
|
|
821
833
|
#invalidateUpgradeState(reason) {
|
|
822
|
-
if (reason === "src-change" || reason === "disconnect") this.rootTimegroup?.qualityUpgradeScheduler?.cancelForOwner(this
|
|
834
|
+
if (reason === "src-change" || reason === "disconnect") this.rootTimegroup?.qualityUpgradeScheduler?.cancelForOwner(this.#upgradeOwnerId);
|
|
823
835
|
this.#upgradeState = null;
|
|
824
836
|
}
|
|
825
837
|
/**
|
|
@@ -862,8 +874,13 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
|
|
|
862
874
|
* @public
|
|
863
875
|
*/
|
|
864
876
|
async renderToVideo(options) {
|
|
865
|
-
|
|
866
|
-
|
|
877
|
+
this.#renderingToVideo = true;
|
|
878
|
+
try {
|
|
879
|
+
const { renderVideoToVideo } = await import("../preview/renderVideoToVideo.js");
|
|
880
|
+
return await renderVideoToVideo(this, options);
|
|
881
|
+
} finally {
|
|
882
|
+
this.#renderingToVideo = false;
|
|
883
|
+
}
|
|
867
884
|
}
|
|
868
885
|
};
|
|
869
886
|
__decorate([state()], EFVideo.prototype, "loadingState", void 0);
|