@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.
Files changed (78) hide show
  1. package/dist/EF_FRAMEGEN.js +1 -0
  2. package/dist/EF_FRAMEGEN.js.map +1 -1
  3. package/dist/elements/EFCaptions.d.ts +2 -2
  4. package/dist/elements/EFCaptions.js +1 -1
  5. package/dist/elements/EFCaptions.js.map +1 -1
  6. package/dist/elements/EFImage.js +3 -4
  7. package/dist/elements/EFImage.js.map +1 -1
  8. package/dist/elements/EFMedia/BufferedSeekingInput.js +1 -1
  9. package/dist/elements/EFMedia/CachedFetcher.js +99 -0
  10. package/dist/elements/EFMedia/CachedFetcher.js.map +1 -0
  11. package/dist/elements/EFMedia/MediaEngine.d.ts +19 -0
  12. package/dist/elements/EFMedia/MediaEngine.js +129 -0
  13. package/dist/elements/EFMedia/MediaEngine.js.map +1 -0
  14. package/dist/elements/EFMedia/SegmentIndex.d.ts +32 -0
  15. package/dist/elements/EFMedia/SegmentIndex.js +185 -0
  16. package/dist/elements/EFMedia/SegmentIndex.js.map +1 -0
  17. package/dist/elements/EFMedia/SegmentTransport.d.ts +12 -0
  18. package/dist/elements/EFMedia/SegmentTransport.js +69 -0
  19. package/dist/elements/EFMedia/SegmentTransport.js.map +1 -0
  20. package/dist/elements/EFMedia/TimingModel.d.ts +10 -0
  21. package/dist/elements/EFMedia/TimingModel.js +28 -0
  22. package/dist/elements/EFMedia/TimingModel.js.map +1 -0
  23. package/dist/elements/EFMedia/shared/AudioSpanUtils.js +7 -6
  24. package/dist/elements/EFMedia/shared/AudioSpanUtils.js.map +1 -1
  25. package/dist/elements/EFMedia/shared/ThumbnailExtractor.js +13 -34
  26. package/dist/elements/EFMedia/shared/ThumbnailExtractor.js.map +1 -1
  27. package/dist/elements/EFMedia.d.ts +4 -3
  28. package/dist/elements/EFMedia.js +14 -31
  29. package/dist/elements/EFMedia.js.map +1 -1
  30. package/dist/elements/EFSourceMixin.js +1 -1
  31. package/dist/elements/EFSourceMixin.js.map +1 -1
  32. package/dist/elements/EFTemporal.js +2 -1
  33. package/dist/elements/EFTemporal.js.map +1 -1
  34. package/dist/elements/EFTimegroup.js +2 -1
  35. package/dist/elements/EFTimegroup.js.map +1 -1
  36. package/dist/elements/EFVideo.js +204 -187
  37. package/dist/elements/EFVideo.js.map +1 -1
  38. package/dist/gui/EFConfiguration.d.ts +0 -7
  39. package/dist/gui/EFConfiguration.js +0 -5
  40. package/dist/gui/EFConfiguration.js.map +1 -1
  41. package/dist/gui/EFWorkbench.d.ts +2 -0
  42. package/dist/gui/EFWorkbench.js +68 -1
  43. package/dist/gui/EFWorkbench.js.map +1 -1
  44. package/dist/gui/PlaybackController.d.ts +2 -0
  45. package/dist/gui/PlaybackController.js +11 -1
  46. package/dist/gui/PlaybackController.js.map +1 -1
  47. package/dist/gui/ef-theme.css +11 -0
  48. package/dist/gui/timeline/tracks/AudioTrack.js +28 -30
  49. package/dist/gui/timeline/tracks/AudioTrack.js.map +1 -1
  50. package/dist/gui/timeline/tracks/EFThumbnailStrip.d.ts +1 -0
  51. package/dist/gui/timeline/tracks/EFThumbnailStrip.js +41 -8
  52. package/dist/gui/timeline/tracks/EFThumbnailStrip.js.map +1 -1
  53. package/dist/gui/timeline/tracks/VideoTrack.js +2 -2
  54. package/dist/gui/timeline/tracks/VideoTrack.js.map +1 -1
  55. package/dist/gui/timeline/tracks/waveformUtils.js +19 -19
  56. package/dist/gui/timeline/tracks/waveformUtils.js.map +1 -1
  57. package/dist/preview/QualityUpgradeScheduler.d.ts +8 -0
  58. package/dist/preview/QualityUpgradeScheduler.js +13 -1
  59. package/dist/preview/QualityUpgradeScheduler.js.map +1 -1
  60. package/dist/preview/renderTimegroupToVideo.js +3 -3
  61. package/dist/preview/renderTimegroupToVideo.js.map +1 -1
  62. package/dist/preview/renderVideoToVideo.js +5 -6
  63. package/dist/preview/renderVideoToVideo.js.map +1 -1
  64. package/dist/transcoding/types/index.d.ts +6 -94
  65. package/dist/transcoding/utils/UrlGenerator.d.ts +3 -12
  66. package/dist/transcoding/utils/UrlGenerator.js +3 -29
  67. package/dist/transcoding/utils/UrlGenerator.js.map +1 -1
  68. package/package.json +2 -2
  69. package/test/setup.ts +1 -1
  70. package/test/useAssetMSW.ts +0 -100
  71. package/dist/elements/EFMedia/AssetMediaEngine.js +0 -284
  72. package/dist/elements/EFMedia/AssetMediaEngine.js.map +0 -1
  73. package/dist/elements/EFMedia/BaseMediaEngine.js +0 -200
  74. package/dist/elements/EFMedia/BaseMediaEngine.js.map +0 -1
  75. package/dist/elements/EFMedia/FileMediaEngine.js +0 -122
  76. package/dist/elements/EFMedia/FileMediaEngine.js.map +0 -1
  77. package/dist/elements/EFMedia/JitMediaEngine.js +0 -157
  78. package/dist/elements/EFMedia/JitMediaEngine.js.map +0 -1
