@editframe/elements 0.19.2-beta.0 → 0.20.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 (96) hide show
  1. package/dist/elements/ContextProxiesController.d.ts +40 -0
  2. package/dist/elements/ContextProxiesController.js +69 -0
  3. package/dist/elements/EFCaptions.d.ts +45 -6
  4. package/dist/elements/EFCaptions.js +220 -26
  5. package/dist/elements/EFImage.js +4 -1
  6. package/dist/elements/EFMedia/AssetIdMediaEngine.d.ts +2 -1
  7. package/dist/elements/EFMedia/AssetIdMediaEngine.js +9 -0
  8. package/dist/elements/EFMedia/AssetMediaEngine.d.ts +1 -0
  9. package/dist/elements/EFMedia/AssetMediaEngine.js +11 -0
  10. package/dist/elements/EFMedia/BaseMediaEngine.d.ts +13 -1
  11. package/dist/elements/EFMedia/BaseMediaEngine.js +9 -0
  12. package/dist/elements/EFMedia/JitMediaEngine.d.ts +7 -1
  13. package/dist/elements/EFMedia/JitMediaEngine.js +24 -0
  14. package/dist/elements/EFMedia/shared/GlobalInputCache.d.ts +39 -0
  15. package/dist/elements/EFMedia/shared/GlobalInputCache.js +57 -0
  16. package/dist/elements/EFMedia/shared/ThumbnailExtractor.d.ts +27 -0
  17. package/dist/elements/EFMedia/shared/ThumbnailExtractor.js +106 -0
  18. package/dist/elements/EFMedia.js +25 -1
  19. package/dist/elements/EFSurface.browsertest.d.ts +0 -0
  20. package/dist/elements/EFSurface.d.ts +30 -0
  21. package/dist/elements/EFSurface.js +96 -0
  22. package/dist/elements/EFTemporal.js +7 -6
  23. package/dist/elements/EFThumbnailStrip.browsertest.d.ts +0 -0
  24. package/dist/elements/EFThumbnailStrip.d.ts +86 -0
  25. package/dist/elements/EFThumbnailStrip.js +490 -0
  26. package/dist/elements/EFThumbnailStrip.media-engine.browsertest.d.ts +0 -0
  27. package/dist/elements/EFTimegroup.d.ts +7 -7
  28. package/dist/elements/EFTimegroup.js +59 -16
  29. package/dist/elements/updateAnimations.browsertest.d.ts +13 -0
  30. package/dist/elements/updateAnimations.d.ts +5 -0
  31. package/dist/elements/updateAnimations.js +37 -13
  32. package/dist/getRenderInfo.js +1 -1
  33. package/dist/gui/ContextMixin.js +27 -14
  34. package/dist/gui/EFControls.browsertest.d.ts +0 -0
  35. package/dist/gui/EFControls.d.ts +38 -0
  36. package/dist/gui/EFControls.js +51 -0
  37. package/dist/gui/EFFilmstrip.d.ts +40 -1
  38. package/dist/gui/EFFilmstrip.js +240 -3
  39. package/dist/gui/EFPreview.js +2 -1
  40. package/dist/gui/EFScrubber.d.ts +6 -5
  41. package/dist/gui/EFScrubber.js +31 -21
  42. package/dist/gui/EFTimeDisplay.browsertest.d.ts +0 -0
  43. package/dist/gui/EFTimeDisplay.d.ts +2 -6
  44. package/dist/gui/EFTimeDisplay.js +13 -23
  45. package/dist/gui/TWMixin.js +1 -1
  46. package/dist/gui/currentTimeContext.d.ts +3 -0
  47. package/dist/gui/currentTimeContext.js +3 -0
  48. package/dist/gui/durationContext.d.ts +3 -0
  49. package/dist/gui/durationContext.js +3 -0
  50. package/dist/index.d.ts +3 -0
  51. package/dist/index.js +4 -1
  52. package/dist/style.css +1 -1
  53. package/dist/transcoding/types/index.d.ts +11 -0
  54. package/dist/utils/LRUCache.d.ts +46 -0
  55. package/dist/utils/LRUCache.js +382 -1
  56. package/dist/utils/LRUCache.test.d.ts +1 -0
  57. package/package.json +2 -2
  58. package/src/elements/ContextProxiesController.ts +123 -0
  59. package/src/elements/EFCaptions.browsertest.ts +1820 -0
  60. package/src/elements/EFCaptions.ts +373 -36
  61. package/src/elements/EFImage.ts +4 -1
  62. package/src/elements/EFMedia/AssetIdMediaEngine.ts +30 -1
  63. package/src/elements/EFMedia/AssetMediaEngine.ts +33 -0
  64. package/src/elements/EFMedia/BaseMediaEngine.browsertest.ts +3 -8
  65. package/src/elements/EFMedia/BaseMediaEngine.ts +35 -0
  66. package/src/elements/EFMedia/JitMediaEngine.ts +48 -0
  67. package/src/elements/EFMedia/shared/GlobalInputCache.ts +77 -0
  68. package/src/elements/EFMedia/shared/ThumbnailExtractor.ts +227 -0
  69. package/src/elements/EFMedia.ts +38 -1
  70. package/src/elements/EFSurface.browsertest.ts +155 -0
  71. package/src/elements/EFSurface.ts +141 -0
  72. package/src/elements/EFTemporal.ts +14 -8
  73. package/src/elements/EFThumbnailStrip.browsertest.ts +591 -0
  74. package/src/elements/EFThumbnailStrip.media-engine.browsertest.ts +713 -0
  75. package/src/elements/EFThumbnailStrip.ts +905 -0
  76. package/src/elements/EFTimegroup.browsertest.ts +56 -7
  77. package/src/elements/EFTimegroup.ts +88 -18
  78. package/src/elements/updateAnimations.browsertest.ts +361 -12
  79. package/src/elements/updateAnimations.ts +68 -19
  80. package/src/gui/ContextMixin.browsertest.ts +0 -25
  81. package/src/gui/ContextMixin.ts +44 -20
  82. package/src/gui/EFControls.browsertest.ts +175 -0
  83. package/src/gui/EFControls.ts +84 -0
  84. package/src/gui/EFFilmstrip.ts +323 -4
  85. package/src/gui/EFPreview.ts +2 -1
  86. package/src/gui/EFScrubber.ts +29 -25
  87. package/src/gui/EFTimeDisplay.browsertest.ts +237 -0
  88. package/src/gui/EFTimeDisplay.ts +12 -40
  89. package/src/gui/currentTimeContext.ts +5 -0
  90. package/src/gui/durationContext.ts +3 -0
  91. package/src/transcoding/types/index.ts +13 -0
  92. package/src/utils/LRUCache.test.ts +272 -0
  93. package/src/utils/LRUCache.ts +543 -0
  94. package/types.json +1 -1
  95. package/dist/transcoding/cache/CacheManager.d.ts +0 -73
  96. package/src/transcoding/cache/CacheManager.ts +0 -208
