@editframe/elements 0.19.4-beta.0 → 0.20.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 (132) 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 +15 -0
  14. package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js +2 -1
  15. package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js +2 -0
  16. package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.d.ts +1 -1
  17. package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.js +3 -1
  18. package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.js +1 -1
  19. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.d.ts +1 -1
  20. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js +6 -5
  21. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js +3 -1
  22. package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js +2 -0
  23. package/dist/elements/EFMedia/shared/AudioSpanUtils.js +2 -2
  24. package/dist/elements/EFMedia/shared/GlobalInputCache.d.ts +39 -0
  25. package/dist/elements/EFMedia/shared/GlobalInputCache.js +57 -0
  26. package/dist/elements/EFMedia/shared/ThumbnailExtractor.d.ts +27 -0
  27. package/dist/elements/EFMedia/shared/ThumbnailExtractor.js +106 -0
  28. package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js +1 -1
  29. package/dist/elements/EFMedia.d.ts +2 -2
  30. package/dist/elements/EFMedia.js +25 -1
  31. package/dist/elements/EFSurface.browsertest.d.ts +0 -0
  32. package/dist/elements/EFSurface.d.ts +30 -0
  33. package/dist/elements/EFSurface.js +96 -0
  34. package/dist/elements/EFTemporal.js +7 -6
  35. package/dist/elements/EFThumbnailStrip.browsertest.d.ts +0 -0
  36. package/dist/elements/EFThumbnailStrip.d.ts +86 -0
  37. package/dist/elements/EFThumbnailStrip.js +490 -0
  38. package/dist/elements/EFThumbnailStrip.media-engine.browsertest.d.ts +0 -0
  39. package/dist/elements/EFTimegroup.d.ts +6 -1
  40. package/dist/elements/EFTimegroup.js +53 -11
  41. package/dist/elements/updateAnimations.browsertest.d.ts +13 -0
  42. package/dist/elements/updateAnimations.d.ts +5 -0
  43. package/dist/elements/updateAnimations.js +37 -13
  44. package/dist/getRenderInfo.js +1 -1
  45. package/dist/gui/ContextMixin.js +27 -14
  46. package/dist/gui/EFControls.browsertest.d.ts +0 -0
  47. package/dist/gui/EFControls.d.ts +38 -0
  48. package/dist/gui/EFControls.js +51 -0
  49. package/dist/gui/EFFilmstrip.d.ts +40 -1
  50. package/dist/gui/EFFilmstrip.js +240 -3
  51. package/dist/gui/EFPreview.js +2 -1
  52. package/dist/gui/EFScrubber.d.ts +6 -5
  53. package/dist/gui/EFScrubber.js +31 -21
  54. package/dist/gui/EFTimeDisplay.browsertest.d.ts +0 -0
  55. package/dist/gui/EFTimeDisplay.d.ts +2 -6
  56. package/dist/gui/EFTimeDisplay.js +13 -23
  57. package/dist/gui/TWMixin.js +1 -1
  58. package/dist/gui/currentTimeContext.d.ts +3 -0
  59. package/dist/gui/currentTimeContext.js +3 -0
  60. package/dist/gui/durationContext.d.ts +3 -0
  61. package/dist/gui/durationContext.js +3 -0
  62. package/dist/index.d.ts +3 -0
  63. package/dist/index.js +4 -1
  64. package/dist/style.css +1 -1
  65. package/dist/transcoding/types/index.d.ts +11 -0
  66. package/dist/utils/LRUCache.d.ts +46 -0
  67. package/dist/utils/LRUCache.js +382 -1
  68. package/dist/utils/LRUCache.test.d.ts +1 -0
  69. package/package.json +2 -2
  70. package/src/elements/ContextProxiesController.ts +124 -0
  71. package/src/elements/EFCaptions.browsertest.ts +1820 -0
  72. package/src/elements/EFCaptions.ts +373 -36
  73. package/src/elements/EFImage.ts +4 -1
  74. package/src/elements/EFMedia/AssetIdMediaEngine.ts +30 -1
  75. package/src/elements/EFMedia/AssetMediaEngine.ts +33 -0
  76. package/src/elements/EFMedia/BaseMediaEngine.browsertest.ts +3 -8
  77. package/src/elements/EFMedia/BaseMediaEngine.ts +35 -0
  78. package/src/elements/EFMedia/JitMediaEngine.ts +34 -0
  79. package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.ts +6 -5
  80. package/src/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.ts +5 -0
  81. package/src/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.ts +8 -5
  82. package/src/elements/EFMedia/audioTasks/makeAudioInputTask.ts +5 -5
  83. package/src/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.ts +11 -12
  84. package/src/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.ts +7 -4
  85. package/src/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.ts +5 -0
  86. package/src/elements/EFMedia/shared/AudioSpanUtils.ts +2 -2
  87. package/src/elements/EFMedia/shared/GlobalInputCache.ts +77 -0
  88. package/src/elements/EFMedia/shared/RenditionHelpers.browsertest.ts +2 -2
  89. package/src/elements/EFMedia/shared/RenditionHelpers.ts +2 -2
  90. package/src/elements/EFMedia/shared/ThumbnailExtractor.ts +227 -0
  91. package/src/elements/EFMedia/tasks/makeMediaEngineTask.ts +1 -1
  92. package/src/elements/EFMedia.ts +38 -1
  93. package/src/elements/EFSurface.browsertest.ts +155 -0
  94. package/src/elements/EFSurface.ts +141 -0
  95. package/src/elements/EFTemporal.ts +14 -8
  96. package/src/elements/EFThumbnailStrip.browsertest.ts +591 -0
  97. package/src/elements/EFThumbnailStrip.media-engine.browsertest.ts +713 -0
  98. package/src/elements/EFThumbnailStrip.ts +905 -0
  99. package/src/elements/EFTimegroup.browsertest.ts +56 -7
  100. package/src/elements/EFTimegroup.ts +88 -16
  101. package/src/elements/updateAnimations.browsertest.ts +333 -11
  102. package/src/elements/updateAnimations.ts +68 -19
  103. package/src/gui/ContextMixin.browsertest.ts +0 -25
  104. package/src/gui/ContextMixin.ts +44 -20
  105. package/src/gui/EFControls.browsertest.ts +175 -0
  106. package/src/gui/EFControls.ts +84 -0
  107. package/src/gui/EFFilmstrip.ts +323 -4
  108. package/src/gui/EFPreview.ts +2 -1
  109. package/src/gui/EFScrubber.ts +29 -25
  110. package/src/gui/EFTimeDisplay.browsertest.ts +237 -0
  111. package/src/gui/EFTimeDisplay.ts +12 -40
  112. package/src/gui/currentTimeContext.ts +5 -0
  113. package/src/gui/durationContext.ts +3 -0
  114. package/src/transcoding/types/index.ts +13 -0
  115. package/src/utils/LRUCache.test.ts +272 -0
  116. package/src/utils/LRUCache.ts +543 -0
  117. package/test/__cache__/GET__api_v1_transcode_high_1_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__26197f6f7c46cacb0a71134131c3f775/data.bin +0 -0
  118. package/test/__cache__/GET__api_v1_transcode_high_1_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__26197f6f7c46cacb0a71134131c3f775/metadata.json +1 -1
  119. package/test/__cache__/GET__api_v1_transcode_high_2_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__4cb6774cd3650ccf59c8f8dc6678c0b9/data.bin +0 -0
  120. package/test/__cache__/GET__api_v1_transcode_high_2_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__4cb6774cd3650ccf59c8f8dc6678c0b9/metadata.json +1 -1
  121. package/test/__cache__/GET__api_v1_transcode_high_3_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__0b3b2b1c8933f7fcf8a9ecaa88d58b41/data.bin +0 -0
  122. package/test/__cache__/GET__api_v1_transcode_high_3_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__0b3b2b1c8933f7fcf8a9ecaa88d58b41/metadata.json +1 -1
  123. package/test/__cache__/GET__api_v1_transcode_high_4_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a6fb05a22b18d850f7f2950bbcdbdeed/data.bin +0 -0
  124. package/test/__cache__/GET__api_v1_transcode_high_4_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a6fb05a22b18d850f7f2950bbcdbdeed/metadata.json +1 -1
  125. package/test/__cache__/GET__api_v1_transcode_high_5_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a50058c7c3602e90879fe3428ed891f4/data.bin +0 -0
  126. package/test/__cache__/GET__api_v1_transcode_high_5_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a50058c7c3602e90879fe3428ed891f4/metadata.json +1 -1
  127. package/test/__cache__/GET__api_v1_transcode_high_init_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__0798c479b44aaeef850609a430f6e613/data.bin +0 -0
  128. package/test/__cache__/GET__api_v1_transcode_manifest_json_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__3be92a0437de726b431ed5af2369158a/data.bin +1 -1
  129. package/test/__cache__/GET__api_v1_transcode_manifest_json_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__3be92a0437de726b431ed5af2369158a/metadata.json +1 -1
  130. package/types.json +1 -1
  131. package/dist/transcoding/cache/CacheManager.d.ts +0 -73
  132. package/src/transcoding/cache/CacheManager.ts +0 -208
