@editframe/elements 0.25.1-beta.0 → 0.26.1-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 (57) hide show
  1. package/dist/elements/EFAudio.d.ts +4 -4
  2. package/dist/elements/EFCaptions.d.ts +12 -12
  3. package/dist/elements/EFImage.d.ts +4 -4
  4. package/dist/elements/EFMedia/AssetMediaEngine.js +2 -1
  5. package/dist/elements/EFMedia/AssetMediaEngine.js.map +1 -1
  6. package/dist/elements/EFMedia/BaseMediaEngine.js +13 -0
  7. package/dist/elements/EFMedia/BaseMediaEngine.js.map +1 -1
  8. package/dist/elements/EFMedia/JitMediaEngine.js +2 -1
  9. package/dist/elements/EFMedia/JitMediaEngine.js.map +1 -1
  10. package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js +11 -4
  11. package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js.map +1 -1
  12. package/dist/elements/EFMedia/shared/BufferUtils.js +16 -1
  13. package/dist/elements/EFMedia/shared/BufferUtils.js.map +1 -1
  14. package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js +11 -4
  15. package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js.map +1 -1
  16. package/dist/elements/EFMedia.d.ts +2 -2
  17. package/dist/elements/EFSurface.d.ts +4 -4
  18. package/dist/elements/EFTemporal.js +16 -2
  19. package/dist/elements/EFTemporal.js.map +1 -1
  20. package/dist/elements/EFThumbnailStrip.d.ts +4 -4
  21. package/dist/elements/EFTimegroup.d.ts +22 -0
  22. package/dist/elements/EFTimegroup.js +39 -0
  23. package/dist/elements/EFTimegroup.js.map +1 -1
  24. package/dist/elements/EFVideo.d.ts +4 -4
  25. package/dist/elements/EFWaveform.d.ts +4 -4
  26. package/dist/elements/updateAnimations.js +3 -1
  27. package/dist/elements/updateAnimations.js.map +1 -1
  28. package/dist/gui/EFConfiguration.d.ts +4 -4
  29. package/dist/gui/EFControls.d.ts +2 -2
  30. package/dist/gui/EFDial.d.ts +4 -4
  31. package/dist/gui/EFFocusOverlay.d.ts +4 -4
  32. package/dist/gui/EFPause.d.ts +4 -4
  33. package/dist/gui/EFPlay.d.ts +4 -4
  34. package/dist/gui/EFPreview.d.ts +4 -4
  35. package/dist/gui/EFResizableBox.d.ts +4 -4
  36. package/dist/gui/EFScrubber.d.ts +4 -4
  37. package/dist/gui/EFTimeDisplay.d.ts +4 -4
  38. package/dist/gui/EFToggleLoop.d.ts +4 -4
  39. package/dist/gui/EFTogglePlay.d.ts +4 -4
  40. package/dist/gui/EFWorkbench.d.ts +6 -6
  41. package/dist/style.css +10 -0
  42. package/dist/transcoding/types/index.d.ts +1 -0
  43. package/package.json +2 -2
  44. package/src/elements/EFMedia/AssetMediaEngine.ts +1 -0
  45. package/src/elements/EFMedia/BaseMediaEngine.ts +20 -0
  46. package/src/elements/EFMedia/JitMediaEngine.browsertest.ts +68 -0
  47. package/src/elements/EFMedia/JitMediaEngine.ts +1 -0
  48. package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.ts +12 -0
  49. package/src/elements/EFMedia/shared/BufferUtils.ts +42 -0
  50. package/src/elements/EFMedia/videoTasks/makeVideoBufferTask.ts +12 -0
  51. package/src/elements/EFTemporal.ts +20 -4
  52. package/src/elements/EFTimegroup.browsertest.ts +198 -0
  53. package/src/elements/EFTimegroup.ts +61 -0
  54. package/src/elements/updateAnimations.browsertest.ts +801 -0
  55. package/src/elements/updateAnimations.ts +12 -1
  56. package/src/transcoding/types/index.ts +1 -0
  57. package/types.json +1 -1
@@ -57,8 +57,19 @@ export const makeVideoBufferTask = (host: EFVideo): VideoBufferTask => {
57
57
  bufferDurationMs,
58
58
  maxParallelFetches,
59
59
  enableBuffering: host.enableVideoBuffering,
60
+ bufferThresholdMs: engineConfig.bufferThresholdMs,
60
61
  };
61
62
 