@@ -420,13 +420,57 @@ describe("setting currentTime", () => {
420
420
  await timegroup.waitForMediaDurations();
421
421
 
422
422
  timegroup.currentTime = 5_000; // 5000 seconds, should clamp to 10s
423
- await timegroup.seekTask.taskComplete;
423
+ await timegroup.seekTask.run();
424
424
 
425
425
  const storedValue = localStorage.getItem(timegroup.storageKey);
426
426
  assert.equal(storedValue, "10"); // Should store 10 (clamped from 5000 to duration)
427
427
  timegroup.remove();
428
428
  });
429
429
 
430
+ test("root timegroup remains visible when currentTime equals duration exactly", async () => {
431
+ const timegroup = renderTimegroup(
432
+ html`<ef-timegroup id="end-time-test" mode="fixed" duration="10s"></ef-timegroup>`,
433
+ );
434
+ await timegroup.waitForMediaDurations();
435
+
436
+ // Set currentTime to exactly the duration
437
+ timegroup.currentTime = 10; // 10 seconds
438
+ await timegroup.seekTask.taskComplete;
439
+ await timegroup.frameTask.taskComplete;
440
+
441
+ // The root timegroup should still be visible at the exact end time
442
+ assert.notEqual(
443
+ timegroup.style.display,
444
+ "none",
445
+ "Root timegroup should be visible at exact end time",
446
+ );
447
+ });
448
+
449
+ test("root timegroup becomes hidden only after currentTime exceeds duration", async () => {
450
+ const timegroup = renderTimegroup(
451
+ html`<ef-timegroup id="beyond-end-test" mode="fixed" duration="10s"></ef-timegroup>`,
452
+ );
453
+ await timegroup.waitForMediaDurations();
454
+
455
+ // Set currentTime beyond the duration (should be clamped to duration)
456
+ timegroup.currentTime = 15; // 15 seconds, should clamp to 10s
457
+ await timegroup.seekTask.taskComplete;
458
+ await timegroup.frameTask.taskComplete;
459
+
460
+ // Even when clamped, it should still be visible at the end
461
+ assert.notEqual(
462
+ timegroup.style.display,
463
+ "none",
464
+ "Root timegroup should be visible even when time is clamped to duration",
465
+ );
466
+ // Verify that the time was actually clamped
467
+ assert.equal(
468
+ timegroup.currentTime,
469
+ 10,
470
+ "Time should be clamped to duration",
471
+ );
472
+ });
473
+
430
474
  test("does not persist in localStorage if the timegroup has no id", async () => {
431
475
  const timegroup = renderTimegroup(
432
476
  html`<ef-timegroup mode="fixed" duration="10s"></ef-timegroup>`,
@@ -572,19 +616,23 @@ describe("Dynamic content updates", () => {
572
616
  const frameTaskB = timegroup.querySelector("test-frame-task-b")!;
573
617
  const frameTaskC = timegroup.querySelector("test-frame-task-c")!;
574
618
 
575
- assert.equal(frameTaskA.frameTaskCount, 0);
619
+ // following the initial update, the first frame tasks have run once.
620
+ await timegroup.updateComplete;
621
+
622
+ assert.equal(frameTaskA.frameTaskCount, 1);
576
623
  assert.equal(frameTaskB.frameTaskCount, 0);
577
- assert.equal(frameTaskC.frameTaskCount, 0);
624
+ assert.equal(frameTaskC.frameTaskCount, 1);
578
625
 
626
+ // Then we run them manually.
579
627
  await timegroup.frameTask.run();
580
628
 
581
629
  // At timeline time 0ms:
582
630
  // - frameTaskA (0-1000ms) should run
583
631
  // - frameTaskB (1000-2000ms) should NOT run
584
632
  // - frameTaskC (0-1000ms) should run (inherits root positioning)
585
- assert.equal(frameTaskA.frameTaskCount, 1);
633
+ assert.equal(frameTaskA.frameTaskCount, 2);
586
634
  assert.equal(frameTaskB.frameTaskCount, 0); // Not visible at time 0
587
- assert.equal(frameTaskC.frameTaskCount, 1); // Nested in B but inherits root positioning
635
+ assert.equal(frameTaskC.frameTaskCount, 2); // Nested in B but inherits root positioning
588
636
  });
589
637
  });
590
638
 
@@ -599,9 +647,10 @@ describe("Dynamic content updates", () => {
599
647
  );
600
648
  const nonRootTimegroup = timegroup.querySelector("ef-timegroup")!;
601
649
  const frameTaskA = timegroup.querySelector("test-frame-task-a")!;
602
- assert.equal(frameTaskA.frameTaskCount, 0);
650
+ await timegroup.updateComplete;
651
+ assert.equal(frameTaskA.frameTaskCount, 1);
603
652
  await nonRootTimegroup.seekTask.run();
604
- assert.equal(frameTaskA.frameTaskCount, 0);
653
+ assert.equal(frameTaskA.frameTaskCount, 1);
605
654
  });