@@ -21,7 +21,7 @@ export const getLatestMediaEngine = async (
21
21
  export const getVideoRendition = (mediaEngine: MediaEngine): VideoRendition => {
22
22
  const videoRendition = mediaEngine.videoRendition;
23
23
  if (!videoRendition) {
24
- throw new Error("Video rendition is not available");
24
+ throw new Error("No video track available in source");
25
25
  }
26
26
  return videoRendition;
27
27
  };
@@ -1,6 +1,6 @@
1
1
  import { css, LitElement, type PropertyValueMap } from "lit";
2
2
  import { property, state } from "lit/decorators.js";
3
-
3
+ import { isContextMixin } from "../gui/ContextMixin.js";
4
4
  import type { AudioSpan } from "../transcoding/types/index.ts";
5
5
  import { UrlGenerator } from "../transcoding/utils/UrlGenerator.ts";
6
6
  import { makeAudioBufferTask } from "./EFMedia/audioTasks/makeAudioBufferTask.ts";
@@ -64,6 +64,8 @@ export class EFMedia extends EFTargetable(
64
64
  "audio-buffer-duration",
65
65
  "max-audio-buffer-fetches",
66
66
  "enable-audio-buffering",
67
+ "sourcein",
68
+ "sourceout",
67
69
  ];
68
70
  }
