@editframe/elements 0.46.4 → 0.47.1
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/EFMedia/BufferedSeekingInput.d.ts +50 -0
- package/dist/elements/EFMedia/BufferedSeekingInput.js +6 -5
- package/dist/elements/EFMedia/BufferedSeekingInput.js.map +1 -1
- package/dist/elements/EFMedia/CachedFetcher.js +23 -33
- package/dist/elements/EFMedia/CachedFetcher.js.map +1 -1
- package/dist/elements/EFMedia/SegmentTransport.d.ts +2 -2
- package/dist/elements/EFMedia/SegmentTransport.js.map +1 -1
- package/dist/elements/EFMedia/shared/ThumbnailExtractor.js +53 -0
- package/dist/elements/EFMedia/shared/ThumbnailExtractor.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js +20 -5
- package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js.map +1 -1
- package/dist/elements/EFMedia/videoTasks/ScrubInputCache.d.ts +48 -0
- package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js +36 -7
- package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js.map +1 -1
- package/dist/elements/EFMotionBlur.d.ts +134 -0
- package/dist/elements/EFMotionBlur.js +809 -0
- package/dist/elements/EFMotionBlur.js.map +1 -0
- package/dist/elements/EFTemporal.js +1 -2
- package/dist/elements/EFTemporal.js.map +1 -1
- package/dist/elements/EFText.d.ts +20 -0
- package/dist/elements/EFText.js +66 -9
- package/dist/elements/EFText.js.map +1 -1
- package/dist/elements/EFTimegroup.d.ts +12 -0
- package/dist/elements/EFTimegroup.js +43 -4
- package/dist/elements/EFTimegroup.js.map +1 -1
- package/dist/elements/EFVideo.d.ts +26 -0
- package/dist/elements/EFVideo.js +114 -36
- package/dist/elements/EFVideo.js.map +1 -1
- package/dist/elements/SampleBuffer.d.ts +19 -0
- package/dist/elements/updateAnimations.js +49 -3
- package/dist/elements/updateAnimations.js.map +1 -1
- package/dist/gui/EFWorkbench.d.ts +1 -0
- package/dist/gui/EFWorkbench.js +15 -0
- package/dist/gui/EFWorkbench.js.map +1 -1
- package/dist/gui/EFWorkbench.spacebar.js +26 -0
- package/dist/gui/EFWorkbench.spacebar.js.map +1 -0
- package/dist/gui/TWMixin.js +1 -1
- package/dist/gui/TWMixin.js.map +1 -1
- package/dist/gui/timeline/EFTimeline.d.ts +18 -1
- package/dist/gui/timeline/EFTimeline.js +119 -25
- package/dist/gui/timeline/EFTimeline.js.map +1 -1
- package/dist/gui/timeline/timelineStateContext.d.ts +2 -0
- package/dist/gui/timeline/timelineStateContext.js.map +1 -1
- package/dist/gui/timeline/tracks/EFThumbnailStrip.js +14 -8
- package/dist/gui/timeline/tracks/EFThumbnailStrip.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/preview/FrameController.d.ts +22 -1
- package/dist/preview/FrameController.js +26 -5
- package/dist/preview/FrameController.js.map +1 -1
- package/dist/preview/QualityUpgradeScheduler.d.ts +11 -2
- package/dist/preview/QualityUpgradeScheduler.js +31 -21
- package/dist/preview/QualityUpgradeScheduler.js.map +1 -1
- package/dist/preview/renderTimegroupToCanvas.js +4 -0
- package/dist/preview/renderTimegroupToCanvas.js.map +1 -1
- package/dist/preview/renderTimegroupToCanvas.types.d.ts +2 -0
- package/dist/preview/renderTimegroupToVideo.js +3 -0
- package/dist/preview/renderTimegroupToVideo.js.map +1 -1
- package/dist/preview/rendering/serializeTimelineDirect.js +30 -35
- package/dist/preview/rendering/serializeTimelineDirect.js.map +1 -1
- package/dist/style.css +4 -0
- package/dist/utils/LRUCache.js +17 -5
- package/dist/utils/LRUCache.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
|
@@ -2,6 +2,7 @@ import { FrameRenderable, FrameState } from "../preview/FrameController.js";
|
|
|
2
2
|
import { EFFramegen } from "../EF_FRAMEGEN.js";
|
|
3
3
|
import { EFMedia } from "./EFMedia.js";
|
|
4
4
|
import { RenderToVideoOptions } from "../preview/renderTimegroupToVideo.types.js";
|
|
5
|
+
import { ScrubInputCache } from "./EFMedia/videoTasks/ScrubInputCache.js";
|
|
5
6
|
import * as lit3 from "lit";
|
|
6
7
|
import { PropertyValueMap } from "lit";
|
|
7
8
|
import { VideoSample } from "mediabunny";
|
|
@@ -167,6 +168,31 @@ declare class EFVideo extends EFVideo_base implements FrameRenderable {
|
|
|
167
168
|
* @public
|
|
168
169
|
*/
|
|
169
170
|
prefetchScrubSegments(timestamps: number[], onProgress?: (loaded: number, total: number, segmentTimeRange: [number, number]) => void, signal?: AbortSignal): Promise<void>;
|
|
171
|
+
/**
|
|
172
|
+
* Warm the scrub BufferedSeekingInput cache for all segments covering [fromMs, toMs].
|
|
173
|
+
*
|
|
174
|
+
* Unlike prefetchScrubSegments (which only warms the network layer), this method
|
|
175
|
+
* constructs BufferedSeekingInput instances for each segment so that subsequent
|
|
176
|
+
* scrub seeks within the range complete without a network round-trip or BSI
|
|
177
|
+
* construction overhead.
|
|
178
|
+
*
|
|
179
|
+
* Returns a Promise that resolves after the range has been computed and segment
|
|
180
|
+
* fetches have been kicked off (but before individual fetches complete).
|
|
181
|
+
* Callers may await this or discard the promise — both are valid.
|
|
182
|
+
*/
|
|
183
|
+
warmScrubCacheForRange(fromMs: number, toMs: number, signal?: AbortSignal): Promise<void>;
|
|
184
|
+
/**
|
|
185
|
+
* Reset per-instance caches and rendition state. Allows tests to force
|
|
186
|
+
* the scrub fallback path on the next render without clearing the shared
|
|
187
|
+
* mediaCache (which races with in-flight fetches from other elements).
|
|
188
|
+
* @public – test-only
|
|
189
|
+
*/
|
|
190
|
+
clearInstanceCaches(): void;
|
|
191
|
+
/**
|
|
192
|
+
* Returns scrub cache statistics for inspection in tests.
|
|
193
|
+
* @public – test-only
|
|
194
|
+
*/
|
|
195
|
+
getScrubCacheStats(): ReturnType<ScrubInputCache["getStats"]>;
|
|
170
196
|
/**
|
|
171
197
|
* Clean up resources when component is disconnected
|
|
172
198
|
*/
|
package/dist/elements/EFVideo.js
CHANGED
|
@@ -14,8 +14,6 @@ import { context, trace } from "@opentelemetry/api";
|
|
|
14
14
|
import { createRef, ref } from "lit/directives/ref.js";
|
|
15
15
|
|
|
16
16
|
//#region src/elements/EFVideo.ts
|
|
17
|
-
const mainVideoInputCache = new MainVideoInputCache();
|
|
18
|
-
const scrubInputCache = new ScrubInputCache();
|
|
19
17
|
const log = debug("ef:elements:EFVideo");
|
|
20
18
|
var VideoSeekTask = class {
|
|
21
19
|
constructor() {
|
|
@@ -99,6 +97,8 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
|
|
|
99
97
|
* Standalone upgrade controller for elements without a timegroup.
|
|
100
98
|
*/
|
|
101
99
|
#standaloneUpgradeController = null;
|
|
100
|
+
#mainVideoInputCache = new MainVideoInputCache();
|
|
101
|
+
#scrubInputCache = new ScrubInputCache();
|
|
102
102
|
/**
|
|
103
103
|
* Set to true while renderToVideo is executing to suppress background
|
|
104
104
|
* quality upgrade tasks that would race with the render pipeline.
|
|
@@ -131,6 +131,20 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
|
|
|
131
131
|
getFrameState(_timeMs) {
|
|
132
132
|
const sourceTimeMs = this.currentSourceTimeMs;
|
|
133
133
|
const hasCache = this.#cachedVideoSample !== void 0 && this.#cachedVideoSampleTimeMs === sourceTimeMs;
|
|
134
|
+
if (this.#currentRenditionId === "scrub" && !this.rootTimegroup?.isRenderClone) {
|
|
135
|
+
const mediaEngine = this.mediaEngineTask.value;
|
|
136
|
+
if (mediaEngine) {
|
|
137
|
+
const mainTrack = mediaEngine.tracks.video;
|
|
138
|
+
if (mainTrack) {
|
|
139
|
+
const mainSegmentId = mediaEngine.index.segmentAt(sourceTimeMs, mainTrack);
|
|
140
|
+
if (mainSegmentId !== void 0 && mediaEngine.transport.isCached(mainSegmentId, mainTrack)) return {
|
|
141
|
+
needsPreparation: true,
|
|
142
|
+
isReady: false,
|
|
143
|
+
priority: PRIORITY_VIDEO
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
134
148
|
return {
|
|
135
149
|
needsPreparation: !hasCache,
|
|
136
150
|
isReady: hasCache,
|
|
@@ -162,10 +176,10 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
|
|
|
162
176
|
signal.throwIfAborted();
|
|
163
177
|
try {
|
|
164
178
|
const videoSample = await this.#fetchVideoSampleForFrame(mediaEngine, sourceTimeMs, signal);
|
|
165
|
-
signal.throwIfAborted();
|
|
166
179
|
this.#cachedVideoSample = videoSample;
|
|
167
180
|
this.#cachedVideoSampleTimeMs = sourceTimeMs;
|
|
168
181
|
this.unifiedVideoSeekTask.complete(videoSample);
|
|
182
|
+
signal.throwIfAborted();
|
|
169
183
|
} catch (error) {
|
|
170
184
|
if (error instanceof DOMException && error.name === "AbortError") {
|
|
171
185
|
this.unifiedVideoSeekTask.abort();
|
|
@@ -189,13 +203,15 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
|
|
|
189
203
|
*/
|
|
190
204
|
renderFrame(_timeMs) {
|
|
191
205
|
const sourceTimeMs = this.currentSourceTimeMs;
|
|
192
|
-
if (this.#cachedVideoSampleTimeMs === sourceTimeMs && this.#cachedVideoSample) {
|
|
206
|
+
if (this.#cachedVideoSampleTimeMs === sourceTimeMs && this.#cachedVideoSample) try {
|
|
193
207
|
const videoFrame = this.#cachedVideoSample.toVideoFrame();
|
|
194
208
|
try {
|
|
195
209
|
this.displayFrame(videoFrame, sourceTimeMs);
|
|
196
210
|
} finally {
|
|
197
211
|
videoFrame.close();
|
|
198
212
|
}
|
|
213
|
+
} catch {
|
|
214
|
+
this.#cachedVideoSample = void 0;
|
|
199
215
|
}
|
|
200
216
|
if (!this.parentTimegroup) updateAnimations(this);
|
|
201
217
|
}
|
|
@@ -240,25 +256,16 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
|
|
|
240
256
|
if (!scrubTrack) return;
|
|
241
257
|
const segmentId = mediaEngine.index.segmentAt(desiredSeekTimeMs, scrubTrack);
|
|
242
258
|
if (segmentId === void 0) return;
|
|
243
|
-
const scrubInput = await scrubInputCache.getOrCreateInput(mediaEngine.src, segmentId, async () => {
|
|
259
|
+
const scrubInput = await this.#scrubInputCache.getOrCreateInput(mediaEngine.src, segmentId, async () => {
|
|
244
260
|
let initSegment;
|
|
245
261
|
let mediaSegment;
|
|
246
262
|
try {
|
|
247
|
-
|
|
248
|
-
const mediaP = mediaEngine.transport.fetchMediaSegment(segmentId, scrubTrack, signal);
|
|
249
|
-
initP.catch(() => {});
|
|
250
|
-
mediaP.catch(() => {});
|
|
251
|
-
[initSegment, mediaSegment] = await Promise.all([initP, mediaP]);
|
|
263
|
+
[initSegment, mediaSegment] = await Promise.all([mediaEngine.transport.fetchInitSegment(scrubTrack), mediaEngine.transport.fetchMediaSegment(segmentId, scrubTrack)]);
|
|
252
264
|
} catch (error) {
|
|
253
|
-
if (error instanceof DOMException && error.name === "AbortError") throw error;
|
|
254
265
|
return;
|
|
255
266
|
}
|
|
256
267
|
if (!initSegment || !mediaSegment) return;
|
|
257
|
-
|
|
258
|
-
const combinedBlob = new Blob([initSegment, mediaSegment]);
|
|
259
|
-
signal.throwIfAborted();
|
|
260
|
-
const arrayBuffer = await combinedBlob.arrayBuffer();
|
|
261
|
-
signal.throwIfAborted();
|
|
268
|
+
const arrayBuffer = await new Blob([initSegment, mediaSegment]).arrayBuffer();
|
|
262
269
|
const { BufferedSeekingInput } = await import("./EFMedia/BufferedSeekingInput.js");
|
|
263
270
|
return new BufferedSeekingInput(arrayBuffer, {
|
|
264
271
|
videoBufferSize: EFMedia.VIDEO_SAMPLE_BUFFER_SIZE,
|
|
@@ -281,26 +288,19 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
|
|
|
281
288
|
if (!videoTrack) return;
|
|
282
289
|
const segmentId = mediaEngine.index.segmentAt(desiredSeekTimeMs, videoTrack);
|
|
283
290
|
if (segmentId === void 0) return;
|
|
284
|
-
const mainInput = await mainVideoInputCache.getOrCreateInput(mediaEngine.src, segmentId, String(videoTrack.id), async () => {
|
|
291
|
+
const mainInput = await this.#mainVideoInputCache.getOrCreateInput(mediaEngine.src, segmentId, String(videoTrack.id), async () => {
|
|
285
292
|
let initSegment;
|
|
286
293
|
let mediaSegment;
|
|
287
294
|
try {
|
|
288
|
-
const initP = mediaEngine.transport.fetchInitSegment(videoTrack
|
|
289
|
-
const mediaP = mediaEngine.transport.fetchMediaSegment(segmentId, videoTrack
|
|
290
|
-
initP.catch(() => {});
|
|
291
|
-
mediaP.catch(() => {});
|
|
295
|
+
const initP = mediaEngine.transport.fetchInitSegment(videoTrack);
|
|
296
|
+
const mediaP = mediaEngine.transport.fetchMediaSegment(segmentId, videoTrack);
|
|
292
297
|
[initSegment, mediaSegment] = await Promise.all([initP, mediaP]);
|
|
293
298
|
} catch (error) {
|
|
294
|
-
if (error instanceof DOMException && error.name === "AbortError") throw error;
|
|
295
299
|
if (error instanceof Error && (error.message.includes("401") || error.message.includes("UNAUTHORIZED") || error.message.includes("Failed to fetch") || error.message.includes("File not found") || error.message.includes("Media segment not found") || error.message.includes("Init segment not found") || error.message.includes("Track not found"))) return;
|
|
296
300
|
throw error;
|
|
297
301
|
}
|
|
298
302
|
if (!initSegment || !mediaSegment) return;
|
|
299
|
-
|
|
300
|
-
const combinedBlob = new Blob([initSegment, mediaSegment]);
|
|
301
|
-
signal.throwIfAborted();
|
|
302
|
-
const arrayBuffer = await combinedBlob.arrayBuffer();
|
|
303
|
-
signal.throwIfAborted();
|
|
303
|
+
const arrayBuffer = await new Blob([initSegment, mediaSegment]).arrayBuffer();
|
|
304
304
|
const { BufferedSeekingInput } = await import("./EFMedia/BufferedSeekingInput.js");
|
|
305
305
|
return new BufferedSeekingInput(arrayBuffer, {
|
|
306
306
|
videoBufferSize: EFMedia.VIDEO_SAMPLE_BUFFER_SIZE,
|
|
@@ -335,6 +335,10 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
|
|
|
335
335
|
updated(changedProperties) {
|
|
336
336
|
super.updated(changedProperties);
|
|
337
337
|
if (changedProperties.has("src") || changedProperties.has("fileId")) {
|
|
338
|
+
this.#cachedVideoSample = void 0;
|
|
339
|
+
this.#cachedVideoSampleTimeMs = void 0;
|
|
340
|
+
this.#mainVideoInputCache.clear();
|
|
341
|
+
this.#scrubInputCache.clear();
|
|
338
342
|
this.#invalidateUpgradeState("src-change");
|
|
339
343
|
this.#prewarmQualityUpgrade();
|
|
340
344
|
}
|
|
@@ -361,8 +365,8 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
|
|
|
361
365
|
if (!this.src && !this.fileId) return;
|
|
362
366
|
this.getMediaEngine().then((engine) => {
|
|
363
367
|
if (!engine) return;
|
|
364
|
-
const
|
|
365
|
-
this.#maybeScheduleQualityUpgrade(engine,
|
|
368
|
+
const targetTimeMs = this.currentSourceTimeMs ?? this.sourceInMs ?? 0;
|
|
369
|
+
this.#maybeScheduleQualityUpgrade(engine, targetTimeMs);
|
|
366
370
|
}).catch(() => {});
|
|
367
371
|
}
|
|
368
372
|
render() {
|
|
@@ -570,9 +574,9 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
|
|
|
570
574
|
if (!videoTrack) throw new Error("No video rendition available");
|
|
571
575
|
const segmentId = mediaEngine.index.segmentAt(sourceTimeMs, videoTrack);
|
|
572
576
|
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
|
|
575
|
-
const mediaP = mediaEngine.transport.fetchMediaSegment(segmentId, videoTrack
|
|
577
|
+
const seekingInput = await this.#mainVideoInputCache.getOrCreateInput(mediaEngine.src, segmentId, String(videoTrack.id), async () => {
|
|
578
|
+
const initP = mediaEngine.transport.fetchInitSegment(videoTrack);
|
|
579
|
+
const mediaP = mediaEngine.transport.fetchMediaSegment(segmentId, videoTrack);
|
|
576
580
|
initP.catch(() => {});
|
|
577
581
|
mediaP.catch(() => {});
|
|
578
582
|
const [initSegment, mediaSegment] = await Promise.all([initP, mediaP]);
|
|
@@ -598,9 +602,9 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
|
|
|
598
602
|
});
|
|
599
603
|
const segmentId = mediaEngine.index.segmentAt(sourceTimeMs, scrubTrack);
|
|
600
604
|
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
|
|
603
|
-
const mediaP = mediaEngine.transport.fetchMediaSegment(segmentId, scrubTrack
|
|
605
|
+
const seekingInput = await this.#scrubInputCache.getOrCreateInput(mediaEngine.src, segmentId, async () => {
|
|
606
|
+
const initP = mediaEngine.transport.fetchInitSegment(scrubTrack);
|
|
607
|
+
const mediaP = mediaEngine.transport.fetchMediaSegment(segmentId, scrubTrack);
|
|
604
608
|
initP.catch(() => {});
|
|
605
609
|
mediaP.catch(() => {});
|
|
606
610
|
const [initSegment, mediaSegment] = await Promise.all([initP, mediaP]);
|
|
@@ -747,11 +751,56 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
|
|
|
747
751
|
log(`prefetchScrubSegments: complete`);
|
|
748
752
|
}
|
|
749
753
|
/**
|
|
754
|
+
* Warm the scrub BufferedSeekingInput cache for all segments covering [fromMs, toMs].
|
|
755
|
+
*
|
|
756
|
+
* Unlike prefetchScrubSegments (which only warms the network layer), this method
|
|
757
|
+
* constructs BufferedSeekingInput instances for each segment so that subsequent
|
|
758
|
+
* scrub seeks within the range complete without a network round-trip or BSI
|
|
759
|
+
* construction overhead.
|
|
760
|
+
*
|
|
761
|
+
* Returns a Promise that resolves after the range has been computed and segment
|
|
762
|
+
* fetches have been kicked off (but before individual fetches complete).
|
|
763
|
+
* Callers may await this or discard the promise — both are valid.
|
|
764
|
+
*/
|
|
765
|
+
async warmScrubCacheForRange(fromMs, toMs, signal) {
|
|
766
|
+
const mediaEngine = await this.getMediaEngine(signal);
|
|
767
|
+
if (!mediaEngine) return;
|
|
768
|
+
if (signal?.aborted) return;
|
|
769
|
+
const scrubTrack = mediaEngine.tracks.scrub;
|
|
770
|
+
if (!scrubTrack) return;
|
|
771
|
+
const segments = mediaEngine.index.segmentsInRange(fromMs, toMs, scrubTrack);
|
|
772
|
+
if (segments.length === 0) return;
|
|
773
|
+
const { src } = mediaEngine;
|
|
774
|
+
for (const { segmentId } of segments) {
|
|
775
|
+
if (signal?.aborted) return;
|
|
776
|
+
const capturedSegmentId = segmentId;
|
|
777
|
+
this.#scrubInputCache.getOrCreateInput(src, capturedSegmentId, async () => {
|
|
778
|
+
if (signal?.aborted) return void 0;
|
|
779
|
+
let initSegment;
|
|
780
|
+
let mediaSegment;
|
|
781
|
+
try {
|
|
782
|
+
[initSegment, mediaSegment] = await Promise.all([mediaEngine.transport.fetchInitSegment(scrubTrack), mediaEngine.transport.fetchMediaSegment(capturedSegmentId, scrubTrack)]);
|
|
783
|
+
} catch {
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
if (!initSegment || !mediaSegment) return void 0;
|
|
787
|
+
const arrayBuffer = await new Blob([initSegment, mediaSegment]).arrayBuffer();
|
|
788
|
+
const { BufferedSeekingInput } = await import("./EFMedia/BufferedSeekingInput.js");
|
|
789
|
+
return new BufferedSeekingInput(arrayBuffer, {
|
|
790
|
+
videoBufferSize: EFMedia.VIDEO_SAMPLE_BUFFER_SIZE,
|
|
791
|
+
audioBufferSize: EFMedia.AUDIO_SAMPLE_BUFFER_SIZE,
|
|
792
|
+
startTimeOffsetMs: scrubTrack.startTimeOffsetMs
|
|
793
|
+
});
|
|
794
|
+
}).catch(() => {});
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
750
798
|
* Maybe schedule quality upgrade tasks for this element.
|
|
751
799
|
* Called when returning a scrub sample - checks if state has changed and submits tasks.
|
|
752
800
|
*/
|
|
753
801
|
#maybeScheduleQualityUpgrade(mediaEngine, sourceTimeMs) {
|
|
754
802
|
if (this.#renderingToVideo) return;
|
|
803
|
+
if (this.rootTimegroup?.isRenderClone) return;
|
|
755
804
|
const mainTrack = mediaEngine.tracks.video;
|
|
756
805
|
if (!mainTrack) return;
|
|
757
806
|
const segmentId = mediaEngine.index.segmentAt(sourceTimeMs, mainTrack);
|
|
@@ -766,12 +815,17 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
|
|
|
766
815
|
}
|
|
767
816
|
const segments = this.#computeLookaheadSegments(mediaEngine, sourceTimeMs, mainTrack);
|
|
768
817
|
if (segments.length === 0) return;
|
|
818
|
+
const capturedSrc = mediaEngine.src;
|
|
769
819
|
const tasks = segments.map((seg) => ({
|
|
770
820
|
key: `${this.#upgradeOwnerId}:${seg.segmentId}:${mainTrack.id}`,
|
|
771
821
|
fetch: async (signal) => {
|
|
772
822
|
await mediaEngine.transport.fetchInitSegment(mainTrack, signal);
|
|
773
823
|
await mediaEngine.transport.fetchMediaSegment(seg.segmentId, mainTrack, signal);
|
|
774
824
|
},
|
|
825
|
+
isCached: () => {
|
|
826
|
+
if (this.mediaEngineTask?.value?.src !== capturedSrc) return false;
|
|
827
|
+
return mediaEngine.transport.isCached(seg.segmentId, mainTrack);
|
|
828
|
+
},
|
|
775
829
|
deadlineMs: seg.deadlineMs,
|
|
776
830
|
owner: this.#upgradeOwnerId
|
|
777
831
|
}));
|
|
@@ -824,7 +878,8 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
|
|
|
824
878
|
await task.fetch(signal);
|
|
825
879
|
} catch {}
|
|
826
880
|
}
|
|
827
|
-
if (!signal.aborted) this.
|
|
881
|
+
if (!signal.aborted) if (this.rootTimegroup) this.rootTimegroup.requestFrameRender();
|
|
882
|
+
else this.playbackController?.runThrottledFrameTask().catch(() => {});
|
|
828
883
|
})().catch(() => {});
|
|
829
884
|
}
|
|
830
885
|
/**
|
|
@@ -835,10 +890,33 @@ let EFVideo = class EFVideo$1 extends TWMixin(EFMedia) {
|
|
|
835
890
|
this.#upgradeState = null;
|
|
836
891
|
}
|
|
837
892
|
/**
|
|
893
|
+
* Reset per-instance caches and rendition state. Allows tests to force
|
|
894
|
+
* the scrub fallback path on the next render without clearing the shared
|
|
895
|
+
* mediaCache (which races with in-flight fetches from other elements).
|
|
896
|
+
* @public – test-only
|
|
897
|
+
*/
|
|
898
|
+
clearInstanceCaches() {
|
|
899
|
+
this.#cachedVideoSample = void 0;
|
|
900
|
+
this.#cachedVideoSampleTimeMs = void 0;
|
|
901
|
+
this.#currentRenditionId = void 0;
|
|
902
|
+
this.#upgradeState = null;
|
|
903
|
+
this.#mainVideoInputCache.clear();
|
|
904
|
+
this.#scrubInputCache.clear();
|
|
905
|
+
}
|
|
906
|
+
/**
|
|
907
|
+
* Returns scrub cache statistics for inspection in tests.
|
|
908
|
+
* @public – test-only
|
|
909
|
+
*/
|
|
910
|
+
getScrubCacheStats() {
|
|
911
|
+
return this.#scrubInputCache.getStats();
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
838
914
|
* Clean up resources when component is disconnected
|
|
839
915
|
*/
|
|
840
916
|
disconnectedCallback() {
|
|
841
917
|
super.disconnectedCallback();
|
|
918
|
+
this.#cachedVideoSample = void 0;
|
|
919
|
+
this.#cachedVideoSampleTimeMs = void 0;
|
|
842
920
|
this.#delayedLoadingState.clearAllLoading();
|
|
843
921
|
this.#invalidateUpgradeState("disconnect");
|
|
844
922
|
this.#standaloneUpgradeController?.abort();
|