63
+ // Timeline context for priority-based buffering
64
+ const timelineContext =
65
+ host.rootTimegroup?.currentTimeMs !== undefined
66
+ ? {
67
+ elementStartMs: host.startTimeMs,
68
+ elementEndMs: host.endTimeMs,
69
+ playheadMs: host.rootTimegroup.currentTimeMs,
70
+ }
71
+ : undefined;
72
+
62
73
  return manageMediaBuffer<VideoRendition>(
63
74
  seekTimeMs,
64
75
  currentConfig,
@@ -91,6 +102,7 @@ export const makeVideoBufferTask = (host: EFVideo): VideoBufferTask => {
91
102
  },
92
103
  logError: console.error,
93
104
  },
105
+ timelineContext,
94
106
  );
95
107
  },
96
108
  });
@@ -256,10 +256,18 @@ export const deepGetElementsWithFrameTasks = (
256
256
  };
257
257
 
258
258
  let temporalCache: Map<Element, TemporalMixinInterface[]>;
259
+ let temporalCacheResetScheduled = false;
259
260
  export const resetTemporalCache = () => {
260
261
  temporalCache = new Map();
261
- if (typeof requestAnimationFrame !== "undefined") {
262
- requestAnimationFrame(resetTemporalCache);
262
+ if (
263
+ typeof requestAnimationFrame !== "undefined" &&
264
+ !temporalCacheResetScheduled
265
+ ) {
266
+ temporalCacheResetScheduled = true;
267
+ requestAnimationFrame(() => {
268
+ temporalCacheResetScheduled = false;
269
+ resetTemporalCache();
270
+ });
263
271
  }
264
272
  };
265
273
  resetTemporalCache();
@@ -303,10 +311,18 @@ export class OwnCurrentTimeController implements ReactiveController {
303
311
  type Constructor<T = {}> = new (...args: any[]) => T;
304
312
 
305
313
  let startTimeMsCache = new WeakMap<Element, number>();
314
+ let startTimeMsCacheResetScheduled = false;
306
315
  const resetStartTimeMsCache = () => {
307
316
  startTimeMsCache = new WeakMap();
308
- if (typeof requestAnimationFrame !== "undefined") {
309
- requestAnimationFrame(resetStartTimeMsCache);
317
+ if (
318
+ typeof requestAnimationFrame !== "undefined" &&
319
+ !startTimeMsCacheResetScheduled
320
+ ) {
321
+ startTimeMsCacheResetScheduled = true;
322
+ requestAnimationFrame(() => {
323
+ startTimeMsCacheResetScheduled = false;
324
+ resetStartTimeMsCache();
325
+ });
310
326
  }
311
327
  };
312
328
  resetStartTimeMsCache();
@@ -669,4 +669,202 @@ describe("Dynamic content updates", () => {
669
669
  assert.equal(media.mediaEngineTaskCount, 1);
670
670
  });
671
671
  });
672
+
673
+ describe("custom frame tasks", () => {
674
+ test("executes registered callback on frame update", async () => {
675
+ const timegroup = renderTimegroup(
676
+ html`<ef-timegroup mode="fixed" duration="5s"></ef-timegroup>`,
677
+ );
678
+
679
+ let callbackExecuted = false;
680
+ const callback = () => {
681
+ callbackExecuted = true;
682
+ };
683
+
684
+ timegroup.addFrameTask(callback);
685
+ await timegroup.seek(1000);
686
+
687
+ assert.equal(callbackExecuted, true);
688
+ }, 1000);
689
+
690
+ test("callback receives correct timing information", async () => {
691
+ const timegroup = renderTimegroup(
692
+ html`<ef-timegroup mode="fixed" duration="5000ms"></ef-timegroup>`,
693
+ );
694
+
695
+ let receivedInfo: any = null;
696
+ const callback = (info: any) => {
697
+ receivedInfo = info;
698
+ };
699
+
700
+ timegroup.addFrameTask(callback);
701
+ await timegroup.seek(2000);
702
+
703
+ assert.equal(receivedInfo.ownCurrentTimeMs, 2000);
704
+ assert.equal(receivedInfo.currentTimeMs, 2000);
705
+ assert.equal(receivedInfo.durationMs, 5000);
706
+ assert.equal(receivedInfo.percentComplete, 0.4);
707
+ assert.equal(receivedInfo.element, timegroup);
708
+ }, 1000);
709
+
710
+ test("executes multiple callbacks in parallel", async () => {
711
+ const timegroup = renderTimegroup(
712
+ html`<ef-timegroup mode="fixed" duration="5s"></ef-timegroup>`,
713
+ );
714
+
715
+ let callback1Executed = false;
716
+ let callback2Executed = false;
717
+ let callback3Executed = false;
718
+
719
+ timegroup.addFrameTask(() => {
720
+ callback1Executed = true;
721
+ });
722
+ timegroup.addFrameTask(() => {
723
+ callback2Executed = true;
724
+ });
725
+ timegroup.addFrameTask(() => {
726
+ callback3Executed = true;
727
+ });
728
+
729
+ await timegroup.seek(1000);
730
+
731
+ assert.equal(callback1Executed, true);
732
+ assert.equal(callback2Executed, true);
733
+ assert.equal(callback3Executed, true);
734
+ }, 1000);
735
+
736
+ test("async callbacks block frame pipeline", async () => {
737
+ const timegroup = renderTimegroup(
738
+ html`<ef-timegroup mode="fixed" duration="5s"></ef-timegroup>`,
739
+ );
740
+
741
+ let asyncCallbackCompleted = false;
742
+ const executionOrder: string[] = [];
743
+
744
+ const asyncCallback = async () => {
745
+ executionOrder.push("async-start");
746
+ await new Promise((resolve) => setTimeout(resolve, 50));
747
+ asyncCallbackCompleted = true;
748
+ executionOrder.push("async-end");
749
+ };
750
+
751
+ timegroup.addFrameTask(asyncCallback);
752
+
753
+ const seekPromise = timegroup.seek(1000);
754
+ executionOrder.push("seek-called");
755
+
756
+ await seekPromise;
757
+ executionOrder.push("seek-complete");
758
+
759
+ assert.equal(asyncCallbackCompleted, true);
760
+ assert.deepEqual(executionOrder, [
761
+ "seek-called",
762
+ "async-start",
763
+ "async-end",
764
+ "seek-complete",
765
+ ]);
766
+ }, 1000);
767
+
768
+ test("cleanup function removes callback", async () => {
769
+ const timegroup = renderTimegroup(
770
+ html`<ef-timegroup mode="fixed" duration="5s"></ef-timegroup>`,
771
+ );
772
+
773
+ let callbackExecutionCount = 0;
774
+ const cleanup = timegroup.addFrameTask(() => {
775
+ callbackExecutionCount++;
776
+ });
777
+
778
+ await timegroup.seek(1000);
779
+ assert.equal(callbackExecutionCount, 1);
780
+
781
+ cleanup();
782
+ await timegroup.seek(2000);
783
+ assert.equal(callbackExecutionCount, 1);
784
+ }, 1000);
785
+
786
+ test("removeFrameTask removes callback", async () => {
787
+ const timegroup = renderTimegroup(
788
+ html`<ef-timegroup mode="fixed" duration="5s"></ef-timegroup>`,
789
+ );
790
+
791
+ let callbackExecutionCount = 0;
792
+ const callback = () => {
793
+ callbackExecutionCount++;
794
+ };
795
+
796
+ timegroup.addFrameTask(callback);
797
+ await timegroup.seek(1000);
798
+ assert.equal(callbackExecutionCount, 1);
799
+
800
+ timegroup.removeFrameTask(callback);
801
+ await timegroup.seek(2000);
802
+ assert.equal(callbackExecutionCount, 1);
803
+ }, 1000);
804
+
805
+ test("addFrameTask throws error for non-function", () => {
806
+ const timegroup = renderTimegroup(
807
+ html`<ef-timegroup mode="fixed" duration="5s"></ef-timegroup>`,
808
+ );
809
+
810
+ assert.throws(() => {
811
+ timegroup.addFrameTask("not a function" as any);
812
+ }, "Frame task callback must be a function");
813
+ }, 1000);
814
+
815
+ test("custom frame tasks persist after disconnect and reconnect", async () => {
816
+ const container = document.createElement("div");
817
+ document.body.appendChild(container);
818
+
819
+ const timegroup = document.createElement("ef-timegroup") as EFTimegroup;
820
+ timegroup.setAttribute("mode", "fixed");
821
+ timegroup.setAttribute("duration", "5s");
822
+ container.appendChild(timegroup);
823
+
824
+ let callbackWorkedAfterReconnect = false;
825
+ const callback = () => {
826
+ callbackWorkedAfterReconnect = true;
827
+ };
828
+
829
+ timegroup.addFrameTask(callback);
830
+
831
+ // Disconnect and reconnect
832
+ container.removeChild(timegroup);
833
+ callbackWorkedAfterReconnect = false; // Reset after disconnect
834
+ container.appendChild(timegroup);
835
+
836
+ // Callback should still work after reconnect
837
+ await timegroup.seek(2000);
838
+ assert.equal(
839
+ callbackWorkedAfterReconnect,
840
+ true,
841
+ "Callback should still work after reconnect",
842
+ );
843
+
844
+ container.remove();
845
+ }, 1000);
846
+
847
+ test("sync and async callbacks execute together", async () => {
848
+ const timegroup = renderTimegroup(
849
+ html`<ef-timegroup mode="fixed" duration="5s"></ef-timegroup>`,
850
+ );
851
+
852
+ let syncExecuted = false;
853
+ let asyncExecuted = false;
854
+
855
+ timegroup.addFrameTask(() => {
856
+ syncExecuted = true;
857
+ });
858
+
859
+ timegroup.addFrameTask(async () => {
860
+ await new Promise((resolve) => setTimeout(resolve, 10));
861
+ asyncExecuted = true;
862
+ });
863
+
864
+ await timegroup.seek(1000);
865
+
866
+ assert.equal(syncExecuted, true);
867
+ assert.equal(asyncExecuted, true);
868
+ }, 1000);
869
+ });
672
870
  });