@@ -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
- bottom: 0;
68
- background: rgba(0, 0, 0, 0.6);
69
- display: flex;
70
- align-items: center;
71
- justify-content: center;
67
+ height: 2px;
68
+ overflow: hidden;
72
69
  z-index: 10;
73
- backdrop-filter: blur(2px);
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
- @keyframes spin {
95
- 0% { transform: rotate(0deg); }
96
- 100% { transform: rotate(360deg); }
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
- .loading-message {
99
- font-size: 12px;
100
- opacity: 0.8;
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
- const sourceTimeMs = this.currentSourceTimeMs;
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 videoSample = await this.#fetchVideoSampleForFrame(mediaEngine, sourceTimeMs, signal);
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
- this.#cachedVideoSample = videoSample;
171
- this.#cachedVideoSampleTimeMs = sourceTimeMs;
172
- this.unifiedVideoSeekTask.complete(videoSample);
173
- } catch (error) {
174
- if (error instanceof DOMException && error.name === "AbortError") {
175
- this.unifiedVideoSeekTask.abort();
176
- throw error;
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
- console.warn(`Video seek error at ${sourceTimeMs}ms:`, error);
179
- this.#cachedVideoSample = void 0;
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 mainRendition = mediaEngine.videoRendition;
213
- if (mainRendition) {
214
- const mainSegmentId = mediaEngine.computeSegmentId(desiredSeekTimeMs, mainRendition);
215
- if (mainSegmentId !== void 0 && mediaEngine.isSegmentCached(mainSegmentId, mainRendition)) {
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.getScrubVideoRendition?.()) {
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 scrubRendition = mediaEngine.getScrubVideoRendition?.();
241
- if (!scrubRendition) return;
242
- const scrubRenditionWithSrc = {
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(scrubRenditionWithSrc, signal);
253
- const mediaP = mediaEngine.fetchMediaSegment(segmentId, scrubRenditionWithSrc, signal);
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: scrubRendition.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 videoRendition = mediaEngine.getVideoRendition?.() ?? mediaEngine.videoRendition;
286
- if (!videoRendition) return;
287
- const segmentId = mediaEngine.computeSegmentId(desiredSeekTimeMs, videoRendition);
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, videoRendition.id, async () => {
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(videoRendition, signal);
294
- const mediaP = mediaEngine.fetchMediaSegment(segmentId, videoRendition, signal);
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: videoRendition.startTimeOffsetMs
308
+ startTimeOffsetMs: videoTrack.startTimeOffsetMs
314
309
  });
315
310
  });
316
311
  if (!mainInput) return;
317
312
  signal.throwIfAborted();
318
- const videoTrack = await mainInput.getFirstVideoTrack();
319
- if (!videoTrack) return;
313
+ const videoTrackInfo = await mainInput.getFirstVideoTrack();
314
+ if (!videoTrackInfo) return;
320
315
  signal.throwIfAborted();
321
- return await mainInput.seek(videoTrack.id, desiredSeekTimeMs);
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(250, (isLoading, message) => {
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")) this.#invalidateUpgradeState("src-change");
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))) this.#invalidateUpgradeState("bounds-change");
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
- const mediaEngine = await this.getMediaEngine(signal);
552
- signal.throwIfAborted();
553
- if (!mediaEngine) throw new Error("No media engine available for frame capture");
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 (!videoTrack) throw new Error("No video track found in segment");
581
- videoSample = await seekingInput.seek(videoTrack.id, sourceTimeMs);
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
- } else {
584
- const scrubRendition = mediaEngine.getScrubVideoRendition?.();
585
- if (!scrubRendition) return this.getVideoFrameAtSourceTime(sourceTimeMs, {
586
- quality: "main",
587
- signal
588
- });
589
- const scrubRenditionWithSrc = {
590
- ...scrubRendition,
591
- src: mediaEngine.src
592
- };
593
- const segmentId = mediaEngine.computeSegmentId(sourceTimeMs, scrubRenditionWithSrc);
594
- if (segmentId === void 0) throw new Error(`Cannot compute scrub segment ID for time ${sourceTimeMs}ms`);
595
- const seekingInput = await scrubInputCache.getOrCreateInput(mediaEngine.src, segmentId, async () => {
596
- const initP = mediaEngine.fetchInitSegment(scrubRenditionWithSrc, signal);
597
- const mediaP = mediaEngine.fetchMediaSegment(segmentId, scrubRenditionWithSrc, signal);
598
- initP.catch(() => {});
599
- mediaP.catch(() => {});
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
- signal.throwIfAborted();
609
- if (!seekingInput) return this.getVideoFrameAtSourceTime(sourceTimeMs, {
610
- quality: "main",
611
- signal
612
- });
613
- const videoTrack = await seekingInput.getFirstVideoTrack();
614
- signal.throwIfAborted();
615
- if (!videoTrack) return this.getVideoFrameAtSourceTime(sourceTimeMs, {
616
- quality: "main",
617
- signal
618
- });
619
- videoSample = await seekingInput.seek(videoTrack.id, sourceTimeMs);
620
- signal.throwIfAborted();
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 scrubRendition = mediaEngine.getScrubVideoRendition();
688
- if (!scrubRendition) {
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.computeSegmentId(ts, scrubRenditionWithSrc);
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.isSegmentCached(firstSegmentId, scrubRenditionWithSrc)) {
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, scrubRenditionWithSrc, fetchSignal);
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
- const mainRendition = mediaEngine.videoRendition;
750
- if (!mainRendition) return;
751
- const segmentId = mediaEngine.computeSegmentId(sourceTimeMs, mainRendition);
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)) return;
755
- const segments = this.#computeLookaheadSegments(mediaEngine, sourceTimeMs, mainRendition);
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.id}:${seg.segmentId}:${mainRendition.id}`,
770
+ key: `${this.#upgradeOwnerId}:${seg.segmentId}:${mainTrack.id}`,
759
771
  fetch: async (signal) => {
760
- await mediaEngine.fetchInitSegment(mainRendition, signal);
761
- await mediaEngine.fetchMediaSegment(seg.segmentId, mainRendition, signal);
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.id
776
+ owner: this.#upgradeOwnerId
765
777
  }));
766
778
  const scheduler = this.rootTimegroup?.qualityUpgradeScheduler;
767
- if (scheduler) scheduler.replaceForOwner(this.id, tasks);
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, rendition, maxLookahead = 5) {
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.computeSegmentId(probeTimeMs, rendition);
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.isSegmentCached(segmentId, rendition)) {
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 = rendition.segmentDurationsMs?.[segmentId - 1] ?? rendition.segmentDurationMs ?? 2e3;
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.id);
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
- const { renderVideoToVideo } = await import("../preview/renderVideoToVideo.js");
866
- return renderVideoToVideo(this, options);
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);