606
655
 
607
656
  test("waits for media durations", async () => {
@@ -1,7 +1,7 @@
1
1
  import { provide } from "@lit/context";
2
2
  import { Task, TaskStatus } from "@lit/task";
3
3
  import debug from "debug";
4
- import { css, html, LitElement } from "lit";
4
+ import { css, html, LitElement, type PropertyValues } from "lit";
5
5
  import { customElement, property } from "lit/decorators.js";
6
6
 
7
7
  import { EF_INTERACTIVE } from "../EF_INTERACTIVE.js";
@@ -17,7 +17,10 @@ import {
17
17
  timegroupContext,
18
18
  } from "./EFTemporal.js";
19
19
  import { TimegroupController } from "./TimegroupController.js";
20
- import { evaluateTemporalState, updateAnimations } from "./updateAnimations.ts";
20
+ import {
21
+ evaluateTemporalStateForAnimation,
22
+ updateAnimations,
23
+ } from "./updateAnimations.ts";
21
24
 
22
25
  const log = debug("ef:elements:EFTimegroup");
23
26
 
@@ -104,8 +107,49 @@ export class EFTimegroup extends EFTemporal(LitElement) {
104
107
 
105
108
  #processingPendingSeek = false;
106
109
 
110
+ #frameTaskInProgress = false;
111
+
112
+ #pendingFrameTaskRun = false;
113
+
114
+ #processingPendingFrameTask = false;
115
+
116
+ /**
117
+ * Throttles frameTask execution to ensure only one runs at a time while preserving the last request
118
+ */
119
+ private async runThrottledFrameTask(): Promise<void> {
120
+ if (this.#frameTaskInProgress) {
121
+ this.#pendingFrameTaskRun = true;
122
+ // Wait for the current frame task to complete
123
+ while (this.#frameTaskInProgress) {
124
+ await this.frameTask.taskComplete;
125
+ }
126
+ return;
127
+ }
128
+
129
+ this.#frameTaskInProgress = true;
130
+
131
+ try {
132
+ await this.frameTask.run();
133
+ } finally {
134
+ this.#frameTaskInProgress = false;
135
+
136
+ if (this.#pendingFrameTaskRun && !this.#processingPendingFrameTask) {
137
+ this.#pendingFrameTaskRun = false;
138
+ this.#processingPendingFrameTask = true;
139
+ try {
140
+ await this.runThrottledFrameTask();
141
+ } finally {
142
+ this.#processingPendingFrameTask = false;
143
+ }
144
+ } else {
145
+ this.#pendingFrameTaskRun = false;
146
+ }
147
+ }
148
+ }
149
+
107
150
  @property({ type: Number, attribute: "currenttime" })
108
151
  set currentTime(time: number) {
152
+ time = Math.max(0, time);
109
153
  if (!this.isRootTimegroup) {
110
154
  return;
111
155
  }
@@ -120,15 +164,12 @@ export class EFTimegroup extends EFTemporal(LitElement) {
120
164
  }
121
165
 
122
166
  if (this.#seekInProgress) {
123
- console.trace("pending seek to", time);
124
167
  this.#pendingSeekTime = time;
125
168
  this.#currentTime = time;
126
169
  return;
127
170
  }
128
- console.trace("seeking to", time);
129
171
 
130
172
  this.#currentTime = time;
131
- // This will be set to false in the seekTask
132
173
  this.#seekInProgress = true;
133
174
 
134
175
  this.seekTask.run().finally(() => {
@@ -212,14 +253,17 @@ export class EFTimegroup extends EFTemporal(LitElement) {
212
253
 
213
254
  connectedCallback() {
214
255
  super.connectedCallback();
215
- if (this.id) {
216
- this.waitForMediaDurations().then(() => {
256
+ this.waitForMediaDurations().then(() => {
257
+ if (this.id) {
217
258
  const maybeLoadedTime = this.maybeLoadTimeFromLocalStorage();
218
259
  if (maybeLoadedTime !== undefined) {
219
260
  this.currentTime = maybeLoadedTime;
220
261
  }
221
- });
222
- }
262
+ }
263
+ if (EF_INTERACTIVE && this.seekTask.status === TaskStatus.INITIAL) {
264
+ this.seekTask.run();
265
+ }
266
+ });
223
267
 
224
268
  if (this.parentTimegroup) {
225
269
  new TimegroupController(this.parentTimegroup, this);
@@ -230,6 +274,15 @@ export class EFTimegroup extends EFTemporal(LitElement) {
230
274
  }
231
275
  }
232
276
 
277
+ #previousDurationMs = 0;
278
+
279
+ protected updated(_changedProperties: PropertyValues): void {
280
+ if (this.#previousDurationMs !== this.durationMs) {
281
+ this.#previousDurationMs = this.durationMs;
282
+ this.runThrottledFrameTask();
283
+ }
284
+ }
285
+
233
286
  disconnectedCallback() {
234
287
  super.disconnectedCallback();
235
288
  this.#resizeObserver?.disconnect();
@@ -331,7 +384,14 @@ export class EFTimegroup extends EFTemporal(LitElement) {
331
384
  const startTimeMs = (temporal as any).startTimeMs as number;
332
385
  const endTimeMs = (temporal as any).endTimeMs as number;
333
386
  const elementStartsBeforeEnd = startTimeMs <= timelineTimeMs + epsilon;
334
- const elementEndsAfterStart = endTimeMs > timelineTimeMs; // Exclusive end for clean transitions
387
+ // Root timegroups should remain visible at exact end time, but other elements use exclusive end for clean transitions
388
+ const isRootTimegroup =
389
+ temporal.tagName.toLowerCase() === "ef-timegroup" &&
390
+ !(temporal as any).parentTimegroup;
391
+ const useInclusiveEnd = isRootTimegroup;
392
+ const elementEndsAfterStart = useInclusiveEnd
393
+ ? endTimeMs >= timelineTimeMs
394
+ : endTimeMs > timelineTimeMs;
335
395
  return elementStartsBeforeEnd && elementEndsAfterStart;
336
396
  });
337
397
 
@@ -364,25 +424,33 @@ export class EFTimegroup extends EFTemporal(LitElement) {
364
424
  const temporalElements = deepGetElementsWithFrameTasks(this);
365
425
 
366
426
  // Filter to only include temporally visible elements for frame processing
427
+ // Use animation-friendly visibility to prevent animation jumps at exact boundaries
367
428
  const visibleElements = temporalElements.filter((element) => {
368
- const temporalState = evaluateTemporalState(element);
369
- return temporalState.isVisible;
429
+ const animationState = evaluateTemporalStateForAnimation(element);
430
+ return animationState.isVisible;
370
431
  });
371
432
 
372
433
  await Promise.all(
373
- visibleElements.map((element) => {
374
- return element.frameTask.run();
375
- }),
434
+ visibleElements.map((element) => element.frameTask.run()),
376
435
  );
377
436
  }
378
437
 
438
+ mediaDurationsPromise: Promise<void> | undefined = undefined;
439
+
440
+ async waitForMediaDurations() {
441
+ if (!this.mediaDurationsPromise) {
442
+ this.mediaDurationsPromise = this.#waitForMediaDurations();
443
+ }
444
+ return this.mediaDurationsPromise;
445
+ }
446
+
379
447
  /**
380
448
  * Wait for all media elements to load their initial segments.
381
449
  * Ideally we would only need the extracted index json data, but
382
450
  * that caused issues with constructing audio data. We had negative durations
383
451
  * in calculations and it was not clear why.
384
452
  */
385
- async waitForMediaDurations() {
453
+ async #waitForMediaDurations() {
386
454
  // We must await updateComplete to ensure all media elements inside this are connected
387
455
  // and will match deepGetMediaElements
388
456
  await this.updateComplete;
@@ -668,9 +736,11 @@ export class EFTimegroup extends EFTemporal(LitElement) {
668
736
  0,
669
737
  Math.min(targetTime ?? 0, this.durationMs / 1000),
670
738
  );
739
+ // Apply the clamped time back to currentTime
740
+ this.#currentTime = newTime;
671
741
  this.requestUpdate("currentTime");
672
- await this.frameTask.run();
673
- this.#saveTimeToLocalStorage(newTime);
742
+ await this.runThrottledFrameTask();
743
+ this.#saveTimeToLocalStorage(this.#currentTime);
674
744
  // This has to be set false here so any following seeks are not treated as pending
675
745
  this.#seekInProgress = false;
676
746
  return newTime;