@@ -34,6 +34,15 @@ declare global {
34
34
 
35
35
  const log = debug("ef:elements:EFTimegroup");
36
36
 
37
+ // Custom frame task callback type
38
+ export type FrameTaskCallback = (info: {
39
+ ownCurrentTimeMs: number;
40
+ currentTimeMs: number;
41
+ durationMs: number;
42
+ percentComplete: number;
43
+ element: EFTimegroup;
44
+ }) => void | Promise<void>;
45
+
37
46
  // Cache for sequence mode duration calculations to avoid O(n) recalculation
38
47
  let sequenceDurationCache: WeakMap<EFTimegroup, number> = new WeakMap();
39
48
 
@@ -125,6 +134,7 @@ export class EFTimegroup extends EFTargetable(EFTemporal(TWMixin(LitElement))) {
125
134
  #seekInProgress = false;
126
135
  #pendingSeekTime: number | undefined;
127
136
  #processingPendingSeek = false;
137
+ #customFrameTasks: Set<FrameTaskCallback> = new Set();
128
138
 
129
139
  /**
130
140
  * Get the effective FPS for this timegroup.
@@ -276,6 +286,33 @@ export class EFTimegroup extends EFTargetable(EFTemporal(TWMixin(LitElement))) {
276
286
  return !this.parentTimegroup;
277
287
  }
278
288
 
289
+ /**
290
+ * Register a custom frame task callback that will be executed during frame rendering.
291
+ * The callback receives timing information and can be async or sync.
292
+ * Multiple callbacks can be registered and will execute in parallel.
293
+ *
294
+ * @param callback - Function to execute on each frame
295
+ * @returns A cleanup function that removes the callback when called
296
+ */
297
+ addFrameTask(callback: FrameTaskCallback): () => void {
298
+ if (typeof callback !== "function") {
299
+ throw new Error("Frame task callback must be a function");
300
+ }
301
+ this.#customFrameTasks.add(callback);
302
+ return () => {
303
+ this.#customFrameTasks.delete(callback);
304
+ };
305
+ }
306
+
307
+ /**
308
+ * Remove a previously registered custom frame task callback.
309
+ *
310
+ * @param callback - The callback function to remove
311
+ */
312
+ removeFrameTask(callback: FrameTaskCallback): void {
313
+ this.#customFrameTasks.delete(callback);
314
+ }
315
+
279
316
  saveTimeToLocalStorage(time: number) {
280
317
  try {
281
318
  if (this.id && this.isConnected && !Number.isNaN(time)) {
@@ -759,13 +796,37 @@ export class EFTimegroup extends EFTargetable(EFTemporal(TWMixin(LitElement))) {
759
796
  undefined,
760
797
  async () => {
761
798
  await this.waitForFrameTasks();
799
+ await this.#executeCustomFrameTasks();
762
800
  updateAnimations(this);
763
801
  },
764
802
  );
803
+ } else {
804
+ // Non-root timegroups execute their custom frame tasks when called
805
+ await this.#executeCustomFrameTasks();
765
806
  }
766
807
  },
767
808
  });
768
809
 
810
+ async #executeCustomFrameTasks() {
811
+ if (this.#customFrameTasks.size > 0) {
812
+ const percentComplete =
813
+ this.durationMs > 0 ? this.ownCurrentTimeMs / this.durationMs : 0;
814
+ const frameInfo = {
815
+ ownCurrentTimeMs: this.ownCurrentTimeMs,
816
+ currentTimeMs: this.currentTimeMs,
817
+ durationMs: this.durationMs,
818
+ percentComplete,
819
+ element: this,
820
+ };
821
+
822
+ await Promise.all(
823
+ Array.from(this.#customFrameTasks).map((callback) =>
824
+ Promise.resolve(callback(frameInfo)),
825
+ ),
826
+ );
827
+ }
828
+ }
829
+
769
830
  seekTask = new Task(this, {
770
831
  autoRun: false,
771
832
  args: () => [this.#pendingSeekTime ?? this.#currentTime] as const,