69
71
 
@@ -212,6 +214,41 @@ export class EFMedia extends EFTargetable(
212
214
  if (changedProperties.has("ownCurrentTimeMs")) {
213
215
  this.executeSeek(this.currentSourceTimeMs);
214
216
  }
217
+
218
+ // Check if trim/source properties changed that affect duration
219
+ const durationAffectingProps = [
220
+ "_trimStartMs",
221
+ "_trimEndMs",
222
+ "_sourceInMs",
223
+ "_sourceOutMs",
224
+ ];
225
+
226
+ const hasDurationChange = durationAffectingProps.some((prop) =>
227
+ changedProperties.has(prop),
228
+ );
229
+
230
+ if (hasDurationChange) {
231
+ // Notify parent timegroup to recalculate its duration (same pattern as EFCaptions)
232
+ if (this.parentTimegroup) {
233
+ this.parentTimegroup.requestUpdate("durationMs");
234
+ this.parentTimegroup.requestUpdate("currentTime");
235
+
236
+ // Also find and directly notify any context provider (ContextMixin)
237
+ let parent = this.parentNode;
238
+ while (parent) {
239
+ if (isContextMixin(parent)) {
240
+ parent.dispatchEvent(
241
+ new CustomEvent("child-duration-changed", {
242
+ detail: { source: this },
243
+ }),
244
+ );
245
+ break;
246
+ }
247
+ parent = parent.parentNode;
248
+ }
249
+ }
250
+ }
251
+
215
252
  // if (
216
253
  // changedProperties.has("currentTime") ||
217
254
  // changedProperties.has("ownCurrentTimeMs")
@@ -0,0 +1,155 @@
1
+ import { html, render } from "lit";
2
+ import { beforeEach, describe } from "vitest";
3
+
4
+ import { test as baseTest } from "../../test/useMSW.js";
5
+
6
+ import "./EFVideo.js";
7
+ import "./EFTimegroup.js";
8
+ import "../gui/EFPreview.js";
9
+ import "../gui/EFWorkbench.js";
10
+ import "./EFSurface.js";
11
+
12
+ import type { EFSurface } from "./EFSurface.js";
13
+ import type { EFTimegroup } from "./EFTimegroup.js";
14
+ import type { EFVideo } from "./EFVideo.js";
15
+
16
+ beforeEach(() => {
17
+ localStorage.clear();
18
+ });
19
+
20
+ const surfaceTest = baseTest.extend<{
21
+ timegroup: EFTimegroup;
22
+ video: EFVideo;
23
+ surface: EFSurface;
24
+ }>({
25
+ timegroup: async ({}, use) => {
26
+ const container = document.createElement("div");
27
+ render(
28
+ html`
29
+ <ef-configuration api-host="http://localhost:63315">
30
+ <ef-preview>
31
+ <ef-timegroup id="tg" mode="sequence" class="relative h-[360px] w-[640px] overflow-hidden bg-black">
32
+ <ef-video id="vid" src="bars-n-tone.mp4" style="width: 100%; height: 100%;"></ef-video>
33
+ <ef-surface id="surf" target="vid" style="position: absolute; inset: 0;"></ef-surface>
34
+ </ef-timegroup>
35
+ </ef-preview>
36
+ </ef-configuration>
37
+ `,
38
+ container,
39
+ );
40
+ document.body.appendChild(container);
41
+ const configuration = container.querySelector("ef-configuration") as any;
42
+ configuration.signingURL = "";
43
+ const tg = container.querySelector("#tg") as EFTimegroup;
44
+ await tg.updateComplete;
45
+ await use(tg);
46
+ container.remove();
47
+ },
48
+ video: async ({ timegroup }, use) => {
49
+ const video = timegroup.querySelector("#vid") as EFVideo;
50
+ await video.updateComplete;
51
+ await use(video);
52
+ },
53
+ surface: async ({ timegroup }, use) => {
54
+ const surface = timegroup.querySelector("#surf") as unknown as EFSurface;
55
+ await surface.updateComplete;
56
+ await use(surface);
57
+ },
58
+ });
59
+
60
+ describe("EFSurface", () => {
61
+ surfaceTest("defines and renders a canvas", async ({ expect }) => {
62
+ const el = document.createElement("ef-surface");
63
+ document.body.appendChild(el);
64
+ await (el as any).updateComplete;
65
+ const canvas = el.shadowRoot?.querySelector("canvas");
66
+ expect(canvas).toBeTruthy();
67
+ expect((canvas as HTMLCanvasElement).tagName).toBe("CANVAS");
68
+ el.remove();
69
+ });
70
+
71
+ surfaceTest(
72
+ "mirrors video canvas after a seek via EFTimegroup",
73
+ async ({ timegroup, video, surface, expect }) => {
74
+ // Ensure media engine initialized
75
+ await video.mediaEngineTask.run();
76
+
77
+ // Seek to a known time through timegroup (triggers frame tasks)
78
+ timegroup.currentTimeMs = 3000;
79
+ await timegroup.seekTask.taskComplete;
80
+
81
+ // After scheduling, surface should have mirrored pixel dimensions
82
+ const videoCanvas = (video as any).canvasElement as
83
+ | HTMLCanvasElement
84
+ | undefined;
85
+ const surfaceCanvas =
86
+ (surface.shadowRoot?.querySelector("canvas") as HTMLCanvasElement) ??
87
+ undefined;
88
+
89
+ expect(videoCanvas).toBeTruthy();
90
+ expect(surfaceCanvas).toBeTruthy();
91
+ expect(videoCanvas!.width).toBeGreaterThan(0);
92
+ expect(videoCanvas!.height).toBeGreaterThan(0);
93
+
94
+ // Surface copies pixel dimensions
95
+ expect(surfaceCanvas!.width).toBe(videoCanvas!.width);
96
+ expect(surfaceCanvas!.height).toBe(videoCanvas!.height);
97
+ },
98
+ );
99
+
100
+ surfaceTest(
101
+ "supports multiple surfaces mirroring the same source",
102
+ async ({ expect }) => {
103
+ const container = document.createElement("div");
104
+ render(
105
+ html`
106
+ <ef-configuration api-host="http://localhost:63315">
107
+ <ef-preview>
108
+ <ef-timegroup mode="sequence" class="relative h-[360px] w-[640px] overflow-hidden bg-black">
109
+ <ef-video id="v" src="bars-n-tone.mp4" style="width: 100%; height: 100%;"></ef-video>
110
+ <ef-surface id="s1" target="v" style="position: absolute; inset: 0;"></ef-surface>
111
+ <ef-surface id="s2" target="v" style="position: absolute; inset: 0;"></ef-surface>
112
+ </ef-timegroup>
113
+ </ef-preview>
114
+ </ef-configuration>
115
+ `,
116
+ container,
117
+ );
118
+ document.body.appendChild(container);
119
+ const configuration = container.querySelector("ef-configuration") as any;
120
+ configuration.signingURL = "";
121
+ const timegroup = container.querySelector("ef-timegroup") as EFTimegroup;
122
+ const video = container.querySelector("ef-video") as EFVideo;
123
+ const s1 = container.querySelector("#s1") as unknown as EFSurface;
124
+ const s2 = container.querySelector("#s2") as unknown as EFSurface;
125
+ await timegroup.updateComplete;
126
+ await video.mediaEngineTask.run();
127
+
128
+ timegroup.currentTimeMs = 1000;
129
+ await timegroup.seekTask.taskComplete;
130
+
131
+ const vCanvas = (video as any).canvasElement as HTMLCanvasElement;
132
+ const c1 = s1.shadowRoot!.querySelector("canvas") as HTMLCanvasElement;
133
+ const c2 = s2.shadowRoot!.querySelector("canvas") as HTMLCanvasElement;
134
+
135
+ expect(vCanvas.width).toBeGreaterThan(0);
136
+ expect(c1.width).toBe(vCanvas.width);
137
+ expect(c2.width).toBe(vCanvas.width);
138
+ expect(c1.height).toBe(vCanvas.height);
139
+ expect(c2.height).toBe(vCanvas.height);
140
+
141
+ container.remove();
142
+ },
143
+ );
144
+
145
+ surfaceTest(
146
+ "handles missing video gracefully (no throw)",
147
+ async ({ expect }) => {
148
+ const el = document.createElement("ef-surface") as any;
149
+ document.body.appendChild(el);
150
+ await el.updateComplete;
151
+ await expect(el.frameTask.run()).resolves.toBeUndefined();
152
+ el.remove();
153
+ },
154
+ );
155
+ });
@@ -0,0 +1,141 @@
1
+ import { Task } from "@lit/task";
2
+ import { css, html, LitElement } from "lit";
3
+ import { customElement, property, state } from "lit/decorators.js";
4
+ import { createRef, ref } from "lit/directives/ref.js";
5
+ import type { ContextMixinInterface } from "../gui/ContextMixin.ts";
6
+ import { TargetController } from "./TargetController.ts";
7
+
8
+ @customElement("ef-surface")
9
+ export class EFSurface extends LitElement {
10
+ static styles = [
11
+ css`
12
+ :host {
13
+ display: block;
14
+ position: relative;
15
+ }
16
+ canvas {
17
+ all: inherit;
18
+ width: 100%;
19
+ height: 100%;
20
+ display: block;
21
+ }
22
+ `,
23
+ ];
24
+
25
+ canvasRef = createRef<HTMLCanvasElement>();
26
+
27
+ // @ts-expect-error controller is intentionally not referenced directly
28
+ #targetController: TargetController = new TargetController(this);
29
+
30
+ @state()
31
+ targetElement: ContextMixinInterface | null = null;
32
+
33
+ @property({ type: String })
34
+ target = "";
35
+
36
+ render() {
37
+ return html`<canvas ${ref(this.canvasRef)}></canvas>`;
38
+ }
39
+
40
+ // Provide minimal temporal-like properties so EFTimegroup can schedule us
41
+ get rootTimegroup(): any {
42
+ // Prefer the target element's root timegroup if available
43
+ const target: any = this.targetElement;
44
+ if (target && "rootTimegroup" in target) {
45
+ return target.rootTimegroup;
46
+ }
47
+ // Fallback: nearest containing timegroup if any
48
+ let root: any = this.closest("ef-timegroup");
49
+ while (root?.parentTimegroup) {
50
+ root = root.parentTimegroup;
51
+ }
52
+ return root;
53
+ }
54
+
55
+ get currentTimeMs(): number {
56
+ return this.rootTimegroup?.currentTimeMs ?? 0;
57
+ }
58
+
59
+ get durationMs(): number {
60
+ return this.rootTimegroup?.durationMs ?? 0;
61
+ }
62
+
63
+ get startTimeMs(): number {
64
+ return this.rootTimegroup?.startTimeMs ?? 0;
65
+ }
66
+
67
+ get endTimeMs(): number {
68
+ return this.startTimeMs + this.durationMs;
69
+ }
70
+
71
+ /**
72
+ * Minimal integration with EFTimegroup's frame scheduling:
73
+ * - Waits for the target video element's frameTask to complete (ensuring it painted)
74
+ * - Copies the target's canvas into this element's canvas
75
+ */
76
+ frameTask = new Task(this, {
77
+ autoRun: false,
78
+ args: () => [this.targetElement] as const,
79
+ task: async ([target]) => {
80
+ if (!target) return;
81
+
82
+ // Ensure the target has painted its frame for this tick
83
+ try {
84
+ const maybeTask = (target as any).frameTask;
85
+ if (maybeTask && typeof maybeTask.run === "function") {
86
+ // Run (idempotent) and then wait for completion
87
+ maybeTask.run();
88
+ await maybeTask.taskComplete;
89
+ }
90
+ } catch (_err) {
91
+ // Best-effort; continue to attempt copy
92
+ }
93
+
94
+ this.copyFromTarget(target);
95
+ },
96
+ });
97
+
98
+ protected updated(): void {
99
+ if (this.targetElement) {
100
+ this.copyFromTarget(this.targetElement);
101
+ }
102
+ }
103
+
104
+ // Target resolution is handled by TargetController. No implicit discovery.
105
+
106
+ private getSourceCanvas(from: Element): HTMLCanvasElement | null {
107
+ const anyEl = from as any;
108
+ if ("canvasElement" in anyEl) {
109
+ return anyEl.canvasElement ?? null;
110
+ }
111
+ const sr = (from as HTMLElement).shadowRoot;
112
+ if (sr) {
113
+ const c = sr.querySelector("canvas");
114
+ return (c as HTMLCanvasElement) ?? null;
115
+ }
116
+ return null;
117
+ }
118
+
119
+ private copyFromTarget(target: Element) {
120
+ const dst = this.canvasRef.value;
121
+ const src = this.getSourceCanvas(target);
122
+ if (!dst || !src) return;
123
+ if (!src.width || !src.height) return;
124
+
125
+ // Match source pixel size for a faithful mirror; layout scaling is handled by CSS
126
+ if (dst.width !== src.width || dst.height !== src.height) {
127
+ dst.width = src.width;
128
+ dst.height = src.height;
129
+ }
130
+
131
+ const ctx = dst.getContext("2d");
132
+ if (!ctx) return;
133
+ ctx.drawImage(src, 0, 0, dst.width, dst.height);
134
+ }
135
+ }
136
+
137
+ declare global {
138
+ interface HTMLElementTagNameMap {
139
+ "ef-surface": EFSurface;
140
+ }
141
+ }
@@ -469,7 +469,7 @@ export const EFTemporal = <T extends Constructor<LitElement>>(
469
469
  }
470
470
 
471
471
  get hasOwnDuration() {
472
- return false;
472
+ return this.intrinsicDurationMs !== undefined || this.hasExplicitDuration;
473
473
  }
474
474
 
475
475
  get intrinsicDurationMs() {
@@ -477,15 +477,21 @@ export const EFTemporal = <T extends Constructor<LitElement>>(
477
477
  }
478
478
 
479
479
  get durationMs() {
480
- if (this.intrinsicDurationMs === undefined) {
481
- return this._durationMs || this.parentTimegroup?.durationMs || 0;
480
+ // Get the base duration - either intrinsic or explicit
481
+ const baseDurationMs =
482
+ this.intrinsicDurationMs ??
483
+ this._durationMs ??
484
+ this.parentTimegroup?.durationMs ??
485
+ 0;
486
+
487
+ if (baseDurationMs === 0) {
488
+ return 0;
482
489
  }
483
490
 
491
+ // Apply trimming logic to any duration source
484
492
  if (this.trimStartMs || this.trimEndMs) {
485
493
  const trimmedDurationMs =
486
- this.intrinsicDurationMs -
487
- (this.trimStartMs ?? 0) -
488
- (this.trimEndMs ?? 0);
494
+ baseDurationMs - (this.trimStartMs ?? 0) - (this.trimEndMs ?? 0);
489
495
  if (trimmedDurationMs < 0) {
490
496
  return 0;
491
497
  }
@@ -494,14 +500,14 @@ export const EFTemporal = <T extends Constructor<LitElement>>(
494
500
 
495
501
  if (this.sourceInMs || this.sourceOutMs) {
496
502
  const sourceInMs = this.sourceInMs ?? 0;
497
- const sourceOutMs = this.sourceOutMs ?? this.intrinsicDurationMs;
503
+ const sourceOutMs = this.sourceOutMs ?? baseDurationMs;
498
504
  if (sourceInMs >= sourceOutMs) {
499
505
  return 0;
500
506
  }
501
507
  return sourceOutMs - sourceInMs;
502
508
  }
503
509
 
504
- return this.intrinsicDurationMs;
510
+ return baseDurationMs;
505
511
  }
506
512
 
507
513
  get sourceStartMs() {