@editframe/elements 0.20.4-beta.0 → 0.21.0-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.
Files changed (92) hide show
  1. package/dist/DelayedLoadingState.js +0 -27
  2. package/dist/EF_FRAMEGEN.d.ts +5 -3
  3. package/dist/EF_FRAMEGEN.js +50 -11
  4. package/dist/_virtual/_@oxc-project_runtime@0.93.0/helpers/decorate.js +7 -0
  5. package/dist/elements/ContextProxiesController.js +2 -22
  6. package/dist/elements/EFAudio.js +4 -8
  7. package/dist/elements/EFCaptions.js +59 -84
  8. package/dist/elements/EFImage.js +5 -6
  9. package/dist/elements/EFMedia/AssetIdMediaEngine.js +2 -4
  10. package/dist/elements/EFMedia/AssetMediaEngine.js +35 -30
  11. package/dist/elements/EFMedia/BaseMediaEngine.js +57 -73
  12. package/dist/elements/EFMedia/BufferedSeekingInput.js +134 -76
  13. package/dist/elements/EFMedia/JitMediaEngine.js +9 -19
  14. package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js +3 -6
  15. package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js +1 -1
  16. package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.js +1 -1
  17. package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.js +6 -5
  18. package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js +1 -3
  19. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js +1 -1
  20. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js +1 -1
  21. package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js +1 -1
  22. package/dist/elements/EFMedia/shared/AudioSpanUtils.js +4 -16
  23. package/dist/elements/EFMedia/shared/BufferUtils.js +2 -15
  24. package/dist/elements/EFMedia/shared/GlobalInputCache.js +0 -24
  25. package/dist/elements/EFMedia/shared/PrecisionUtils.js +0 -21
  26. package/dist/elements/EFMedia/shared/ThumbnailExtractor.js +0 -17
  27. package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js +1 -10
  28. package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.d.ts +29 -0
  29. package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js +32 -0
  30. package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js +1 -15
  31. package/dist/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.js +1 -7
  32. package/dist/elements/EFMedia/videoTasks/makeScrubVideoInputTask.js +8 -5
  33. package/dist/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.js +12 -13
  34. package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.js +1 -1
  35. package/dist/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.js +134 -70
  36. package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js +7 -11
  37. package/dist/elements/EFMedia.js +26 -24
  38. package/dist/elements/EFSourceMixin.js +5 -7
  39. package/dist/elements/EFSurface.js +6 -9
  40. package/dist/elements/EFTemporal.js +19 -37
  41. package/dist/elements/EFThumbnailStrip.js +16 -59
  42. package/dist/elements/EFTimegroup.js +95 -90
  43. package/dist/elements/EFVideo.d.ts +6 -2
  44. package/dist/elements/EFVideo.js +142 -107
  45. package/dist/elements/EFWaveform.js +18 -27
  46. package/dist/elements/SampleBuffer.js +2 -5
  47. package/dist/elements/TargetController.js +3 -3
  48. package/dist/elements/durationConverter.js +4 -4
  49. package/dist/elements/updateAnimations.js +14 -35
  50. package/dist/gui/ContextMixin.js +23 -52
  51. package/dist/gui/EFConfiguration.js +7 -7
  52. package/dist/gui/EFControls.js +5 -5
  53. package/dist/gui/EFFilmstrip.js +77 -98
  54. package/dist/gui/EFFitScale.js +5 -6
  55. package/dist/gui/EFFocusOverlay.js +4 -4
  56. package/dist/gui/EFPreview.js +4 -4
  57. package/dist/gui/EFScrubber.js +9 -9
  58. package/dist/gui/EFTimeDisplay.js +5 -5
  59. package/dist/gui/EFToggleLoop.js +4 -4
  60. package/dist/gui/EFTogglePlay.js +5 -5
  61. package/dist/gui/EFWorkbench.js +5 -5
  62. package/dist/gui/TWMixin2.js +1 -1
  63. package/dist/index.d.ts +1 -0
  64. package/dist/otel/BridgeSpanExporter.d.ts +13 -0
  65. package/dist/otel/BridgeSpanExporter.js +87 -0
  66. package/dist/otel/setupBrowserTracing.d.ts +12 -0
  67. package/dist/otel/setupBrowserTracing.js +30 -0
  68. package/dist/otel/tracingHelpers.d.ts +34 -0
  69. package/dist/otel/tracingHelpers.js +113 -0
  70. package/dist/transcoding/cache/RequestDeduplicator.js +0 -21
  71. package/dist/transcoding/cache/URLTokenDeduplicator.js +1 -21
  72. package/dist/transcoding/utils/UrlGenerator.js +2 -19
  73. package/dist/utils/LRUCache.js +6 -53
  74. package/package.json +10 -2
  75. package/src/elements/EFCaptions.browsertest.ts +2 -0
  76. package/src/elements/EFMedia/AssetMediaEngine.ts +65 -37
  77. package/src/elements/EFMedia/BaseMediaEngine.ts +110 -52
  78. package/src/elements/EFMedia/BufferedSeekingInput.ts +218 -101
  79. package/src/elements/EFMedia/audioTasks/makeAudioInputTask.ts +7 -3
  80. package/src/elements/EFMedia/videoTasks/MainVideoInputCache.ts +76 -0
  81. package/src/elements/EFMedia/videoTasks/makeScrubVideoInputTask.ts +16 -10
  82. package/src/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.ts +7 -1
  83. package/src/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.ts +222 -116
  84. package/src/elements/EFMedia.ts +16 -1
  85. package/src/elements/EFTimegroup.browsertest.ts +10 -8
  86. package/src/elements/EFTimegroup.ts +164 -76
  87. package/src/elements/EFVideo.browsertest.ts +19 -27
  88. package/src/elements/EFVideo.ts +203 -101
  89. package/src/otel/BridgeSpanExporter.ts +150 -0
  90. package/src/otel/setupBrowserTracing.ts +68 -0
  91. package/src/otel/tracingHelpers.ts +251 -0
  92. package/types.json +1 -1
@@ -6,6 +6,7 @@ import { customElement, property } from "lit/decorators.js";
6
6
 
7
7
  import { EF_INTERACTIVE } from "../EF_INTERACTIVE.js";
8
8
  import { isContextMixin } from "../gui/ContextMixin.js";
9
+ import { isTracingEnabled, withSpan } from "../otel/tracingHelpers.js";
9
10
  import type { AudioSpan } from "../transcoding/types/index.ts";
10
11
  import { durationConverter } from "./durationConverter.js";
11
12
  import { deepGetMediaElements } from "./EFMedia.js";
@@ -422,18 +423,47 @@ export class EFTimegroup extends EFTemporal(LitElement) {
422
423
  }
423
424
 
424
425
  async waitForFrameTasks() {
425
- const temporalElements = deepGetElementsWithFrameTasks(this);
426
+ const result = await withSpan(
427
+ "timegroup.waitForFrameTasks",
428
+ {
429
+ timegroupId: this.id || "unknown",
430
+ mode: this.mode,
431
+ },
432
+ undefined,
433
+ async (span) => {
434
+ const innerStart = performance.now();
435
+
436
+ const temporalElements = deepGetElementsWithFrameTasks(this);
437
+ if (isTracingEnabled()) {
438
+ span.setAttribute("temporalElementsCount", temporalElements.length);
439
+ }
426
440
 
427
- // Filter to only include temporally visible elements for frame processing
428
- // Use animation-friendly visibility to prevent animation jumps at exact boundaries
429
- const visibleElements = temporalElements.filter((element) => {
430
- const animationState = evaluateTemporalStateForAnimation(element);
431
- return animationState.isVisible;
432
- });
441
+ // Filter to only include temporally visible elements for frame processing
442
+ // Use animation-friendly visibility to prevent animation jumps at exact boundaries
443
+ const visibleElements = temporalElements.filter((element) => {
444
+ const animationState = evaluateTemporalStateForAnimation(element);
445
+ return animationState.isVisible;
446
+ });
447
+ if (isTracingEnabled()) {
448
+ span.setAttribute("visibleElementsCount", visibleElements.length);
449
+ }
433
450
 
434
- await Promise.all(
435
- visibleElements.map((element) => element.frameTask.run()),
451
+ const promiseStart = performance.now();
452
+
453
+ await Promise.all(
454
+ visibleElements.map((element) => element.frameTask.run()),
455
+ );
456
+ const promiseEnd = performance.now();
457
+
458
+ const innerEnd = performance.now();
459
+ if (isTracingEnabled()) {
460
+ span.setAttribute("actualInnerMs", innerEnd - innerStart);
461
+ span.setAttribute("promiseAwaitMs", promiseEnd - promiseStart);
462
+ }
463
+ },
436
464
  );
465
+
466
+ return result;
437
467
  }
438
468
 
439
469
  mediaDurationsPromise: Promise<void> | undefined = undefined;
@@ -452,37 +482,53 @@ export class EFTimegroup extends EFTemporal(LitElement) {
452
482
  * in calculations and it was not clear why.
453
483
  */
454
484
  async #waitForMediaDurations() {
455
- // We must await updateComplete to ensure all media elements inside this are connected
456
- // and will match deepGetMediaElements
457
- await this.updateComplete;
458
- const mediaElements = deepGetMediaElements(this);
459
- // Then, we must await the fragmentIndexTask to ensure all media elements have their
460
- // fragment index loaded, which is where their duration is parsed from.
461
- await Promise.all(
462
- mediaElements.map((m) =>
463
- m.mediaEngineTask.value ? Promise.resolve() : m.mediaEngineTask.run(),
464
- ),
465
- );
485
+ return withSpan(
486
+ "timegroup.waitForMediaDurations",
487
+ {
488
+ timegroupId: this.id || "unknown",
489
+ mode: this.mode,
490
+ },
491
+ undefined,
492
+ async (span) => {
493
+ // We must await updateComplete to ensure all media elements inside this are connected
494
+ // and will match deepGetMediaElements
495
+ await this.updateComplete;
496
+ const mediaElements = deepGetMediaElements(this);
497
+ if (isTracingEnabled()) {
498
+ span.setAttribute("mediaElementsCount", mediaElements.length);
499
+ }
466
500
 
467
- // After waiting for durations, we must force some updates to cascade and ensure all temporal elements
468
- // have correct durations and start times. It is not ideal that we have to do this inside here,
469
- // but it is the best current way to ensure that all temporal elements have correct durations and start times.
501
+ // Then, we must await the fragmentIndexTask to ensure all media elements have their
502
+ // fragment index loaded, which is where their duration is parsed from.
503
+ await Promise.all(
504
+ mediaElements.map((m) =>
505
+ m.mediaEngineTask.value
506
+ ? Promise.resolve()
507
+ : m.mediaEngineTask.run(),
508
+ ),
509
+ );
470
510
 
471
- // Next, we must flush the startTimeMs cache to ensure all media elements have their
472
- // startTimeMs parsed fresh, otherwise the startTimeMs is cached per animation frame.
473
- flushStartTimeMsCache();
511
+ // After waiting for durations, we must force some updates to cascade and ensure all temporal elements
512
+ // have correct durations and start times. It is not ideal that we have to do this inside here,
513
+ // but it is the best current way to ensure that all temporal elements have correct durations and start times.
474
514
 
475
- // Flush duration cache since child durations may have changed
476
- flushSequenceDurationCache();
515
+ // Next, we must flush the startTimeMs cache to ensure all media elements have their
516
+ // startTimeMs parsed fresh, otherwise the startTimeMs is cached per animation frame.
517
+ flushStartTimeMsCache();
477
518
 
478
- // Request an update to the currentTime of this group, ensuring that time updates will cascade
479
- // down to children, forcing sequence groups to arrange correctly.
480
- // This also makes the filmstrip update correctly.
481
- this.requestUpdate("currentTime");
482
- // Finally, we must await updateComplete to ensure all temporal elements have their
483
- // currentTime updated and all animations have run.
519
+ // Flush duration cache since child durations may have changed
520
+ flushSequenceDurationCache();
484
521
 
485
- await this.updateComplete;
522
+ // Request an update to the currentTime of this group, ensuring that time updates will cascade
523
+ // down to children, forcing sequence groups to arrange correctly.
524
+ // This also makes the filmstrip update correctly.
525
+ this.requestUpdate("currentTime");
526
+ // Finally, we must await updateComplete to ensure all temporal elements have their
527
+ // currentTime updated and all animations have run.
528
+
529
+ await this.updateComplete;
530
+ },
531
+ );
486
532
  }
487
533
 
488
534
  get childTemporals() {
@@ -645,35 +691,52 @@ export class EFTimegroup extends EFTemporal(LitElement) {
645
691
  }
646
692
 
647
693
  async renderAudio(fromMs: number, toMs: number) {
648
- // Here we determine the number of samples we need to render rather than the duration.
649
- // We cannot tolerate having more or fewer samples than fit exactlly into AAC frames.
650
- const durationMs = toMs - fromMs;
651
- const duration = durationMs / 1000;
652
- const exactSamples = 48000 * duration;
653
- const aacFrames = exactSamples / 1024;
654
- const alignedFrames = Math.round(aacFrames);
655
- const contextSize = alignedFrames * 1024; // AAC-aligned sample count
656
-
657
- // Debug logging for audio duration calculations
658
- if (contextSize <= 0) {
659
- throw new Error(
660
- `Duration must be greater than 0 when rendering audio. ${contextSize}ms`,
661
- );
662
- }
694
+ return withSpan(
695
+ "timegroup.renderAudio",
696
+ {
697
+ timegroupId: this.id || "unknown",
698
+ fromMs,
699
+ toMs,
700
+ durationMs: toMs - fromMs,
701
+ },
702
+ undefined,
703
+ async (span) => {
704
+ // Here we determine the number of samples we need to render rather than the duration.
705
+ // We cannot tolerate having more or fewer samples than fit exactlly into AAC frames.
706
+ const durationMs = toMs - fromMs;
707
+ const duration = durationMs / 1000;
708
+ const exactSamples = 48000 * duration;
709
+ const aacFrames = exactSamples / 1024;
710
+ const alignedFrames = Math.round(aacFrames);
711
+ const contextSize = alignedFrames * 1024; // AAC-aligned sample count
712
+
713
+ if (isTracingEnabled()) {
714
+ span.setAttribute("contextSize", contextSize);
715
+ span.setAttribute("alignedFrames", alignedFrames);
716
+ }
663
717
 
664
- let audioContext: OfflineAudioContext;
665
- try {
666
- audioContext = new OfflineAudioContext(2, contextSize, 48000);
667
- } catch (error) {
668
- throw new Error(
669
- `[EFTimegroup.renderAudio] Failed to create OfflineAudioContext(2, ${contextSize}, 48000) for renderAudio(${fromMs}, ${toMs}) with contextSize=${contextSize}: ${error instanceof Error ? error.message : String(error)}. This typically happens when audio parameters are invalid (e.g., contextSize <= 0).`,
670
- );
671
- }
718
+ // Debug logging for audio duration calculations
719
+ if (contextSize <= 0) {
720
+ throw new Error(
721
+ `Duration must be greater than 0 when rendering audio. ${contextSize}ms`,
722
+ );
723
+ }
672
724
 
673
- await this.#addAudioToContext(audioContext, fromMs, toMs);
674
- const renderedBuffer = await audioContext.startRendering();
725
+ let audioContext: OfflineAudioContext;
726
+ try {
727
+ audioContext = new OfflineAudioContext(2, contextSize, 48000);
728
+ } catch (error) {
729
+ throw new Error(
730
+ `[EFTimegroup.renderAudio] Failed to create OfflineAudioContext(2, ${contextSize}, 48000) for renderAudio(${fromMs}, ${toMs}) with contextSize=${contextSize}: ${error instanceof Error ? error.message : String(error)}. This typically happens when audio parameters are invalid (e.g., contextSize <= 0).`,
731
+ );
732
+ }
675
733
 
676
- return renderedBuffer;
734
+ await this.#addAudioToContext(audioContext, fromMs, toMs);
735
+ const renderedBuffer = await audioContext.startRendering();
736
+
737
+ return renderedBuffer;
738
+ },
739
+ );
677
740
  }
678
741
 
679
742
  /**
@@ -728,10 +791,21 @@ export class EFTimegroup extends EFTemporal(LitElement) {
728
791
  // autoRun: EF_INTERACTIVE,
729
792
  autoRun: false,
730
793
  args: () => [this.ownCurrentTimeMs, this.currentTimeMs] as const,
731
- task: async ([]) => {
794
+ task: async ([ownCurrentTimeMs, currentTimeMs]) => {
732
795
  if (this.isRootTimegroup) {
733
- await this.waitForFrameTasks();
734
- updateAnimations(this);
796
+ await withSpan(
797
+ "timegroup.frameTask",
798
+ {
799
+ timegroupId: this.id || "unknown",
800
+ ownCurrentTimeMs,
801
+ currentTimeMs,
802
+ },
803
+ undefined,
804
+ async () => {
805
+ await this.waitForFrameTasks();
806
+ updateAnimations(this);
807
+ },
808
+ );
735
809
  }
736
810
  },
737
811
  });
@@ -744,19 +818,33 @@ export class EFTimegroup extends EFTemporal(LitElement) {
744
818
  if (!this.isRootTimegroup) {
745
819
  return;
746
820
  }
747
- await this.waitForMediaDurations();
748
- const newTime = Math.max(
749
- 0,
750
- Math.min(targetTime ?? 0, this.durationMs / 1000),
821
+ return withSpan(
822
+ "timegroup.seekTask",
823
+ {
824
+ timegroupId: this.id || "unknown",
825
+ targetTime: targetTime ?? 0,
826
+ durationMs: this.durationMs,
827
+ },
828
+ undefined,
829
+ async (span) => {
830
+ await this.waitForMediaDurations();
831
+ const newTime = Math.max(
832
+ 0,
833
+ Math.min(targetTime ?? 0, this.durationMs / 1000),
834
+ );
835
+ if (isTracingEnabled()) {
836
+ span.setAttribute("newTime", newTime);
837
+ }
838
+ // Apply the clamped time back to currentTime
839
+ this.#currentTime = newTime;
840
+ this.requestUpdate("currentTime");
841
+ await this.runThrottledFrameTask();
842
+ this.#saveTimeToLocalStorage(this.#currentTime);
843
+ // This has to be set false here so any following seeks are not treated as pending
844
+ this.#seekInProgress = false;
845
+ return newTime;
846
+ },
751
847
  );
752
- // Apply the clamped time back to currentTime
753
- this.#currentTime = newTime;
754
- this.requestUpdate("currentTime");
755
- await this.runThrottledFrameTask();
756
- this.#saveTimeToLocalStorage(this.#currentTime);
757
- // This has to be set false here so any following seeks are not treated as pending
758
- this.#seekInProgress = false;
759
- return newTime;
760
848
  },
761
849
  });
762
850
  }
@@ -238,7 +238,7 @@ describe("EFVideo", () => {
238
238
 
239
239
  // Should not throw when video asset is missing
240
240
  expect(() => {
241
- video.paintTask.run();
241
+ video.paint(0);
242
242
  }).not.toThrow();
243
243
  });
244
244
  });
@@ -267,7 +267,7 @@ describe("EFVideo", () => {
267
267
  close: vi.fn(),
268
268
  } as unknown as VideoFrame;
269
269
 
270
- // Simulate frame painting (this would normally happen through paintTask)
270
+ // Simulate frame painting (this would normally happen through paint method)
271
271
  const ctx = canvas.getContext("2d");
272
272
  if (ctx && mockFrame.codedWidth && mockFrame.codedHeight) {
273
273
  canvas.width = mockFrame.codedWidth;
@@ -355,20 +355,16 @@ describe("EFVideo", () => {
355
355
 
356
356
  // Simulate the decoder being in use
357
357
  if (decoderLockDescriptor) {
358
- // We can't directly access private fields in tests, but we can test
359
- // that multiple paint calls don't cause issues
360
- const paintPromise1 = video.paintTask.run();
361
- const paintPromise2 = video.paintTask.run();
362
- const paintPromise3 = video.paintTask.run();
363
-
364
- // All should complete without throwing
365
- await expect(
366
- Promise.allSettled([paintPromise1, paintPromise2, paintPromise3]),
367
- ).resolves.toBeDefined();
358
+ // We can test that multiple paint calls don't cause issues
359
+ expect(() => {
360
+ video.paint(0);
361
+ video.paint(0);
362
+ video.paint(0);
363
+ }).not.toThrow();
368
364
  }
369
365
  });
370
366
 
371
- test("paintTask handles missing canvas gracefully", ({ expect }) => {
367
+ test("paint handles missing canvas gracefully", ({ expect }) => {
372
368
  const container = document.createElement("div");
373
369
  render(html`<ef-video></ef-video>`, container);
374
370
  document.body.appendChild(container);
@@ -379,23 +375,19 @@ describe("EFVideo", () => {
379
375
  const canvas = video.canvasElement;
380
376
  canvas?.remove();
381
377
 
382
- // Paint task should handle missing canvas
383
- expect(() => {
384
- video.paintTask.run();
385
- }).not.toThrow();
378
+ // Paint should handle missing canvas
379
+ expect(() => video.paint(0)).not.toThrow();
386
380
  });
387
381
 
388
- test("handles paint task with no video asset", ({ expect }) => {
382
+ test("handles paint with no video asset", ({ expect }) => {
389
383
  const container = document.createElement("div");
390
384
  render(html`<ef-video></ef-video>`, container);
391
385
  document.body.appendChild(container);
392
386
 
393
387
  const video = container.querySelector("ef-video") as EFVideo;
394
388
 
395
- // Paint task should handle missing video asset gracefully
396
- expect(() => {
397
- video.paintTask.run();
398
- }).not.toThrow();
389
+ // Paint should handle missing video asset gracefully
390
+ expect(() => video.paint(0)).not.toThrow();
399
391
  });
400
392
  });
401
393
 
@@ -445,12 +437,12 @@ describe("EFVideo", () => {
445
437
  // Should handle invalid seek times gracefully
446
438
  expect(() => {
447
439
  video.desiredSeekTimeMs = -1000; // Invalid negative time
448
- video.paintTask.run();
440
+ video.paint(-1000);
449
441
  }).not.toThrow();
450
442
 
451
443
  expect(() => {
452
444
  video.desiredSeekTimeMs = Number.POSITIVE_INFINITY;
453
- video.paintTask.run();
445
+ video.paint(Number.POSITIVE_INFINITY);
454
446
  }).not.toThrow();
455
447
  });
456
448
 
@@ -462,14 +454,14 @@ describe("EFVideo", () => {
462
454
  const video = container.querySelector("ef-video") as EFVideo;
463
455
 
464
456
  // Start some operations
465
- video.paintTask.run();
457
+ video.paint(0);
466
458
 
467
459
  // Remove element
468
460
  video.remove();
469
461
 
470
462
  // Should not cause errors
471
463
  expect(() => {
472
- video.paintTask.run();
464
+ video.paint(0);
473
465
  }).not.toThrow();
474
466
  });
475
467
 
@@ -491,7 +483,7 @@ describe("EFVideo", () => {
491
483
 
492
484
  // Should handle context loss gracefully
493
485
  expect(() => {
494
- video.paintTask.run();
486
+ video.paint(0);
495
487
  }).not.toThrow();
496
488
 
497
489
  // Restore original method