@editframe/elements 0.40.1-beta.0 → 0.40.2
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.
- package/dist/EF_FRAMEGEN.d.ts +2 -2
- package/dist/EF_FRAMEGEN.js +10 -30
- package/dist/EF_FRAMEGEN.js.map +1 -1
- package/dist/elements/EFTimegroup.d.ts +12 -8
- package/dist/elements/EFTimegroup.js +24 -17
- package/dist/elements/EFTimegroup.js.map +1 -1
- package/dist/preview/FrameController.d.ts +11 -7
- package/dist/preview/FrameController.js +12 -39
- package/dist/preview/FrameController.js.map +1 -1
- package/package.json +3 -3
package/dist/EF_FRAMEGEN.d.ts
CHANGED
|
@@ -42,8 +42,8 @@ declare class EFFramegen {
|
|
|
42
42
|
private logSequence;
|
|
43
43
|
frameTasksInProgress: boolean;
|
|
44
44
|
currentFrameNumber: number;
|
|
45
|
-
private
|
|
46
|
-
private
|
|
45
|
+
private timingFrameCount;
|
|
46
|
+
private timingAccum;
|
|
47
47
|
trace(...args: any[]): void;
|
|
48
48
|
syncLog(...args: any[]): Promise<void>;
|
|
49
49
|
private initializeVerificationCanvas;
|
package/dist/EF_FRAMEGEN.js
CHANGED
|
@@ -91,7 +91,8 @@ var EFFramegen = class {
|
|
|
91
91
|
this.logSequence = 0;
|
|
92
92
|
this.frameTasksInProgress = false;
|
|
93
93
|
this.currentFrameNumber = 0;
|
|
94
|
-
this.
|
|
94
|
+
this.timingFrameCount = 0;
|
|
95
|
+
this.timingAccum = {
|
|
95
96
|
updateComplete1Ms: 0,
|
|
96
97
|
updateComplete2Ms: 0,
|
|
97
98
|
updateComplete3Ms: 0,
|
|
@@ -104,7 +105,6 @@ var EFFramegen = class {
|
|
|
104
105
|
frameTasksMs: 0,
|
|
105
106
|
totalMs: 0
|
|
106
107
|
};
|
|
107
|
-
this.seekTimingCount = 0;
|
|
108
108
|
this.BRIDGE = window.FRAMEGEN_BRIDGE;
|
|
109
109
|
if (this.BRIDGE) this.connectToBridge();
|
|
110
110
|
}
|
|
@@ -221,34 +221,14 @@ var EFFramegen = class {
|
|
|
221
221
|
if (!firstGroup) throw new Error("No temporal elements found");
|
|
222
222
|
const frameTime = this.renderOptions.encoderOptions.fromMs + frameNumber * this.frameDurationMs;
|
|
223
223
|
const frameTimeMs = Number(Number(frameTime).toFixed(5));
|
|
224
|
-
const
|
|
225
|
-
this.
|
|
226
|
-
this.
|
|
227
|
-
this.
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
this.seekTimingAccum.renderFrameDrawMs += seekTiming.renderFrameDrawMs;
|
|
233
|
-
this.seekTimingAccum.renderFrameAnimsMs += seekTiming.renderFrameAnimsMs;
|
|
234
|
-
this.seekTimingAccum.frameTasksMs += seekTiming.frameTasksMs;
|
|
235
|
-
this.seekTimingAccum.totalMs += seekTiming.totalMs;
|
|
236
|
-
this.seekTimingCount++;
|
|
237
|
-
if (this.seekTimingCount % 30 === 0) {
|
|
238
|
-
const n = this.seekTimingCount;
|
|
239
|
-
console.log("[EF_FRAMEGEN] seekForRender phase avg (ms) over", n, "frames:", JSON.stringify({
|
|
240
|
-
uc1: (this.seekTimingAccum.updateComplete1Ms / n).toFixed(2),
|
|
241
|
-
uc2: (this.seekTimingAccum.updateComplete2Ms / n).toFixed(2),
|
|
242
|
-
uc3: (this.seekTimingAccum.updateComplete3Ms / n).toFixed(2),
|
|
243
|
-
textSegments: (this.seekTimingAccum.textSegmentsMs / n).toFixed(2),
|
|
244
|
-
renderFrame: (this.seekTimingAccum.renderFrameMs / n).toFixed(2),
|
|
245
|
-
rf_query: (this.seekTimingAccum.renderFrameQueryMs / n).toFixed(2),
|
|
246
|
-
rf_prepare: (this.seekTimingAccum.renderFramePrepareMs / n).toFixed(2),
|
|
247
|
-
rf_draw: (this.seekTimingAccum.renderFrameDrawMs / n).toFixed(2),
|
|
248
|
-
rf_anims: (this.seekTimingAccum.renderFrameAnimsMs / n).toFixed(2),
|
|
249
|
-
frameTasks: (this.seekTimingAccum.frameTasksMs / n).toFixed(2),
|
|
250
|
-
total: (this.seekTimingAccum.totalMs / n).toFixed(2)
|
|
251
|
-
}));
|
|
224
|
+
const timing = await firstGroup.seekForRender(frameTimeMs);
|
|
225
|
+
this.timingFrameCount++;
|
|
226
|
+
for (const key of Object.keys(this.timingAccum)) this.timingAccum[key] += timing[key];
|
|
227
|
+
if (this.timingFrameCount >= 30) {
|
|
228
|
+
const n = this.timingFrameCount;
|
|
229
|
+
console.log(`[EF_FRAMEGEN] seekForRender phase avg (${n} frames):`, `total=${(this.timingAccum.totalMs / n).toFixed(1)}ms`, `uc1=${(this.timingAccum.updateComplete1Ms / n).toFixed(1)}ms`, `uc2=${(this.timingAccum.updateComplete2Ms / n).toFixed(1)}ms`, `uc3=${(this.timingAccum.updateComplete3Ms / n).toFixed(1)}ms`, `text=${(this.timingAccum.textSegmentsMs / n).toFixed(1)}ms`, `renderFrame=${(this.timingAccum.renderFrameMs / n).toFixed(1)}ms`, `rf.query=${(this.timingAccum.renderFrameQueryMs / n).toFixed(1)}ms`, `rf.prepare=${(this.timingAccum.renderFramePrepareMs / n).toFixed(1)}ms`, `rf.draw=${(this.timingAccum.renderFrameDrawMs / n).toFixed(1)}ms`, `rf.anims=${(this.timingAccum.renderFrameAnimsMs / n).toFixed(1)}ms`, `frameTasks=${(this.timingAccum.frameTasksMs / n).toFixed(1)}ms`);
|
|
230
|
+
this.timingFrameCount = 0;
|
|
231
|
+
for (const key of Object.keys(this.timingAccum)) this.timingAccum[key] = 0;
|
|
252
232
|
}
|
|
253
233
|
if (this.showFrameBox) this.frameBox.innerHTML = `
|
|
254
234
|
<div>🖼️ Frame: ${frameNumber}</div>
|
package/dist/EF_FRAMEGEN.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EF_FRAMEGEN.js","names":[],"sources":["../src/EF_FRAMEGEN.ts"],"sourcesContent":["import type { VideoRenderOptions } from \"@editframe/assets\";\n\nimport {\n shallowGetTimegroups,\n type SeekForRenderTiming,\n} from \"./elements/EFTimegroup.js\";\nimport { setupTemporalHierarchy } from \"./elements/setupTemporalHierarchy.js\";\n\nimport { setupBrowserTracing } from \"./otel/setupBrowserTracing.js\";\nimport {\n clearCurrentFrameSpan,\n enableTracing,\n extractParentContext,\n setCurrentFrameSpan,\n type TraceContext,\n withSpan,\n withSpanAndContext,\n} from \"./otel/tracingHelpers.js\";\n\ninterface Bridge {\n onInitialize: (\n callback: (\n renderOptions: VideoRenderOptions,\n traceContext?: TraceContext,\n otelEndpoint?: string,\n ) => void,\n ) => void;\n\n initialized(): void;\n\n onBeginFrame(\n callback: (\n frameNumber: number,\n isLast: boolean,\n traceContext?: TraceContext,\n ) => void,\n ): void;\n\n onTriggerCanvas(callback: (traceContext?: TraceContext) => void): void;\n\n frameReady(frameNumber: number, audioSamples: ArrayBuffer): void;\n\n error(error: Error): void;\n\n syncLog(sequence: number, message: string, callback: () => void): void;\n\n exportSpans?: (endpoint: string, payload: string) => void;\n}\n\ndeclare global {\n interface Window {\n EF_FRAMEGEN?: EFFramegen;\n FRAMEGEN_BRIDGE?: Bridge;\n FRAMEGEN_BINDING?: any;\n FRAMEGEN_BINDING_error?: (error: Error) => void;\n EF_RENDERING?: () => boolean;\n }\n}\n\nclass TriggerCanvas {\n private canvas: HTMLCanvasElement;\n private ctx: CanvasRenderingContext2D;\n\n private canvasInitialized = false;\n\n constructor() {\n this.canvas = document.createElement(\"canvas\");\n const ctx = this.canvas.getContext(\"2d\", { willReadFrequently: true });\n if (!ctx) throw new Error(\"Canvas 2d context not ready\");\n this.ctx = ctx;\n this.ctx.fillStyle = \"transparent\";\n }\n\n initialize() {\n if (this.canvasInitialized) return;\n this.canvasInitialized = true;\n this.canvas.width = 1;\n this.canvas.height = 1;\n Object.assign(this.canvas.style, {\n position: \"fixed\",\n top: \"0px\",\n left: \"0px\",\n width: \"100%\",\n height: \"100%\",\n zIndex: \"100000\",\n });\n document.body.appendChild(this.canvas);\n }\n\n trigger() {\n this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);\n }\n}\n\nexport class EFFramegen {\n time = 0;\n frameDurationMs = 0;\n audioBufferPromise?: Promise<AudioBuffer>;\n renderOptions?: VideoRenderOptions;\n frameBox = document.createElement(\"div\");\n BRIDGE: typeof window.FRAMEGEN_BRIDGE;\n triggerCanvas = new TriggerCanvas();\n verificationCanvas?: HTMLCanvasElement;\n verificationCtx?: CanvasRenderingContext2D;\n private logSequence = 0;\n\n // Frame sequence coordination\n public frameTasksInProgress = false;\n public currentFrameNumber = 0;\n\n // Accumulated per-phase seek timings for periodic reporting\n private seekTimingAccum: SeekForRenderTiming = {\n updateComplete1Ms: 0,\n updateComplete2Ms: 0,\n updateComplete3Ms: 0,\n textSegmentsMs: 0,\n renderFrameMs: 0,\n renderFrameQueryMs: 0,\n renderFramePrepareMs: 0,\n renderFrameDrawMs: 0,\n renderFrameAnimsMs: 0,\n frameTasksMs: 0,\n totalMs: 0,\n };\n private seekTimingCount = 0;\n\n trace(...args: any[]) {\n console.trace(\"[EF_FRAMEGEN]\", ...args);\n }\n\n async syncLog(...args: any[]): Promise<void> {\n if (!this.BRIDGE) {\n // Fallback to regular console.log if no bridge\n console.log(\"[EF_FRAMEGEN]\", ...args);\n return;\n }\n\n const sequence = ++this.logSequence;\n const message = args\n .map((arg) =>\n typeof arg === \"object\" ? JSON.stringify(arg) : String(arg),\n )\n .join(\" \");\n\n return new Promise<void>((resolve) => {\n // biome-ignore lint/style/noNonNullAssertion: We know BRIDGE is set due to the guard above\n this.BRIDGE!.syncLog(sequence, message, () => {\n resolve();\n });\n });\n }\n\n private initializeVerificationCanvas() {\n if (this.verificationCanvas) {\n return;\n }\n\n this.verificationCanvas = document.createElement(\"canvas\");\n const ctx = this.verificationCanvas.getContext(\"2d\");\n if (!ctx) throw new Error(\"Verification canvas 2d context not ready\");\n this.verificationCtx = ctx;\n\n // Size to match the workbench width, or fall back to renderOptions dimensions.\n // Without ef-workbench (e.g. API renders), the canvas was never sized or appended,\n // causing frame verification to fail on every frame.\n const workbench = document.querySelector(\"ef-workbench\") as HTMLElement;\n const canvasWidth = workbench\n ? workbench.clientWidth\n : (this.renderOptions?.encoderOptions.video.width ?? 0);\n\n if (canvasWidth > 0) {\n this.verificationCanvas.width = canvasWidth;\n this.verificationCanvas.height = 1;\n\n Object.assign(this.verificationCanvas.style, {\n position: \"fixed\",\n left: \"0px\",\n bottom: \"0px\",\n width: `${canvasWidth}px`,\n height: \"1px\",\n zIndex: \"99999\",\n });\n\n document.body.appendChild(this.verificationCanvas);\n }\n }\n\n private drawVerificationStrip(frameNumber: number) {\n this.initializeVerificationCanvas();\n\n if (!this.verificationCanvas || !this.verificationCtx) {\n return;\n }\n\n const width = this.verificationCanvas.width;\n const height = this.verificationCanvas.height;\n\n // Clear the strip\n this.verificationCtx.clearRect(0, 0, width, height);\n\n // Encode frame number into RGB (24-bit)\n // R=high byte, G=middle byte, B=low byte\n const red = Math.floor(frameNumber / (256 * 256)) % 256;\n const green = Math.floor(frameNumber / 256) % 256;\n const blue = frameNumber % 256;\n\n // Fill the entire strip with the encoded frame number\n this.verificationCtx.fillStyle = `rgb(${red}, ${green}, ${blue})`;\n this.verificationCtx.fillRect(0, 0, width, height);\n }\n\n constructor() {\n this.BRIDGE = window.FRAMEGEN_BRIDGE;\n if (this.BRIDGE) {\n this.connectToBridge();\n }\n }\n\n /**\n * Helper method to get the workbench and set its rendering state.\n * This ensures consistent state management across the framegen lifecycle.\n */\n private setWorkbenchRendering(isRendering: boolean) {\n const workbench = document.querySelector(\"ef-workbench\");\n if (workbench) {\n workbench.rendering = isRendering;\n }\n }\n\n connectToBridge() {\n const BRIDGE = this.BRIDGE;\n if (!BRIDGE) {\n throw new Error(\"No BRIDGE when attempting to connect to bridge\");\n }\n\n BRIDGE.onInitialize(async (renderOptions, traceContext, otelEndpoint) => {\n // Only enable tracing if explicitly requested in renderOptions\n if (renderOptions.enableTracing && otelEndpoint) {\n enableTracing();\n await setupBrowserTracing({\n otelEndpoint,\n serviceName: \"telecine-browser\",\n bridge: BRIDGE,\n useBatching: true, // Batch spans to reduce overhead during rendering\n });\n }\n\n const parentContext = extractParentContext(traceContext);\n\n await withSpan(\n \"browser.initialize\",\n {\n width: renderOptions.encoderOptions.video.width,\n height: renderOptions.encoderOptions.video.height,\n fps: renderOptions.encoderOptions.video.framerate,\n durationMs:\n renderOptions.encoderOptions.toMs -\n renderOptions.encoderOptions.fromMs,\n },\n parentContext,\n async () => {\n try {\n await this.initialize(renderOptions);\n } catch (error) {\n // If initialization fails, ensure rendering state is cleared\n this.setWorkbenchRendering(false);\n console.error(\n \"[EF_FRAMEGEN.connectToBridge] error initializing\",\n error,\n );\n throw error;\n }\n },\n );\n\n BRIDGE.initialized();\n });\n\n BRIDGE.onBeginFrame((frameNumber, isLast, traceContext) => {\n const parentContext = extractParentContext(traceContext);\n withSpanAndContext(\n \"browser.frame.render\",\n {\n frameNumber,\n isLast,\n },\n parentContext,\n async (span, _spanContext) => {\n // Store the span itself for child operations\n // This allows spans created in Lit Tasks to use it as their parent\n setCurrentFrameSpan(span);\n\n try {\n await this.beginFrame(frameNumber, isLast);\n } catch (error) {\n // If an error occurs during rendering, ensure rendering state is cleared\n this.setWorkbenchRendering(false);\n throw error;\n } finally {\n clearCurrentFrameSpan();\n }\n },\n ).catch((error) => {\n console.error(\"[EF_FRAMEGEN.beginFrame] error:\", error);\n // Ensure rendering state is cleared on error\n this.setWorkbenchRendering(false);\n clearCurrentFrameSpan();\n throw error;\n });\n });\n\n BRIDGE.onTriggerCanvas((traceContext) => {\n const parentContext = extractParentContext(traceContext);\n\n withSpan(\"browser.canvas.trigger\", {}, parentContext, async () => {\n this.triggerCanvas.trigger();\n }).catch((error) => {\n console.error(\"[EF_FRAMEGEN.triggerCanvas] error:\", error);\n });\n });\n }\n\n get showFrameBox() {\n return this.renderOptions?.showFrameBox ?? false;\n }\n\n async initialize(renderOptions: VideoRenderOptions) {\n this.renderOptions = renderOptions;\n\n // Workbench is optional - look for it but don't require it\n const workbench = document.querySelector(\"ef-workbench\");\n if (workbench) {\n this.setWorkbenchRendering(true);\n workbench.playing = false;\n }\n\n // Find timegroups either in workbench or directly in document\n const searchRoot = workbench || document.body;\n const timegroups = shallowGetTimegroups(searchRoot);\n const firstGroup = timegroups[0];\n if (!firstGroup) {\n throw new Error(\"No temporal elements found\");\n }\n const startingTimeMs = renderOptions.encoderOptions.fromMs;\n await firstGroup.waitForMediaDurations();\n\n // CRITICAL: Manually wire up temporal hierarchy since Lit Context fails with our connection order\n // When loading via loadURL(), elements connect depth-first (children before parents), causing\n // children to miss the context-request event since parents aren't listening yet.\n // See setupTemporalHierarchy.ts for detailed explanation.\n setupTemporalHierarchy(searchRoot, firstGroup);\n\n // Suppress autonomous re-renders (EFTemporal/EFTimegroup.updated) while\n // seekForRender is in progress — same protection applied to render clones.\n firstGroup.setAttribute(\"data-no-playback-controller\", \"\");\n\n // Use seekForRender for proper time seeking during rendering\n await firstGroup.seekForRender(startingTimeMs);\n\n this.frameDurationMs = 1000 / renderOptions.encoderOptions.video.framerate;\n\n this.time = startingTimeMs;\n if (this.showFrameBox) {\n Object.assign(this.frameBox.style, {\n width: \"200px\",\n height: \"100px\",\n font: \"10px Arial\",\n backgroundColor: \"white\",\n position: \"absolute\",\n top: \"0px\",\n right: \"0px\",\n zIndex: \"100000\",\n });\n document.body.prepend(this.frameBox);\n }\n\n this.triggerCanvas.initialize();\n\n // These times are aligned to the audio frame boundaries\n // And they include padding if any.\n this.audioBufferPromise = firstGroup.renderAudio(\n renderOptions.encoderOptions.alignedFromUs / 1000,\n renderOptions.encoderOptions.alignedToUs / 1000,\n );\n // Suppress unhandled rejection while the promise sits in storage before being awaited.\n this.audioBufferPromise.catch(() => {});\n }\n\n async beginFrame(frameNumber: number, isLast: boolean) {\n if (this.renderOptions === undefined) {\n throw new Error(\"No renderOptions\");\n }\n const workbench = document.querySelector(\"ef-workbench\");\n if (workbench) {\n this.setWorkbenchRendering(true);\n }\n const searchRoot = workbench || document.body;\n const timegroups = shallowGetTimegroups(searchRoot);\n const firstGroup = timegroups[0];\n if (!firstGroup) {\n throw new Error(\"No temporal elements found\");\n }\n\n // Calculate base frame time using normal progression\n const frameTime =\n this.renderOptions.encoderOptions.fromMs +\n frameNumber * this.frameDurationMs;\n const frameTimeMs = Number(Number(frameTime).toFixed(5));\n\n // Use seekForRender for proper time seeking during rendering\n const seekTiming = await firstGroup.seekForRender(frameTimeMs);\n this.seekTimingAccum.updateComplete1Ms += seekTiming.updateComplete1Ms;\n this.seekTimingAccum.updateComplete2Ms += seekTiming.updateComplete2Ms;\n this.seekTimingAccum.updateComplete3Ms += seekTiming.updateComplete3Ms;\n this.seekTimingAccum.textSegmentsMs += seekTiming.textSegmentsMs;\n this.seekTimingAccum.renderFrameMs += seekTiming.renderFrameMs;\n this.seekTimingAccum.renderFrameQueryMs += seekTiming.renderFrameQueryMs;\n this.seekTimingAccum.renderFramePrepareMs += seekTiming.renderFramePrepareMs;\n this.seekTimingAccum.renderFrameDrawMs += seekTiming.renderFrameDrawMs;\n this.seekTimingAccum.renderFrameAnimsMs += seekTiming.renderFrameAnimsMs;\n this.seekTimingAccum.frameTasksMs += seekTiming.frameTasksMs;\n this.seekTimingAccum.totalMs += seekTiming.totalMs;\n this.seekTimingCount++;\n\n if (this.seekTimingCount % 30 === 0) {\n const n = this.seekTimingCount;\n console.log(\n \"[EF_FRAMEGEN] seekForRender phase avg (ms) over\",\n n,\n \"frames:\",\n JSON.stringify({\n uc1: (this.seekTimingAccum.updateComplete1Ms / n).toFixed(2),\n uc2: (this.seekTimingAccum.updateComplete2Ms / n).toFixed(2),\n uc3: (this.seekTimingAccum.updateComplete3Ms / n).toFixed(2),\n textSegments: (this.seekTimingAccum.textSegmentsMs / n).toFixed(2),\n renderFrame: (this.seekTimingAccum.renderFrameMs / n).toFixed(2),\n rf_query: (this.seekTimingAccum.renderFrameQueryMs / n).toFixed(2),\n rf_prepare: (this.seekTimingAccum.renderFramePrepareMs / n).toFixed(2),\n rf_draw: (this.seekTimingAccum.renderFrameDrawMs / n).toFixed(2),\n rf_anims: (this.seekTimingAccum.renderFrameAnimsMs / n).toFixed(2),\n frameTasks: (this.seekTimingAccum.frameTasksMs / n).toFixed(2),\n total: (this.seekTimingAccum.totalMs / n).toFixed(2),\n }),\n );\n }\n\n if (this.showFrameBox) {\n this.frameBox.innerHTML = `\n <div>🖼️ Frame: ${frameNumber}</div>\n <div>🕛 Segment: ${this.time.toFixed(4)}</div>\n <div>🕛 Frame: ${firstGroup.currentTimeMs.toFixed(4)}</div>\n <div> from-to: ${this.renderOptions.encoderOptions.fromMs.toFixed(4)} - ${this.renderOptions.encoderOptions.toMs.toFixed(4)}</div>\n `;\n }\n\n // Draw verification pixel strip for frame verification\n this.drawVerificationStrip(frameNumber);\n\n if (isLast && this.audioBufferPromise) {\n // Currently we emit the audio in one belch at the end of the render.\n // This is not ideal, but it's the simplest thing that could possibly work.\n // We could either emit it slices, or in parallel with the video.\n // But in any case, it's fine for now.\n const renderedAudio = await this.audioBufferPromise;\n\n const channelCount = renderedAudio.numberOfChannels;\n\n const interleavedSamples = new Float32Array(\n channelCount * renderedAudio.length,\n );\n\n for (let i = 0; i < renderedAudio.length; i++) {\n for (let j = 0; j < channelCount; j++) {\n interleavedSamples.set(\n renderedAudio.getChannelData(j).slice(i, i + 1),\n i * channelCount + j,\n );\n }\n }\n\n if (this.BRIDGE) {\n this.BRIDGE.frameReady(frameNumber, interleavedSamples.buffer);\n } else {\n const fileReader = new FileReader();\n fileReader.readAsDataURL(new Blob([interleavedSamples.buffer]));\n await new Promise((resolve, reject) => {\n fileReader.onload = resolve;\n fileReader.onerror = reject;\n });\n return fileReader.result;\n }\n\n // Rendering is complete after the last frame\n this.setWorkbenchRendering(false);\n } else {\n if (this.BRIDGE) {\n this.BRIDGE.frameReady(frameNumber, new ArrayBuffer(0));\n } else {\n const fileReader = new FileReader();\n fileReader.readAsDataURL(new Blob([]));\n await new Promise((resolve, reject) => {\n fileReader.onload = resolve;\n fileReader.onerror = reject;\n });\n return fileReader.result;\n }\n }\n }\n}\n\nif (typeof window !== \"undefined\") {\n window.EF_FRAMEGEN = new EFFramegen();\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"EF_FRAMEGEN.js","names":[],"sources":["../src/EF_FRAMEGEN.ts"],"sourcesContent":["import type { VideoRenderOptions } from \"@editframe/assets\";\n\nimport {\n shallowGetTimegroups,\n type SeekForRenderTiming,\n} from \"./elements/EFTimegroup.js\";\nimport { setupTemporalHierarchy } from \"./elements/setupTemporalHierarchy.js\";\n\nimport { setupBrowserTracing } from \"./otel/setupBrowserTracing.js\";\nimport {\n clearCurrentFrameSpan,\n enableTracing,\n extractParentContext,\n setCurrentFrameSpan,\n type TraceContext,\n withSpan,\n withSpanAndContext,\n} from \"./otel/tracingHelpers.js\";\n\ninterface Bridge {\n onInitialize: (\n callback: (\n renderOptions: VideoRenderOptions,\n traceContext?: TraceContext,\n otelEndpoint?: string,\n ) => void,\n ) => void;\n\n initialized(): void;\n\n onBeginFrame(\n callback: (\n frameNumber: number,\n isLast: boolean,\n traceContext?: TraceContext,\n ) => void,\n ): void;\n\n onTriggerCanvas(callback: (traceContext?: TraceContext) => void): void;\n\n frameReady(frameNumber: number, audioSamples: ArrayBuffer): void;\n\n error(error: Error): void;\n\n syncLog(sequence: number, message: string, callback: () => void): void;\n\n exportSpans?: (endpoint: string, payload: string) => void;\n}\n\ndeclare global {\n interface Window {\n EF_FRAMEGEN?: EFFramegen;\n FRAMEGEN_BRIDGE?: Bridge;\n FRAMEGEN_BINDING?: any;\n FRAMEGEN_BINDING_error?: (error: Error) => void;\n EF_RENDERING?: () => boolean;\n }\n}\n\nclass TriggerCanvas {\n private canvas: HTMLCanvasElement;\n private ctx: CanvasRenderingContext2D;\n\n private canvasInitialized = false;\n\n constructor() {\n this.canvas = document.createElement(\"canvas\");\n const ctx = this.canvas.getContext(\"2d\", { willReadFrequently: true });\n if (!ctx) throw new Error(\"Canvas 2d context not ready\");\n this.ctx = ctx;\n this.ctx.fillStyle = \"transparent\";\n }\n\n initialize() {\n if (this.canvasInitialized) return;\n this.canvasInitialized = true;\n this.canvas.width = 1;\n this.canvas.height = 1;\n Object.assign(this.canvas.style, {\n position: \"fixed\",\n top: \"0px\",\n left: \"0px\",\n width: \"100%\",\n height: \"100%\",\n zIndex: \"100000\",\n });\n document.body.appendChild(this.canvas);\n }\n\n trigger() {\n this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);\n }\n}\n\nexport class EFFramegen {\n time = 0;\n frameDurationMs = 0;\n audioBufferPromise?: Promise<AudioBuffer>;\n renderOptions?: VideoRenderOptions;\n frameBox = document.createElement(\"div\");\n BRIDGE: typeof window.FRAMEGEN_BRIDGE;\n triggerCanvas = new TriggerCanvas();\n verificationCanvas?: HTMLCanvasElement;\n verificationCtx?: CanvasRenderingContext2D;\n private logSequence = 0;\n\n // Frame sequence coordination\n public frameTasksInProgress = false;\n public currentFrameNumber = 0;\n\n // Per-phase timing accumulators (reset every 30 frames)\n private timingFrameCount = 0;\n private timingAccum: SeekForRenderTiming = {\n updateComplete1Ms: 0,\n updateComplete2Ms: 0,\n updateComplete3Ms: 0,\n textSegmentsMs: 0,\n renderFrameMs: 0,\n renderFrameQueryMs: 0,\n renderFramePrepareMs: 0,\n renderFrameDrawMs: 0,\n renderFrameAnimsMs: 0,\n frameTasksMs: 0,\n totalMs: 0,\n };\n\n trace(...args: any[]) {\n console.trace(\"[EF_FRAMEGEN]\", ...args);\n }\n\n async syncLog(...args: any[]): Promise<void> {\n if (!this.BRIDGE) {\n // Fallback to regular console.log if no bridge\n console.log(\"[EF_FRAMEGEN]\", ...args);\n return;\n }\n\n const sequence = ++this.logSequence;\n const message = args\n .map((arg) =>\n typeof arg === \"object\" ? JSON.stringify(arg) : String(arg),\n )\n .join(\" \");\n\n return new Promise<void>((resolve) => {\n // biome-ignore lint/style/noNonNullAssertion: We know BRIDGE is set due to the guard above\n this.BRIDGE!.syncLog(sequence, message, () => {\n resolve();\n });\n });\n }\n\n private initializeVerificationCanvas() {\n if (this.verificationCanvas) {\n return;\n }\n\n this.verificationCanvas = document.createElement(\"canvas\");\n const ctx = this.verificationCanvas.getContext(\"2d\");\n if (!ctx) throw new Error(\"Verification canvas 2d context not ready\");\n this.verificationCtx = ctx;\n\n // Size to match the workbench width, or fall back to renderOptions dimensions.\n // Without ef-workbench (e.g. API renders), the canvas was never sized or appended,\n // causing frame verification to fail on every frame.\n const workbench = document.querySelector(\"ef-workbench\") as HTMLElement;\n const canvasWidth = workbench\n ? workbench.clientWidth\n : (this.renderOptions?.encoderOptions.video.width ?? 0);\n\n if (canvasWidth > 0) {\n this.verificationCanvas.width = canvasWidth;\n this.verificationCanvas.height = 1;\n\n Object.assign(this.verificationCanvas.style, {\n position: \"fixed\",\n left: \"0px\",\n bottom: \"0px\",\n width: `${canvasWidth}px`,\n height: \"1px\",\n zIndex: \"99999\",\n });\n\n document.body.appendChild(this.verificationCanvas);\n }\n }\n\n private drawVerificationStrip(frameNumber: number) {\n this.initializeVerificationCanvas();\n\n if (!this.verificationCanvas || !this.verificationCtx) {\n return;\n }\n\n const width = this.verificationCanvas.width;\n const height = this.verificationCanvas.height;\n\n // Clear the strip\n this.verificationCtx.clearRect(0, 0, width, height);\n\n // Encode frame number into RGB (24-bit)\n // R=high byte, G=middle byte, B=low byte\n const red = Math.floor(frameNumber / (256 * 256)) % 256;\n const green = Math.floor(frameNumber / 256) % 256;\n const blue = frameNumber % 256;\n\n // Fill the entire strip with the encoded frame number\n this.verificationCtx.fillStyle = `rgb(${red}, ${green}, ${blue})`;\n this.verificationCtx.fillRect(0, 0, width, height);\n }\n\n constructor() {\n this.BRIDGE = window.FRAMEGEN_BRIDGE;\n if (this.BRIDGE) {\n this.connectToBridge();\n }\n }\n\n /**\n * Helper method to get the workbench and set its rendering state.\n * This ensures consistent state management across the framegen lifecycle.\n */\n private setWorkbenchRendering(isRendering: boolean) {\n const workbench = document.querySelector(\"ef-workbench\");\n if (workbench) {\n workbench.rendering = isRendering;\n }\n }\n\n connectToBridge() {\n const BRIDGE = this.BRIDGE;\n if (!BRIDGE) {\n throw new Error(\"No BRIDGE when attempting to connect to bridge\");\n }\n\n BRIDGE.onInitialize(async (renderOptions, traceContext, otelEndpoint) => {\n // Only enable tracing if explicitly requested in renderOptions\n if (renderOptions.enableTracing && otelEndpoint) {\n enableTracing();\n await setupBrowserTracing({\n otelEndpoint,\n serviceName: \"telecine-browser\",\n bridge: BRIDGE,\n useBatching: true, // Batch spans to reduce overhead during rendering\n });\n }\n\n const parentContext = extractParentContext(traceContext);\n\n await withSpan(\n \"browser.initialize\",\n {\n width: renderOptions.encoderOptions.video.width,\n height: renderOptions.encoderOptions.video.height,\n fps: renderOptions.encoderOptions.video.framerate,\n durationMs:\n renderOptions.encoderOptions.toMs -\n renderOptions.encoderOptions.fromMs,\n },\n parentContext,\n async () => {\n try {\n await this.initialize(renderOptions);\n } catch (error) {\n // If initialization fails, ensure rendering state is cleared\n this.setWorkbenchRendering(false);\n console.error(\n \"[EF_FRAMEGEN.connectToBridge] error initializing\",\n error,\n );\n throw error;\n }\n },\n );\n\n BRIDGE.initialized();\n });\n\n BRIDGE.onBeginFrame((frameNumber, isLast, traceContext) => {\n const parentContext = extractParentContext(traceContext);\n withSpanAndContext(\n \"browser.frame.render\",\n {\n frameNumber,\n isLast,\n },\n parentContext,\n async (span, _spanContext) => {\n // Store the span itself for child operations\n // This allows spans created in Lit Tasks to use it as their parent\n setCurrentFrameSpan(span);\n\n try {\n await this.beginFrame(frameNumber, isLast);\n } catch (error) {\n // If an error occurs during rendering, ensure rendering state is cleared\n this.setWorkbenchRendering(false);\n throw error;\n } finally {\n clearCurrentFrameSpan();\n }\n },\n ).catch((error) => {\n console.error(\"[EF_FRAMEGEN.beginFrame] error:\", error);\n // Ensure rendering state is cleared on error\n this.setWorkbenchRendering(false);\n clearCurrentFrameSpan();\n throw error;\n });\n });\n\n BRIDGE.onTriggerCanvas((traceContext) => {\n const parentContext = extractParentContext(traceContext);\n\n withSpan(\"browser.canvas.trigger\", {}, parentContext, async () => {\n this.triggerCanvas.trigger();\n }).catch((error) => {\n console.error(\"[EF_FRAMEGEN.triggerCanvas] error:\", error);\n });\n });\n }\n\n get showFrameBox() {\n return this.renderOptions?.showFrameBox ?? false;\n }\n\n async initialize(renderOptions: VideoRenderOptions) {\n this.renderOptions = renderOptions;\n\n // Workbench is optional - look for it but don't require it\n const workbench = document.querySelector(\"ef-workbench\");\n if (workbench) {\n this.setWorkbenchRendering(true);\n workbench.playing = false;\n }\n\n // Find timegroups either in workbench or directly in document\n const searchRoot = workbench || document.body;\n const timegroups = shallowGetTimegroups(searchRoot);\n const firstGroup = timegroups[0];\n if (!firstGroup) {\n throw new Error(\"No temporal elements found\");\n }\n const startingTimeMs = renderOptions.encoderOptions.fromMs;\n await firstGroup.waitForMediaDurations();\n\n // CRITICAL: Manually wire up temporal hierarchy since Lit Context fails with our connection order\n // When loading via loadURL(), elements connect depth-first (children before parents), causing\n // children to miss the context-request event since parents aren't listening yet.\n // See setupTemporalHierarchy.ts for detailed explanation.\n setupTemporalHierarchy(searchRoot, firstGroup);\n\n // Suppress autonomous re-renders (EFTemporal/EFTimegroup.updated) while\n // seekForRender is in progress — same protection applied to render clones.\n firstGroup.setAttribute(\"data-no-playback-controller\", \"\");\n\n // Use seekForRender for proper time seeking during rendering\n await firstGroup.seekForRender(startingTimeMs);\n\n this.frameDurationMs = 1000 / renderOptions.encoderOptions.video.framerate;\n\n this.time = startingTimeMs;\n if (this.showFrameBox) {\n Object.assign(this.frameBox.style, {\n width: \"200px\",\n height: \"100px\",\n font: \"10px Arial\",\n backgroundColor: \"white\",\n position: \"absolute\",\n top: \"0px\",\n right: \"0px\",\n zIndex: \"100000\",\n });\n document.body.prepend(this.frameBox);\n }\n\n this.triggerCanvas.initialize();\n\n // These times are aligned to the audio frame boundaries\n // And they include padding if any.\n this.audioBufferPromise = firstGroup.renderAudio(\n renderOptions.encoderOptions.alignedFromUs / 1000,\n renderOptions.encoderOptions.alignedToUs / 1000,\n );\n // Suppress unhandled rejection while the promise sits in storage before being awaited.\n this.audioBufferPromise.catch(() => {});\n }\n\n async beginFrame(frameNumber: number, isLast: boolean) {\n if (this.renderOptions === undefined) {\n throw new Error(\"No renderOptions\");\n }\n const workbench = document.querySelector(\"ef-workbench\");\n if (workbench) {\n this.setWorkbenchRendering(true);\n }\n const searchRoot = workbench || document.body;\n const timegroups = shallowGetTimegroups(searchRoot);\n const firstGroup = timegroups[0];\n if (!firstGroup) {\n throw new Error(\"No temporal elements found\");\n }\n\n // Calculate base frame time using normal progression\n const frameTime =\n this.renderOptions.encoderOptions.fromMs +\n frameNumber * this.frameDurationMs;\n const frameTimeMs = Number(Number(frameTime).toFixed(5));\n\n // Use seekForRender for proper time seeking during rendering\n const timing = await firstGroup.seekForRender(frameTimeMs);\n this.timingFrameCount++;\n for (const key of Object.keys(\n this.timingAccum,\n ) as (keyof SeekForRenderTiming)[]) {\n this.timingAccum[key] += timing[key];\n }\n if (this.timingFrameCount >= 30) {\n const n = this.timingFrameCount;\n console.log(\n `[EF_FRAMEGEN] seekForRender phase avg (${n} frames):`,\n `total=${(this.timingAccum.totalMs / n).toFixed(1)}ms`,\n `uc1=${(this.timingAccum.updateComplete1Ms / n).toFixed(1)}ms`,\n `uc2=${(this.timingAccum.updateComplete2Ms / n).toFixed(1)}ms`,\n `uc3=${(this.timingAccum.updateComplete3Ms / n).toFixed(1)}ms`,\n `text=${(this.timingAccum.textSegmentsMs / n).toFixed(1)}ms`,\n `renderFrame=${(this.timingAccum.renderFrameMs / n).toFixed(1)}ms`,\n `rf.query=${(this.timingAccum.renderFrameQueryMs / n).toFixed(1)}ms`,\n `rf.prepare=${(this.timingAccum.renderFramePrepareMs / n).toFixed(1)}ms`,\n `rf.draw=${(this.timingAccum.renderFrameDrawMs / n).toFixed(1)}ms`,\n `rf.anims=${(this.timingAccum.renderFrameAnimsMs / n).toFixed(1)}ms`,\n `frameTasks=${(this.timingAccum.frameTasksMs / n).toFixed(1)}ms`,\n );\n this.timingFrameCount = 0;\n for (const key of Object.keys(\n this.timingAccum,\n ) as (keyof SeekForRenderTiming)[]) {\n this.timingAccum[key] = 0;\n }\n }\n if (this.showFrameBox) {\n this.frameBox.innerHTML = `\n <div>🖼️ Frame: ${frameNumber}</div>\n <div>🕛 Segment: ${this.time.toFixed(4)}</div>\n <div>🕛 Frame: ${firstGroup.currentTimeMs.toFixed(4)}</div>\n <div> from-to: ${this.renderOptions.encoderOptions.fromMs.toFixed(4)} - ${this.renderOptions.encoderOptions.toMs.toFixed(4)}</div>\n `;\n }\n\n // Draw verification pixel strip for frame verification\n this.drawVerificationStrip(frameNumber);\n\n if (isLast && this.audioBufferPromise) {\n // Currently we emit the audio in one belch at the end of the render.\n // This is not ideal, but it's the simplest thing that could possibly work.\n // We could either emit it slices, or in parallel with the video.\n // But in any case, it's fine for now.\n const renderedAudio = await this.audioBufferPromise;\n\n const channelCount = renderedAudio.numberOfChannels;\n\n const interleavedSamples = new Float32Array(\n channelCount * renderedAudio.length,\n );\n\n for (let i = 0; i < renderedAudio.length; i++) {\n for (let j = 0; j < channelCount; j++) {\n interleavedSamples.set(\n renderedAudio.getChannelData(j).slice(i, i + 1),\n i * channelCount + j,\n );\n }\n }\n\n if (this.BRIDGE) {\n this.BRIDGE.frameReady(frameNumber, interleavedSamples.buffer);\n } else {\n const fileReader = new FileReader();\n fileReader.readAsDataURL(new Blob([interleavedSamples.buffer]));\n await new Promise((resolve, reject) => {\n fileReader.onload = resolve;\n fileReader.onerror = reject;\n });\n return fileReader.result;\n }\n\n // Rendering is complete after the last frame\n this.setWorkbenchRendering(false);\n } else {\n if (this.BRIDGE) {\n this.BRIDGE.frameReady(frameNumber, new ArrayBuffer(0));\n } else {\n const fileReader = new FileReader();\n fileReader.readAsDataURL(new Blob([]));\n await new Promise((resolve, reject) => {\n fileReader.onload = resolve;\n fileReader.onerror = reject;\n });\n return fileReader.result;\n }\n }\n }\n}\n\nif (typeof window !== \"undefined\") {\n window.EF_FRAMEGEN = new EFFramegen();\n}\n"],"mappings":";;;;;;AA2DA,IAAM,gBAAN,MAAoB;CAMlB,cAAc;2BAFc;AAG1B,OAAK,SAAS,SAAS,cAAc,SAAS;EAC9C,MAAM,MAAM,KAAK,OAAO,WAAW,MAAM,EAAE,oBAAoB,MAAM,CAAC;AACtE,MAAI,CAAC,IAAK,OAAM,IAAI,MAAM,8BAA8B;AACxD,OAAK,MAAM;AACX,OAAK,IAAI,YAAY;;CAGvB,aAAa;AACX,MAAI,KAAK,kBAAmB;AAC5B,OAAK,oBAAoB;AACzB,OAAK,OAAO,QAAQ;AACpB,OAAK,OAAO,SAAS;AACrB,SAAO,OAAO,KAAK,OAAO,OAAO;GAC/B,UAAU;GACV,KAAK;GACL,MAAM;GACN,OAAO;GACP,QAAQ;GACR,QAAQ;GACT,CAAC;AACF,WAAS,KAAK,YAAY,KAAK,OAAO;;CAGxC,UAAU;AACR,OAAK,IAAI,UAAU,GAAG,GAAG,KAAK,OAAO,OAAO,KAAK,OAAO,OAAO;;;AAInE,IAAa,aAAb,MAAwB;CAgCtB,MAAM,GAAG,MAAa;AACpB,UAAQ,MAAM,iBAAiB,GAAG,KAAK;;CAGzC,MAAM,QAAQ,GAAG,MAA4B;AAC3C,MAAI,CAAC,KAAK,QAAQ;AAEhB,WAAQ,IAAI,iBAAiB,GAAG,KAAK;AACrC;;EAGF,MAAM,WAAW,EAAE,KAAK;EACxB,MAAM,UAAU,KACb,KAAK,QACJ,OAAO,QAAQ,WAAW,KAAK,UAAU,IAAI,GAAG,OAAO,IAAI,CAC5D,CACA,KAAK,IAAI;AAEZ,SAAO,IAAI,SAAe,YAAY;AAEpC,QAAK,OAAQ,QAAQ,UAAU,eAAe;AAC5C,aAAS;KACT;IACF;;CAGJ,AAAQ,+BAA+B;AACrC,MAAI,KAAK,mBACP;AAGF,OAAK,qBAAqB,SAAS,cAAc,SAAS;EAC1D,MAAM,MAAM,KAAK,mBAAmB,WAAW,KAAK;AACpD,MAAI,CAAC,IAAK,OAAM,IAAI,MAAM,2CAA2C;AACrE,OAAK,kBAAkB;EAKvB,MAAM,YAAY,SAAS,cAAc,eAAe;EACxD,MAAM,cAAc,YAChB,UAAU,cACT,KAAK,eAAe,eAAe,MAAM,SAAS;AAEvD,MAAI,cAAc,GAAG;AACnB,QAAK,mBAAmB,QAAQ;AAChC,QAAK,mBAAmB,SAAS;AAEjC,UAAO,OAAO,KAAK,mBAAmB,OAAO;IAC3C,UAAU;IACV,MAAM;IACN,QAAQ;IACR,OAAO,GAAG,YAAY;IACtB,QAAQ;IACR,QAAQ;IACT,CAAC;AAEF,YAAS,KAAK,YAAY,KAAK,mBAAmB;;;CAItD,AAAQ,sBAAsB,aAAqB;AACjD,OAAK,8BAA8B;AAEnC,MAAI,CAAC,KAAK,sBAAsB,CAAC,KAAK,gBACpC;EAGF,MAAM,QAAQ,KAAK,mBAAmB;EACtC,MAAM,SAAS,KAAK,mBAAmB;AAGvC,OAAK,gBAAgB,UAAU,GAAG,GAAG,OAAO,OAAO;EAInD,MAAM,MAAM,KAAK,MAAM,eAAe,MAAM,KAAK,GAAG;EACpD,MAAM,QAAQ,KAAK,MAAM,cAAc,IAAI,GAAG;EAC9C,MAAM,OAAO,cAAc;AAG3B,OAAK,gBAAgB,YAAY,OAAO,IAAI,IAAI,MAAM,IAAI,KAAK;AAC/D,OAAK,gBAAgB,SAAS,GAAG,GAAG,OAAO,OAAO;;CAGpD,cAAc;cApHP;yBACW;kBAGP,SAAS,cAAc,MAAM;uBAExB,IAAI,eAAe;qBAGb;8BAGQ;4BACF;0BAGD;qBACgB;GACzC,mBAAmB;GACnB,mBAAmB;GACnB,mBAAmB;GACnB,gBAAgB;GAChB,eAAe;GACf,oBAAoB;GACpB,sBAAsB;GACtB,mBAAmB;GACnB,oBAAoB;GACpB,cAAc;GACd,SAAS;GACV;AAwFC,OAAK,SAAS,OAAO;AACrB,MAAI,KAAK,OACP,MAAK,iBAAiB;;;;;;CAQ1B,AAAQ,sBAAsB,aAAsB;EAClD,MAAM,YAAY,SAAS,cAAc,eAAe;AACxD,MAAI,UACF,WAAU,YAAY;;CAI1B,kBAAkB;EAChB,MAAM,SAAS,KAAK;AACpB,MAAI,CAAC,OACH,OAAM,IAAI,MAAM,iDAAiD;AAGnE,SAAO,aAAa,OAAO,eAAe,cAAc,iBAAiB;AAEvE,OAAI,cAAc,iBAAiB,cAAc;AAC/C,mBAAe;AACf,UAAM,oBAAoB;KACxB;KACA,aAAa;KACb,QAAQ;KACR,aAAa;KACd,CAAC;;GAGJ,MAAM,gBAAgB,qBAAqB,aAAa;AAExD,SAAM,SACJ,sBACA;IACE,OAAO,cAAc,eAAe,MAAM;IAC1C,QAAQ,cAAc,eAAe,MAAM;IAC3C,KAAK,cAAc,eAAe,MAAM;IACxC,YACE,cAAc,eAAe,OAC7B,cAAc,eAAe;IAChC,EACD,eACA,YAAY;AACV,QAAI;AACF,WAAM,KAAK,WAAW,cAAc;aAC7B,OAAO;AAEd,UAAK,sBAAsB,MAAM;AACjC,aAAQ,MACN,oDACA,MACD;AACD,WAAM;;KAGX;AAED,UAAO,aAAa;IACpB;AAEF,SAAO,cAAc,aAAa,QAAQ,iBAAiB;GACzD,MAAM,gBAAgB,qBAAqB,aAAa;AACxD,sBACE,wBACA;IACE;IACA;IACD,EACD,eACA,OAAO,MAAM,iBAAiB;AAG5B,wBAAoB,KAAK;AAEzB,QAAI;AACF,WAAM,KAAK,WAAW,aAAa,OAAO;aACnC,OAAO;AAEd,UAAK,sBAAsB,MAAM;AACjC,WAAM;cACE;AACR,4BAAuB;;KAG5B,CAAC,OAAO,UAAU;AACjB,YAAQ,MAAM,mCAAmC,MAAM;AAEvD,SAAK,sBAAsB,MAAM;AACjC,2BAAuB;AACvB,UAAM;KACN;IACF;AAEF,SAAO,iBAAiB,iBAAiB;AAGvC,YAAS,0BAA0B,EAAE,EAFf,qBAAqB,aAAa,EAEF,YAAY;AAChE,SAAK,cAAc,SAAS;KAC5B,CAAC,OAAO,UAAU;AAClB,YAAQ,MAAM,sCAAsC,MAAM;KAC1D;IACF;;CAGJ,IAAI,eAAe;AACjB,SAAO,KAAK,eAAe,gBAAgB;;CAG7C,MAAM,WAAW,eAAmC;AAClD,OAAK,gBAAgB;EAGrB,MAAM,YAAY,SAAS,cAAc,eAAe;AACxD,MAAI,WAAW;AACb,QAAK,sBAAsB,KAAK;AAChC,aAAU,UAAU;;EAItB,MAAM,aAAa,aAAa,SAAS;EAEzC,MAAM,aADa,qBAAqB,WAAW,CACrB;AAC9B,MAAI,CAAC,WACH,OAAM,IAAI,MAAM,6BAA6B;EAE/C,MAAM,iBAAiB,cAAc,eAAe;AACpD,QAAM,WAAW,uBAAuB;AAMxC,yBAAuB,YAAY,WAAW;AAI9C,aAAW,aAAa,+BAA+B,GAAG;AAG1D,QAAM,WAAW,cAAc,eAAe;AAE9C,OAAK,kBAAkB,MAAO,cAAc,eAAe,MAAM;AAEjE,OAAK,OAAO;AACZ,MAAI,KAAK,cAAc;AACrB,UAAO,OAAO,KAAK,SAAS,OAAO;IACjC,OAAO;IACP,QAAQ;IACR,MAAM;IACN,iBAAiB;IACjB,UAAU;IACV,KAAK;IACL,OAAO;IACP,QAAQ;IACT,CAAC;AACF,YAAS,KAAK,QAAQ,KAAK,SAAS;;AAGtC,OAAK,cAAc,YAAY;AAI/B,OAAK,qBAAqB,WAAW,YACnC,cAAc,eAAe,gBAAgB,KAC7C,cAAc,eAAe,cAAc,IAC5C;AAED,OAAK,mBAAmB,YAAY,GAAG;;CAGzC,MAAM,WAAW,aAAqB,QAAiB;AACrD,MAAI,KAAK,kBAAkB,OACzB,OAAM,IAAI,MAAM,mBAAmB;EAErC,MAAM,YAAY,SAAS,cAAc,eAAe;AACxD,MAAI,UACF,MAAK,sBAAsB,KAAK;EAIlC,MAAM,aADa,qBADA,aAAa,SAAS,KACU,CACrB;AAC9B,MAAI,CAAC,WACH,OAAM,IAAI,MAAM,6BAA6B;EAI/C,MAAM,YACJ,KAAK,cAAc,eAAe,SAClC,cAAc,KAAK;EACrB,MAAM,cAAc,OAAO,OAAO,UAAU,CAAC,QAAQ,EAAE,CAAC;EAGxD,MAAM,SAAS,MAAM,WAAW,cAAc,YAAY;AAC1D,OAAK;AACL,OAAK,MAAM,OAAO,OAAO,KACvB,KAAK,YACN,CACC,MAAK,YAAY,QAAQ,OAAO;AAElC,MAAI,KAAK,oBAAoB,IAAI;GAC/B,MAAM,IAAI,KAAK;AACf,WAAQ,IACN,0CAA0C,EAAE,YAC5C,UAAU,KAAK,YAAY,UAAU,GAAG,QAAQ,EAAE,CAAC,KACnD,QAAQ,KAAK,YAAY,oBAAoB,GAAG,QAAQ,EAAE,CAAC,KAC3D,QAAQ,KAAK,YAAY,oBAAoB,GAAG,QAAQ,EAAE,CAAC,KAC3D,QAAQ,KAAK,YAAY,oBAAoB,GAAG,QAAQ,EAAE,CAAC,KAC3D,SAAS,KAAK,YAAY,iBAAiB,GAAG,QAAQ,EAAE,CAAC,KACzD,gBAAgB,KAAK,YAAY,gBAAgB,GAAG,QAAQ,EAAE,CAAC,KAC/D,aAAa,KAAK,YAAY,qBAAqB,GAAG,QAAQ,EAAE,CAAC,KACjE,eAAe,KAAK,YAAY,uBAAuB,GAAG,QAAQ,EAAE,CAAC,KACrE,YAAY,KAAK,YAAY,oBAAoB,GAAG,QAAQ,EAAE,CAAC,KAC/D,aAAa,KAAK,YAAY,qBAAqB,GAAG,QAAQ,EAAE,CAAC,KACjE,eAAe,KAAK,YAAY,eAAe,GAAG,QAAQ,EAAE,CAAC,IAC9D;AACD,QAAK,mBAAmB;AACxB,QAAK,MAAM,OAAO,OAAO,KACvB,KAAK,YACN,CACC,MAAK,YAAY,OAAO;;AAG5B,MAAI,KAAK,aACP,MAAK,SAAS,YAAY;4BACJ,YAAY;2BACb,KAAK,KAAK,QAAQ,EAAE,CAAC;2BACrB,WAAW,cAAc,QAAQ,EAAE,CAAC;0BACrC,KAAK,cAAc,eAAe,OAAO,QAAQ,EAAE,CAAC,KAAK,KAAK,cAAc,eAAe,KAAK,QAAQ,EAAE,CAAC;;AAKjI,OAAK,sBAAsB,YAAY;AAEvC,MAAI,UAAU,KAAK,oBAAoB;GAKrC,MAAM,gBAAgB,MAAM,KAAK;GAEjC,MAAM,eAAe,cAAc;GAEnC,MAAM,qBAAqB,IAAI,aAC7B,eAAe,cAAc,OAC9B;AAED,QAAK,IAAI,IAAI,GAAG,IAAI,cAAc,QAAQ,IACxC,MAAK,IAAI,IAAI,GAAG,IAAI,cAAc,IAChC,oBAAmB,IACjB,cAAc,eAAe,EAAE,CAAC,MAAM,GAAG,IAAI,EAAE,EAC/C,IAAI,eAAe,EACpB;AAIL,OAAI,KAAK,OACP,MAAK,OAAO,WAAW,aAAa,mBAAmB,OAAO;QACzD;IACL,MAAM,aAAa,IAAI,YAAY;AACnC,eAAW,cAAc,IAAI,KAAK,CAAC,mBAAmB,OAAO,CAAC,CAAC;AAC/D,UAAM,IAAI,SAAS,SAAS,WAAW;AACrC,gBAAW,SAAS;AACpB,gBAAW,UAAU;MACrB;AACF,WAAO,WAAW;;AAIpB,QAAK,sBAAsB,MAAM;aAE7B,KAAK,OACP,MAAK,OAAO,WAAW,6BAAa,IAAI,YAAY,EAAE,CAAC;OAClD;GACL,MAAM,aAAa,IAAI,YAAY;AACnC,cAAW,cAAc,IAAI,KAAK,EAAE,CAAC,CAAC;AACtC,SAAM,IAAI,SAAS,SAAS,WAAW;AACrC,eAAW,SAAS;AACpB,eAAW,UAAU;KACrB;AACF,UAAO,WAAW;;;;AAM1B,IAAI,OAAO,WAAW,YACpB,QAAO,cAAc,IAAI,YAAY"}
|
|
@@ -45,6 +45,18 @@ interface RenderCloneResult {
|
|
|
45
45
|
* - GPU operations (WebGL context creation, shader compilation) may take up to ~1s
|
|
46
46
|
*/
|
|
47
47
|
type TimegroupInitializer = (timegroup: EFTimegroup) => void;
|
|
48
|
+
/**
|
|
49
|
+
* The four timegroup modes define how duration is calculated:
|
|
50
|
+
* - "fit": Inherits duration from parent timegroup
|
|
51
|
+
* - "fixed": Uses explicit duration attribute
|
|
52
|
+
* - "sequence": Sum of child durations minus overlaps
|
|
53
|
+
* - "contain": Maximum of child durations
|
|
54
|
+
*/
|
|
55
|
+
type TimeMode = "fit" | "fixed" | "sequence" | "contain";
|
|
56
|
+
/**
|
|
57
|
+
* Per-phase timing data returned by seekForRender().
|
|
58
|
+
* All values are in milliseconds.
|
|
59
|
+
*/
|
|
48
60
|
interface SeekForRenderTiming {
|
|
49
61
|
updateComplete1Ms: number;
|
|
50
62
|
updateComplete2Ms: number;
|
|
@@ -58,14 +70,6 @@ interface SeekForRenderTiming {
|
|
|
58
70
|
frameTasksMs: number;
|
|
59
71
|
totalMs: number;
|
|
60
72
|
}
|
|
61
|
-
/**
|
|
62
|
-
* The four timegroup modes define how duration is calculated:
|
|
63
|
-
* - "fit": Inherits duration from parent timegroup
|
|
64
|
-
* - "fixed": Uses explicit duration attribute
|
|
65
|
-
* - "sequence": Sum of child durations minus overlaps
|
|
66
|
-
* - "contain": Maximum of child durations
|
|
67
|
-
*/
|
|
68
|
-
type TimeMode = "fit" | "fixed" | "sequence" | "contain";
|
|
69
73
|
declare const EFTimegroup_base: (new (...args: any[]) => TemporalMixinInterface) & typeof LitElement;
|
|
70
74
|
declare class EFTimegroup extends EFTimegroup_base implements FrameRenderable {
|
|
71
75
|
#private;
|
|
@@ -575,14 +575,18 @@ let EFTimegroup = class EFTimegroup$1 extends EFTargetable(EFTemporal(TWMixin(Li
|
|
|
575
575
|
if (this.playbackController) this.playbackController.currentTime = newTime;
|
|
576
576
|
this._setLocalTimeMs(timeMs);
|
|
577
577
|
this.requestUpdate("currentTime");
|
|
578
|
-
await this.updateComplete;
|
|
579
578
|
const t1 = performance.now();
|
|
579
|
+
await this.updateComplete;
|
|
580
|
+
const updateComplete1Ms = performance.now() - t1;
|
|
580
581
|
const allLitElements = this.#getAllLitElementDescendants();
|
|
581
|
-
await Promise.all(allLitElements.map((el) => el.updateComplete));
|
|
582
582
|
const t2 = performance.now();
|
|
583
583
|
await Promise.all(allLitElements.map((el) => el.updateComplete));
|
|
584
|
+
const updateComplete2Ms = performance.now() - t2;
|
|
584
585
|
const t3 = performance.now();
|
|
586
|
+
await Promise.all(allLitElements.map((el) => el.updateComplete));
|
|
587
|
+
const updateComplete3Ms = performance.now() - t3;
|
|
585
588
|
const textElements = allLitElements.filter((el) => el.tagName === "EF-TEXT");
|
|
589
|
+
const t4 = performance.now();
|
|
586
590
|
if (textElements.length > 0) {
|
|
587
591
|
await Promise.all(textElements.map((el) => {
|
|
588
592
|
if ("whenSegmentsReady" in el && typeof el.whenSegmentsReady === "function") return el.whenSegmentsReady();
|
|
@@ -590,29 +594,32 @@ let EFTimegroup = class EFTimegroup$1 extends EFTargetable(EFTemporal(TWMixin(Li
|
|
|
590
594
|
}));
|
|
591
595
|
this.offsetHeight;
|
|
592
596
|
}
|
|
593
|
-
const
|
|
594
|
-
const
|
|
597
|
+
const textSegmentsMs = performance.now() - t4;
|
|
598
|
+
const t5 = performance.now();
|
|
599
|
+
const frameControllerTiming = await this.#frameController.renderFrame(timeMs, {
|
|
595
600
|
waitForLitUpdate: false,
|
|
596
601
|
onAnimationsUpdate: (root) => {
|
|
597
602
|
updateAnimations(root);
|
|
598
603
|
root.offsetWidth;
|
|
599
604
|
}
|
|
600
605
|
});
|
|
601
|
-
const
|
|
602
|
-
await this.#executeCustomFrameTasks();
|
|
606
|
+
const renderFrameMs = performance.now() - t5;
|
|
603
607
|
const t6 = performance.now();
|
|
608
|
+
await this.#executeCustomFrameTasks();
|
|
609
|
+
const frameTasksMs = performance.now() - t6;
|
|
610
|
+
const totalMs = performance.now() - t0;
|
|
604
611
|
return {
|
|
605
|
-
updateComplete1Ms
|
|
606
|
-
updateComplete2Ms
|
|
607
|
-
updateComplete3Ms
|
|
608
|
-
textSegmentsMs
|
|
609
|
-
renderFrameMs
|
|
610
|
-
renderFrameQueryMs:
|
|
611
|
-
renderFramePrepareMs:
|
|
612
|
-
renderFrameDrawMs:
|
|
613
|
-
renderFrameAnimsMs:
|
|
614
|
-
frameTasksMs
|
|
615
|
-
totalMs
|
|
612
|
+
updateComplete1Ms,
|
|
613
|
+
updateComplete2Ms,
|
|
614
|
+
updateComplete3Ms,
|
|
615
|
+
textSegmentsMs,
|
|
616
|
+
renderFrameMs,
|
|
617
|
+
renderFrameQueryMs: frameControllerTiming?.queryMs ?? 0,
|
|
618
|
+
renderFramePrepareMs: frameControllerTiming?.prepareMs ?? 0,
|
|
619
|
+
renderFrameDrawMs: frameControllerTiming?.renderMs ?? 0,
|
|
620
|
+
renderFrameAnimsMs: frameControllerTiming?.animsMs ?? 0,
|
|
621
|
+
frameTasksMs,
|
|
622
|
+
totalMs
|
|
616
623
|
};
|
|
617
624
|
}
|
|
618
625
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EFTimegroup.js","names":["durationCache: WeakMap<EFTimegroup, number>","ancestor: Node | null","EFTimegroup","taskObj: {\n run(): void | Promise<number | undefined>;\n taskComplete: Promise<number | undefined>;\n }","#seekTaskAbortController","#pendingSeekTime","#currentTime","#seekTaskPromise","#runSeekTask","#recomputeAggregateReadyState","state","#trackedChildren","#childReadyStateHandler","#childContentChangeHandler","#mode","#overlapMs","#initializer","#restoringFromLocalStorage","#frameController","#qualityUpgradeScheduler","#customFrameTasks","#executeCustomFrameTasks","#contentEpoch","#runThrottledFrameTask","#userTimeMs","#processingPendingSeek","#seekInProgress","#getAllLitElementDescendants","result: LitElement[]","#onFrameCallback","#onFrameCleanup","#handleSlotChange","#syncChildListeners","#runInitializer","#setupPlaybackListener","#playbackListener","#previousDurationMs","#resizeObserver","#removePlaybackListener","#initializerHasRun","result: unknown","#copyTextSegmentData","updatePromises: Promise<any>[]","#waitForCaptionsData","waitPromises: Promise<unknown>[]","#copyInitializersToClone","#createRenderCloneFromFactory","#createRenderCloneFromDOM","#waitForDescendants","renderTarget: HTMLElement","#finalizeRenderClone","#copyCaptionsData","#copyTextContent","#mediaDurationsPromise","#waitForMediaDurations","rafId1: number","rafId2: number","timeoutId: ReturnType<typeof setTimeout>"],"sources":["../../src/elements/EFTimegroup.ts"],"sourcesContent":["import { provide } from \"@lit/context\";\nimport debug from \"debug\";\nimport { css, html, LitElement, type PropertyValues } from \"lit\";\nimport { customElement, property } from \"lit/decorators.js\";\n\nimport { quantizeToFrameTimeS } from \"../utils/frameTime.js\";\nimport { getCloneFactory } from \"./cloneFactoryRegistry.js\";\nimport { EF_RENDERING } from \"../EF_RENDERING.js\";\nimport { efContext } from \"../gui/efContext.js\";\nimport { TWMixin } from \"../gui/TWMixin.js\";\nimport { isTracingEnabled, withSpan } from \"../otel/tracingHelpers.js\";\nimport {\n FrameController,\n type FrameRenderable,\n type FrameState,\n PRIORITY_DEFAULT,\n} from \"../preview/FrameController.js\";\nimport { QualityUpgradeScheduler } from \"../preview/QualityUpgradeScheduler.js\";\nimport { deepGetMediaElements, type EFMedia } from \"./EFMedia.js\";\nimport {\n EFTemporal,\n flushStartTimeMsCache,\n resetTemporalCache,\n shallowGetTemporalElements,\n timegroupContext,\n type TemporalMixinInterface,\n} from \"./EFTemporal.js\";\nimport { parseTimeToMs } from \"./parseTimeToMs.js\";\nimport { renderTemporalAudio } from \"./renderTemporalAudio.js\";\nimport { EFTargetable } from \"./TargetController.js\";\nimport { TimegroupController } from \"./TimegroupController.js\";\nimport { updateAnimations } from \"./updateAnimations.js\";\nimport {\n type ContainerInfo,\n getContainerInfoFromElement,\n} from \"./ContainerInfo.js\";\nimport {\n type ElementPositionInfo,\n getPositionInfoFromElement,\n} from \"./ElementPositionInfo.js\";\n// Import only types - actual function loaded dynamically\nimport type { RenderToVideoOptions } from \"../preview/renderTimegroupToVideo.types.js\";\nimport type { PlaybackControllerUpdateEvent } from \"../gui/PlaybackController.js\";\n\n// Side-effect imports for workbench wrapping\nimport \"../canvas/EFCanvas.js\";\nimport \"../gui/hierarchy/EFHierarchy.js\";\nimport \"../gui/EFFilmstrip.js\";\nimport \"../gui/EFWorkbench.js\";\nimport \"../gui/EFFitScale.js\";\nimport \"./EFPanZoom.js\";\n\nconst log = debug(\"ef:elements:EFTimegroup\");\n\n// Custom frame task callback type\nexport type FrameTaskCallback = (info: {\n ownCurrentTimeMs: number;\n currentTimeMs: number;\n durationMs: number;\n percentComplete: number;\n element: EFTimegroup;\n}) => void | Promise<void>;\n\n/**\n * Result of createRenderClone() - contains the clone, its container, and cleanup function.\n */\nexport interface RenderCloneResult {\n /** The cloned timegroup, fully functional with its own time state */\n clone: EFTimegroup;\n /** The offscreen container holding the clone */\n container: HTMLElement;\n /** Call this to remove the clone from DOM and clean up */\n cleanup: () => void;\n}\n\n/**\n * Initializer function type for setting up JavaScript behavior on timegroup instances.\n * This function is called on both the prime timeline and each render clone.\n *\n * CONSTRAINTS:\n * - MUST be synchronous (no async/await, no Promise return)\n * - MUST complete in <2000ms (error) or <100ms (warning)\n * - Should only register callbacks and set up behavior, not do expensive work\n * - GPU operations (WebGL context creation, shader compilation) may take up to ~1s\n */\nexport type TimegroupInitializer = (timegroup: EFTimegroup) => void;\n\nexport interface SeekForRenderTiming {\n updateComplete1Ms: number;\n updateComplete2Ms: number;\n updateComplete3Ms: number;\n textSegmentsMs: number;\n renderFrameMs: number;\n renderFrameQueryMs: number;\n renderFramePrepareMs: number;\n renderFrameDrawMs: number;\n renderFrameAnimsMs: number;\n frameTasksMs: number;\n totalMs: number;\n}\n\n// Constants for initializer time budget enforcement\nconst INITIALIZER_ERROR_THRESHOLD_MS = 2000;\nconst INITIALIZER_WARN_THRESHOLD_MS = 100;\n\n// ============================================================================\n// Purpose 1: Composition Rules - How Duration is Determined\n// ============================================================================\n//\n// A timegroup's duration is determined by its mode:\n// - \"fixed\": Uses explicit duration attribute (base case)\n// - \"sequence\": Sum of child durations minus overlaps\n// - \"contain\": Maximum of child durations\n// - \"fit\": Inherits duration from parent timegroup\n//\n// Core invariant: Every timegroup has exactly one duration value at any moment,\n// computed from either explicit specification (fixed mode) or child relationships\n// (sequence/contain/fit modes).\n//\n// ============================================================================\n\n/**\n * The four timegroup modes define how duration is calculated:\n * - \"fit\": Inherits duration from parent timegroup\n * - \"fixed\": Uses explicit duration attribute\n * - \"sequence\": Sum of child durations minus overlaps\n * - \"contain\": Maximum of child durations\n */\nexport type TimeMode = \"fit\" | \"fixed\" | \"sequence\" | \"contain\";\n\n// Cache for duration calculations to avoid O(n) recalculation on every access\n// Used by all modes (sequence, contain) to avoid repeated iteration through children\nlet durationCache: WeakMap<EFTimegroup, number> = new WeakMap();\n\nexport const flushDurationCache = () => {\n durationCache = new WeakMap();\n};\n\n// Keep alias for backwards compatibility\nexport const flushSequenceDurationCache = flushDurationCache;\n\n// Track timegroups currently calculating duration to prevent infinite loops\nconst durationCalculationInProgress = new WeakSet<EFTimegroup>();\n\n// Export function to check if a timegroup is currently calculating duration\n// This is used by EFTemporal to prevent calling parent.durationMs during calculation\nexport const isTimegroupCalculatingDuration = (\n timegroup: EFTimegroup | undefined,\n): boolean => {\n return (\n timegroup !== undefined && durationCalculationInProgress.has(timegroup)\n );\n};\n\n// Register this function with EFTemporal to break circular dependency\n// EFTemporal needs this function but can't import it directly due to circular dependency\nimport { registerIsTimegroupCalculatingDuration } from \"./EFTemporal.js\";\nregisterIsTimegroupCalculatingDuration(isTimegroupCalculatingDuration);\n\n/**\n * Determines if a timegroup has its own duration based on its mode.\n * This is the semantic rule: which modes produce independent durations.\n */\nfunction hasOwnDurationForMode(\n mode: TimeMode,\n hasExplicitDuration: boolean,\n): boolean {\n return (\n mode === \"contain\" ||\n mode === \"sequence\" ||\n (mode === \"fixed\" && hasExplicitDuration)\n );\n}\n\n/**\n * Determines if a child temporal element should participate in parent duration calculation.\n *\n * Semantic rule: Fit-mode children inherit from parent, so they don't contribute to parent's\n * duration calculation (to avoid circular dependencies). Children without own duration\n * also don't contribute.\n */\nfunction shouldParticipateInDurationCalculation(\n child: TemporalMixinInterface & HTMLElement,\n): boolean {\n // Fit timegroups look \"up\" to their parent for duration, so skip to avoid infinite loop\n if (child instanceof EFTimegroup && child.mode === \"fit\") {\n return false;\n }\n // Only children with their own duration contribute\n if (!child.hasOwnDuration) {\n return false;\n }\n return true;\n}\n\n/**\n * Evaluates duration for \"fit\" mode: inherits from parent.\n * Semantic rule: fit mode always matches parent duration, or 0 if no parent.\n */\nfunction evaluateFitDuration(parentTimegroup: EFTimegroup | undefined): number {\n if (!parentTimegroup) {\n return 0;\n }\n return parentTimegroup.durationMs;\n}\n\n/**\n * Evaluates duration for \"sequence\" mode: sum of children minus overlaps.\n * Semantic rule: sequence mode sums child durations, subtracting overlap between consecutive items.\n * Fit-mode children are excluded to avoid circular dependencies.\n */\nfunction evaluateSequenceDuration(\n timegroup: EFTimegroup,\n childTemporals: Array<TemporalMixinInterface & HTMLElement>,\n overlapMs: number,\n): number {\n // Check cache first to avoid expensive O(n) recalculation\n const cachedDuration = durationCache.get(timegroup);\n if (cachedDuration !== undefined) {\n return cachedDuration;\n }\n\n let duration = 0;\n let participatingIndex = 0;\n childTemporals.forEach((child) => {\n if (!shouldParticipateInDurationCalculation(child)) {\n return;\n }\n // Prevent infinite loops: skip children that are already calculating their duration\n if (\n child instanceof EFTimegroup &&\n durationCalculationInProgress.has(child)\n ) {\n return;\n }\n\n // Additional safety: if child is a timegroup, check if any of its ancestors\n // (EXCLUDING the current timegroup) are calculating.\n // This prevents cycles where a child's descendant eventually calls back to an ancestor,\n // but allows direct children of the current timegroup to participate.\n if (child instanceof EFTimegroup) {\n let ancestor: Node | null = child.parentNode;\n let shouldSkip = false;\n while (ancestor) {\n // Stop FIRST if we've reached the current timegroup - direct children are allowed\n if (ancestor === timegroup) {\n break;\n }\n if (\n ancestor instanceof EFTimegroup &&\n durationCalculationInProgress.has(ancestor)\n ) {\n // Found a calculating ancestor (not the current timegroup) - skip this child to prevent cycle\n shouldSkip = true;\n break;\n }\n ancestor = ancestor.parentNode;\n }\n if (shouldSkip) {\n return;\n }\n }\n\n // Subtract overlap for all items after the first\n if (participatingIndex > 0) {\n duration -= overlapMs;\n }\n duration += child.durationMs;\n participatingIndex++;\n });\n\n // Ensure non-negative duration (invariant)\n duration = Math.max(0, duration);\n\n // Cache the calculated duration\n durationCache.set(timegroup, duration);\n return duration;\n}\n\n/**\n * Evaluates duration for \"contain\" mode: maximum of children.\n * Semantic rule: contain mode takes the maximum child duration.\n * Fit-mode children and children without own duration are excluded.\n */\nfunction evaluateContainDuration(\n timegroup: EFTimegroup,\n childTemporals: Array<TemporalMixinInterface & HTMLElement>,\n): number {\n // Check cache first to avoid expensive O(n) recalculation\n const cachedDuration = durationCache.get(timegroup);\n if (cachedDuration !== undefined) {\n return cachedDuration;\n }\n\n let maxDuration = 0;\n for (const child of childTemporals) {\n if (!shouldParticipateInDurationCalculation(child)) {\n continue;\n }\n // Prevent infinite loops: skip children that are already calculating their duration\n // This check applies to all timegroup children, not just contain mode, because\n // a sequence-mode child could contain a contain-mode grandchild that\n // eventually references back to the parent through the parent chain\n if (\n child instanceof EFTimegroup &&\n durationCalculationInProgress.has(child)\n ) {\n continue;\n }\n\n // Additional safety: if child is a timegroup, check if any of its ancestors\n // (EXCLUDING the current timegroup) are calculating.\n // This prevents cycles where a child's descendant eventually calls back to an ancestor,\n // but allows direct children of the current timegroup to participate.\n if (child instanceof EFTimegroup) {\n let ancestor: Node | null = child.parentNode;\n let shouldSkip = false;\n while (ancestor) {\n // Stop FIRST if we've reached the current timegroup - direct children are allowed\n if (ancestor === timegroup) {\n break;\n }\n if (\n ancestor instanceof EFTimegroup &&\n durationCalculationInProgress.has(ancestor)\n ) {\n // Found a calculating ancestor (not the current timegroup) - skip this child to prevent cycle\n shouldSkip = true;\n break;\n }\n ancestor = ancestor.parentNode;\n }\n if (shouldSkip) {\n continue;\n }\n }\n\n maxDuration = Math.max(maxDuration, child.durationMs);\n }\n // Ensure non-negative duration (invariant)\n const duration = Math.max(0, maxDuration);\n\n // Cache the calculated duration\n durationCache.set(timegroup, duration);\n return duration;\n}\n\n/**\n * Evaluates duration based on timegroup mode.\n * This is the semantic evaluation function - it determines what duration should be.\n *\n * Note: Fixed mode is handled inline in the getter because it needs to call super.durationMs\n * which requires the class context. The other modes are extracted for clarity.\n */\nfunction evaluateDurationForMode(\n timegroup: EFTimegroup,\n mode: TimeMode,\n childTemporals: Array<TemporalMixinInterface & HTMLElement>,\n): number {\n switch (mode) {\n case \"fit\":\n return evaluateFitDuration(timegroup.parentTimegroup);\n case \"sequence\": {\n // Mark this timegroup as calculating duration to prevent infinite loops\n durationCalculationInProgress.add(timegroup);\n try {\n return evaluateSequenceDuration(\n timegroup,\n childTemporals,\n timegroup.overlapMs,\n );\n } finally {\n // Always remove the marker, even if an error occurs\n durationCalculationInProgress.delete(timegroup);\n }\n }\n case \"contain\": {\n // Mark this timegroup as calculating duration to prevent infinite loops\n durationCalculationInProgress.add(timegroup);\n try {\n return evaluateContainDuration(timegroup, childTemporals);\n } finally {\n // Always remove the marker, even if an error occurs\n durationCalculationInProgress.delete(timegroup);\n }\n }\n default:\n throw new Error(`Invalid time mode: ${mode}`);\n }\n}\n\nexport const shallowGetTimegroups = (\n element: Element,\n groups: EFTimegroup[] = [],\n) => {\n for (const child of Array.from(element.children)) {\n if (child instanceof EFTimegroup) {\n groups.push(child);\n } else {\n shallowGetTimegroups(child, groups);\n }\n }\n return groups;\n};\n\n// ============================================================================\n// Purpose 2: Time Propagation - How currentTime Flows Root to Children\n// ============================================================================\n//\n// Time propagation determines how the root timegroup's currentTime flows to child\n// temporal elements, computing each child's ownCurrentTime based on:\n// - The root's currentTime (global coordinate)\n// - The child's startTimeMs (determined by parent's composition mode)\n// - The parent's mode (sequence/contain/fit/fixed)\n//\n// Propagation rules by mode:\n// - Sequence: Each child's ownCurrentTime progresses within its time-shifted window\n// - Contain: All children share the same ownCurrentTime as parent\n// - Fit: Child ownCurrentTime = parent ownCurrentTime (identity mapping)\n//\n// Core invariant: Only root timegroup's currentTime should be written.\n// Child times are computed from parent state via ownCurrentTimeMs.\n//\n// Note: Time propagation logic is primarily implemented in EFTemporal.ts\n// (ownCurrentTimeMs getter and startTimeMs calculation). The timegroup's\n// currentTime setter triggers propagation by updating root time.\n//\n// ============================================================================\n\n// ============================================================================\n// Purpose 3: Seeking - Moving to a Specific Time\n// ============================================================================\n//\n// Seeking moves the timeline to a specific time position. This involves:\n// 1. Quantizing the requested time to frame boundaries (based on fps)\n// 2. Clamping to valid range [0, duration]\n// 3. Updating root timegroup's currentTime (which triggers time propagation)\n// 4. Waiting for all media and frame tasks to complete\n//\n// Core invariant: All time values snap to frame boundaries when FPS is set.\n// This ensures consistent seek/render behavior.\n//\n// ============================================================================\n\n/**\n * Evaluates the target time for a seek operation.\n * Applies quantization and clamping to determine the valid seek target.\n */\nfunction evaluateSeekTarget(\n requestedTime: number,\n durationMs: number,\n fps: number,\n): number {\n // Quantize to frame boundaries\n const quantizedTime = quantizeToFrameTimeS(requestedTime, fps);\n // Clamp to valid range [0, duration]\n return Math.max(0, Math.min(quantizedTime, durationMs / 1000));\n}\n\n@customElement(\"ef-timegroup\")\nexport class EFTimegroup\n extends EFTargetable(EFTemporal(TWMixin(LitElement)))\n implements FrameRenderable\n{\n static get observedAttributes(): string[] {\n const parentAttributes = super.observedAttributes || [];\n return [\n ...parentAttributes,\n \"mode\",\n \"overlap\",\n \"currenttime\",\n \"fit\",\n \"fps\",\n \"auto-init\",\n \"workbench\",\n ];\n }\n\n static styles = css`\n :host {\n display: block;\n position: relative;\n overflow: hidden;\n }\n\n ::slotted(ef-timegroup) {\n position: absolute;\n width: 100%;\n height: 100%;\n top: 0;\n left: 0;\n overflow: initial;\n }\n `;\n\n /** @internal */\n @provide({ context: timegroupContext })\n _timeGroupContext = this;\n\n /** @internal */\n @provide({ context: efContext })\n efContext = this;\n\n // ---- Content Readiness Aggregation ----\n\n #trackedChildren = new Set<TemporalMixinInterface & HTMLElement>();\n\n override shouldAutoReady(): boolean {\n return false;\n }\n\n #childReadyStateHandler = () => {\n this.#recomputeAggregateReadyState();\n };\n\n #childContentChangeHandler = (e: Event) => {\n const detail = (e as CustomEvent).detail;\n this.emitContentChange(detail?.reason ?? \"content\");\n };\n\n #recomputeAggregateReadyState(): void {\n const children = shallowGetTemporalElements(this);\n if (children.length === 0) {\n this.setContentReadyState(\"ready\");\n return;\n }\n\n let hasLoading = false;\n let hasError = false;\n let hasIdle = false;\n\n for (const child of children) {\n const state = child.contentReadyState;\n if (state === \"loading\") hasLoading = true;\n else if (state === \"error\") hasError = true;\n else if (state === \"idle\") hasIdle = true;\n }\n\n if (hasError) {\n this.setContentReadyState(\"error\");\n } else if (hasLoading) {\n this.setContentReadyState(\"loading\");\n } else if (hasIdle) {\n this.setContentReadyState(\"loading\");\n } else {\n this.setContentReadyState(\"ready\");\n }\n }\n\n #syncChildListeners(): void {\n const currentChildren = new Set(\n shallowGetTemporalElements(this) as Array<\n TemporalMixinInterface & HTMLElement\n >,\n );\n\n // Remove listeners from children that left\n for (const child of this.#trackedChildren) {\n if (!currentChildren.has(child)) {\n child.removeEventListener(\n \"readystatechange\",\n this.#childReadyStateHandler,\n );\n child.removeEventListener(\n \"contentchange\",\n this.#childContentChangeHandler,\n );\n }\n }\n\n // Add listeners to new children\n for (const child of currentChildren) {\n if (!this.#trackedChildren.has(child)) {\n child.addEventListener(\n \"readystatechange\",\n this.#childReadyStateHandler,\n );\n child.addEventListener(\n \"contentchange\",\n this.#childContentChangeHandler,\n );\n }\n }\n\n this.#trackedChildren = currentChildren;\n this.#recomputeAggregateReadyState();\n }\n\n // ---- End Content Readiness Aggregation ----\n\n /** @public */\n #mode: TimeMode = \"contain\";\n get mode(): TimeMode {\n return this.#mode;\n }\n set mode(value: TimeMode) {\n if (this.#mode === value) return;\n const old = this.#mode;\n this.#mode = value;\n this.requestUpdate(\"mode\", old);\n if (this.getAttribute(\"mode\") !== value) {\n this.setAttribute(\"mode\", value);\n }\n }\n\n /** @public */\n #overlapMs = 0;\n get overlapMs(): number {\n return this.#overlapMs;\n }\n set overlapMs(value: number) {\n if (this.#overlapMs === value) return;\n const old = this.#overlapMs;\n this.#overlapMs = value;\n this.requestUpdate(\"overlapMs\", old);\n const attrVal = value > 0 ? `${value}ms` : null;\n if (attrVal && this.getAttribute(\"overlap\") !== attrVal) {\n this.setAttribute(\"overlap\", attrVal);\n } else if (!attrVal && this.hasAttribute(\"overlap\")) {\n this.removeAttribute(\"overlap\");\n }\n }\n\n #initializer?: TimegroupInitializer;\n\n /**\n * Initializer function for setting up JavaScript behavior on this timegroup.\n * This function is called ONCE per instance - on the prime timeline when first connected,\n * and on each render clone when created.\n *\n * Use this to register frame callbacks, set up event listeners, or initialize state.\n * The same initializer code runs on both prime and clones, eliminating duplication.\n *\n * CONSTRAINTS:\n * - MUST be synchronous (no async/await, no Promise return)\n * - MUST complete in <100ms (error thrown) or <10ms (warning logged)\n * - Should only register callbacks and set up behavior, not do expensive work\n *\n * TIMING:\n * - If set before element connects to DOM: runs automatically after connectedCallback\n * - If set after element is connected: runs immediately\n * - Clones automatically copy and run the initializer when created\n *\n * @example\n * ```javascript\n * const tg = document.querySelector('ef-timegroup');\n * tg.initializer = (instance) => {\n * // Runs once on prime timeline, once on each clone\n * instance.addFrameTask((info) => {\n * // Update content based on time\n * });\n * };\n * ```\n * @public\n */\n get initializer(): TimegroupInitializer | undefined {\n return this.#initializer;\n }\n\n set initializer(fn: TimegroupInitializer | undefined) {\n this.#initializer = fn;\n // Just store the function. Execution is handled by:\n // - connectedCallback (for elements that have initializer before connection)\n // - #copyInitializersToClone (explicitly triggers for render clones)\n }\n\n /**\n * Track if initializer has run on this instance to prevent double execution.\n * @internal\n */\n #initializerHasRun = false;\n\n /** @public */\n @property({ type: Number })\n fps = 30;\n\n /**\n * When true, automatically seeks to frame 0 after media durations are loaded.\n * Only applies to root timegroups (timegroups that are not nested inside another timegroup).\n * This ensures the first frame is rendered immediately on initialization.\n */\n @property({ type: Boolean, attribute: \"auto-init\" })\n autoInit = false;\n\n /**\n * When true, automatically wraps this root timegroup with an ef-workbench element.\n * The workbench provides development UI including hierarchy panel, timeline, and playback controls.\n * Only applies to root timegroups.\n * @public\n */\n @property({ type: Boolean, reflect: true })\n workbench = false;\n\n attributeChangedCallback(\n name: string,\n old: string | null,\n value: string | null,\n ): void {\n if (name === \"mode\" && value) {\n this.mode = value as typeof this.mode;\n }\n if (name === \"overlap\" && value) {\n this.overlapMs = parseTimeToMs(value);\n }\n if (name === \"auto-init\") {\n this.autoInit = value !== null;\n }\n if (name === \"fps\" && value) {\n this.fps = Number.parseFloat(value);\n }\n if (name === \"workbench\") {\n this.workbench = value !== null;\n }\n super.attributeChangedCallback(name, old, value);\n }\n\n /** @public */\n @property({ type: String })\n fit: \"none\" | \"contain\" | \"cover\" = \"none\";\n\n #resizeObserver?: ResizeObserver;\n\n /** Content epoch - increments when visual content changes (used by thumbnail cache) */\n #contentEpoch = 0;\n\n #currentTime: number | undefined = undefined;\n #userTimeMs: number = 0; // What the user last requested (for preview display)\n #seekInProgress = false;\n #pendingSeekTime: number | undefined;\n #processingPendingSeek = false;\n #restoringFromLocalStorage = false; // Guard to prevent recursive seeks during localStorage restoration\n\n /** @internal */\n isRestoringFromLocalStorage(): boolean {\n return this.#restoringFromLocalStorage;\n }\n\n /** @internal - Used by PlaybackController to set restoration state */\n setRestoringFromLocalStorage(value: boolean): void {\n this.#restoringFromLocalStorage = value;\n }\n #customFrameTasks: Set<FrameTaskCallback> = new Set();\n #onFrameCallback: FrameTaskCallback | null = null;\n #onFrameCleanup: (() => void) | null = null;\n #playbackListener: ((event: PlaybackControllerUpdateEvent) => void) | null =\n null;\n\n /**\n * Centralized frame controller for coordinating element rendering.\n * Replaces the distributed Lit Task hierarchy with a single control point.\n */\n #frameController: FrameController = new FrameController(this);\n\n /**\n * Get the frame controller for centralized rendering coordination.\n * @public\n */\n get frameController(): FrameController {\n return this.#frameController;\n }\n\n /**\n * Centralized quality upgrade scheduler for coordinating main-quality segment fetching.\n * Lives alongside FrameController to manage background quality upgrades.\n */\n #qualityUpgradeScheduler: QualityUpgradeScheduler =\n new QualityUpgradeScheduler({\n requestFrameRender: () => this.requestFrameRender(),\n });\n\n /**\n * Get the quality upgrade scheduler for background segment fetching.\n * @public\n */\n get qualityUpgradeScheduler(): QualityUpgradeScheduler {\n return this.#qualityUpgradeScheduler;\n }\n\n // ============================================================================\n // FrameRenderable Interface Implementation\n // ============================================================================\n // Allows FrameController to discover and coordinate nested timegroups.\n // This ensures frame callbacks registered on nested timegroups are executed.\n // ============================================================================\n\n /**\n * Query timegroup's readiness state for a given time.\n * Timegroups are always ready (no async preparation needed).\n * @public\n */\n getFrameState(_timeMs: number): FrameState {\n return {\n needsPreparation: false,\n isReady: true,\n priority: PRIORITY_DEFAULT,\n };\n }\n\n /**\n * Async preparation phase (no-op for timegroups).\n * Timegroups don't need preparation - they just coordinate child rendering.\n * @public\n */\n async prepareFrame(_timeMs: number, _signal: AbortSignal): Promise<void> {\n // No preparation needed for timegroups\n }\n\n /**\n * Synchronous render phase - executes custom frame callbacks.\n * Called by FrameController after all preparation is complete.\n * Kicks off async frame callbacks without blocking (they run in background).\n * @public\n */\n renderFrame(_timeMs: number): void {\n // Execute custom frame tasks registered via addFrameTask()\n // Fire and forget - callbacks can be async but we don't block here\n // The frameTask.taskComplete promise tracks completion if needed\n if (this.#customFrameTasks.size > 0) {\n this.#executeCustomFrameTasks().catch((error) => {\n console.error(\"EFTimegroup custom frame task error:\", error);\n });\n }\n }\n\n /**\n * Get the effective FPS for this timegroup.\n * During rendering, uses the render options FPS if available.\n * Otherwise uses the configured fps property.\n * @public\n */\n get effectiveFps(): number {\n // During rendering, prefer the render options FPS\n if (typeof window !== \"undefined\" && window.EF_FRAMEGEN?.renderOptions) {\n return window.EF_FRAMEGEN.renderOptions.encoderOptions.video.framerate;\n }\n return this.fps;\n }\n\n /**\n * Get the current content epoch (used by thumbnail cache).\n * The epoch increments whenever visual content changes.\n * @public\n */\n get contentEpoch(): number {\n return this.#contentEpoch;\n }\n\n /**\n * Increment content epoch (called when visual content changes).\n * This invalidates cached thumbnails by changing their cache keys.\n * @public\n */\n incrementContentEpoch(): void {\n this.#contentEpoch++;\n }\n\n /**\n * Request a frame re-render at the current time.\n *\n * Use this when the source-to-timeline mapping has changed (e.g., sourcein/sourceout)\n * but currentTimeMs hasn't. The FrameController only re-renders when currentTimeMs\n * or durationMs change, so this provides a way for child elements to request a\n * re-render when their internal state changes the visual output.\n * @public\n */\n requestFrameRender(): void {\n this.#frameController.abort();\n this.#runThrottledFrameTask();\n }\n\n async #runThrottledFrameTask(): Promise<void> {\n if (this.playbackController) {\n return this.playbackController.runThrottledFrameTask();\n }\n // Use FrameController directly (no frameTask fallback)\n try {\n await this.#frameController.renderFrame(this.currentTimeMs, {\n onAnimationsUpdate: (root) => {\n updateAnimations(root as typeof this);\n },\n });\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n return;\n }\n console.error(\"FrameController error:\", error);\n }\n }\n\n // ============================================================================\n // Purpose 3: Seeking Implementation\n // ============================================================================\n\n /** @public */\n @property({ type: Number, attribute: \"currenttime\" })\n set currentTime(time: number) {\n // Evaluate seek target (quantization and clamping)\n const seekTarget = evaluateSeekTarget(\n time,\n this.durationMs,\n this.effectiveFps,\n );\n\n // Delegate to playbackController if available\n if (this.playbackController) {\n this.playbackController.currentTime = seekTarget;\n this.#userTimeMs = seekTarget * 1000; // User-initiated time change\n return;\n }\n\n // Only root timegroups can have their currentTime set directly\n if (!this.isRootTimegroup) {\n return;\n }\n\n // Validate seek target\n if (Number.isNaN(seekTarget)) {\n return;\n }\n\n // Skip if already at target time (unless processing pending seek or restoring from localStorage)\n if (\n seekTarget === this.#currentTime &&\n !this.#processingPendingSeek &&\n !this.#restoringFromLocalStorage\n ) {\n return;\n }\n\n // Skip if this is the same as pending seek\n if (this.#pendingSeekTime === seekTarget) {\n return;\n }\n\n // Prevent recursive seeks during localStorage restoration\n if (this.#restoringFromLocalStorage && seekTarget !== this.#currentTime) {\n // Allow the restoration seek to proceed, but prevent subsequent seeks\n // The flag will be cleared after the seek completes\n }\n\n // Handle concurrent seeks by queuing pending seek\n // This ensures we only have ONE seek in flight at a time, avoiding wasted work.\n // When scrubbing quickly, intermediate positions are skipped entirely - we don't\n // start work we know will be thrown away.\n if (this.#seekInProgress) {\n this.#pendingSeekTime = seekTarget;\n this.#currentTime = seekTarget;\n this.#userTimeMs = seekTarget * 1000; // User-initiated time change\n return;\n }\n\n // Execute seek - update both source time and user time\n this.#currentTime = seekTarget;\n this.#userTimeMs = seekTarget * 1000; // User-initiated time change\n this.#seekInProgress = true;\n\n // Attach .catch() to prevent unhandled rejection warning - errors are handled by seekTask.onError\n Promise.resolve(this.seekTask.run())\n .catch(() => {})\n .finally(async () => {\n this.#seekInProgress = false;\n\n // CRITICAL: Coordinate animations after seekTask completes\n // This handles seeks from currentTime setter (like localStorage restore)\n const { updateAnimations } = await import(\"./updateAnimations.js\");\n updateAnimations(this);\n\n // Process pending seek if it differs from completed seek\n // This jumps directly to wherever the user ended up, skipping intermediates\n if (\n this.#pendingSeekTime !== undefined &&\n this.#pendingSeekTime !== seekTarget\n ) {\n const pendingTime = this.#pendingSeekTime;\n this.#pendingSeekTime = undefined;\n this.#processingPendingSeek = true;\n try {\n this.currentTime = pendingTime;\n } finally {\n this.#processingPendingSeek = false;\n }\n } else {\n this.#pendingSeekTime = undefined;\n }\n });\n }\n\n /** @public */\n get currentTime() {\n if (this.playbackController) {\n return this.playbackController.currentTime;\n }\n return this.#currentTime ?? 0;\n }\n\n /** @public */\n set currentTimeMs(ms: number) {\n this.currentTime = ms / 1000;\n }\n\n /** @public */\n get currentTimeMs() {\n return this.currentTime * 1000;\n }\n\n /**\n * The time the user last requested via seek/scrub.\n * Preview systems should use this instead of currentTimeMs to avoid\n * seeing intermediate times during batch operations (thumbnails, export).\n * @public\n */\n get userTimeMs(): number {\n return this.#userTimeMs;\n }\n\n /**\n * Seek to a specific time and wait for all frames to be ready.\n * This is the recommended way to seek in tests and programmatic control.\n *\n * Combines seeking (Purpose 3) with frame rendering (Purpose 4) to ensure\n * all visible elements are ready after the seek completes.\n *\n * Updates both the source time AND userTimeMs (what the preview displays).\n *\n * @param timeMs - Time in milliseconds to seek to\n * @returns Promise that resolves when the seek is complete and all visible children are ready\n * @public\n */\n async seek(timeMs: number): Promise<void> {\n // Update user time - this is what the preview should display\n this.#userTimeMs = timeMs;\n\n // Execute seek (Purpose 3)\n this.currentTimeMs = timeMs;\n await this.seekTask.taskComplete;\n\n // Handle localStorage when playbackController delegates seek\n if (this.playbackController) {\n this.saveTimeToLocalStorage(this.currentTime);\n }\n\n // Wait for frame rendering via FrameController\n await this.#frameController.renderFrame(timeMs, {\n onAnimationsUpdate: (root) => {\n updateAnimations(root as typeof this);\n },\n });\n }\n\n /**\n * Optimized seek for render loops.\n * Unlike `seek()`, this:\n * - Skips waitForMediaDurations (already loaded at render setup)\n * - Skips localStorage persistence\n * - Uses FrameController for centralized element coordination\n *\n * Still waits for all content to be ready (Lit updates, element preparation, rendering).\n *\n * @param timeMs - Time in milliseconds to seek to\n * @internal\n */\n async seekForRender(timeMs: number): Promise<SeekForRenderTiming> {\n const t0 = performance.now();\n // Set time directly (skip seekTask overhead)\n const newTime = timeMs / 1000;\n this.#userTimeMs = timeMs;\n this.#currentTime = newTime;\n // Sync playbackController if present so currentTime getter returns\n // the correct value regardless of which code path reads it.\n if (this.playbackController) {\n this.playbackController.currentTime = newTime;\n }\n // Sync the base mixin's local time so ownCurrentTimeMs returns the\n // correct value in render clones (which have no playback controller).\n this._setLocalTimeMs(timeMs);\n this.requestUpdate(\"currentTime\");\n\n // First await: let Lit propagate time to children\n await this.updateComplete;\n const t1 = performance.now();\n\n // Collect all LitElement descendants (not just those with frameTask)\n // This ensures ef-text, ef-captions, and other reactive elements update\n const allLitElements = this.#getAllLitElementDescendants();\n\n // Wait for ALL LitElement descendants to complete their reactive updates\n // This is critical for elements like ef-text and ef-captions that don't have frameTask\n await Promise.all(allLitElements.map((el) => el.updateComplete));\n const t2 = performance.now();\n\n // OwnCurrentTimeController defers child updates via queueMicrotask.\n // Those microtasks have fired by this point (between await boundaries).\n // Await a second pass of updateComplete to catch those deferred updates.\n await Promise.all(allLitElements.map((el) => el.updateComplete));\n const t3 = performance.now();\n\n // Wait for ef-text elements to have their segments ready\n // ef-text creates segments asynchronously via requestAnimationFrame\n const textElements = allLitElements.filter(\n (el) => el.tagName === \"EF-TEXT\",\n );\n if (textElements.length > 0) {\n await Promise.all(\n textElements.map((el) => {\n if (\n \"whenSegmentsReady\" in el &&\n typeof el.whenSegmentsReady === \"function\"\n ) {\n return (el as any).whenSegmentsReady();\n }\n return Promise.resolve();\n }),\n );\n\n // Force synchronous layout reflow after text segments are created/updated.\n // offsetHeight triggers layout computation — no need to yield a full rAF\n // (which costs 16-40ms and is throttled in hidden tabs).\n void this.offsetHeight;\n }\n const t4 = performance.now();\n\n // Use FrameController for centralized element coordination\n // This replaces the old distributed frameTask system\n // Animation updates are handled via the onAnimationsUpdate callback\n const rfTiming = await this.#frameController.renderFrame(timeMs, {\n waitForLitUpdate: false,\n onAnimationsUpdate: (root) => {\n updateAnimations(root as typeof this);\n // CRITICAL: Force style recalculation after updateAnimations sets animation.currentTime\n // Without this, getComputedStyle may return stale values (e.g., opacity: 0 instead of 1)\n // Accessing offsetWidth triggers synchronous style recalc\n void (root as HTMLElement).offsetWidth;\n },\n });\n const t5 = performance.now();\n\n // Execute custom frame tasks registered via addFrameTask()\n await this.#executeCustomFrameTasks();\n const t6 = performance.now();\n\n return {\n updateComplete1Ms: t1 - t0,\n updateComplete2Ms: t2 - t1,\n updateComplete3Ms: t3 - t2,\n textSegmentsMs: t4 - t3,\n renderFrameMs: t5 - t4,\n renderFrameQueryMs: rfTiming.queryMs,\n renderFramePrepareMs: rfTiming.prepareMs,\n renderFrameDrawMs: rfTiming.renderMs,\n renderFrameAnimsMs: rfTiming.animsMs,\n frameTasksMs: t6 - t5,\n totalMs: t6 - t0,\n };\n }\n\n /**\n * Collects all LitElement descendants recursively.\n * Used by seekForRender to ensure all reactive elements have updated.\n * Prunes subtrees of temporally-invisible elements — their Lit updates\n * still fire via microtasks (OwnCurrentTimeController), so skipping\n * the explicit await is safe.\n */\n #getAllLitElementDescendants(): LitElement[] {\n const result: LitElement[] = [];\n const currentTimeMs = this.currentTimeMs;\n\n const walk = (el: Element) => {\n for (const child of el.children) {\n // Temporal pruning: skip invisible temporal elements and their subtrees\n if (\"startTimeMs\" in child && \"endTimeMs\" in child) {\n const startMs = (child as any).startTimeMs ?? -Infinity;\n const endMs = (child as any).endTimeMs ?? Infinity;\n if (\n endMs > startMs &&\n (currentTimeMs < startMs || currentTimeMs >= endMs)\n ) {\n continue; // skip entire subtree\n }\n }\n\n if (child instanceof LitElement) {\n result.push(child);\n }\n walk(child);\n }\n };\n walk(this);\n\n return result;\n }\n\n /**\n * Determines if this is a root timegroup (no parent timegroups)\n * @public\n */\n get isRootTimegroup(): boolean {\n return !this.parentTimegroup;\n }\n\n /**\n * Property-based frame task callback for React integration.\n * When set, automatically registers the callback as a frame task.\n * Setting a new value automatically cleans up the previous callback.\n * Set to null or undefined to remove the callback.\n *\n * @example\n * // React usage:\n * <Timegroup onFrame={({ ownCurrentTimeMs, percentComplete }) => {\n * // Per-frame updates\n * }} />\n *\n * @public\n */\n get onFrame(): FrameTaskCallback | null {\n return this.#onFrameCallback;\n }\n\n set onFrame(callback: FrameTaskCallback | null | undefined) {\n // Clean up previous callback if exists\n if (this.#onFrameCleanup) {\n this.#onFrameCleanup();\n this.#onFrameCleanup = null;\n }\n this.#onFrameCallback = callback ?? null;\n\n // Register new callback if provided\n if (callback) {\n this.#onFrameCleanup = this.addFrameTask(callback);\n }\n }\n\n /**\n * Register a custom frame task callback that will be executed during frame rendering.\n * The callback receives timing information and can be async or sync.\n * Multiple callbacks can be registered and will execute in parallel.\n *\n * @param callback - Function to execute on each frame\n * @returns A cleanup function that removes the callback when called\n * @public\n */\n addFrameTask(callback: FrameTaskCallback): () => void {\n if (typeof callback !== \"function\") {\n throw new Error(\"Frame task callback must be a function\");\n }\n this.#customFrameTasks.add(callback);\n return () => {\n this.#customFrameTasks.delete(callback);\n };\n }\n\n /**\n * Remove a previously registered custom frame task callback.\n *\n * @param callback - The callback function to remove\n * @public\n */\n removeFrameTask(callback: FrameTaskCallback): void {\n this.#customFrameTasks.delete(callback);\n }\n\n /** @internal */\n saveTimeToLocalStorage(time: number) {\n try {\n if (this.id && this.isConnected && !Number.isNaN(time)) {\n localStorage.setItem(this.storageKey, time.toString());\n }\n } catch (error) {\n log(\"Failed to save time to localStorage\", error);\n }\n }\n\n render() {\n return html`<slot @slotchange=${this.#handleSlotChange}></slot> `;\n }\n\n #handleSlotChange = () => {\n resetTemporalCache();\n flushSequenceDurationCache();\n flushStartTimeMsCache();\n\n this.requestUpdate();\n this.#syncChildListeners();\n this.emitContentChange(\"structure\");\n };\n\n /** @internal */\n loadTimeFromLocalStorage(): number | undefined {\n if (this.id) {\n try {\n const storedValue = localStorage.getItem(this.storageKey);\n if (storedValue === null) {\n return undefined;\n }\n const parsedValue = Number.parseFloat(storedValue);\n // Guard against NaN and Infinity which could cause issues\n if (Number.isNaN(parsedValue) || !Number.isFinite(parsedValue)) {\n return undefined;\n }\n return parsedValue;\n } catch (error) {\n log(\"Failed to load time from localStorage\", error);\n }\n }\n return undefined;\n }\n\n connectedCallback() {\n // CRITICAL: super.connectedCallback() MUST be synchronous for Lit lifecycle to work correctly.\n // Deferring it breaks render clones because updateComplete resolves before Lit initializes.\n //\n // EFTemporal.connectedCallback() handles root detection after Lit Context propagates:\n // - Schedules updateComplete.then(didBecomeRoot check)\n // - Only true roots (no parent after context) create PlaybackController\n //\n // PlaybackController.hostConnected() owns ALL root initialization:\n // - waitForMediaDurations\n // - localStorage time restoration\n // - initial seek\n //\n // This avoids the previous race conditions where both EFTimegroup.connectedCallback\n // and PlaybackController.hostConnected tried to initialize, causing concurrent seeks.\n super.connectedCallback();\n\n // Skip re-initialization when being moved for canvas preview capture.\n // EFTemporal.connectedCallback (super) already guards its own logic;\n // we guard the EFTimegroup-specific parts here (initializer, child\n // listeners, TimegroupController, wrapWithWorkbench).\n if ((this as any).canvasPreviewActive) return;\n\n // Run initializer after element is fully connected and Lit has updated\n // This ensures the element is in a stable state before user code runs\n this.updateComplete.then(() => {\n this.#runInitializer();\n // slotchange may not fire for empty timegroups, so run initial aggregation\n this.#syncChildListeners();\n });\n\n // Defer TimegroupController creation and workbench wrapping to next frame\n // These operations involve DOM queries (closest, getBoundingClientRect) which\n // can be expensive when many elements initialize simultaneously\n requestAnimationFrame(() => {\n requestAnimationFrame(() => {\n if (this.parentTimegroup) {\n new TimegroupController(this.parentTimegroup, this);\n }\n\n if (this.shouldWrapWithWorkbench()) {\n this.wrapWithWorkbench();\n }\n });\n });\n }\n\n /**\n * Called when this timegroup becomes a root (no parent timegroup).\n * Sets up the playback listener after PlaybackController is created.\n * @internal\n */\n didBecomeRoot() {\n super.didBecomeRoot();\n this.#setupPlaybackListener();\n }\n\n /**\n * Setup listener on playbackController to sync userTimeMs during playback.\n */\n #setupPlaybackListener(): void {\n // Already setup or no controller\n if (this.#playbackListener || !this.playbackController) return;\n\n this.#playbackListener = (event: PlaybackControllerUpdateEvent) => {\n // Update userTimeMs during playback time changes\n // Clone-timeline: captures use separate clones, so Prime-timeline updates freely\n // Canvas preview reads userTimeMs to know what to render\n if (\n event.property === \"currentTimeMs\" &&\n typeof event.value === \"number\"\n ) {\n this.#userTimeMs = event.value;\n }\n };\n\n this.playbackController.addListener(this.#playbackListener);\n }\n\n /**\n * Remove playback listener on disconnect.\n */\n #removePlaybackListener(): void {\n if (this.#playbackListener && this.playbackController) {\n this.playbackController.removeListener(this.#playbackListener);\n }\n this.#playbackListener = null;\n }\n\n #previousDurationMs = 0;\n\n protected updated(changedProperties: PropertyValues): void {\n super.updated(changedProperties);\n\n if (changedProperties.has(\"mode\") || changedProperties.has(\"overlapMs\")) {\n durationCache.delete(this);\n }\n\n if (this.#previousDurationMs !== this.durationMs) {\n this.#previousDurationMs = this.durationMs;\n // Render clones are sequenced via seekForRender — don't trigger autonomous re-renders.\n // This prevents FrameController.abort() from interrupting an in-progress seekForRender.\n if (!this.hasAttribute(\"data-no-playback-controller\")) {\n this.#runThrottledFrameTask();\n }\n }\n }\n\n disconnectedCallback() {\n super.disconnectedCallback();\n\n // Skip teardown when being moved for canvas preview capture.\n // EFTemporal.disconnectedCallback (super) already guards its own logic.\n if ((this as any).canvasPreviewActive) return;\n\n this.#resizeObserver?.disconnect();\n this.#removePlaybackListener();\n for (const child of this.#trackedChildren) {\n child.removeEventListener(\n \"readystatechange\",\n this.#childReadyStateHandler,\n );\n child.removeEventListener(\n \"contentchange\",\n this.#childContentChangeHandler,\n );\n }\n this.#trackedChildren.clear();\n this.#qualityUpgradeScheduler.dispose();\n }\n\n /**\n * Render the timegroup to an MP4 video file and trigger download.\n * Captures each frame at the specified fps, encodes using WebCodecs via\n * MediaBunny, and downloads the resulting video.\n *\n * Uses dynamic import to only load render utilities in browser context.\n *\n * @param options - Rendering options (fps, codec, bitrate, filename, etc.)\n * @returns Promise that resolves when video is downloaded\n * @public\n */\n async renderToVideo(\n options?: RenderToVideoOptions,\n ): Promise<Uint8Array | undefined> {\n // Dynamic import - only loads in browser context when actually called\n const { renderTimegroupToVideo } =\n await import(\"../preview/renderTimegroupToVideo.js\");\n return renderTimegroupToVideo(this, options);\n }\n\n /**\n * Runs the initializer function with validation for synchronous execution and time budget.\n * Only runs once per instance. Safe to call multiple times - will skip if already run.\n * @throws Error if initializer returns a Promise (async not allowed)\n * @throws Error if initializer takes more than INITIALIZER_ERROR_THRESHOLD_MS\n * @internal\n */\n #runInitializer(): void {\n // Skip if no initializer or already run\n if (!this.initializer || this.#initializerHasRun) {\n return;\n }\n\n // Mark as run before executing to prevent recursion\n this.#initializerHasRun = true;\n\n const startTime = performance.now();\n const result: unknown = this.initializer(this);\n const elapsed = performance.now() - startTime;\n\n // Check for async (Promise return) - initializers MUST be synchronous\n if (\n result !== undefined &&\n result !== null &&\n typeof (result as any).then === \"function\"\n ) {\n throw new Error(\n \"Timeline initializer must be synchronous. \" +\n \"Do not return a Promise from the initializer function.\",\n );\n }\n\n // Time budget enforcement - initializers run for EVERY instance\n if (elapsed > INITIALIZER_ERROR_THRESHOLD_MS) {\n throw new Error(\n `Timeline initializer took ${elapsed.toFixed(1)}ms, exceeding the ${INITIALIZER_ERROR_THRESHOLD_MS}ms limit. ` +\n \"Initializers must be fast - move expensive work outside the initializer.\",\n );\n }\n\n if (elapsed > INITIALIZER_WARN_THRESHOLD_MS) {\n console.warn(\n `[ef-timegroup] Initializer took ${elapsed.toFixed(1)}ms, exceeding ${INITIALIZER_WARN_THRESHOLD_MS}ms. ` +\n \"Consider optimizing for better render performance.\",\n );\n }\n }\n\n /**\n * Copy captionsData property from original to clone.\n * cloneNode() only copies attributes, not JavaScript properties.\n * captionsData is often set via JS (e.g., captionsEl.captionsData = {...}),\n * so we must manually copy it to the cloned elements.\n * @internal\n */\n #copyCaptionsData(original: Element, clone: Element): void {\n // Find matching caption elements by position (querySelectorAll returns in document order)\n const originalCaptions = original.querySelectorAll(\"ef-captions\");\n const cloneCaptions = clone.querySelectorAll(\"ef-captions\");\n\n for (\n let i = 0;\n i < originalCaptions.length && i < cloneCaptions.length;\n i++\n ) {\n const origCap = originalCaptions[i] as any;\n const cloneCap = cloneCaptions[i] as any;\n\n // Copy loaded captions data from any source (JS property, captions-src, script element).\n // The loaded data is stored in unifiedCaptionsDataTask.value after async loading.\n // Setting captionsData on the clone gives it Priority 1, bypassing async loading.\n const loadedData =\n origCap.captionsData ?? origCap.unifiedCaptionsDataTask?.value;\n if (loadedData) {\n cloneCap.captionsData = loadedData;\n }\n }\n }\n\n /**\n * Copy ef-text _textContent property from original to cloned elements.\n * This MUST be called BEFORE elements upgrade (before updateComplete)\n * because splitText() runs in connectedCallback and will clear segments\n * if _textContent is null/empty.\n * @internal\n */\n #copyTextContent(original: Element, clone: Element): void {\n const originalTexts = original.querySelectorAll(\"ef-text\");\n const cloneTexts = clone.querySelectorAll(\"ef-text\");\n\n for (let i = 0; i < originalTexts.length && i < cloneTexts.length; i++) {\n const origText = originalTexts[i] as any;\n const cloneText = cloneTexts[i] as any;\n\n // Copy _textContent if it exists\n // This is a private property, so we access it via any\n if (origText._textContent !== undefined) {\n cloneText._textContent = origText._textContent;\n }\n // Also copy the segments getter to ensure we can read them\n if (origText._templateElement !== undefined) {\n cloneText._templateElement = origText._templateElement;\n }\n }\n }\n\n /**\n * Copy ef-text-segment properties from original to cloned elements.\n * segmentText and other properties are set via JS, not attributes,\n * so we must manually copy them to the cloned elements.\n * @internal\n */\n async #copyTextSegmentData(original: Element, clone: Element): Promise<void> {\n // Find matching text segment elements by position\n const originalSegments = original.querySelectorAll(\"ef-text-segment\");\n const cloneSegments = clone.querySelectorAll(\"ef-text-segment\");\n\n const updatePromises: Promise<any>[] = [];\n\n for (\n let i = 0;\n i < originalSegments.length && i < cloneSegments.length;\n i++\n ) {\n const origSeg = originalSegments[i] as any;\n const cloneSeg = cloneSegments[i] as any;\n\n // Copy all segment properties\n if (origSeg.segmentText !== undefined) {\n cloneSeg.segmentText = origSeg.segmentText;\n }\n if (origSeg.segmentIndex !== undefined) {\n cloneSeg.segmentIndex = origSeg.segmentIndex;\n }\n if (origSeg.staggerOffsetMs !== undefined) {\n cloneSeg.staggerOffsetMs = origSeg.staggerOffsetMs;\n }\n if (origSeg.segmentStartMs !== undefined) {\n cloneSeg.segmentStartMs = origSeg.segmentStartMs;\n }\n if (origSeg.segmentEndMs !== undefined) {\n cloneSeg.segmentEndMs = origSeg.segmentEndMs;\n }\n\n // Wait for Lit to render the updated segmentText to shadow DOM\n if (cloneSeg.updateComplete) {\n updatePromises.push(cloneSeg.updateComplete);\n }\n }\n\n // Wait for all segment updates to complete\n await Promise.all(updatePromises);\n }\n\n /**\n * Wait for all ef-captions elements to have their data loaded.\n * This is needed because EFCaptions is not an EFMedia, so waitForMediaDurations doesn't cover it.\n * Used by createRenderClone to ensure captions are ready before rendering.\n * @internal\n */\n async #waitForCaptionsData(root: Element): Promise<void> {\n // Find all ef-captions elements (including nested in timegroups)\n const captionsElements = root.querySelectorAll(\"ef-captions\");\n if (captionsElements.length === 0) return;\n\n // Wait for each caption element's data to load\n // Use duck-typing to check for loadCaptionsData method\n const waitPromises: Promise<unknown>[] = [];\n for (const el of captionsElements) {\n const captions = el as any;\n // Try new async method first\n if (typeof captions.loadCaptionsData === \"function\") {\n waitPromises.push(captions.loadCaptionsData().catch(() => {}));\n }\n // Fallback to task if present\n else if (captions.unifiedCaptionsDataTask?.taskComplete) {\n waitPromises.push(\n captions.unifiedCaptionsDataTask.taskComplete.catch(() => {}),\n );\n }\n }\n\n if (waitPromises.length > 0) {\n await Promise.all(waitPromises);\n }\n }\n\n /**\n * Copies initializers from original timegroup tree to cloned timegroup tree.\n * Handles both the root timegroup and all nested timegroups recursively.\n * @internal\n */\n async #copyInitializersToClone(\n original: EFTimegroup,\n clone: EFTimegroup,\n ): Promise<void> {\n // Copy and execute initializer at this level\n if (original.initializer) {\n clone.initializer = original.initializer;\n // Explicitly run the initializer on the clone\n // Wait for Lit update cycle to complete first so the element is stable\n await clone.updateComplete;\n clone.#runInitializer();\n }\n\n // Find all nested timegroups in both original and clone\n const originalNested = Array.from(\n original.querySelectorAll(\"ef-timegroup\"),\n ) as EFTimegroup[];\n const cloneNested = Array.from(\n clone.querySelectorAll(\"ef-timegroup\"),\n ) as EFTimegroup[];\n\n // Match up nested timegroups by index (they should correspond 1:1)\n for (let i = 0; i < originalNested.length && i < cloneNested.length; i++) {\n const origNested = originalNested[i];\n const cloneNestedItem = cloneNested[i];\n\n if (origNested!.initializer) {\n cloneNestedItem!.initializer = origNested!.initializer;\n await cloneNestedItem!.updateComplete;\n cloneNestedItem!.#runInitializer();\n }\n }\n }\n\n /**\n * Create an independent clone of this timegroup for rendering.\n * The clone is a fully functional ef-timegroup with its own animations\n * and time state, isolated from the original (Prime-timeline).\n *\n * OPTIONAL: An initializer can be set via `timegroup.initializer = (tg) => { ... }`\n * to re-run JavaScript setup (frame callbacks, React components) on each clone.\n *\n * This enables:\n * - Rendering without affecting user's preview position\n * - Concurrent renders with different clones\n * - Re-running JavaScript setup on each clone (if initializer is provided)\n *\n * @returns Promise resolving to clone, container, and cleanup function\n * @throws Error if initializer is async or takes too long\n * @public\n */\n async createRenderClone(): Promise<RenderCloneResult> {\n const factory = getCloneFactory(this);\n\n if (factory) {\n return this.#createRenderCloneFromFactory(factory);\n }\n return this.#createRenderCloneFromDOM();\n }\n\n /**\n * Wait for all LitElement descendants to update and for text segments to be ready.\n * This ensures the clone is fully initialized before rendering.\n * @internal\n */\n async #waitForDescendants(actualClone: EFTimegroup): Promise<void> {\n // Wait for all LitElement descendants\n const allLitElements = Array.from(actualClone.querySelectorAll(\"*\")).filter(\n (el) => el instanceof LitElement,\n ) as LitElement[];\n await Promise.all(allLitElements.map((el) => el.updateComplete));\n\n // Wait for text segments\n const textElements = allLitElements.filter(\n (el) => el.tagName === \"EF-TEXT\",\n );\n if (textElements.length > 0) {\n await Promise.all(\n textElements.map((el) => {\n if (\n \"whenSegmentsReady\" in el &&\n typeof el.whenSegmentsReady === \"function\"\n ) {\n return (el as any).whenSegmentsReady();\n }\n return Promise.resolve();\n }),\n );\n void actualClone.offsetHeight;\n await new Promise((resolve) => requestAnimationFrame(resolve));\n }\n }\n\n /**\n * Factory path: mount a fresh component tree (React, etc.) to produce\n * a fully functional clone. The factory is responsible for rendering\n * the component into the container and returning the root ef-timegroup.\n */\n async #createRenderCloneFromFactory(\n factory: NonNullable<ReturnType<typeof getCloneFactory>>,\n ): Promise<RenderCloneResult> {\n const width = this.offsetWidth || 1920;\n const height = this.offsetHeight || 1080;\n\n const container = document.createElement(\"div\");\n container.className = \"ef-render-clone-container\";\n container.style.cssText = `\n position: fixed;\n left: -9999px;\n top: 0;\n width: ${width}px;\n height: ${height}px;\n pointer-events: none;\n overflow: hidden;\n `;\n\n // Preserve ef-configuration context\n let renderTarget: HTMLElement = container;\n const originalConfig = this.closest(\"ef-configuration\");\n if (originalConfig) {\n const configClone = originalConfig.cloneNode(false) as HTMLElement;\n container.appendChild(configClone);\n renderTarget = configClone;\n }\n\n document.body.appendChild(container);\n\n // Mount the component tree — this produces a live ef-timegroup\n const { timegroup: actualClone, cleanup: factoryCleanup } =\n factory(renderTarget);\n\n if (!actualClone) {\n throw new Error(\n \"Clone factory did not produce an ef-timegroup. \" +\n \"Ensure the factory renders a component containing a Timegroup.\",\n );\n }\n\n // Mark as render clone\n actualClone.setAttribute(\"data-no-workbench\", \"true\");\n actualClone.setAttribute(\"data-no-playback-controller\", \"true\");\n actualClone.style.width = `${width}px`;\n actualClone.style.height = `${height}px`;\n actualClone.style.display = \"block\";\n\n // Wait for custom elements to upgrade and Lit to update\n await customElements.whenDefined(\"ef-timegroup\");\n customElements.upgrade(container);\n await actualClone.updateComplete;\n\n // Wait for all descendants to be ready\n await this.#waitForDescendants(actualClone);\n\n // Finalize clone: parent-child relationships, lock root, remove PlaybackController\n await this.#finalizeRenderClone(actualClone);\n\n return {\n clone: actualClone,\n container,\n cleanup: () => {\n container.remove();\n factoryCleanup();\n },\n };\n }\n\n /**\n * DOM path: deep clone the DOM tree and copy JavaScript properties.\n * Used for vanilla HTML/JS timelines that don't have a factory registered.\n */\n async #createRenderCloneFromDOM(): Promise<RenderCloneResult> {\n // 1. Create offscreen container\n const container = document.createElement(\"div\");\n container.className = \"ef-render-clone-container\";\n container.style.cssText = `\n position: fixed;\n left: -9999px;\n top: 0;\n width: ${this.offsetWidth || 1920}px;\n height: ${this.offsetHeight || 1080}px;\n pointer-events: none;\n overflow: hidden;\n `;\n\n // 2. Deep clone the DOM\n const cloneEl = this.cloneNode(true) as EFTimegroup;\n // Strip all id attributes from clone tree to prevent duplicate IDs in the document\n cloneEl.removeAttribute(\"id\");\n for (const el of cloneEl.querySelectorAll(\"[id]\")) {\n el.removeAttribute(\"id\");\n }\n cloneEl.setAttribute(\"data-no-workbench\", \"true\");\n cloneEl.setAttribute(\"data-no-playback-controller\", \"true\");\n\n const width = this.offsetWidth || 1920;\n const height = this.offsetHeight || 1080;\n cloneEl.style.width = `${width}px`;\n cloneEl.style.height = `${height}px`;\n cloneEl.style.display = \"block\";\n\n // 2b. Copy JavaScript properties that aren't cloned by cloneNode()\n this.#copyCaptionsData(this, cloneEl);\n this.#copyTextContent(this, cloneEl);\n\n // 3. Preserve ef-configuration context\n const originalConfig = this.closest(\"ef-configuration\");\n if (originalConfig) {\n const configClone = originalConfig.cloneNode(false) as HTMLElement;\n configClone.appendChild(cloneEl);\n container.appendChild(configClone);\n } else {\n container.appendChild(cloneEl);\n }\n\n document.body.appendChild(container);\n\n // Wait for custom elements to upgrade\n await cloneEl.updateComplete;\n\n // Copy initializers and run them on clones\n await this.#copyInitializersToClone(this, cloneEl);\n\n // Copy text segment data\n await this.#copyTextSegmentData(this, cloneEl);\n\n // Find the actual timegroup (initializer may have replaced the DOM)\n let actualClone = container.querySelector(\"ef-timegroup\") as EFTimegroup;\n if (!actualClone) {\n throw new Error(\n \"No ef-timegroup found after initializer. \" +\n \"Ensure your initializer renders a Timegroup (React) or does not remove the cloned element (vanilla JS).\",\n );\n }\n\n // Wait for custom elements to upgrade\n await customElements.whenDefined(\"ef-timegroup\");\n customElements.upgrade(container);\n actualClone = container.querySelector(\"ef-timegroup\") as EFTimegroup;\n if (!actualClone) {\n throw new Error(\"ef-timegroup element lost after upgrade\");\n }\n\n // Wait for LitElement updates\n await actualClone.updateComplete;\n\n // Wait for all descendants to be ready\n await this.#waitForDescendants(actualClone);\n\n // Copy text segment data again after initializer may have replaced DOM\n await this.#copyTextSegmentData(this, actualClone);\n\n // Finalize clone\n await this.#finalizeRenderClone(actualClone);\n\n return {\n clone: actualClone,\n container,\n cleanup: () => {\n container.remove();\n const reactRoot = (actualClone as any)._reactRoot;\n if (reactRoot) {\n queueMicrotask(() => {\n reactRoot.unmount();\n });\n }\n },\n };\n }\n\n /**\n * Shared finalization for both factory and DOM clone paths:\n * - Set up parent-child temporal relationships\n * - Lock root timegroup references\n * - Wait for media durations and captions\n * - Remove PlaybackController\n * - Initial seek to frame 0\n */\n async #finalizeRenderClone(actualClone: EFTimegroup): Promise<void> {\n // Set up parent-child relationships for temporal elements\n const setupParentChildRelationships = (\n parent: EFTimegroup,\n root: EFTimegroup,\n ) => {\n for (const child of parent.children) {\n if (child.tagName === \"EF-TIMEGROUP\") {\n const childTG = child as EFTimegroup;\n childTG.parentTimegroup = parent;\n childTG.rootTimegroup = root;\n (childTG as any).lockRootTimegroup();\n setupParentChildRelationships(childTG, root);\n } else if (\"parentTimegroup\" in child && \"rootTimegroup\" in child) {\n const temporal = child as TemporalMixinInterface & HTMLElement;\n temporal.parentTimegroup = parent;\n temporal.rootTimegroup = root;\n if (\n \"lockRootTimegroup\" in temporal &&\n typeof temporal.lockRootTimegroup === \"function\"\n ) {\n temporal.lockRootTimegroup();\n }\n } else if (child instanceof Element) {\n setupInContainer(child, parent, root);\n }\n }\n };\n\n const setupInContainer = (\n container: Element,\n nearestParentTG: EFTimegroup,\n root: EFTimegroup,\n ) => {\n for (const child of container.children) {\n if (child.tagName === \"EF-TIMEGROUP\") {\n const childTG = child as EFTimegroup;\n childTG.parentTimegroup = nearestParentTG;\n childTG.rootTimegroup = root;\n (childTG as any).lockRootTimegroup();\n setupParentChildRelationships(childTG, root);\n } else if (\"parentTimegroup\" in child && \"rootTimegroup\" in child) {\n const temporal = child as TemporalMixinInterface & HTMLElement;\n temporal.parentTimegroup = nearestParentTG;\n temporal.rootTimegroup = root;\n if (\n \"lockRootTimegroup\" in temporal &&\n typeof temporal.lockRootTimegroup === \"function\"\n ) {\n temporal.lockRootTimegroup();\n }\n } else if (child instanceof Element) {\n setupInContainer(child, nearestParentTG, root);\n }\n }\n };\n\n actualClone.rootTimegroup = actualClone;\n setupParentChildRelationships(actualClone, actualClone);\n\n await actualClone.updateComplete;\n\n // Lock root references to prevent Lit Context from overwriting\n actualClone.rootTimegroup = actualClone;\n (actualClone as any).lockRootTimegroup();\n const finalizeRootTimegroup = (el: Element) => {\n if (\"rootTimegroup\" in el && \"lockRootTimegroup\" in el) {\n (el as any).rootTimegroup = actualClone;\n (el as any).lockRootTimegroup();\n }\n for (const child of el.children) {\n finalizeRootTimegroup(child);\n }\n };\n finalizeRootTimegroup(actualClone);\n\n await actualClone.waitForMediaDurations();\n await this.#waitForCaptionsData(actualClone);\n\n // Remove PlaybackController — render clones use seekForRender directly\n if (actualClone.playbackController) {\n actualClone.playbackController.remove();\n actualClone.playbackController = undefined;\n }\n\n // Initial seek to frame 0\n await actualClone.seek(0);\n }\n\n /** @internal */\n get storageKey() {\n if (!this.id) {\n throw new Error(\"Timegroup must have an id to use localStorage.\");\n }\n return `ef-timegroup-${this.id}`;\n }\n\n /** @internal */\n get intrinsicDurationMs() {\n if (this.hasExplicitDuration) {\n return this.explicitDurationMs;\n }\n return undefined;\n }\n\n /** @internal */\n get hasOwnDuration() {\n return hasOwnDurationForMode(this.mode, this.hasExplicitDuration);\n }\n\n // ============================================================================\n // Purpose 1: Composition Rules Implementation\n // ============================================================================\n\n /** @public */\n get durationMs(): number {\n // Fixed mode delegates to parent class durationMs which handles trimming, source in/out, etc.\n if (this.mode === \"fixed\") {\n return super.durationMs;\n }\n\n // Evaluate duration semantics based on mode (Purpose 1)\n // childTemporals returns TemporalMixinInterface[], but we need HTMLElement intersection\n const childTemporalsAsElements = this.childTemporals as Array<\n TemporalMixinInterface & HTMLElement\n >;\n return evaluateDurationForMode(this, this.mode, childTemporalsAsElements);\n }\n\n // ============================================================================\n // Purpose 4: Frame Rendering - What Happens Each Frame\n // ============================================================================\n\n #mediaDurationsPromise: Promise<void> | undefined = undefined;\n\n /** @internal */\n async waitForMediaDurations(signal?: AbortSignal) {\n // Check abort before starting\n signal?.throwIfAborted();\n\n // Start loading media durations in background, but don't block if already in progress\n // This prevents multiple concurrent calls from creating redundant work\n if (!this.#mediaDurationsPromise) {\n this.#mediaDurationsPromise = this.#waitForMediaDurations(signal).catch(\n (err) => {\n // Re-throw AbortError to propagate cancellation\n if (err instanceof DOMException && err.name === \"AbortError\") {\n this.#mediaDurationsPromise = undefined;\n throw err;\n }\n console.error(\n `[EFTimegroup] waitForMediaDurations failed for ${this.id || \"unnamed\"}:`,\n err,\n );\n // Clear promise on error so it can be retried\n this.#mediaDurationsPromise = undefined;\n throw err;\n },\n );\n }\n\n // If signal is provided and aborted, throw immediately\n if (signal?.aborted) {\n throw new DOMException(\"Aborted\", \"AbortError\");\n }\n\n return this.#mediaDurationsPromise;\n }\n\n /**\n * Wait for all media elements to load their initial segments.\n * Ideally we would only need the extracted index json data, but\n * that caused issues with constructing audio data. We had negative durations\n * in calculations and it was not clear why.\n */\n async #waitForMediaDurations(signal?: AbortSignal) {\n return withSpan(\n \"timegroup.waitForMediaDurations\",\n {\n timegroupId: this.id || \"unknown\",\n mode: this.mode,\n },\n undefined,\n async (span) => {\n // Check abort before starting\n signal?.throwIfAborted();\n\n // Don't wait for updateComplete during initialization - it causes deadlocks with nested timegroups\n // Instead, use a short delay to let elements connect, then scan for media elements\n // If elements aren't ready yet, we'll retry or they'll be picked up on the next update cycle\n await new Promise<void>((resolve, reject) => {\n if (signal?.aborted) {\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n return;\n }\n\n const abortHandler = () => {\n clearTimeout(timeoutId);\n cancelAnimationFrame(rafId2);\n cancelAnimationFrame(rafId1);\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n };\n signal?.addEventListener(\"abort\", abortHandler, { once: true });\n\n let rafId1: number;\n let rafId2: number;\n let timeoutId: ReturnType<typeof setTimeout>;\n\n // Use multiple animation frames to ensure DOM is ready, but don't wait for all children\n rafId1 = requestAnimationFrame(() => {\n if (signal?.aborted) {\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n return;\n }\n rafId2 = requestAnimationFrame(() => {\n if (signal?.aborted) {\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n return;\n }\n // Small additional delay to let custom elements upgrade\n timeoutId = setTimeout(() => {\n signal?.removeEventListener(\"abort\", abortHandler);\n resolve();\n }, 10);\n });\n });\n });\n\n // Check abort after delay\n signal?.throwIfAborted();\n\n const mediaElements = deepGetMediaElements(this);\n if (isTracingEnabled()) {\n span.setAttribute(\"mediaElementsCount\", mediaElements.length);\n }\n\n // Check abort after getting elements\n signal?.throwIfAborted();\n\n // Then, we must await the fragmentIndexTask to ensure all media elements have their\n // fragment index loaded, which is where their duration is parsed from.\n // Use Promise.allSettled with timeout to avoid blocking if asset server is slow\n const mediaLoadStart = Date.now();\n const MEDIA_LOAD_TIMEOUT_MS = 30000; // 30 second timeout per element\n\n const loadPromises = mediaElements.map(async (m, index) => {\n // Check abort before each element\n signal?.throwIfAborted();\n\n const elementStart = Date.now();\n try {\n // Use getMediaEngine async method if available\n if (typeof m.getMediaEngine === \"function\") {\n // Add timeout to prevent indefinite blocking\n const timeoutPromise = new Promise<undefined>((_, reject) => {\n if (signal?.aborted) {\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n return;\n }\n const timeoutId = setTimeout(\n () =>\n reject(\n new Error(\n `Media element ${index} load timeout after ${MEDIA_LOAD_TIMEOUT_MS}ms`,\n ),\n ),\n MEDIA_LOAD_TIMEOUT_MS,\n );\n signal?.addEventListener(\n \"abort\",\n () => {\n clearTimeout(timeoutId);\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n },\n { once: true },\n );\n });\n\n await Promise.race([m.getMediaEngine(signal), timeoutPromise]);\n }\n // Fallback: check status and use taskComplete\n else if (m.mediaEngineTask) {\n // Status: INITIAL=0, PENDING=1, COMPLETE=2, ERROR=3\n const status = m.mediaEngineTask.status;\n\n // Already complete or errored - no need to wait\n if (status === 2 || status === 3) {\n return;\n }\n\n // Attach .catch() to taskComplete to prevent unhandled rejection\n const taskPromise = m.mediaEngineTask.taskComplete;\n taskPromise?.catch(() => {});\n\n if (taskPromise) {\n const timeoutPromise = new Promise<undefined>((_, reject) => {\n if (signal?.aborted) {\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n return;\n }\n const timeoutId = setTimeout(\n () =>\n reject(\n new Error(\n `Media element ${index} load timeout after ${MEDIA_LOAD_TIMEOUT_MS}ms`,\n ),\n ),\n MEDIA_LOAD_TIMEOUT_MS,\n );\n signal?.addEventListener(\n \"abort\",\n () => {\n clearTimeout(timeoutId);\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n },\n { once: true },\n );\n });\n\n await Promise.race([taskPromise, timeoutPromise]);\n }\n }\n } catch (error) {\n // Re-throw AbortError to propagate cancellation\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n // Log only if tracing is enabled to reduce console noise\n if (isTracingEnabled()) {\n const elementElapsed = Date.now() - elementStart;\n console.error(\n `[EFTimegroup] Media element ${index} failed after ${elementElapsed}ms:`,\n error,\n );\n }\n // Don't throw - continue with other elements\n }\n });\n\n const results = await Promise.allSettled(loadPromises);\n\n // Check if any were aborted\n const aborted = results.some(\n (r) =>\n r.status === \"rejected\" &&\n r.reason instanceof DOMException &&\n r.reason.name === \"AbortError\",\n );\n if (aborted) {\n throw new DOMException(\"Aborted\", \"AbortError\");\n }\n\n // Log any failures but don't throw - we want to continue even if some media fails\n const failures = results.filter((r) => r.status === \"rejected\");\n if (failures.length > 0 && isTracingEnabled()) {\n const mediaLoadElapsed = Date.now() - mediaLoadStart;\n console.warn(\n `[EFTimegroup] ${failures.length} media elements failed to load in ${mediaLoadElapsed}ms:`,\n failures.map((r) => (r.status === \"rejected\" ? r.reason : null)),\n );\n }\n\n // After waiting for durations, we must force some updates to cascade and ensure all temporal elements\n // have correct durations and start times. It is not ideal that we have to do this inside here,\n // but it is the best current way to ensure that all temporal elements have correct durations and start times.\n\n // Next, we must flush the startTimeMs cache to ensure all media elements have their\n // startTimeMs parsed fresh, otherwise the startTimeMs is cached per animation frame.\n flushStartTimeMsCache();\n\n // Flush duration cache since child durations may have changed\n flushSequenceDurationCache();\n\n // Request an update to the currentTime of this group, ensuring that time updates will cascade\n // down to children, forcing sequence groups to arrange correctly.\n // This also makes the filmstrip update correctly.\n // Defer using setTimeout(0) to avoid Lit warning about scheduling updates after update completed.\n // This method can be called during a task or after an update cycle completes, and using\n // setTimeout ensures we're completely outside any Lit update cycle.\n setTimeout(() => this.requestUpdate(\"currentTime\"), 0);\n // Note: We don't await updateComplete here during initialization to avoid deadlocks.\n // The update will complete asynchronously, and sequence groups will arrange correctly\n // once all timegroups have finished initializing. During normal operation (seeks, etc.),\n // the caller will wait for updateComplete explicitly if needed.\n },\n );\n }\n\n /** @internal */\n get childTemporals() {\n return shallowGetTemporalElements(this);\n }\n\n /**\n * Returns true if the timegroup should be wrapped with a workbench.\n *\n * A timegroup should be wrapped with a workbench if:\n * - It's being rendered (EF_RENDERING), OR\n * - The workbench property is set to true\n *\n * If the timegroup is already wrapped in a context provider like ef-preview,\n * it should NOT be wrapped in a workbench.\n * @internal\n */\n shouldWrapWithWorkbench() {\n // Never wrap when being captured by canvas preview — the element is\n // temporarily reparented for native rendering and must not spawn a\n // new workbench (which would read \"canvas\" from localStorage and\n // re-enter initCanvasMode, creating an infinite loop).\n if ((this as any).canvasPreviewActive) {\n return false;\n }\n\n // Only root timegroups should wrap with workbench\n if (!this.isRootTimegroup) {\n return false;\n }\n\n // Never wrap with workbench when inside a canvas\n // Canvas manages its own layout and coordinate system\n if (this.closest(\"ef-canvas\") !== null) {\n return false;\n }\n\n // Never wrap if already inside preview, workbench, or preview context\n if (\n this.closest(\"ef-preview\") !== null ||\n this.closest(\"ef-workbench\") !== null ||\n this.closest(\"ef-preview-context\") !== null\n ) {\n return false;\n }\n\n // Skip wrapping in test contexts or if explicitly disabled\n // Test contexts and render clones provide their own rendering infrastructure\n if (\n this.closest(\"test-context\") !== null ||\n this.hasAttribute(\"data-no-workbench\")\n ) {\n return false;\n }\n\n // During rendering, never wrap with workbench - timegroups can seek without it\n const isRendering = EF_RENDERING?.() === true;\n if (isRendering) {\n return false;\n }\n\n // Check URL param to disable workbench (only applies in non-rendering mode)\n if (typeof window !== \"undefined\") {\n const params = new URLSearchParams(window.location.search);\n if (\n params.get(\"noWorkbench\") === \"true\" ||\n params.get(\"no-workbench\") === \"true\"\n ) {\n return false;\n }\n }\n\n // Respect the explicit workbench property\n return this.workbench;\n }\n\n /** @internal */\n wrapWithWorkbench() {\n const workbench = document.createElement(\"ef-workbench\") as any;\n const parent = this.parentElement;\n\n // When in rendering mode, immediately set rendering=true before insertion\n // This prevents the workbench UI from ever being visible in rendered frames\n if (EF_RENDERING()) {\n // Use setAttribute to ensure it's set before the element connects and renders\n workbench.setAttribute(\"rendering\", \"\");\n workbench.rendering = true;\n }\n\n // Apply explicit sizing to ensure workbench fills its container\n if (parent === document.body) {\n // Direct child of body: use viewport units with fixed positioning\n workbench.style.position = \"fixed\";\n workbench.style.top = \"0\";\n workbench.style.left = \"0\";\n workbench.style.width = \"100vw\";\n workbench.style.height = \"100vh\";\n workbench.style.zIndex = \"0\";\n } else {\n // Embedded in container: ensure it fills the container\n // Use absolute positioning to prevent content-based sizing\n workbench.style.position = \"absolute\";\n workbench.style.top = \"0\";\n workbench.style.left = \"0\";\n workbench.style.width = \"100%\";\n workbench.style.height = \"100%\";\n }\n\n parent?.append(workbench);\n if (!this.hasAttribute(\"id\")) {\n this.setAttribute(\"id\", \"root-timegroup\");\n }\n\n // Create pan-zoom for selection overlay support\n // Must be in light DOM so canvas can find it via closest()\n const panZoom = document.createElement(\"ef-pan-zoom\");\n panZoom.id = \"workbench-panzoom\";\n panZoom.setAttribute(\"slot\", \"canvas\");\n panZoom.setAttribute(\"auto-fit\", \"\"); // Fit content to view on first render\n panZoom.style.width = \"100%\";\n panZoom.style.height = \"100%\";\n\n // Create canvas wrapper for selection/highlighting support\n // Get dimensions from the timegroup for explicit canvas sizing\n const rect = this.getBoundingClientRect();\n const canvas = document.createElement(\"ef-canvas\");\n canvas.id = \"workbench-canvas\";\n canvas.style.width = `${rect.width}px`;\n canvas.style.height = `${rect.height}px`;\n canvas.style.display = \"block\";\n\n // Move timegroup into canvas, canvas into pan-zoom\n canvas.append(this as unknown as Element);\n panZoom.append(canvas);\n workbench.append(panZoom);\n\n // Add hierarchy panel - targets canvas for selection support\n const hierarchy = document.createElement(\"ef-hierarchy\");\n hierarchy.setAttribute(\"slot\", \"hierarchy\");\n hierarchy.setAttribute(\"target\", \"workbench-canvas\");\n hierarchy.setAttribute(\"header\", \"Scenes\");\n workbench.append(hierarchy);\n\n // Add filmstrip/timeline - targets timegroup for playback\n const filmstrip = document.createElement(\"ef-filmstrip\");\n filmstrip.setAttribute(\"slot\", \"timeline\");\n filmstrip.setAttribute(\"target\", this.id);\n workbench.append(filmstrip);\n }\n\n /**\n * Returns media elements for playback audio rendering\n * For standalone media, returns [this]; for timegroups, returns all descendants\n * Used by PlaybackController for audio-driven playback\n * @internal\n */\n getMediaElements(): EFMedia[] {\n return deepGetMediaElements(this);\n }\n\n /**\n * Render audio buffer for playback\n * Called by PlaybackController during live playback\n * Delegates to shared renderTemporalAudio utility for consistent behavior\n * @internal\n */\n async renderAudio(\n fromMs: number,\n toMs: number,\n signal?: AbortSignal,\n ): Promise<AudioBuffer> {\n return renderTemporalAudio(this, fromMs, toMs, signal);\n }\n\n async #executeCustomFrameTasks() {\n if (this.#customFrameTasks.size > 0) {\n const percentComplete =\n this.durationMs > 0 ? this.ownCurrentTimeMs / this.durationMs : 0;\n const frameInfo = {\n ownCurrentTimeMs: this.ownCurrentTimeMs,\n currentTimeMs: this.currentTimeMs,\n durationMs: this.durationMs,\n percentComplete,\n element: this,\n };\n\n await Promise.all(\n Array.from(this.#customFrameTasks).map((callback) =>\n Promise.resolve(callback(frameInfo)),\n ),\n );\n }\n }\n\n /** @internal */\n #seekTaskPromise: Promise<number | undefined> = Promise.resolve(undefined);\n #seekTaskAbortController: AbortController | null = null;\n\n seekTask = (() => {\n const self = this;\n const taskObj: {\n run(): void | Promise<number | undefined>;\n taskComplete: Promise<number | undefined>;\n } = {\n run: () => {\n // Abort any in-flight task\n self.#seekTaskAbortController?.abort();\n self.#seekTaskAbortController = new AbortController();\n const signal = self.#seekTaskAbortController.signal;\n\n const targetTime = self.#pendingSeekTime ?? self.#currentTime;\n self.#seekTaskPromise = self.#runSeekTask(targetTime, signal);\n taskObj.taskComplete = self.#seekTaskPromise;\n return self.#seekTaskPromise;\n },\n taskComplete: Promise.resolve(undefined),\n };\n return taskObj;\n })();\n\n async #runSeekTask(\n targetTime: number | undefined,\n signal: AbortSignal,\n ): Promise<number | undefined> {\n try {\n signal.throwIfAborted();\n\n // Delegate to playbackController if available\n if (this.playbackController) {\n // Wait for playbackController's seek to complete\n await this.playbackController.currentTime; // Trigger seek\n signal.throwIfAborted();\n return this.currentTime;\n }\n\n // Only root timegroups execute seek tasks\n if (!this.isRootTimegroup) {\n return undefined;\n }\n\n return await withSpan(\n \"timegroup.seekTask\",\n {\n timegroupId: this.id || \"unknown\",\n targetTime: targetTime ?? 0,\n durationMs: this.durationMs,\n },\n undefined,\n async (span) => {\n // Wait for media durations to be loaded\n try {\n await Promise.race([\n this.waitForMediaDurations(signal),\n new Promise<void>((_, reject) => {\n if (signal.aborted) {\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n return;\n }\n const timeoutId = setTimeout(\n () => reject(new Error(\"waitForMediaDurations timeout\")),\n 10000,\n );\n signal.addEventListener(\n \"abort\",\n () => {\n clearTimeout(timeoutId);\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n },\n { once: true },\n );\n }),\n ]);\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n // Continue with seek even if durations aren't loaded yet\n }\n\n signal.throwIfAborted();\n\n // Evaluate and apply seek target\n const newTime = evaluateSeekTarget(\n targetTime ?? 0,\n this.durationMs,\n this.effectiveFps,\n );\n if (isTracingEnabled()) {\n span.setAttribute(\"newTime\", newTime);\n }\n\n this.#currentTime = newTime;\n this.requestUpdate(\"currentTime\");\n\n await this.updateComplete;\n signal.throwIfAborted();\n\n await this.#runThrottledFrameTask();\n signal.throwIfAborted();\n\n if (!this.#restoringFromLocalStorage) {\n this.saveTimeToLocalStorage(this.#currentTime);\n }\n this.#seekInProgress = false;\n if (this.#restoringFromLocalStorage) {\n this.#restoringFromLocalStorage = false;\n }\n return newTime;\n },\n );\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n return undefined;\n }\n console.error(\"EFTimegroup seekTask error\", error);\n return undefined;\n }\n }\n\n /**\n * Get container information for this timegroup.\n * Timegroups are always containers and can contain children.\n * Display mode is determined from computed styles.\n *\n * @public\n */\n getContainerInfo(): ContainerInfo {\n const info = getContainerInfoFromElement(this);\n // Timegroups are always containers and can contain children\n return {\n ...info,\n isContainer: true,\n canContainChildren: true,\n };\n }\n\n /**\n * Get position information for this timegroup.\n * Returns computed bounds, transform, and rotation.\n *\n * @public\n */\n getPositionInfo(): ElementPositionInfo | null {\n return getPositionInfoFromElement(this);\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-timegroup\": EFTimegroup & Element;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"EFTimegroup.js","names":["durationCache: WeakMap<EFTimegroup, number>","ancestor: Node | null","EFTimegroup","taskObj: {\n run(): void | Promise<number | undefined>;\n taskComplete: Promise<number | undefined>;\n }","#seekTaskAbortController","#pendingSeekTime","#currentTime","#seekTaskPromise","#runSeekTask","#recomputeAggregateReadyState","state","#trackedChildren","#childReadyStateHandler","#childContentChangeHandler","#mode","#overlapMs","#initializer","#restoringFromLocalStorage","#frameController","#qualityUpgradeScheduler","#customFrameTasks","#executeCustomFrameTasks","#contentEpoch","#runThrottledFrameTask","#userTimeMs","#processingPendingSeek","#seekInProgress","#getAllLitElementDescendants","result: LitElement[]","#onFrameCallback","#onFrameCleanup","#handleSlotChange","#syncChildListeners","#runInitializer","#setupPlaybackListener","#playbackListener","#previousDurationMs","#resizeObserver","#removePlaybackListener","#initializerHasRun","result: unknown","#copyTextSegmentData","updatePromises: Promise<any>[]","#waitForCaptionsData","waitPromises: Promise<unknown>[]","#copyInitializersToClone","#createRenderCloneFromFactory","#createRenderCloneFromDOM","#waitForDescendants","renderTarget: HTMLElement","#finalizeRenderClone","#copyCaptionsData","#copyTextContent","#mediaDurationsPromise","#waitForMediaDurations","rafId1: number","rafId2: number","timeoutId: ReturnType<typeof setTimeout>"],"sources":["../../src/elements/EFTimegroup.ts"],"sourcesContent":["import { provide } from \"@lit/context\";\nimport debug from \"debug\";\nimport { css, html, LitElement, type PropertyValues } from \"lit\";\nimport { customElement, property } from \"lit/decorators.js\";\n\nimport { quantizeToFrameTimeS } from \"../utils/frameTime.js\";\nimport { getCloneFactory } from \"./cloneFactoryRegistry.js\";\nimport { EF_RENDERING } from \"../EF_RENDERING.js\";\nimport { efContext } from \"../gui/efContext.js\";\nimport { TWMixin } from \"../gui/TWMixin.js\";\nimport { isTracingEnabled, withSpan } from \"../otel/tracingHelpers.js\";\nimport {\n FrameController,\n type FrameRenderable,\n type FrameState,\n PRIORITY_DEFAULT,\n} from \"../preview/FrameController.js\";\nimport { QualityUpgradeScheduler } from \"../preview/QualityUpgradeScheduler.js\";\nimport { deepGetMediaElements, type EFMedia } from \"./EFMedia.js\";\nimport {\n EFTemporal,\n flushStartTimeMsCache,\n resetTemporalCache,\n shallowGetTemporalElements,\n timegroupContext,\n type TemporalMixinInterface,\n} from \"./EFTemporal.js\";\nimport { parseTimeToMs } from \"./parseTimeToMs.js\";\nimport { renderTemporalAudio } from \"./renderTemporalAudio.js\";\nimport { EFTargetable } from \"./TargetController.js\";\nimport { TimegroupController } from \"./TimegroupController.js\";\nimport { updateAnimations } from \"./updateAnimations.js\";\nimport {\n type ContainerInfo,\n getContainerInfoFromElement,\n} from \"./ContainerInfo.js\";\nimport {\n type ElementPositionInfo,\n getPositionInfoFromElement,\n} from \"./ElementPositionInfo.js\";\n// Import only types - actual function loaded dynamically\nimport type { RenderToVideoOptions } from \"../preview/renderTimegroupToVideo.types.js\";\nimport type { PlaybackControllerUpdateEvent } from \"../gui/PlaybackController.js\";\n\n// Side-effect imports for workbench wrapping\nimport \"../canvas/EFCanvas.js\";\nimport \"../gui/hierarchy/EFHierarchy.js\";\nimport \"../gui/EFFilmstrip.js\";\nimport \"../gui/EFWorkbench.js\";\nimport \"../gui/EFFitScale.js\";\nimport \"./EFPanZoom.js\";\n\nconst log = debug(\"ef:elements:EFTimegroup\");\n\n// Custom frame task callback type\nexport type FrameTaskCallback = (info: {\n ownCurrentTimeMs: number;\n currentTimeMs: number;\n durationMs: number;\n percentComplete: number;\n element: EFTimegroup;\n}) => void | Promise<void>;\n\n/**\n * Result of createRenderClone() - contains the clone, its container, and cleanup function.\n */\nexport interface RenderCloneResult {\n /** The cloned timegroup, fully functional with its own time state */\n clone: EFTimegroup;\n /** The offscreen container holding the clone */\n container: HTMLElement;\n /** Call this to remove the clone from DOM and clean up */\n cleanup: () => void;\n}\n\n/**\n * Initializer function type for setting up JavaScript behavior on timegroup instances.\n * This function is called on both the prime timeline and each render clone.\n *\n * CONSTRAINTS:\n * - MUST be synchronous (no async/await, no Promise return)\n * - MUST complete in <2000ms (error) or <100ms (warning)\n * - Should only register callbacks and set up behavior, not do expensive work\n * - GPU operations (WebGL context creation, shader compilation) may take up to ~1s\n */\nexport type TimegroupInitializer = (timegroup: EFTimegroup) => void;\n\n// Constants for initializer time budget enforcement\nconst INITIALIZER_ERROR_THRESHOLD_MS = 2000;\nconst INITIALIZER_WARN_THRESHOLD_MS = 100;\n\n// ============================================================================\n// Purpose 1: Composition Rules - How Duration is Determined\n// ============================================================================\n//\n// A timegroup's duration is determined by its mode:\n// - \"fixed\": Uses explicit duration attribute (base case)\n// - \"sequence\": Sum of child durations minus overlaps\n// - \"contain\": Maximum of child durations\n// - \"fit\": Inherits duration from parent timegroup\n//\n// Core invariant: Every timegroup has exactly one duration value at any moment,\n// computed from either explicit specification (fixed mode) or child relationships\n// (sequence/contain/fit modes).\n//\n// ============================================================================\n\n/**\n * The four timegroup modes define how duration is calculated:\n * - \"fit\": Inherits duration from parent timegroup\n * - \"fixed\": Uses explicit duration attribute\n * - \"sequence\": Sum of child durations minus overlaps\n * - \"contain\": Maximum of child durations\n */\nexport type TimeMode = \"fit\" | \"fixed\" | \"sequence\" | \"contain\";\n\n/**\n * Per-phase timing data returned by seekForRender().\n * All values are in milliseconds.\n */\nexport interface SeekForRenderTiming {\n updateComplete1Ms: number;\n updateComplete2Ms: number;\n updateComplete3Ms: number;\n textSegmentsMs: number;\n renderFrameMs: number;\n renderFrameQueryMs: number;\n renderFramePrepareMs: number;\n renderFrameDrawMs: number;\n renderFrameAnimsMs: number;\n frameTasksMs: number;\n totalMs: number;\n}\n\n// Cache for duration calculations to avoid O(n) recalculation on every access\n// Used by all modes (sequence, contain) to avoid repeated iteration through children\nlet durationCache: WeakMap<EFTimegroup, number> = new WeakMap();\n\nexport const flushDurationCache = () => {\n durationCache = new WeakMap();\n};\n\n// Keep alias for backwards compatibility\nexport const flushSequenceDurationCache = flushDurationCache;\n\n// Track timegroups currently calculating duration to prevent infinite loops\nconst durationCalculationInProgress = new WeakSet<EFTimegroup>();\n\n// Export function to check if a timegroup is currently calculating duration\n// This is used by EFTemporal to prevent calling parent.durationMs during calculation\nexport const isTimegroupCalculatingDuration = (\n timegroup: EFTimegroup | undefined,\n): boolean => {\n return (\n timegroup !== undefined && durationCalculationInProgress.has(timegroup)\n );\n};\n\n// Register this function with EFTemporal to break circular dependency\n// EFTemporal needs this function but can't import it directly due to circular dependency\nimport { registerIsTimegroupCalculatingDuration } from \"./EFTemporal.js\";\nregisterIsTimegroupCalculatingDuration(isTimegroupCalculatingDuration);\n\n/**\n * Determines if a timegroup has its own duration based on its mode.\n * This is the semantic rule: which modes produce independent durations.\n */\nfunction hasOwnDurationForMode(\n mode: TimeMode,\n hasExplicitDuration: boolean,\n): boolean {\n return (\n mode === \"contain\" ||\n mode === \"sequence\" ||\n (mode === \"fixed\" && hasExplicitDuration)\n );\n}\n\n/**\n * Determines if a child temporal element should participate in parent duration calculation.\n *\n * Semantic rule: Fit-mode children inherit from parent, so they don't contribute to parent's\n * duration calculation (to avoid circular dependencies). Children without own duration\n * also don't contribute.\n */\nfunction shouldParticipateInDurationCalculation(\n child: TemporalMixinInterface & HTMLElement,\n): boolean {\n // Fit timegroups look \"up\" to their parent for duration, so skip to avoid infinite loop\n if (child instanceof EFTimegroup && child.mode === \"fit\") {\n return false;\n }\n // Only children with their own duration contribute\n if (!child.hasOwnDuration) {\n return false;\n }\n return true;\n}\n\n/**\n * Evaluates duration for \"fit\" mode: inherits from parent.\n * Semantic rule: fit mode always matches parent duration, or 0 if no parent.\n */\nfunction evaluateFitDuration(parentTimegroup: EFTimegroup | undefined): number {\n if (!parentTimegroup) {\n return 0;\n }\n return parentTimegroup.durationMs;\n}\n\n/**\n * Evaluates duration for \"sequence\" mode: sum of children minus overlaps.\n * Semantic rule: sequence mode sums child durations, subtracting overlap between consecutive items.\n * Fit-mode children are excluded to avoid circular dependencies.\n */\nfunction evaluateSequenceDuration(\n timegroup: EFTimegroup,\n childTemporals: Array<TemporalMixinInterface & HTMLElement>,\n overlapMs: number,\n): number {\n // Check cache first to avoid expensive O(n) recalculation\n const cachedDuration = durationCache.get(timegroup);\n if (cachedDuration !== undefined) {\n return cachedDuration;\n }\n\n let duration = 0;\n let participatingIndex = 0;\n childTemporals.forEach((child) => {\n if (!shouldParticipateInDurationCalculation(child)) {\n return;\n }\n // Prevent infinite loops: skip children that are already calculating their duration\n if (\n child instanceof EFTimegroup &&\n durationCalculationInProgress.has(child)\n ) {\n return;\n }\n\n // Additional safety: if child is a timegroup, check if any of its ancestors\n // (EXCLUDING the current timegroup) are calculating.\n // This prevents cycles where a child's descendant eventually calls back to an ancestor,\n // but allows direct children of the current timegroup to participate.\n if (child instanceof EFTimegroup) {\n let ancestor: Node | null = child.parentNode;\n let shouldSkip = false;\n while (ancestor) {\n // Stop FIRST if we've reached the current timegroup - direct children are allowed\n if (ancestor === timegroup) {\n break;\n }\n if (\n ancestor instanceof EFTimegroup &&\n durationCalculationInProgress.has(ancestor)\n ) {\n // Found a calculating ancestor (not the current timegroup) - skip this child to prevent cycle\n shouldSkip = true;\n break;\n }\n ancestor = ancestor.parentNode;\n }\n if (shouldSkip) {\n return;\n }\n }\n\n // Subtract overlap for all items after the first\n if (participatingIndex > 0) {\n duration -= overlapMs;\n }\n duration += child.durationMs;\n participatingIndex++;\n });\n\n // Ensure non-negative duration (invariant)\n duration = Math.max(0, duration);\n\n // Cache the calculated duration\n durationCache.set(timegroup, duration);\n return duration;\n}\n\n/**\n * Evaluates duration for \"contain\" mode: maximum of children.\n * Semantic rule: contain mode takes the maximum child duration.\n * Fit-mode children and children without own duration are excluded.\n */\nfunction evaluateContainDuration(\n timegroup: EFTimegroup,\n childTemporals: Array<TemporalMixinInterface & HTMLElement>,\n): number {\n // Check cache first to avoid expensive O(n) recalculation\n const cachedDuration = durationCache.get(timegroup);\n if (cachedDuration !== undefined) {\n return cachedDuration;\n }\n\n let maxDuration = 0;\n for (const child of childTemporals) {\n if (!shouldParticipateInDurationCalculation(child)) {\n continue;\n }\n // Prevent infinite loops: skip children that are already calculating their duration\n // This check applies to all timegroup children, not just contain mode, because\n // a sequence-mode child could contain a contain-mode grandchild that\n // eventually references back to the parent through the parent chain\n if (\n child instanceof EFTimegroup &&\n durationCalculationInProgress.has(child)\n ) {\n continue;\n }\n\n // Additional safety: if child is a timegroup, check if any of its ancestors\n // (EXCLUDING the current timegroup) are calculating.\n // This prevents cycles where a child's descendant eventually calls back to an ancestor,\n // but allows direct children of the current timegroup to participate.\n if (child instanceof EFTimegroup) {\n let ancestor: Node | null = child.parentNode;\n let shouldSkip = false;\n while (ancestor) {\n // Stop FIRST if we've reached the current timegroup - direct children are allowed\n if (ancestor === timegroup) {\n break;\n }\n if (\n ancestor instanceof EFTimegroup &&\n durationCalculationInProgress.has(ancestor)\n ) {\n // Found a calculating ancestor (not the current timegroup) - skip this child to prevent cycle\n shouldSkip = true;\n break;\n }\n ancestor = ancestor.parentNode;\n }\n if (shouldSkip) {\n continue;\n }\n }\n\n maxDuration = Math.max(maxDuration, child.durationMs);\n }\n // Ensure non-negative duration (invariant)\n const duration = Math.max(0, maxDuration);\n\n // Cache the calculated duration\n durationCache.set(timegroup, duration);\n return duration;\n}\n\n/**\n * Evaluates duration based on timegroup mode.\n * This is the semantic evaluation function - it determines what duration should be.\n *\n * Note: Fixed mode is handled inline in the getter because it needs to call super.durationMs\n * which requires the class context. The other modes are extracted for clarity.\n */\nfunction evaluateDurationForMode(\n timegroup: EFTimegroup,\n mode: TimeMode,\n childTemporals: Array<TemporalMixinInterface & HTMLElement>,\n): number {\n switch (mode) {\n case \"fit\":\n return evaluateFitDuration(timegroup.parentTimegroup);\n case \"sequence\": {\n // Mark this timegroup as calculating duration to prevent infinite loops\n durationCalculationInProgress.add(timegroup);\n try {\n return evaluateSequenceDuration(\n timegroup,\n childTemporals,\n timegroup.overlapMs,\n );\n } finally {\n // Always remove the marker, even if an error occurs\n durationCalculationInProgress.delete(timegroup);\n }\n }\n case \"contain\": {\n // Mark this timegroup as calculating duration to prevent infinite loops\n durationCalculationInProgress.add(timegroup);\n try {\n return evaluateContainDuration(timegroup, childTemporals);\n } finally {\n // Always remove the marker, even if an error occurs\n durationCalculationInProgress.delete(timegroup);\n }\n }\n default:\n throw new Error(`Invalid time mode: ${mode}`);\n }\n}\n\nexport const shallowGetTimegroups = (\n element: Element,\n groups: EFTimegroup[] = [],\n) => {\n for (const child of Array.from(element.children)) {\n if (child instanceof EFTimegroup) {\n groups.push(child);\n } else {\n shallowGetTimegroups(child, groups);\n }\n }\n return groups;\n};\n\n// ============================================================================\n// Purpose 2: Time Propagation - How currentTime Flows Root to Children\n// ============================================================================\n//\n// Time propagation determines how the root timegroup's currentTime flows to child\n// temporal elements, computing each child's ownCurrentTime based on:\n// - The root's currentTime (global coordinate)\n// - The child's startTimeMs (determined by parent's composition mode)\n// - The parent's mode (sequence/contain/fit/fixed)\n//\n// Propagation rules by mode:\n// - Sequence: Each child's ownCurrentTime progresses within its time-shifted window\n// - Contain: All children share the same ownCurrentTime as parent\n// - Fit: Child ownCurrentTime = parent ownCurrentTime (identity mapping)\n//\n// Core invariant: Only root timegroup's currentTime should be written.\n// Child times are computed from parent state via ownCurrentTimeMs.\n//\n// Note: Time propagation logic is primarily implemented in EFTemporal.ts\n// (ownCurrentTimeMs getter and startTimeMs calculation). The timegroup's\n// currentTime setter triggers propagation by updating root time.\n//\n// ============================================================================\n\n// ============================================================================\n// Purpose 3: Seeking - Moving to a Specific Time\n// ============================================================================\n//\n// Seeking moves the timeline to a specific time position. This involves:\n// 1. Quantizing the requested time to frame boundaries (based on fps)\n// 2. Clamping to valid range [0, duration]\n// 3. Updating root timegroup's currentTime (which triggers time propagation)\n// 4. Waiting for all media and frame tasks to complete\n//\n// Core invariant: All time values snap to frame boundaries when FPS is set.\n// This ensures consistent seek/render behavior.\n//\n// ============================================================================\n\n/**\n * Evaluates the target time for a seek operation.\n * Applies quantization and clamping to determine the valid seek target.\n */\nfunction evaluateSeekTarget(\n requestedTime: number,\n durationMs: number,\n fps: number,\n): number {\n // Quantize to frame boundaries\n const quantizedTime = quantizeToFrameTimeS(requestedTime, fps);\n // Clamp to valid range [0, duration]\n return Math.max(0, Math.min(quantizedTime, durationMs / 1000));\n}\n\n@customElement(\"ef-timegroup\")\nexport class EFTimegroup\n extends EFTargetable(EFTemporal(TWMixin(LitElement)))\n implements FrameRenderable\n{\n static get observedAttributes(): string[] {\n const parentAttributes = super.observedAttributes || [];\n return [\n ...parentAttributes,\n \"mode\",\n \"overlap\",\n \"currenttime\",\n \"fit\",\n \"fps\",\n \"auto-init\",\n \"workbench\",\n ];\n }\n\n static styles = css`\n :host {\n display: block;\n position: relative;\n overflow: hidden;\n }\n\n ::slotted(ef-timegroup) {\n position: absolute;\n width: 100%;\n height: 100%;\n top: 0;\n left: 0;\n overflow: initial;\n }\n `;\n\n /** @internal */\n @provide({ context: timegroupContext })\n _timeGroupContext = this;\n\n /** @internal */\n @provide({ context: efContext })\n efContext = this;\n\n // ---- Content Readiness Aggregation ----\n\n #trackedChildren = new Set<TemporalMixinInterface & HTMLElement>();\n\n override shouldAutoReady(): boolean {\n return false;\n }\n\n #childReadyStateHandler = () => {\n this.#recomputeAggregateReadyState();\n };\n\n #childContentChangeHandler = (e: Event) => {\n const detail = (e as CustomEvent).detail;\n this.emitContentChange(detail?.reason ?? \"content\");\n };\n\n #recomputeAggregateReadyState(): void {\n const children = shallowGetTemporalElements(this);\n if (children.length === 0) {\n this.setContentReadyState(\"ready\");\n return;\n }\n\n let hasLoading = false;\n let hasError = false;\n let hasIdle = false;\n\n for (const child of children) {\n const state = child.contentReadyState;\n if (state === \"loading\") hasLoading = true;\n else if (state === \"error\") hasError = true;\n else if (state === \"idle\") hasIdle = true;\n }\n\n if (hasError) {\n this.setContentReadyState(\"error\");\n } else if (hasLoading) {\n this.setContentReadyState(\"loading\");\n } else if (hasIdle) {\n this.setContentReadyState(\"loading\");\n } else {\n this.setContentReadyState(\"ready\");\n }\n }\n\n #syncChildListeners(): void {\n const currentChildren = new Set(\n shallowGetTemporalElements(this) as Array<\n TemporalMixinInterface & HTMLElement\n >,\n );\n\n // Remove listeners from children that left\n for (const child of this.#trackedChildren) {\n if (!currentChildren.has(child)) {\n child.removeEventListener(\n \"readystatechange\",\n this.#childReadyStateHandler,\n );\n child.removeEventListener(\n \"contentchange\",\n this.#childContentChangeHandler,\n );\n }\n }\n\n // Add listeners to new children\n for (const child of currentChildren) {\n if (!this.#trackedChildren.has(child)) {\n child.addEventListener(\n \"readystatechange\",\n this.#childReadyStateHandler,\n );\n child.addEventListener(\n \"contentchange\",\n this.#childContentChangeHandler,\n );\n }\n }\n\n this.#trackedChildren = currentChildren;\n this.#recomputeAggregateReadyState();\n }\n\n // ---- End Content Readiness Aggregation ----\n\n /** @public */\n #mode: TimeMode = \"contain\";\n get mode(): TimeMode {\n return this.#mode;\n }\n set mode(value: TimeMode) {\n if (this.#mode === value) return;\n const old = this.#mode;\n this.#mode = value;\n this.requestUpdate(\"mode\", old);\n if (this.getAttribute(\"mode\") !== value) {\n this.setAttribute(\"mode\", value);\n }\n }\n\n /** @public */\n #overlapMs = 0;\n get overlapMs(): number {\n return this.#overlapMs;\n }\n set overlapMs(value: number) {\n if (this.#overlapMs === value) return;\n const old = this.#overlapMs;\n this.#overlapMs = value;\n this.requestUpdate(\"overlapMs\", old);\n const attrVal = value > 0 ? `${value}ms` : null;\n if (attrVal && this.getAttribute(\"overlap\") !== attrVal) {\n this.setAttribute(\"overlap\", attrVal);\n } else if (!attrVal && this.hasAttribute(\"overlap\")) {\n this.removeAttribute(\"overlap\");\n }\n }\n\n #initializer?: TimegroupInitializer;\n\n /**\n * Initializer function for setting up JavaScript behavior on this timegroup.\n * This function is called ONCE per instance - on the prime timeline when first connected,\n * and on each render clone when created.\n *\n * Use this to register frame callbacks, set up event listeners, or initialize state.\n * The same initializer code runs on both prime and clones, eliminating duplication.\n *\n * CONSTRAINTS:\n * - MUST be synchronous (no async/await, no Promise return)\n * - MUST complete in <100ms (error thrown) or <10ms (warning logged)\n * - Should only register callbacks and set up behavior, not do expensive work\n *\n * TIMING:\n * - If set before element connects to DOM: runs automatically after connectedCallback\n * - If set after element is connected: runs immediately\n * - Clones automatically copy and run the initializer when created\n *\n * @example\n * ```javascript\n * const tg = document.querySelector('ef-timegroup');\n * tg.initializer = (instance) => {\n * // Runs once on prime timeline, once on each clone\n * instance.addFrameTask((info) => {\n * // Update content based on time\n * });\n * };\n * ```\n * @public\n */\n get initializer(): TimegroupInitializer | undefined {\n return this.#initializer;\n }\n\n set initializer(fn: TimegroupInitializer | undefined) {\n this.#initializer = fn;\n // Just store the function. Execution is handled by:\n // - connectedCallback (for elements that have initializer before connection)\n // - #copyInitializersToClone (explicitly triggers for render clones)\n }\n\n /**\n * Track if initializer has run on this instance to prevent double execution.\n * @internal\n */\n #initializerHasRun = false;\n\n /** @public */\n @property({ type: Number })\n fps = 30;\n\n /**\n * When true, automatically seeks to frame 0 after media durations are loaded.\n * Only applies to root timegroups (timegroups that are not nested inside another timegroup).\n * This ensures the first frame is rendered immediately on initialization.\n */\n @property({ type: Boolean, attribute: \"auto-init\" })\n autoInit = false;\n\n /**\n * When true, automatically wraps this root timegroup with an ef-workbench element.\n * The workbench provides development UI including hierarchy panel, timeline, and playback controls.\n * Only applies to root timegroups.\n * @public\n */\n @property({ type: Boolean, reflect: true })\n workbench = false;\n\n attributeChangedCallback(\n name: string,\n old: string | null,\n value: string | null,\n ): void {\n if (name === \"mode\" && value) {\n this.mode = value as typeof this.mode;\n }\n if (name === \"overlap\" && value) {\n this.overlapMs = parseTimeToMs(value);\n }\n if (name === \"auto-init\") {\n this.autoInit = value !== null;\n }\n if (name === \"fps\" && value) {\n this.fps = Number.parseFloat(value);\n }\n if (name === \"workbench\") {\n this.workbench = value !== null;\n }\n super.attributeChangedCallback(name, old, value);\n }\n\n /** @public */\n @property({ type: String })\n fit: \"none\" | \"contain\" | \"cover\" = \"none\";\n\n #resizeObserver?: ResizeObserver;\n\n /** Content epoch - increments when visual content changes (used by thumbnail cache) */\n #contentEpoch = 0;\n\n #currentTime: number | undefined = undefined;\n #userTimeMs: number = 0; // What the user last requested (for preview display)\n #seekInProgress = false;\n #pendingSeekTime: number | undefined;\n #processingPendingSeek = false;\n #restoringFromLocalStorage = false; // Guard to prevent recursive seeks during localStorage restoration\n\n /** @internal */\n isRestoringFromLocalStorage(): boolean {\n return this.#restoringFromLocalStorage;\n }\n\n /** @internal - Used by PlaybackController to set restoration state */\n setRestoringFromLocalStorage(value: boolean): void {\n this.#restoringFromLocalStorage = value;\n }\n #customFrameTasks: Set<FrameTaskCallback> = new Set();\n #onFrameCallback: FrameTaskCallback | null = null;\n #onFrameCleanup: (() => void) | null = null;\n #playbackListener: ((event: PlaybackControllerUpdateEvent) => void) | null =\n null;\n\n /**\n * Centralized frame controller for coordinating element rendering.\n * Replaces the distributed Lit Task hierarchy with a single control point.\n */\n #frameController: FrameController = new FrameController(this);\n\n /**\n * Get the frame controller for centralized rendering coordination.\n * @public\n */\n get frameController(): FrameController {\n return this.#frameController;\n }\n\n /**\n * Centralized quality upgrade scheduler for coordinating main-quality segment fetching.\n * Lives alongside FrameController to manage background quality upgrades.\n */\n #qualityUpgradeScheduler: QualityUpgradeScheduler =\n new QualityUpgradeScheduler({\n requestFrameRender: () => this.requestFrameRender(),\n });\n\n /**\n * Get the quality upgrade scheduler for background segment fetching.\n * @public\n */\n get qualityUpgradeScheduler(): QualityUpgradeScheduler {\n return this.#qualityUpgradeScheduler;\n }\n\n // ============================================================================\n // FrameRenderable Interface Implementation\n // ============================================================================\n // Allows FrameController to discover and coordinate nested timegroups.\n // This ensures frame callbacks registered on nested timegroups are executed.\n // ============================================================================\n\n /**\n * Query timegroup's readiness state for a given time.\n * Timegroups are always ready (no async preparation needed).\n * @public\n */\n getFrameState(_timeMs: number): FrameState {\n return {\n needsPreparation: false,\n isReady: true,\n priority: PRIORITY_DEFAULT,\n };\n }\n\n /**\n * Async preparation phase (no-op for timegroups).\n * Timegroups don't need preparation - they just coordinate child rendering.\n * @public\n */\n async prepareFrame(_timeMs: number, _signal: AbortSignal): Promise<void> {\n // No preparation needed for timegroups\n }\n\n /**\n * Synchronous render phase - executes custom frame callbacks.\n * Called by FrameController after all preparation is complete.\n * Kicks off async frame callbacks without blocking (they run in background).\n * @public\n */\n renderFrame(_timeMs: number): void {\n // Execute custom frame tasks registered via addFrameTask()\n // Fire and forget - callbacks can be async but we don't block here\n // The frameTask.taskComplete promise tracks completion if needed\n if (this.#customFrameTasks.size > 0) {\n this.#executeCustomFrameTasks().catch((error) => {\n console.error(\"EFTimegroup custom frame task error:\", error);\n });\n }\n }\n\n /**\n * Get the effective FPS for this timegroup.\n * During rendering, uses the render options FPS if available.\n * Otherwise uses the configured fps property.\n * @public\n */\n get effectiveFps(): number {\n // During rendering, prefer the render options FPS\n if (typeof window !== \"undefined\" && window.EF_FRAMEGEN?.renderOptions) {\n return window.EF_FRAMEGEN.renderOptions.encoderOptions.video.framerate;\n }\n return this.fps;\n }\n\n /**\n * Get the current content epoch (used by thumbnail cache).\n * The epoch increments whenever visual content changes.\n * @public\n */\n get contentEpoch(): number {\n return this.#contentEpoch;\n }\n\n /**\n * Increment content epoch (called when visual content changes).\n * This invalidates cached thumbnails by changing their cache keys.\n * @public\n */\n incrementContentEpoch(): void {\n this.#contentEpoch++;\n }\n\n /**\n * Request a frame re-render at the current time.\n *\n * Use this when the source-to-timeline mapping has changed (e.g., sourcein/sourceout)\n * but currentTimeMs hasn't. The FrameController only re-renders when currentTimeMs\n * or durationMs change, so this provides a way for child elements to request a\n * re-render when their internal state changes the visual output.\n * @public\n */\n requestFrameRender(): void {\n this.#frameController.abort();\n this.#runThrottledFrameTask();\n }\n\n async #runThrottledFrameTask(): Promise<void> {\n if (this.playbackController) {\n return this.playbackController.runThrottledFrameTask();\n }\n // Use FrameController directly (no frameTask fallback)\n try {\n await this.#frameController.renderFrame(this.currentTimeMs, {\n onAnimationsUpdate: (root) => {\n updateAnimations(root as typeof this);\n },\n });\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n return;\n }\n console.error(\"FrameController error:\", error);\n }\n }\n\n // ============================================================================\n // Purpose 3: Seeking Implementation\n // ============================================================================\n\n /** @public */\n @property({ type: Number, attribute: \"currenttime\" })\n set currentTime(time: number) {\n // Evaluate seek target (quantization and clamping)\n const seekTarget = evaluateSeekTarget(\n time,\n this.durationMs,\n this.effectiveFps,\n );\n\n // Delegate to playbackController if available\n if (this.playbackController) {\n this.playbackController.currentTime = seekTarget;\n this.#userTimeMs = seekTarget * 1000; // User-initiated time change\n return;\n }\n\n // Only root timegroups can have their currentTime set directly\n if (!this.isRootTimegroup) {\n return;\n }\n\n // Validate seek target\n if (Number.isNaN(seekTarget)) {\n return;\n }\n\n // Skip if already at target time (unless processing pending seek or restoring from localStorage)\n if (\n seekTarget === this.#currentTime &&\n !this.#processingPendingSeek &&\n !this.#restoringFromLocalStorage\n ) {\n return;\n }\n\n // Skip if this is the same as pending seek\n if (this.#pendingSeekTime === seekTarget) {\n return;\n }\n\n // Prevent recursive seeks during localStorage restoration\n if (this.#restoringFromLocalStorage && seekTarget !== this.#currentTime) {\n // Allow the restoration seek to proceed, but prevent subsequent seeks\n // The flag will be cleared after the seek completes\n }\n\n // Handle concurrent seeks by queuing pending seek\n // This ensures we only have ONE seek in flight at a time, avoiding wasted work.\n // When scrubbing quickly, intermediate positions are skipped entirely - we don't\n // start work we know will be thrown away.\n if (this.#seekInProgress) {\n this.#pendingSeekTime = seekTarget;\n this.#currentTime = seekTarget;\n this.#userTimeMs = seekTarget * 1000; // User-initiated time change\n return;\n }\n\n // Execute seek - update both source time and user time\n this.#currentTime = seekTarget;\n this.#userTimeMs = seekTarget * 1000; // User-initiated time change\n this.#seekInProgress = true;\n\n // Attach .catch() to prevent unhandled rejection warning - errors are handled by seekTask.onError\n Promise.resolve(this.seekTask.run())\n .catch(() => {})\n .finally(async () => {\n this.#seekInProgress = false;\n\n // CRITICAL: Coordinate animations after seekTask completes\n // This handles seeks from currentTime setter (like localStorage restore)\n const { updateAnimations } = await import(\"./updateAnimations.js\");\n updateAnimations(this);\n\n // Process pending seek if it differs from completed seek\n // This jumps directly to wherever the user ended up, skipping intermediates\n if (\n this.#pendingSeekTime !== undefined &&\n this.#pendingSeekTime !== seekTarget\n ) {\n const pendingTime = this.#pendingSeekTime;\n this.#pendingSeekTime = undefined;\n this.#processingPendingSeek = true;\n try {\n this.currentTime = pendingTime;\n } finally {\n this.#processingPendingSeek = false;\n }\n } else {\n this.#pendingSeekTime = undefined;\n }\n });\n }\n\n /** @public */\n get currentTime() {\n if (this.playbackController) {\n return this.playbackController.currentTime;\n }\n return this.#currentTime ?? 0;\n }\n\n /** @public */\n set currentTimeMs(ms: number) {\n this.currentTime = ms / 1000;\n }\n\n /** @public */\n get currentTimeMs() {\n return this.currentTime * 1000;\n }\n\n /**\n * The time the user last requested via seek/scrub.\n * Preview systems should use this instead of currentTimeMs to avoid\n * seeing intermediate times during batch operations (thumbnails, export).\n * @public\n */\n get userTimeMs(): number {\n return this.#userTimeMs;\n }\n\n /**\n * Seek to a specific time and wait for all frames to be ready.\n * This is the recommended way to seek in tests and programmatic control.\n *\n * Combines seeking (Purpose 3) with frame rendering (Purpose 4) to ensure\n * all visible elements are ready after the seek completes.\n *\n * Updates both the source time AND userTimeMs (what the preview displays).\n *\n * @param timeMs - Time in milliseconds to seek to\n * @returns Promise that resolves when the seek is complete and all visible children are ready\n * @public\n */\n async seek(timeMs: number): Promise<void> {\n // Update user time - this is what the preview should display\n this.#userTimeMs = timeMs;\n\n // Execute seek (Purpose 3)\n this.currentTimeMs = timeMs;\n await this.seekTask.taskComplete;\n\n // Handle localStorage when playbackController delegates seek\n if (this.playbackController) {\n this.saveTimeToLocalStorage(this.currentTime);\n }\n\n // Wait for frame rendering via FrameController\n await this.#frameController.renderFrame(timeMs, {\n onAnimationsUpdate: (root) => {\n updateAnimations(root as typeof this);\n },\n });\n }\n\n /**\n * Optimized seek for render loops.\n * Unlike `seek()`, this:\n * - Skips waitForMediaDurations (already loaded at render setup)\n * - Skips localStorage persistence\n * - Uses FrameController for centralized element coordination\n *\n * Still waits for all content to be ready (Lit updates, element preparation, rendering).\n *\n * @param timeMs - Time in milliseconds to seek to\n * @internal\n */\n async seekForRender(timeMs: number): Promise<SeekForRenderTiming> {\n const t0 = performance.now();\n\n // Set time directly (skip seekTask overhead)\n const newTime = timeMs / 1000;\n this.#userTimeMs = timeMs;\n this.#currentTime = newTime;\n // Sync playbackController if present so currentTime getter returns\n // the correct value regardless of which code path reads it.\n if (this.playbackController) {\n this.playbackController.currentTime = newTime;\n }\n // Sync the base mixin's local time so ownCurrentTimeMs returns the\n // correct value in render clones (which have no playback controller).\n this._setLocalTimeMs(timeMs);\n this.requestUpdate(\"currentTime\");\n\n // First await: let Lit propagate time to children\n const t1 = performance.now();\n await this.updateComplete;\n const updateComplete1Ms = performance.now() - t1;\n\n // Collect all LitElement descendants (not just those with frameTask)\n // This ensures ef-text, ef-captions, and other reactive elements update\n const allLitElements = this.#getAllLitElementDescendants();\n\n // Wait for ALL LitElement descendants to complete their reactive updates\n // This is critical for elements like ef-text and ef-captions that don't have frameTask\n const t2 = performance.now();\n await Promise.all(allLitElements.map((el) => el.updateComplete));\n const updateComplete2Ms = performance.now() - t2;\n\n // OwnCurrentTimeController defers child updates via queueMicrotask.\n // Those microtasks have fired by this point (between await boundaries).\n // Await a second pass of updateComplete to catch those deferred updates.\n const t3 = performance.now();\n await Promise.all(allLitElements.map((el) => el.updateComplete));\n const updateComplete3Ms = performance.now() - t3;\n\n // Wait for ef-text elements to have their segments ready\n // ef-text creates segments asynchronously via requestAnimationFrame\n const textElements = allLitElements.filter(\n (el) => el.tagName === \"EF-TEXT\",\n );\n const t4 = performance.now();\n if (textElements.length > 0) {\n await Promise.all(\n textElements.map((el) => {\n if (\n \"whenSegmentsReady\" in el &&\n typeof el.whenSegmentsReady === \"function\"\n ) {\n return (el as any).whenSegmentsReady();\n }\n return Promise.resolve();\n }),\n );\n\n // Force synchronous layout reflow after text segments are created/updated.\n // offsetHeight triggers layout computation — no need to yield a full rAF\n // (which costs 16-40ms and is throttled in hidden tabs).\n void this.offsetHeight;\n }\n const textSegmentsMs = performance.now() - t4;\n\n // Use FrameController for centralized element coordination\n // This replaces the old distributed frameTask system\n // Animation updates are handled via the onAnimationsUpdate callback\n const t5 = performance.now();\n const frameControllerTiming = await this.#frameController.renderFrame(\n timeMs,\n {\n waitForLitUpdate: false,\n onAnimationsUpdate: (root) => {\n updateAnimations(root as typeof this);\n // CRITICAL: Force style recalculation after updateAnimations sets animation.currentTime\n // Without this, getComputedStyle may return stale values (e.g., opacity: 0 instead of 1)\n // Accessing offsetWidth triggers synchronous style recalc\n void (root as HTMLElement).offsetWidth;\n },\n },\n );\n const renderFrameMs = performance.now() - t5;\n\n // Execute custom frame tasks registered via addFrameTask()\n const t6 = performance.now();\n await this.#executeCustomFrameTasks();\n const frameTasksMs = performance.now() - t6;\n\n const totalMs = performance.now() - t0;\n\n return {\n updateComplete1Ms,\n updateComplete2Ms,\n updateComplete3Ms,\n textSegmentsMs,\n renderFrameMs,\n renderFrameQueryMs: frameControllerTiming?.queryMs ?? 0,\n renderFramePrepareMs: frameControllerTiming?.prepareMs ?? 0,\n renderFrameDrawMs: frameControllerTiming?.renderMs ?? 0,\n renderFrameAnimsMs: frameControllerTiming?.animsMs ?? 0,\n frameTasksMs,\n totalMs,\n };\n }\n\n /**\n * Collects all LitElement descendants recursively.\n * Used by seekForRender to ensure all reactive elements have updated.\n * Prunes subtrees of temporally-invisible elements — their Lit updates\n * still fire via microtasks (OwnCurrentTimeController), so skipping\n * the explicit await is safe.\n */\n #getAllLitElementDescendants(): LitElement[] {\n const result: LitElement[] = [];\n const currentTimeMs = this.currentTimeMs;\n\n const walk = (el: Element) => {\n for (const child of el.children) {\n // Temporal pruning: skip invisible temporal elements and their subtrees\n if (\"startTimeMs\" in child && \"endTimeMs\" in child) {\n const startMs = (child as any).startTimeMs ?? -Infinity;\n const endMs = (child as any).endTimeMs ?? Infinity;\n if (\n endMs > startMs &&\n (currentTimeMs < startMs || currentTimeMs >= endMs)\n ) {\n continue; // skip entire subtree\n }\n }\n\n if (child instanceof LitElement) {\n result.push(child);\n }\n walk(child);\n }\n };\n walk(this);\n\n return result;\n }\n\n /**\n * Determines if this is a root timegroup (no parent timegroups)\n * @public\n */\n get isRootTimegroup(): boolean {\n return !this.parentTimegroup;\n }\n\n /**\n * Property-based frame task callback for React integration.\n * When set, automatically registers the callback as a frame task.\n * Setting a new value automatically cleans up the previous callback.\n * Set to null or undefined to remove the callback.\n *\n * @example\n * // React usage:\n * <Timegroup onFrame={({ ownCurrentTimeMs, percentComplete }) => {\n * // Per-frame updates\n * }} />\n *\n * @public\n */\n get onFrame(): FrameTaskCallback | null {\n return this.#onFrameCallback;\n }\n\n set onFrame(callback: FrameTaskCallback | null | undefined) {\n // Clean up previous callback if exists\n if (this.#onFrameCleanup) {\n this.#onFrameCleanup();\n this.#onFrameCleanup = null;\n }\n this.#onFrameCallback = callback ?? null;\n\n // Register new callback if provided\n if (callback) {\n this.#onFrameCleanup = this.addFrameTask(callback);\n }\n }\n\n /**\n * Register a custom frame task callback that will be executed during frame rendering.\n * The callback receives timing information and can be async or sync.\n * Multiple callbacks can be registered and will execute in parallel.\n *\n * @param callback - Function to execute on each frame\n * @returns A cleanup function that removes the callback when called\n * @public\n */\n addFrameTask(callback: FrameTaskCallback): () => void {\n if (typeof callback !== \"function\") {\n throw new Error(\"Frame task callback must be a function\");\n }\n this.#customFrameTasks.add(callback);\n return () => {\n this.#customFrameTasks.delete(callback);\n };\n }\n\n /**\n * Remove a previously registered custom frame task callback.\n *\n * @param callback - The callback function to remove\n * @public\n */\n removeFrameTask(callback: FrameTaskCallback): void {\n this.#customFrameTasks.delete(callback);\n }\n\n /** @internal */\n saveTimeToLocalStorage(time: number) {\n try {\n if (this.id && this.isConnected && !Number.isNaN(time)) {\n localStorage.setItem(this.storageKey, time.toString());\n }\n } catch (error) {\n log(\"Failed to save time to localStorage\", error);\n }\n }\n\n render() {\n return html`<slot @slotchange=${this.#handleSlotChange}></slot> `;\n }\n\n #handleSlotChange = () => {\n resetTemporalCache();\n flushSequenceDurationCache();\n flushStartTimeMsCache();\n\n this.requestUpdate();\n this.#syncChildListeners();\n this.emitContentChange(\"structure\");\n };\n\n /** @internal */\n loadTimeFromLocalStorage(): number | undefined {\n if (this.id) {\n try {\n const storedValue = localStorage.getItem(this.storageKey);\n if (storedValue === null) {\n return undefined;\n }\n const parsedValue = Number.parseFloat(storedValue);\n // Guard against NaN and Infinity which could cause issues\n if (Number.isNaN(parsedValue) || !Number.isFinite(parsedValue)) {\n return undefined;\n }\n return parsedValue;\n } catch (error) {\n log(\"Failed to load time from localStorage\", error);\n }\n }\n return undefined;\n }\n\n connectedCallback() {\n // CRITICAL: super.connectedCallback() MUST be synchronous for Lit lifecycle to work correctly.\n // Deferring it breaks render clones because updateComplete resolves before Lit initializes.\n //\n // EFTemporal.connectedCallback() handles root detection after Lit Context propagates:\n // - Schedules updateComplete.then(didBecomeRoot check)\n // - Only true roots (no parent after context) create PlaybackController\n //\n // PlaybackController.hostConnected() owns ALL root initialization:\n // - waitForMediaDurations\n // - localStorage time restoration\n // - initial seek\n //\n // This avoids the previous race conditions where both EFTimegroup.connectedCallback\n // and PlaybackController.hostConnected tried to initialize, causing concurrent seeks.\n super.connectedCallback();\n\n // Skip re-initialization when being moved for canvas preview capture.\n // EFTemporal.connectedCallback (super) already guards its own logic;\n // we guard the EFTimegroup-specific parts here (initializer, child\n // listeners, TimegroupController, wrapWithWorkbench).\n if ((this as any).canvasPreviewActive) return;\n\n // Run initializer after element is fully connected and Lit has updated\n // This ensures the element is in a stable state before user code runs\n this.updateComplete.then(() => {\n this.#runInitializer();\n // slotchange may not fire for empty timegroups, so run initial aggregation\n this.#syncChildListeners();\n });\n\n // Defer TimegroupController creation and workbench wrapping to next frame\n // These operations involve DOM queries (closest, getBoundingClientRect) which\n // can be expensive when many elements initialize simultaneously\n requestAnimationFrame(() => {\n requestAnimationFrame(() => {\n if (this.parentTimegroup) {\n new TimegroupController(this.parentTimegroup, this);\n }\n\n if (this.shouldWrapWithWorkbench()) {\n this.wrapWithWorkbench();\n }\n });\n });\n }\n\n /**\n * Called when this timegroup becomes a root (no parent timegroup).\n * Sets up the playback listener after PlaybackController is created.\n * @internal\n */\n didBecomeRoot() {\n super.didBecomeRoot();\n this.#setupPlaybackListener();\n }\n\n /**\n * Setup listener on playbackController to sync userTimeMs during playback.\n */\n #setupPlaybackListener(): void {\n // Already setup or no controller\n if (this.#playbackListener || !this.playbackController) return;\n\n this.#playbackListener = (event: PlaybackControllerUpdateEvent) => {\n // Update userTimeMs during playback time changes\n // Clone-timeline: captures use separate clones, so Prime-timeline updates freely\n // Canvas preview reads userTimeMs to know what to render\n if (\n event.property === \"currentTimeMs\" &&\n typeof event.value === \"number\"\n ) {\n this.#userTimeMs = event.value;\n }\n };\n\n this.playbackController.addListener(this.#playbackListener);\n }\n\n /**\n * Remove playback listener on disconnect.\n */\n #removePlaybackListener(): void {\n if (this.#playbackListener && this.playbackController) {\n this.playbackController.removeListener(this.#playbackListener);\n }\n this.#playbackListener = null;\n }\n\n #previousDurationMs = 0;\n\n protected updated(changedProperties: PropertyValues): void {\n super.updated(changedProperties);\n\n if (changedProperties.has(\"mode\") || changedProperties.has(\"overlapMs\")) {\n durationCache.delete(this);\n }\n\n if (this.#previousDurationMs !== this.durationMs) {\n this.#previousDurationMs = this.durationMs;\n // Render clones are sequenced via seekForRender — don't trigger autonomous re-renders.\n // This prevents FrameController.abort() from interrupting an in-progress seekForRender.\n if (!this.hasAttribute(\"data-no-playback-controller\")) {\n this.#runThrottledFrameTask();\n }\n }\n }\n\n disconnectedCallback() {\n super.disconnectedCallback();\n\n // Skip teardown when being moved for canvas preview capture.\n // EFTemporal.disconnectedCallback (super) already guards its own logic.\n if ((this as any).canvasPreviewActive) return;\n\n this.#resizeObserver?.disconnect();\n this.#removePlaybackListener();\n for (const child of this.#trackedChildren) {\n child.removeEventListener(\n \"readystatechange\",\n this.#childReadyStateHandler,\n );\n child.removeEventListener(\n \"contentchange\",\n this.#childContentChangeHandler,\n );\n }\n this.#trackedChildren.clear();\n this.#qualityUpgradeScheduler.dispose();\n }\n\n /**\n * Render the timegroup to an MP4 video file and trigger download.\n * Captures each frame at the specified fps, encodes using WebCodecs via\n * MediaBunny, and downloads the resulting video.\n *\n * Uses dynamic import to only load render utilities in browser context.\n *\n * @param options - Rendering options (fps, codec, bitrate, filename, etc.)\n * @returns Promise that resolves when video is downloaded\n * @public\n */\n async renderToVideo(\n options?: RenderToVideoOptions,\n ): Promise<Uint8Array | undefined> {\n // Dynamic import - only loads in browser context when actually called\n const { renderTimegroupToVideo } =\n await import(\"../preview/renderTimegroupToVideo.js\");\n return renderTimegroupToVideo(this, options);\n }\n\n /**\n * Runs the initializer function with validation for synchronous execution and time budget.\n * Only runs once per instance. Safe to call multiple times - will skip if already run.\n * @throws Error if initializer returns a Promise (async not allowed)\n * @throws Error if initializer takes more than INITIALIZER_ERROR_THRESHOLD_MS\n * @internal\n */\n #runInitializer(): void {\n // Skip if no initializer or already run\n if (!this.initializer || this.#initializerHasRun) {\n return;\n }\n\n // Mark as run before executing to prevent recursion\n this.#initializerHasRun = true;\n\n const startTime = performance.now();\n const result: unknown = this.initializer(this);\n const elapsed = performance.now() - startTime;\n\n // Check for async (Promise return) - initializers MUST be synchronous\n if (\n result !== undefined &&\n result !== null &&\n typeof (result as any).then === \"function\"\n ) {\n throw new Error(\n \"Timeline initializer must be synchronous. \" +\n \"Do not return a Promise from the initializer function.\",\n );\n }\n\n // Time budget enforcement - initializers run for EVERY instance\n if (elapsed > INITIALIZER_ERROR_THRESHOLD_MS) {\n throw new Error(\n `Timeline initializer took ${elapsed.toFixed(1)}ms, exceeding the ${INITIALIZER_ERROR_THRESHOLD_MS}ms limit. ` +\n \"Initializers must be fast - move expensive work outside the initializer.\",\n );\n }\n\n if (elapsed > INITIALIZER_WARN_THRESHOLD_MS) {\n console.warn(\n `[ef-timegroup] Initializer took ${elapsed.toFixed(1)}ms, exceeding ${INITIALIZER_WARN_THRESHOLD_MS}ms. ` +\n \"Consider optimizing for better render performance.\",\n );\n }\n }\n\n /**\n * Copy captionsData property from original to clone.\n * cloneNode() only copies attributes, not JavaScript properties.\n * captionsData is often set via JS (e.g., captionsEl.captionsData = {...}),\n * so we must manually copy it to the cloned elements.\n * @internal\n */\n #copyCaptionsData(original: Element, clone: Element): void {\n // Find matching caption elements by position (querySelectorAll returns in document order)\n const originalCaptions = original.querySelectorAll(\"ef-captions\");\n const cloneCaptions = clone.querySelectorAll(\"ef-captions\");\n\n for (\n let i = 0;\n i < originalCaptions.length && i < cloneCaptions.length;\n i++\n ) {\n const origCap = originalCaptions[i] as any;\n const cloneCap = cloneCaptions[i] as any;\n\n // Copy loaded captions data from any source (JS property, captions-src, script element).\n // The loaded data is stored in unifiedCaptionsDataTask.value after async loading.\n // Setting captionsData on the clone gives it Priority 1, bypassing async loading.\n const loadedData =\n origCap.captionsData ?? origCap.unifiedCaptionsDataTask?.value;\n if (loadedData) {\n cloneCap.captionsData = loadedData;\n }\n }\n }\n\n /**\n * Copy ef-text _textContent property from original to cloned elements.\n * This MUST be called BEFORE elements upgrade (before updateComplete)\n * because splitText() runs in connectedCallback and will clear segments\n * if _textContent is null/empty.\n * @internal\n */\n #copyTextContent(original: Element, clone: Element): void {\n const originalTexts = original.querySelectorAll(\"ef-text\");\n const cloneTexts = clone.querySelectorAll(\"ef-text\");\n\n for (let i = 0; i < originalTexts.length && i < cloneTexts.length; i++) {\n const origText = originalTexts[i] as any;\n const cloneText = cloneTexts[i] as any;\n\n // Copy _textContent if it exists\n // This is a private property, so we access it via any\n if (origText._textContent !== undefined) {\n cloneText._textContent = origText._textContent;\n }\n // Also copy the segments getter to ensure we can read them\n if (origText._templateElement !== undefined) {\n cloneText._templateElement = origText._templateElement;\n }\n }\n }\n\n /**\n * Copy ef-text-segment properties from original to cloned elements.\n * segmentText and other properties are set via JS, not attributes,\n * so we must manually copy them to the cloned elements.\n * @internal\n */\n async #copyTextSegmentData(original: Element, clone: Element): Promise<void> {\n // Find matching text segment elements by position\n const originalSegments = original.querySelectorAll(\"ef-text-segment\");\n const cloneSegments = clone.querySelectorAll(\"ef-text-segment\");\n\n const updatePromises: Promise<any>[] = [];\n\n for (\n let i = 0;\n i < originalSegments.length && i < cloneSegments.length;\n i++\n ) {\n const origSeg = originalSegments[i] as any;\n const cloneSeg = cloneSegments[i] as any;\n\n // Copy all segment properties\n if (origSeg.segmentText !== undefined) {\n cloneSeg.segmentText = origSeg.segmentText;\n }\n if (origSeg.segmentIndex !== undefined) {\n cloneSeg.segmentIndex = origSeg.segmentIndex;\n }\n if (origSeg.staggerOffsetMs !== undefined) {\n cloneSeg.staggerOffsetMs = origSeg.staggerOffsetMs;\n }\n if (origSeg.segmentStartMs !== undefined) {\n cloneSeg.segmentStartMs = origSeg.segmentStartMs;\n }\n if (origSeg.segmentEndMs !== undefined) {\n cloneSeg.segmentEndMs = origSeg.segmentEndMs;\n }\n\n // Wait for Lit to render the updated segmentText to shadow DOM\n if (cloneSeg.updateComplete) {\n updatePromises.push(cloneSeg.updateComplete);\n }\n }\n\n // Wait for all segment updates to complete\n await Promise.all(updatePromises);\n }\n\n /**\n * Wait for all ef-captions elements to have their data loaded.\n * This is needed because EFCaptions is not an EFMedia, so waitForMediaDurations doesn't cover it.\n * Used by createRenderClone to ensure captions are ready before rendering.\n * @internal\n */\n async #waitForCaptionsData(root: Element): Promise<void> {\n // Find all ef-captions elements (including nested in timegroups)\n const captionsElements = root.querySelectorAll(\"ef-captions\");\n if (captionsElements.length === 0) return;\n\n // Wait for each caption element's data to load\n // Use duck-typing to check for loadCaptionsData method\n const waitPromises: Promise<unknown>[] = [];\n for (const el of captionsElements) {\n const captions = el as any;\n // Try new async method first\n if (typeof captions.loadCaptionsData === \"function\") {\n waitPromises.push(captions.loadCaptionsData().catch(() => {}));\n }\n // Fallback to task if present\n else if (captions.unifiedCaptionsDataTask?.taskComplete) {\n waitPromises.push(\n captions.unifiedCaptionsDataTask.taskComplete.catch(() => {}),\n );\n }\n }\n\n if (waitPromises.length > 0) {\n await Promise.all(waitPromises);\n }\n }\n\n /**\n * Copies initializers from original timegroup tree to cloned timegroup tree.\n * Handles both the root timegroup and all nested timegroups recursively.\n * @internal\n */\n async #copyInitializersToClone(\n original: EFTimegroup,\n clone: EFTimegroup,\n ): Promise<void> {\n // Copy and execute initializer at this level\n if (original.initializer) {\n clone.initializer = original.initializer;\n // Explicitly run the initializer on the clone\n // Wait for Lit update cycle to complete first so the element is stable\n await clone.updateComplete;\n clone.#runInitializer();\n }\n\n // Find all nested timegroups in both original and clone\n const originalNested = Array.from(\n original.querySelectorAll(\"ef-timegroup\"),\n ) as EFTimegroup[];\n const cloneNested = Array.from(\n clone.querySelectorAll(\"ef-timegroup\"),\n ) as EFTimegroup[];\n\n // Match up nested timegroups by index (they should correspond 1:1)\n for (let i = 0; i < originalNested.length && i < cloneNested.length; i++) {\n const origNested = originalNested[i];\n const cloneNestedItem = cloneNested[i];\n\n if (origNested!.initializer) {\n cloneNestedItem!.initializer = origNested!.initializer;\n await cloneNestedItem!.updateComplete;\n cloneNestedItem!.#runInitializer();\n }\n }\n }\n\n /**\n * Create an independent clone of this timegroup for rendering.\n * The clone is a fully functional ef-timegroup with its own animations\n * and time state, isolated from the original (Prime-timeline).\n *\n * OPTIONAL: An initializer can be set via `timegroup.initializer = (tg) => { ... }`\n * to re-run JavaScript setup (frame callbacks, React components) on each clone.\n *\n * This enables:\n * - Rendering without affecting user's preview position\n * - Concurrent renders with different clones\n * - Re-running JavaScript setup on each clone (if initializer is provided)\n *\n * @returns Promise resolving to clone, container, and cleanup function\n * @throws Error if initializer is async or takes too long\n * @public\n */\n async createRenderClone(): Promise<RenderCloneResult> {\n const factory = getCloneFactory(this);\n\n if (factory) {\n return this.#createRenderCloneFromFactory(factory);\n }\n return this.#createRenderCloneFromDOM();\n }\n\n /**\n * Wait for all LitElement descendants to update and for text segments to be ready.\n * This ensures the clone is fully initialized before rendering.\n * @internal\n */\n async #waitForDescendants(actualClone: EFTimegroup): Promise<void> {\n // Wait for all LitElement descendants\n const allLitElements = Array.from(actualClone.querySelectorAll(\"*\")).filter(\n (el) => el instanceof LitElement,\n ) as LitElement[];\n await Promise.all(allLitElements.map((el) => el.updateComplete));\n\n // Wait for text segments\n const textElements = allLitElements.filter(\n (el) => el.tagName === \"EF-TEXT\",\n );\n if (textElements.length > 0) {\n await Promise.all(\n textElements.map((el) => {\n if (\n \"whenSegmentsReady\" in el &&\n typeof el.whenSegmentsReady === \"function\"\n ) {\n return (el as any).whenSegmentsReady();\n }\n return Promise.resolve();\n }),\n );\n void actualClone.offsetHeight;\n await new Promise((resolve) => requestAnimationFrame(resolve));\n }\n }\n\n /**\n * Factory path: mount a fresh component tree (React, etc.) to produce\n * a fully functional clone. The factory is responsible for rendering\n * the component into the container and returning the root ef-timegroup.\n */\n async #createRenderCloneFromFactory(\n factory: NonNullable<ReturnType<typeof getCloneFactory>>,\n ): Promise<RenderCloneResult> {\n const width = this.offsetWidth || 1920;\n const height = this.offsetHeight || 1080;\n\n const container = document.createElement(\"div\");\n container.className = \"ef-render-clone-container\";\n container.style.cssText = `\n position: fixed;\n left: -9999px;\n top: 0;\n width: ${width}px;\n height: ${height}px;\n pointer-events: none;\n overflow: hidden;\n `;\n\n // Preserve ef-configuration context\n let renderTarget: HTMLElement = container;\n const originalConfig = this.closest(\"ef-configuration\");\n if (originalConfig) {\n const configClone = originalConfig.cloneNode(false) as HTMLElement;\n container.appendChild(configClone);\n renderTarget = configClone;\n }\n\n document.body.appendChild(container);\n\n // Mount the component tree — this produces a live ef-timegroup\n const { timegroup: actualClone, cleanup: factoryCleanup } =\n factory(renderTarget);\n\n if (!actualClone) {\n throw new Error(\n \"Clone factory did not produce an ef-timegroup. \" +\n \"Ensure the factory renders a component containing a Timegroup.\",\n );\n }\n\n // Mark as render clone\n actualClone.setAttribute(\"data-no-workbench\", \"true\");\n actualClone.setAttribute(\"data-no-playback-controller\", \"true\");\n actualClone.style.width = `${width}px`;\n actualClone.style.height = `${height}px`;\n actualClone.style.display = \"block\";\n\n // Wait for custom elements to upgrade and Lit to update\n await customElements.whenDefined(\"ef-timegroup\");\n customElements.upgrade(container);\n await actualClone.updateComplete;\n\n // Wait for all descendants to be ready\n await this.#waitForDescendants(actualClone);\n\n // Finalize clone: parent-child relationships, lock root, remove PlaybackController\n await this.#finalizeRenderClone(actualClone);\n\n return {\n clone: actualClone,\n container,\n cleanup: () => {\n container.remove();\n factoryCleanup();\n },\n };\n }\n\n /**\n * DOM path: deep clone the DOM tree and copy JavaScript properties.\n * Used for vanilla HTML/JS timelines that don't have a factory registered.\n */\n async #createRenderCloneFromDOM(): Promise<RenderCloneResult> {\n // 1. Create offscreen container\n const container = document.createElement(\"div\");\n container.className = \"ef-render-clone-container\";\n container.style.cssText = `\n position: fixed;\n left: -9999px;\n top: 0;\n width: ${this.offsetWidth || 1920}px;\n height: ${this.offsetHeight || 1080}px;\n pointer-events: none;\n overflow: hidden;\n `;\n\n // 2. Deep clone the DOM\n const cloneEl = this.cloneNode(true) as EFTimegroup;\n // Strip all id attributes from clone tree to prevent duplicate IDs in the document\n cloneEl.removeAttribute(\"id\");\n for (const el of cloneEl.querySelectorAll(\"[id]\")) {\n el.removeAttribute(\"id\");\n }\n cloneEl.setAttribute(\"data-no-workbench\", \"true\");\n cloneEl.setAttribute(\"data-no-playback-controller\", \"true\");\n\n const width = this.offsetWidth || 1920;\n const height = this.offsetHeight || 1080;\n cloneEl.style.width = `${width}px`;\n cloneEl.style.height = `${height}px`;\n cloneEl.style.display = \"block\";\n\n // 2b. Copy JavaScript properties that aren't cloned by cloneNode()\n this.#copyCaptionsData(this, cloneEl);\n this.#copyTextContent(this, cloneEl);\n\n // 3. Preserve ef-configuration context\n const originalConfig = this.closest(\"ef-configuration\");\n if (originalConfig) {\n const configClone = originalConfig.cloneNode(false) as HTMLElement;\n configClone.appendChild(cloneEl);\n container.appendChild(configClone);\n } else {\n container.appendChild(cloneEl);\n }\n\n document.body.appendChild(container);\n\n // Wait for custom elements to upgrade\n await cloneEl.updateComplete;\n\n // Copy initializers and run them on clones\n await this.#copyInitializersToClone(this, cloneEl);\n\n // Copy text segment data\n await this.#copyTextSegmentData(this, cloneEl);\n\n // Find the actual timegroup (initializer may have replaced the DOM)\n let actualClone = container.querySelector(\"ef-timegroup\") as EFTimegroup;\n if (!actualClone) {\n throw new Error(\n \"No ef-timegroup found after initializer. \" +\n \"Ensure your initializer renders a Timegroup (React) or does not remove the cloned element (vanilla JS).\",\n );\n }\n\n // Wait for custom elements to upgrade\n await customElements.whenDefined(\"ef-timegroup\");\n customElements.upgrade(container);\n actualClone = container.querySelector(\"ef-timegroup\") as EFTimegroup;\n if (!actualClone) {\n throw new Error(\"ef-timegroup element lost after upgrade\");\n }\n\n // Wait for LitElement updates\n await actualClone.updateComplete;\n\n // Wait for all descendants to be ready\n await this.#waitForDescendants(actualClone);\n\n // Copy text segment data again after initializer may have replaced DOM\n await this.#copyTextSegmentData(this, actualClone);\n\n // Finalize clone\n await this.#finalizeRenderClone(actualClone);\n\n return {\n clone: actualClone,\n container,\n cleanup: () => {\n container.remove();\n const reactRoot = (actualClone as any)._reactRoot;\n if (reactRoot) {\n queueMicrotask(() => {\n reactRoot.unmount();\n });\n }\n },\n };\n }\n\n /**\n * Shared finalization for both factory and DOM clone paths:\n * - Set up parent-child temporal relationships\n * - Lock root timegroup references\n * - Wait for media durations and captions\n * - Remove PlaybackController\n * - Initial seek to frame 0\n */\n async #finalizeRenderClone(actualClone: EFTimegroup): Promise<void> {\n // Set up parent-child relationships for temporal elements\n const setupParentChildRelationships = (\n parent: EFTimegroup,\n root: EFTimegroup,\n ) => {\n for (const child of parent.children) {\n if (child.tagName === \"EF-TIMEGROUP\") {\n const childTG = child as EFTimegroup;\n childTG.parentTimegroup = parent;\n childTG.rootTimegroup = root;\n (childTG as any).lockRootTimegroup();\n setupParentChildRelationships(childTG, root);\n } else if (\"parentTimegroup\" in child && \"rootTimegroup\" in child) {\n const temporal = child as TemporalMixinInterface & HTMLElement;\n temporal.parentTimegroup = parent;\n temporal.rootTimegroup = root;\n if (\n \"lockRootTimegroup\" in temporal &&\n typeof temporal.lockRootTimegroup === \"function\"\n ) {\n temporal.lockRootTimegroup();\n }\n } else if (child instanceof Element) {\n setupInContainer(child, parent, root);\n }\n }\n };\n\n const setupInContainer = (\n container: Element,\n nearestParentTG: EFTimegroup,\n root: EFTimegroup,\n ) => {\n for (const child of container.children) {\n if (child.tagName === \"EF-TIMEGROUP\") {\n const childTG = child as EFTimegroup;\n childTG.parentTimegroup = nearestParentTG;\n childTG.rootTimegroup = root;\n (childTG as any).lockRootTimegroup();\n setupParentChildRelationships(childTG, root);\n } else if (\"parentTimegroup\" in child && \"rootTimegroup\" in child) {\n const temporal = child as TemporalMixinInterface & HTMLElement;\n temporal.parentTimegroup = nearestParentTG;\n temporal.rootTimegroup = root;\n if (\n \"lockRootTimegroup\" in temporal &&\n typeof temporal.lockRootTimegroup === \"function\"\n ) {\n temporal.lockRootTimegroup();\n }\n } else if (child instanceof Element) {\n setupInContainer(child, nearestParentTG, root);\n }\n }\n };\n\n actualClone.rootTimegroup = actualClone;\n setupParentChildRelationships(actualClone, actualClone);\n\n await actualClone.updateComplete;\n\n // Lock root references to prevent Lit Context from overwriting\n actualClone.rootTimegroup = actualClone;\n (actualClone as any).lockRootTimegroup();\n const finalizeRootTimegroup = (el: Element) => {\n if (\"rootTimegroup\" in el && \"lockRootTimegroup\" in el) {\n (el as any).rootTimegroup = actualClone;\n (el as any).lockRootTimegroup();\n }\n for (const child of el.children) {\n finalizeRootTimegroup(child);\n }\n };\n finalizeRootTimegroup(actualClone);\n\n await actualClone.waitForMediaDurations();\n await this.#waitForCaptionsData(actualClone);\n\n // Remove PlaybackController — render clones use seekForRender directly\n if (actualClone.playbackController) {\n actualClone.playbackController.remove();\n actualClone.playbackController = undefined;\n }\n\n // Initial seek to frame 0\n await actualClone.seek(0);\n }\n\n /** @internal */\n get storageKey() {\n if (!this.id) {\n throw new Error(\"Timegroup must have an id to use localStorage.\");\n }\n return `ef-timegroup-${this.id}`;\n }\n\n /** @internal */\n get intrinsicDurationMs() {\n if (this.hasExplicitDuration) {\n return this.explicitDurationMs;\n }\n return undefined;\n }\n\n /** @internal */\n get hasOwnDuration() {\n return hasOwnDurationForMode(this.mode, this.hasExplicitDuration);\n }\n\n // ============================================================================\n // Purpose 1: Composition Rules Implementation\n // ============================================================================\n\n /** @public */\n get durationMs(): number {\n // Fixed mode delegates to parent class durationMs which handles trimming, source in/out, etc.\n if (this.mode === \"fixed\") {\n return super.durationMs;\n }\n\n // Evaluate duration semantics based on mode (Purpose 1)\n // childTemporals returns TemporalMixinInterface[], but we need HTMLElement intersection\n const childTemporalsAsElements = this.childTemporals as Array<\n TemporalMixinInterface & HTMLElement\n >;\n return evaluateDurationForMode(this, this.mode, childTemporalsAsElements);\n }\n\n // ============================================================================\n // Purpose 4: Frame Rendering - What Happens Each Frame\n // ============================================================================\n\n #mediaDurationsPromise: Promise<void> | undefined = undefined;\n\n /** @internal */\n async waitForMediaDurations(signal?: AbortSignal) {\n // Check abort before starting\n signal?.throwIfAborted();\n\n // Start loading media durations in background, but don't block if already in progress\n // This prevents multiple concurrent calls from creating redundant work\n if (!this.#mediaDurationsPromise) {\n this.#mediaDurationsPromise = this.#waitForMediaDurations(signal).catch(\n (err) => {\n // Re-throw AbortError to propagate cancellation\n if (err instanceof DOMException && err.name === \"AbortError\") {\n this.#mediaDurationsPromise = undefined;\n throw err;\n }\n console.error(\n `[EFTimegroup] waitForMediaDurations failed for ${this.id || \"unnamed\"}:`,\n err,\n );\n // Clear promise on error so it can be retried\n this.#mediaDurationsPromise = undefined;\n throw err;\n },\n );\n }\n\n // If signal is provided and aborted, throw immediately\n if (signal?.aborted) {\n throw new DOMException(\"Aborted\", \"AbortError\");\n }\n\n return this.#mediaDurationsPromise;\n }\n\n /**\n * Wait for all media elements to load their initial segments.\n * Ideally we would only need the extracted index json data, but\n * that caused issues with constructing audio data. We had negative durations\n * in calculations and it was not clear why.\n */\n async #waitForMediaDurations(signal?: AbortSignal) {\n return withSpan(\n \"timegroup.waitForMediaDurations\",\n {\n timegroupId: this.id || \"unknown\",\n mode: this.mode,\n },\n undefined,\n async (span) => {\n // Check abort before starting\n signal?.throwIfAborted();\n\n // Don't wait for updateComplete during initialization - it causes deadlocks with nested timegroups\n // Instead, use a short delay to let elements connect, then scan for media elements\n // If elements aren't ready yet, we'll retry or they'll be picked up on the next update cycle\n await new Promise<void>((resolve, reject) => {\n if (signal?.aborted) {\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n return;\n }\n\n const abortHandler = () => {\n clearTimeout(timeoutId);\n cancelAnimationFrame(rafId2);\n cancelAnimationFrame(rafId1);\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n };\n signal?.addEventListener(\"abort\", abortHandler, { once: true });\n\n let rafId1: number;\n let rafId2: number;\n let timeoutId: ReturnType<typeof setTimeout>;\n\n // Use multiple animation frames to ensure DOM is ready, but don't wait for all children\n rafId1 = requestAnimationFrame(() => {\n if (signal?.aborted) {\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n return;\n }\n rafId2 = requestAnimationFrame(() => {\n if (signal?.aborted) {\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n return;\n }\n // Small additional delay to let custom elements upgrade\n timeoutId = setTimeout(() => {\n signal?.removeEventListener(\"abort\", abortHandler);\n resolve();\n }, 10);\n });\n });\n });\n\n // Check abort after delay\n signal?.throwIfAborted();\n\n const mediaElements = deepGetMediaElements(this);\n if (isTracingEnabled()) {\n span.setAttribute(\"mediaElementsCount\", mediaElements.length);\n }\n\n // Check abort after getting elements\n signal?.throwIfAborted();\n\n // Then, we must await the fragmentIndexTask to ensure all media elements have their\n // fragment index loaded, which is where their duration is parsed from.\n // Use Promise.allSettled with timeout to avoid blocking if asset server is slow\n const mediaLoadStart = Date.now();\n const MEDIA_LOAD_TIMEOUT_MS = 30000; // 30 second timeout per element\n\n const loadPromises = mediaElements.map(async (m, index) => {\n // Check abort before each element\n signal?.throwIfAborted();\n\n const elementStart = Date.now();\n try {\n // Use getMediaEngine async method if available\n if (typeof m.getMediaEngine === \"function\") {\n // Add timeout to prevent indefinite blocking\n const timeoutPromise = new Promise<undefined>((_, reject) => {\n if (signal?.aborted) {\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n return;\n }\n const timeoutId = setTimeout(\n () =>\n reject(\n new Error(\n `Media element ${index} load timeout after ${MEDIA_LOAD_TIMEOUT_MS}ms`,\n ),\n ),\n MEDIA_LOAD_TIMEOUT_MS,\n );\n signal?.addEventListener(\n \"abort\",\n () => {\n clearTimeout(timeoutId);\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n },\n { once: true },\n );\n });\n\n await Promise.race([m.getMediaEngine(signal), timeoutPromise]);\n }\n // Fallback: check status and use taskComplete\n else if (m.mediaEngineTask) {\n // Status: INITIAL=0, PENDING=1, COMPLETE=2, ERROR=3\n const status = m.mediaEngineTask.status;\n\n // Already complete or errored - no need to wait\n if (status === 2 || status === 3) {\n return;\n }\n\n // Attach .catch() to taskComplete to prevent unhandled rejection\n const taskPromise = m.mediaEngineTask.taskComplete;\n taskPromise?.catch(() => {});\n\n if (taskPromise) {\n const timeoutPromise = new Promise<undefined>((_, reject) => {\n if (signal?.aborted) {\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n return;\n }\n const timeoutId = setTimeout(\n () =>\n reject(\n new Error(\n `Media element ${index} load timeout after ${MEDIA_LOAD_TIMEOUT_MS}ms`,\n ),\n ),\n MEDIA_LOAD_TIMEOUT_MS,\n );\n signal?.addEventListener(\n \"abort\",\n () => {\n clearTimeout(timeoutId);\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n },\n { once: true },\n );\n });\n\n await Promise.race([taskPromise, timeoutPromise]);\n }\n }\n } catch (error) {\n // Re-throw AbortError to propagate cancellation\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n // Log only if tracing is enabled to reduce console noise\n if (isTracingEnabled()) {\n const elementElapsed = Date.now() - elementStart;\n console.error(\n `[EFTimegroup] Media element ${index} failed after ${elementElapsed}ms:`,\n error,\n );\n }\n // Don't throw - continue with other elements\n }\n });\n\n const results = await Promise.allSettled(loadPromises);\n\n // Check if any were aborted\n const aborted = results.some(\n (r) =>\n r.status === \"rejected\" &&\n r.reason instanceof DOMException &&\n r.reason.name === \"AbortError\",\n );\n if (aborted) {\n throw new DOMException(\"Aborted\", \"AbortError\");\n }\n\n // Log any failures but don't throw - we want to continue even if some media fails\n const failures = results.filter((r) => r.status === \"rejected\");\n if (failures.length > 0 && isTracingEnabled()) {\n const mediaLoadElapsed = Date.now() - mediaLoadStart;\n console.warn(\n `[EFTimegroup] ${failures.length} media elements failed to load in ${mediaLoadElapsed}ms:`,\n failures.map((r) => (r.status === \"rejected\" ? r.reason : null)),\n );\n }\n\n // After waiting for durations, we must force some updates to cascade and ensure all temporal elements\n // have correct durations and start times. It is not ideal that we have to do this inside here,\n // but it is the best current way to ensure that all temporal elements have correct durations and start times.\n\n // Next, we must flush the startTimeMs cache to ensure all media elements have their\n // startTimeMs parsed fresh, otherwise the startTimeMs is cached per animation frame.\n flushStartTimeMsCache();\n\n // Flush duration cache since child durations may have changed\n flushSequenceDurationCache();\n\n // Request an update to the currentTime of this group, ensuring that time updates will cascade\n // down to children, forcing sequence groups to arrange correctly.\n // This also makes the filmstrip update correctly.\n // Defer using setTimeout(0) to avoid Lit warning about scheduling updates after update completed.\n // This method can be called during a task or after an update cycle completes, and using\n // setTimeout ensures we're completely outside any Lit update cycle.\n setTimeout(() => this.requestUpdate(\"currentTime\"), 0);\n // Note: We don't await updateComplete here during initialization to avoid deadlocks.\n // The update will complete asynchronously, and sequence groups will arrange correctly\n // once all timegroups have finished initializing. During normal operation (seeks, etc.),\n // the caller will wait for updateComplete explicitly if needed.\n },\n );\n }\n\n /** @internal */\n get childTemporals() {\n return shallowGetTemporalElements(this);\n }\n\n /**\n * Returns true if the timegroup should be wrapped with a workbench.\n *\n * A timegroup should be wrapped with a workbench if:\n * - It's being rendered (EF_RENDERING), OR\n * - The workbench property is set to true\n *\n * If the timegroup is already wrapped in a context provider like ef-preview,\n * it should NOT be wrapped in a workbench.\n * @internal\n */\n shouldWrapWithWorkbench() {\n // Never wrap when being captured by canvas preview — the element is\n // temporarily reparented for native rendering and must not spawn a\n // new workbench (which would read \"canvas\" from localStorage and\n // re-enter initCanvasMode, creating an infinite loop).\n if ((this as any).canvasPreviewActive) {\n return false;\n }\n\n // Only root timegroups should wrap with workbench\n if (!this.isRootTimegroup) {\n return false;\n }\n\n // Never wrap with workbench when inside a canvas\n // Canvas manages its own layout and coordinate system\n if (this.closest(\"ef-canvas\") !== null) {\n return false;\n }\n\n // Never wrap if already inside preview, workbench, or preview context\n if (\n this.closest(\"ef-preview\") !== null ||\n this.closest(\"ef-workbench\") !== null ||\n this.closest(\"ef-preview-context\") !== null\n ) {\n return false;\n }\n\n // Skip wrapping in test contexts or if explicitly disabled\n // Test contexts and render clones provide their own rendering infrastructure\n if (\n this.closest(\"test-context\") !== null ||\n this.hasAttribute(\"data-no-workbench\")\n ) {\n return false;\n }\n\n // During rendering, never wrap with workbench - timegroups can seek without it\n const isRendering = EF_RENDERING?.() === true;\n if (isRendering) {\n return false;\n }\n\n // Check URL param to disable workbench (only applies in non-rendering mode)\n if (typeof window !== \"undefined\") {\n const params = new URLSearchParams(window.location.search);\n if (\n params.get(\"noWorkbench\") === \"true\" ||\n params.get(\"no-workbench\") === \"true\"\n ) {\n return false;\n }\n }\n\n // Respect the explicit workbench property\n return this.workbench;\n }\n\n /** @internal */\n wrapWithWorkbench() {\n const workbench = document.createElement(\"ef-workbench\") as any;\n const parent = this.parentElement;\n\n // When in rendering mode, immediately set rendering=true before insertion\n // This prevents the workbench UI from ever being visible in rendered frames\n if (EF_RENDERING()) {\n // Use setAttribute to ensure it's set before the element connects and renders\n workbench.setAttribute(\"rendering\", \"\");\n workbench.rendering = true;\n }\n\n // Apply explicit sizing to ensure workbench fills its container\n if (parent === document.body) {\n // Direct child of body: use viewport units with fixed positioning\n workbench.style.position = \"fixed\";\n workbench.style.top = \"0\";\n workbench.style.left = \"0\";\n workbench.style.width = \"100vw\";\n workbench.style.height = \"100vh\";\n workbench.style.zIndex = \"0\";\n } else {\n // Embedded in container: ensure it fills the container\n // Use absolute positioning to prevent content-based sizing\n workbench.style.position = \"absolute\";\n workbench.style.top = \"0\";\n workbench.style.left = \"0\";\n workbench.style.width = \"100%\";\n workbench.style.height = \"100%\";\n }\n\n parent?.append(workbench);\n if (!this.hasAttribute(\"id\")) {\n this.setAttribute(\"id\", \"root-timegroup\");\n }\n\n // Create pan-zoom for selection overlay support\n // Must be in light DOM so canvas can find it via closest()\n const panZoom = document.createElement(\"ef-pan-zoom\");\n panZoom.id = \"workbench-panzoom\";\n panZoom.setAttribute(\"slot\", \"canvas\");\n panZoom.setAttribute(\"auto-fit\", \"\"); // Fit content to view on first render\n panZoom.style.width = \"100%\";\n panZoom.style.height = \"100%\";\n\n // Create canvas wrapper for selection/highlighting support\n // Get dimensions from the timegroup for explicit canvas sizing\n const rect = this.getBoundingClientRect();\n const canvas = document.createElement(\"ef-canvas\");\n canvas.id = \"workbench-canvas\";\n canvas.style.width = `${rect.width}px`;\n canvas.style.height = `${rect.height}px`;\n canvas.style.display = \"block\";\n\n // Move timegroup into canvas, canvas into pan-zoom\n canvas.append(this as unknown as Element);\n panZoom.append(canvas);\n workbench.append(panZoom);\n\n // Add hierarchy panel - targets canvas for selection support\n const hierarchy = document.createElement(\"ef-hierarchy\");\n hierarchy.setAttribute(\"slot\", \"hierarchy\");\n hierarchy.setAttribute(\"target\", \"workbench-canvas\");\n hierarchy.setAttribute(\"header\", \"Scenes\");\n workbench.append(hierarchy);\n\n // Add filmstrip/timeline - targets timegroup for playback\n const filmstrip = document.createElement(\"ef-filmstrip\");\n filmstrip.setAttribute(\"slot\", \"timeline\");\n filmstrip.setAttribute(\"target\", this.id);\n workbench.append(filmstrip);\n }\n\n /**\n * Returns media elements for playback audio rendering\n * For standalone media, returns [this]; for timegroups, returns all descendants\n * Used by PlaybackController for audio-driven playback\n * @internal\n */\n getMediaElements(): EFMedia[] {\n return deepGetMediaElements(this);\n }\n\n /**\n * Render audio buffer for playback\n * Called by PlaybackController during live playback\n * Delegates to shared renderTemporalAudio utility for consistent behavior\n * @internal\n */\n async renderAudio(\n fromMs: number,\n toMs: number,\n signal?: AbortSignal,\n ): Promise<AudioBuffer> {\n return renderTemporalAudio(this, fromMs, toMs, signal);\n }\n\n async #executeCustomFrameTasks() {\n if (this.#customFrameTasks.size > 0) {\n const percentComplete =\n this.durationMs > 0 ? this.ownCurrentTimeMs / this.durationMs : 0;\n const frameInfo = {\n ownCurrentTimeMs: this.ownCurrentTimeMs,\n currentTimeMs: this.currentTimeMs,\n durationMs: this.durationMs,\n percentComplete,\n element: this,\n };\n\n await Promise.all(\n Array.from(this.#customFrameTasks).map((callback) =>\n Promise.resolve(callback(frameInfo)),\n ),\n );\n }\n }\n\n /** @internal */\n #seekTaskPromise: Promise<number | undefined> = Promise.resolve(undefined);\n #seekTaskAbortController: AbortController | null = null;\n\n seekTask = (() => {\n const self = this;\n const taskObj: {\n run(): void | Promise<number | undefined>;\n taskComplete: Promise<number | undefined>;\n } = {\n run: () => {\n // Abort any in-flight task\n self.#seekTaskAbortController?.abort();\n self.#seekTaskAbortController = new AbortController();\n const signal = self.#seekTaskAbortController.signal;\n\n const targetTime = self.#pendingSeekTime ?? self.#currentTime;\n self.#seekTaskPromise = self.#runSeekTask(targetTime, signal);\n taskObj.taskComplete = self.#seekTaskPromise;\n return self.#seekTaskPromise;\n },\n taskComplete: Promise.resolve(undefined),\n };\n return taskObj;\n })();\n\n async #runSeekTask(\n targetTime: number | undefined,\n signal: AbortSignal,\n ): Promise<number | undefined> {\n try {\n signal.throwIfAborted();\n\n // Delegate to playbackController if available\n if (this.playbackController) {\n // Wait for playbackController's seek to complete\n await this.playbackController.currentTime; // Trigger seek\n signal.throwIfAborted();\n return this.currentTime;\n }\n\n // Only root timegroups execute seek tasks\n if (!this.isRootTimegroup) {\n return undefined;\n }\n\n return await withSpan(\n \"timegroup.seekTask\",\n {\n timegroupId: this.id || \"unknown\",\n targetTime: targetTime ?? 0,\n durationMs: this.durationMs,\n },\n undefined,\n async (span) => {\n // Wait for media durations to be loaded\n try {\n await Promise.race([\n this.waitForMediaDurations(signal),\n new Promise<void>((_, reject) => {\n if (signal.aborted) {\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n return;\n }\n const timeoutId = setTimeout(\n () => reject(new Error(\"waitForMediaDurations timeout\")),\n 10000,\n );\n signal.addEventListener(\n \"abort\",\n () => {\n clearTimeout(timeoutId);\n reject(new DOMException(\"Aborted\", \"AbortError\"));\n },\n { once: true },\n );\n }),\n ]);\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n // Continue with seek even if durations aren't loaded yet\n }\n\n signal.throwIfAborted();\n\n // Evaluate and apply seek target\n const newTime = evaluateSeekTarget(\n targetTime ?? 0,\n this.durationMs,\n this.effectiveFps,\n );\n if (isTracingEnabled()) {\n span.setAttribute(\"newTime\", newTime);\n }\n\n this.#currentTime = newTime;\n this.requestUpdate(\"currentTime\");\n\n await this.updateComplete;\n signal.throwIfAborted();\n\n await this.#runThrottledFrameTask();\n signal.throwIfAborted();\n\n if (!this.#restoringFromLocalStorage) {\n this.saveTimeToLocalStorage(this.#currentTime);\n }\n this.#seekInProgress = false;\n if (this.#restoringFromLocalStorage) {\n this.#restoringFromLocalStorage = false;\n }\n return newTime;\n },\n );\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n return undefined;\n }\n console.error(\"EFTimegroup seekTask error\", error);\n return undefined;\n }\n }\n\n /**\n * Get container information for this timegroup.\n * Timegroups are always containers and can contain children.\n * Display mode is determined from computed styles.\n *\n * @public\n */\n getContainerInfo(): ContainerInfo {\n const info = getContainerInfoFromElement(this);\n // Timegroups are always containers and can contain children\n return {\n ...info,\n isContainer: true,\n canContainChildren: true,\n };\n }\n\n /**\n * Get position information for this timegroup.\n * Returns computed bounds, transform, and rotation.\n *\n * @public\n */\n getPositionInfo(): ElementPositionInfo | null {\n return getPositionInfoFromElement(this);\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-timegroup\": EFTimegroup & Element;\n }\n}\n"],"mappings":"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"}
|
|
@@ -56,6 +56,16 @@ interface FrameRenderable {
|
|
|
56
56
|
*/
|
|
57
57
|
renderFrame(timeMs: number): void;
|
|
58
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Per-phase timing data returned by FrameController.renderFrame().
|
|
61
|
+
* All values are in milliseconds.
|
|
62
|
+
*/
|
|
63
|
+
interface RenderFrameTiming {
|
|
64
|
+
queryMs: number;
|
|
65
|
+
prepareMs: number;
|
|
66
|
+
renderMs: number;
|
|
67
|
+
animsMs: number;
|
|
68
|
+
}
|
|
59
69
|
/**
|
|
60
70
|
* Options for FrameController.renderFrame()
|
|
61
71
|
*/
|
|
@@ -72,12 +82,6 @@ interface RenderFrameOptions {
|
|
|
72
82
|
*/
|
|
73
83
|
onAnimationsUpdate?: (rootElement: Element) => void;
|
|
74
84
|
}
|
|
75
|
-
interface RenderFrameTiming {
|
|
76
|
-
queryMs: number;
|
|
77
|
-
prepareMs: number;
|
|
78
|
-
renderMs: number;
|
|
79
|
-
animsMs: number;
|
|
80
|
-
}
|
|
81
85
|
/**
|
|
82
86
|
* Central controller for frame rendering.
|
|
83
87
|
* Lives at the root timegroup and orchestrates all element rendering.
|
|
@@ -103,7 +107,7 @@ declare class FrameController {
|
|
|
103
107
|
* @param timeMs - The time in milliseconds to render
|
|
104
108
|
* @param options - Optional configuration
|
|
105
109
|
*/
|
|
106
|
-
renderFrame(timeMs: number, options?: RenderFrameOptions): Promise<RenderFrameTiming>;
|
|
110
|
+
renderFrame(timeMs: number, options?: RenderFrameOptions): Promise<RenderFrameTiming | null>;
|
|
107
111
|
/**
|
|
108
112
|
* Check if a render is currently in progress.
|
|
109
113
|
*/
|
|
@@ -44,11 +44,6 @@ var FrameController = class {
|
|
|
44
44
|
#abortController = null;
|
|
45
45
|
#renderInProgress = false;
|
|
46
46
|
#pendingRenderTime = null;
|
|
47
|
-
#frameCount = 0;
|
|
48
|
-
#totalQueryMs = 0;
|
|
49
|
-
#totalPrepareMs = 0;
|
|
50
|
-
#totalRenderMs = 0;
|
|
51
|
-
#totalAnimsMs = 0;
|
|
52
47
|
/**
|
|
53
48
|
* Last successfully rendered time. Used for deduplication when multiple
|
|
54
49
|
* callers (e.g., PlaybackController RAF loop and canvas render loop)
|
|
@@ -80,25 +75,15 @@ var FrameController = class {
|
|
|
80
75
|
*/
|
|
81
76
|
async renderFrame(timeMs, options = {}) {
|
|
82
77
|
const { waitForLitUpdate = true, onAnimationsUpdate } = options;
|
|
83
|
-
|
|
84
|
-
queryMs: 0,
|
|
85
|
-
prepareMs: 0,
|
|
86
|
-
renderMs: 0,
|
|
87
|
-
animsMs: 0
|
|
88
|
-
};
|
|
89
|
-
if (timeMs === this.#lastRenderedTimeMs) return zero;
|
|
78
|
+
if (timeMs === this.#lastRenderedTimeMs) return null;
|
|
90
79
|
if (this.#renderInProgress) {
|
|
91
80
|
this.#pendingRenderTime = timeMs;
|
|
92
|
-
return
|
|
81
|
+
return null;
|
|
93
82
|
}
|
|
94
83
|
this.#abortController?.abort();
|
|
95
84
|
this.#abortController = new AbortController();
|
|
96
85
|
const signal = this.#abortController.signal;
|
|
97
86
|
this.#renderInProgress = true;
|
|
98
|
-
let queryMs = 0;
|
|
99
|
-
let prepareMs = 0;
|
|
100
|
-
let renderMs = 0;
|
|
101
|
-
let animsMs = 0;
|
|
102
87
|
try {
|
|
103
88
|
if (waitForLitUpdate) {
|
|
104
89
|
await this.#rootElement.updateComplete;
|
|
@@ -106,7 +91,7 @@ var FrameController = class {
|
|
|
106
91
|
}
|
|
107
92
|
const tQuery = performance.now();
|
|
108
93
|
const elements = this.#queryVisibleElements(timeMs);
|
|
109
|
-
queryMs = performance.now() - tQuery;
|
|
94
|
+
const queryMs = performance.now() - tQuery;
|
|
110
95
|
signal.throwIfAborted();
|
|
111
96
|
const tPrepare = performance.now();
|
|
112
97
|
const elementsNeedingPreparation = elements.filter((el) => el.getFrameState(timeMs).needsPreparation);
|
|
@@ -114,30 +99,24 @@ var FrameController = class {
|
|
|
114
99
|
await Promise.all(elementsNeedingPreparation.map((el) => el.prepareFrame(timeMs, signal)));
|
|
115
100
|
signal.throwIfAborted();
|
|
116
101
|
}
|
|
117
|
-
prepareMs = performance.now() - tPrepare;
|
|
102
|
+
const prepareMs = performance.now() - tPrepare;
|
|
118
103
|
const tRender = performance.now();
|
|
119
104
|
const sortedElements = [...elements].sort((a, b) => a.getFrameState(timeMs).priority - b.getFrameState(timeMs).priority);
|
|
120
105
|
for (const element of sortedElements) {
|
|
121
106
|
signal.throwIfAborted();
|
|
122
107
|
element.renderFrame(timeMs);
|
|
123
108
|
}
|
|
124
|
-
renderMs = performance.now() - tRender;
|
|
109
|
+
const renderMs = performance.now() - tRender;
|
|
125
110
|
const tAnims = performance.now();
|
|
126
111
|
if (onAnimationsUpdate) onAnimationsUpdate(this.#rootElement);
|
|
127
|
-
animsMs = performance.now() - tAnims;
|
|
128
|
-
this.#frameCount++;
|
|
129
|
-
this.#totalQueryMs += queryMs;
|
|
130
|
-
this.#totalPrepareMs += prepareMs;
|
|
131
|
-
this.#totalRenderMs += renderMs;
|
|
132
|
-
this.#totalAnimsMs += animsMs;
|
|
133
|
-
if (this.#frameCount % 60 === 0) {
|
|
134
|
-
this.#frameCount = 0;
|
|
135
|
-
this.#totalQueryMs = 0;
|
|
136
|
-
this.#totalPrepareMs = 0;
|
|
137
|
-
this.#totalRenderMs = 0;
|
|
138
|
-
this.#totalAnimsMs = 0;
|
|
139
|
-
}
|
|
112
|
+
const animsMs = performance.now() - tAnims;
|
|
140
113
|
this.#lastRenderedTimeMs = timeMs;
|
|
114
|
+
return {
|
|
115
|
+
queryMs,
|
|
116
|
+
prepareMs,
|
|
117
|
+
renderMs,
|
|
118
|
+
animsMs
|
|
119
|
+
};
|
|
141
120
|
} finally {
|
|
142
121
|
this.#renderInProgress = false;
|
|
143
122
|
if (this.#pendingRenderTime !== null) {
|
|
@@ -146,12 +125,6 @@ var FrameController = class {
|
|
|
146
125
|
this.renderFrame(pendingTime, options).catch(() => {});
|
|
147
126
|
}
|
|
148
127
|
}
|
|
149
|
-
return {
|
|
150
|
-
queryMs,
|
|
151
|
-
prepareMs,
|
|
152
|
-
renderMs,
|
|
153
|
-
animsMs
|
|
154
|
-
};
|
|
155
128
|
}
|
|
156
129
|
/**
|
|
157
130
|
* Query all visible FrameRenderable elements in the tree.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FrameController.js","names":["#rootElement","#abortController","#lastRenderedTimeMs","zero: RenderFrameTiming","#renderInProgress","#pendingRenderTime","#queryVisibleElements","#frameCount","#totalQueryMs","#totalPrepareMs","#totalRenderMs","#totalAnimsMs","result: FrameRenderable[]","#getChildrenIncludingSlotted","assignedElements: Element[]"],"sources":["../../src/preview/FrameController.ts"],"sourcesContent":["/**\n * FrameController: Centralized frame rendering control\n *\n * Replaces the distributed Lit Task hierarchy with a single control loop\n * that queries elements and coordinates rendering directly.\n *\n * Benefits over the previous Task-based system:\n * - Single abort controller instead of distributed abort handling\n * - Clear prepare → render phases\n * - All coordination visible in one place\n * - No Lit Task reactivity overhead\n */\n\nimport type { LitElement } from \"lit\";\n\n// ============================================================================\n// Priority Constants\n// ============================================================================\n// Lower numbers render first. Elements with dependencies should have higher\n// priority numbers than their dependencies.\n//\n// Example: Waveform depends on audio analysis data, so it renders after audio.\n// ============================================================================\n\n/**\n * Priority for video elements.\n * Video renders first as other elements may depend on video frames being ready.\n */\nexport const PRIORITY_VIDEO = 1;\n\n/**\n * Priority for captions elements.\n * Captions render after video so they can overlay correctly.\n */\nexport const PRIORITY_CAPTIONS = 2;\n\n/**\n * Priority for audio elements.\n * Audio renders after captions (no visual dependency, but keeps consistent ordering).\n */\nexport const PRIORITY_AUDIO = 3;\n\n/**\n * Priority for waveform elements.\n * Waveform renders after audio because it depends on audio analysis data.\n */\nexport const PRIORITY_WAVEFORM = 4;\n\n/**\n * Priority for image elements.\n * Images render with low priority as they're typically static.\n */\nexport const PRIORITY_IMAGE = 5;\n\n/**\n * Default priority for elements that don't specify one.\n * High number ensures custom elements render after standard elements.\n */\nexport const PRIORITY_DEFAULT = 100;\n\n/**\n * State returned by elements describing their readiness for a given time.\n */\nexport interface FrameState {\n /**\n * Whether async preparation is needed before rendering.\n * Examples: video needs to seek, captions need to load data.\n */\n needsPreparation: boolean;\n\n /**\n * Whether the element is ready to render synchronously.\n * True when all async work is complete and renderFrame() can be called.\n */\n isReady: boolean;\n\n /**\n * Rendering priority hint. Lower numbers render first.\n * Used to order render calls for elements with dependencies.\n *\n * Standard priorities:\n * - PRIORITY_VIDEO (1): Video elements\n * - PRIORITY_CAPTIONS (2): Caption overlays\n * - PRIORITY_AUDIO (3): Audio elements\n * - PRIORITY_WAVEFORM (4): Audio visualizers (depend on audio)\n * - PRIORITY_IMAGE (5): Static images\n * - PRIORITY_DEFAULT (100): Fallback for custom elements\n */\n priority: number;\n}\n\n/**\n * Interface that elements implement to participate in centralized frame rendering.\n * Elements keep their rendering logic local but expose a standardized interface.\n */\nexport interface FrameRenderable {\n /**\n * Query the element's readiness state for a given time.\n * Must be synchronous and cheap to call.\n */\n getFrameState(timeMs: number): FrameState;\n\n /**\n * Async preparation phase. Called when getFrameState().needsPreparation is true.\n * Performs any async work needed before rendering (seeking, loading, etc.).\n *\n * @param timeMs - The time to prepare for\n * @param signal - Abort signal for cancellation\n */\n prepareFrame(timeMs: number, signal: AbortSignal): Promise<void>;\n\n /**\n * Synchronous render phase. Called after all preparation is complete.\n * Performs the actual rendering (paint to canvas, update DOM, etc.).\n *\n * @param timeMs - The time to render\n */\n renderFrame(timeMs: number): void;\n}\n\n/**\n * Type guard to check if an element implements FrameRenderable.\n */\nexport function isFrameRenderable(\n element: unknown,\n): element is FrameRenderable {\n return (\n typeof element === \"object\" &&\n element !== null &&\n \"getFrameState\" in element &&\n \"prepareFrame\" in element &&\n \"renderFrame\" in element &&\n typeof (element as FrameRenderable).getFrameState === \"function\" &&\n typeof (element as FrameRenderable).prepareFrame === \"function\" &&\n typeof (element as FrameRenderable).renderFrame === \"function\"\n );\n}\n\n/**\n * Options for FrameController.renderFrame()\n */\nexport interface RenderFrameOptions {\n /**\n * Whether to wait for Lit updateComplete before querying elements.\n * Default: true\n */\n waitForLitUpdate?: boolean;\n\n /**\n * Callback to update CSS animations after frame rendering completes.\n * Called with the root element after all elements have rendered.\n * This centralizes animation synchronization in one place.\n */\n onAnimationsUpdate?: (rootElement: Element) => void;\n}\n\nexport interface RenderFrameTiming {\n queryMs: number;\n prepareMs: number;\n renderMs: number;\n animsMs: number;\n}\n\n/**\n * Central controller for frame rendering.\n * Lives at the root timegroup and orchestrates all element rendering.\n */\nexport class FrameController {\n #rootElement: LitElement & { currentTimeMs: number };\n #abortController: AbortController | null = null;\n #renderInProgress = false;\n #pendingRenderTime: number | null = null;\n #frameCount = 0;\n #totalQueryMs = 0;\n #totalPrepareMs = 0;\n #totalRenderMs = 0;\n #totalAnimsMs = 0;\n\n /**\n * Last successfully rendered time. Used for deduplication when multiple\n * callers (e.g., PlaybackController RAF loop and canvas render loop)\n * both try to render the same frame within one animation frame.\n */\n #lastRenderedTimeMs: number = -1;\n\n constructor(rootElement: LitElement & { currentTimeMs: number }) {\n this.#rootElement = rootElement;\n }\n\n /**\n * Cancel any in-progress render operation and reset deduplication state.\n */\n abort(): void {\n this.#abortController?.abort();\n this.#abortController = null;\n // Reset deduplication state so next render goes through even if same time\n this.#lastRenderedTimeMs = -1;\n }\n\n /**\n * Render a frame at the specified time.\n *\n * This is the main entry point for frame rendering. It:\n * 1. Cancels any previous in-progress render\n * 2. Queries all visible FrameRenderable elements\n * 3. Runs preparation in parallel for elements that need it\n * 4. Runs render in priority order\n *\n * @param timeMs - The time in milliseconds to render\n * @param options - Optional configuration\n */\n async renderFrame(\n timeMs: number,\n options: RenderFrameOptions = {},\n ): Promise<RenderFrameTiming> {\n const { waitForLitUpdate = true, onAnimationsUpdate } = options;\n const zero: RenderFrameTiming = { queryMs: 0, prepareMs: 0, renderMs: 0, animsMs: 0 };\n\n // Deduplicate: skip if we just rendered this exact time.\n // This prevents double-rendering when multiple RAF loops (e.g., PlaybackController\n // and canvas render loop) both call renderFrame() for the same frame.\n if (timeMs === this.#lastRenderedTimeMs) {\n return zero;\n }\n\n // If a render is in progress, queue this one\n if (this.#renderInProgress) {\n this.#pendingRenderTime = timeMs;\n return zero;\n }\n\n // Cancel any previous render operation\n this.#abortController?.abort();\n this.#abortController = new AbortController();\n const signal = this.#abortController.signal;\n\n this.#renderInProgress = true;\n\n let queryMs = 0;\n let prepareMs = 0;\n let renderMs = 0;\n let animsMs = 0;\n\n try {\n if (waitForLitUpdate) {\n await this.#rootElement.updateComplete;\n signal.throwIfAborted();\n }\n\n const tQuery = performance.now();\n const elements = this.#queryVisibleElements(timeMs);\n queryMs = performance.now() - tQuery;\n signal.throwIfAborted();\n\n const tPrepare = performance.now();\n const elementsNeedingPreparation = elements.filter(\n (el) => el.getFrameState(timeMs).needsPreparation,\n );\n\n if (elementsNeedingPreparation.length > 0) {\n await Promise.all(\n elementsNeedingPreparation.map((el) =>\n el.prepareFrame(timeMs, signal),\n ),\n );\n signal.throwIfAborted();\n }\n prepareMs = performance.now() - tPrepare;\n\n const tRender = performance.now();\n const sortedElements = [...elements].sort(\n (a, b) =>\n a.getFrameState(timeMs).priority - b.getFrameState(timeMs).priority,\n );\n\n for (const element of sortedElements) {\n signal.throwIfAborted();\n element.renderFrame(timeMs);\n }\n renderMs = performance.now() - tRender;\n\n const tAnims = performance.now();\n if (onAnimationsUpdate) {\n onAnimationsUpdate(this.#rootElement);\n }\n animsMs = performance.now() - tAnims;\n\n this.#frameCount++;\n this.#totalQueryMs += queryMs;\n this.#totalPrepareMs += prepareMs;\n this.#totalRenderMs += renderMs;\n this.#totalAnimsMs += animsMs;\n\n if (this.#frameCount % 60 === 0) {\n this.#frameCount = 0;\n this.#totalQueryMs = 0;\n this.#totalPrepareMs = 0;\n this.#totalRenderMs = 0;\n this.#totalAnimsMs = 0;\n }\n\n this.#lastRenderedTimeMs = timeMs;\n } finally {\n this.#renderInProgress = false;\n\n // Process any queued render\n if (this.#pendingRenderTime !== null) {\n const pendingTime = this.#pendingRenderTime;\n this.#pendingRenderTime = null;\n // Don't await - fire and forget to avoid recursive waiting\n this.renderFrame(pendingTime, options).catch(() => {\n // Silently ignore errors from queued renders (likely aborted)\n });\n }\n }\n\n return { queryMs, prepareMs, renderMs, animsMs };\n }\n\n /**\n * Query all visible FrameRenderable elements in the tree.\n * Uses temporal visibility to filter out elements not visible at current time.\n *\n * IMPORTANT: For temporal elements, we use temporal visibility (startTimeMs/endTimeMs)\n * instead of CSS visibility. This is because updateAnimations sets display:none on\n * elements outside their time range, but that CSS state is from the PREVIOUS frame.\n * When seeking, we need to evaluate visibility based on the NEW time, not stale CSS.\n *\n * @param timeMs - The time to use for visibility checks. This should be the target\n * render time, not read from root element (which may be stale).\n */\n #queryVisibleElements(timeMs: number): FrameRenderable[] {\n const result: FrameRenderable[] = [];\n const currentTimeMs = timeMs;\n\n const walk = (element: Element): void => {\n // For temporal elements (ef-timegroup, ef-video, etc.), use temporal visibility\n // instead of CSS visibility. CSS display:none may be stale from previous frame.\n const isTemporal = \"startTimeMs\" in element && \"endTimeMs\" in element;\n\n if (isTemporal) {\n // Temporal element: check time-based visibility\n // Use exclusive end (< not <=) to avoid overlap at boundaries\n const startMs =\n (element as { startTimeMs?: number }).startTimeMs ?? -Infinity;\n const endMs = (element as { endTimeMs?: number }).endTimeMs ?? Infinity;\n const isTemporallyVisible =\n currentTimeMs >= startMs && currentTimeMs < endMs;\n\n if (!isTemporallyVisible) {\n // Skip this element AND its children (children's times are relative to parent)\n return;\n }\n\n // Element is temporally visible - include if it implements FrameRenderable\n if (isFrameRenderable(element)) {\n result.push(element);\n }\n } else {\n // Non-temporal element: only check inline display style (fast path).\n // Skip getComputedStyle — it forces synchronous style recalc and is\n // unnecessary because FrameRenderable elements are always temporal.\n // We only walk non-temporal elements to reach temporal children.\n if (\n element instanceof HTMLElement &&\n element.style.display === \"none\"\n ) {\n return;\n }\n\n if (isFrameRenderable(element)) {\n result.push(element);\n }\n }\n\n // Walk children - handle both regular children and slotted content\n const children = this.#getChildrenIncludingSlotted(element);\n for (const child of children) {\n walk(child);\n }\n };\n\n walk(this.#rootElement);\n return result;\n }\n\n /**\n * Gets all child elements including slotted content for shadow DOM elements.\n * For elements with shadow DOM that contain slots, this returns the assigned\n * elements (slotted content) instead of just the shadow DOM children.\n */\n #getChildrenIncludingSlotted(element: Element): Iterable<Element> {\n // If element has shadowRoot with slots, get assigned elements\n if (element.shadowRoot) {\n const slots = element.shadowRoot.querySelectorAll(\"slot\");\n if (slots.length > 0) {\n const assignedElements: Element[] = [];\n for (const slot of slots) {\n assignedElements.push(...slot.assignedElements());\n }\n // Also include shadow DOM children that aren't slots (for mixed content)\n for (const child of element.shadowRoot.children) {\n if (child.tagName !== \"SLOT\") {\n assignedElements.push(child);\n }\n }\n return assignedElements;\n }\n }\n\n // Return HTMLCollection directly (iterable, no allocation)\n return element.children;\n }\n\n /**\n * Check if a render is currently in progress.\n */\n get isRendering(): boolean {\n return this.#renderInProgress;\n }\n}\n\n/**\n * Default frame state for elements that don't need special handling.\n * Use this for simple elements that are always ready.\n */\nexport const DEFAULT_FRAME_STATE: FrameState = {\n needsPreparation: false,\n isReady: true,\n priority: PRIORITY_DEFAULT,\n};\n\n/**\n * Helper to create a FrameRenderable mixin for elements.\n * Provides default implementations that can be overridden.\n */\nexport function createFrameRenderableMixin<\n T extends { new (...args: any[]): HTMLElement },\n>(Base: T) {\n return class FrameRenderableMixin extends Base implements FrameRenderable {\n getFrameState(_timeMs: number): FrameState {\n return DEFAULT_FRAME_STATE;\n }\n\n async prepareFrame(_timeMs: number, _signal: AbortSignal): Promise<void> {\n // Default: no preparation needed\n }\n\n renderFrame(_timeMs: number): void {\n // Default: no explicit render needed\n }\n };\n}\n"],"mappings":";;;;;AA4BA,MAAa,iBAAiB;;;;;AAM9B,MAAa,oBAAoB;;;;;AAMjC,MAAa,iBAAiB;;;;;AAM9B,MAAa,oBAAoB;;;;;AAMjC,MAAa,iBAAiB;;;;;AAM9B,MAAa,mBAAmB;;;;AAiEhC,SAAgB,kBACd,SAC4B;AAC5B,QACE,OAAO,YAAY,YACnB,YAAY,QACZ,mBAAmB,WACnB,kBAAkB,WAClB,iBAAiB,WACjB,OAAQ,QAA4B,kBAAkB,cACtD,OAAQ,QAA4B,iBAAiB,cACrD,OAAQ,QAA4B,gBAAgB;;;;;;AAiCxD,IAAa,kBAAb,MAA6B;CAC3B;CACA,mBAA2C;CAC3C,oBAAoB;CACpB,qBAAoC;CACpC,cAAc;CACd,gBAAgB;CAChB,kBAAkB;CAClB,iBAAiB;CACjB,gBAAgB;;;;;;CAOhB,sBAA8B;CAE9B,YAAY,aAAqD;AAC/D,QAAKA,cAAe;;;;;CAMtB,QAAc;AACZ,QAAKC,iBAAkB,OAAO;AAC9B,QAAKA,kBAAmB;AAExB,QAAKC,qBAAsB;;;;;;;;;;;;;;CAe7B,MAAM,YACJ,QACA,UAA8B,EAAE,EACJ;EAC5B,MAAM,EAAE,mBAAmB,MAAM,uBAAuB;EACxD,MAAMC,OAA0B;GAAE,SAAS;GAAG,WAAW;GAAG,UAAU;GAAG,SAAS;GAAG;AAKrF,MAAI,WAAW,MAAKD,mBAClB,QAAO;AAIT,MAAI,MAAKE,kBAAmB;AAC1B,SAAKC,oBAAqB;AAC1B,UAAO;;AAIT,QAAKJ,iBAAkB,OAAO;AAC9B,QAAKA,kBAAmB,IAAI,iBAAiB;EAC7C,MAAM,SAAS,MAAKA,gBAAiB;AAErC,QAAKG,mBAAoB;EAEzB,IAAI,UAAU;EACd,IAAI,YAAY;EAChB,IAAI,WAAW;EACf,IAAI,UAAU;AAEd,MAAI;AACF,OAAI,kBAAkB;AACpB,UAAM,MAAKJ,YAAa;AACxB,WAAO,gBAAgB;;GAGzB,MAAM,SAAS,YAAY,KAAK;GAChC,MAAM,WAAW,MAAKM,qBAAsB,OAAO;AACnD,aAAU,YAAY,KAAK,GAAG;AAC9B,UAAO,gBAAgB;GAEvB,MAAM,WAAW,YAAY,KAAK;GAClC,MAAM,6BAA6B,SAAS,QACzC,OAAO,GAAG,cAAc,OAAO,CAAC,iBAClC;AAED,OAAI,2BAA2B,SAAS,GAAG;AACzC,UAAM,QAAQ,IACZ,2BAA2B,KAAK,OAC9B,GAAG,aAAa,QAAQ,OAAO,CAChC,CACF;AACD,WAAO,gBAAgB;;AAEzB,eAAY,YAAY,KAAK,GAAG;GAEhC,MAAM,UAAU,YAAY,KAAK;GACjC,MAAM,iBAAiB,CAAC,GAAG,SAAS,CAAC,MAClC,GAAG,MACF,EAAE,cAAc,OAAO,CAAC,WAAW,EAAE,cAAc,OAAO,CAAC,SAC9D;AAED,QAAK,MAAM,WAAW,gBAAgB;AACpC,WAAO,gBAAgB;AACvB,YAAQ,YAAY,OAAO;;AAE7B,cAAW,YAAY,KAAK,GAAG;GAE/B,MAAM,SAAS,YAAY,KAAK;AAChC,OAAI,mBACF,oBAAmB,MAAKN,YAAa;AAEvC,aAAU,YAAY,KAAK,GAAG;AAE9B,SAAKO;AACL,SAAKC,gBAAiB;AACtB,SAAKC,kBAAmB;AACxB,SAAKC,iBAAkB;AACvB,SAAKC,gBAAiB;AAEtB,OAAI,MAAKJ,aAAc,OAAO,GAAG;AAC/B,UAAKA,aAAc;AACnB,UAAKC,eAAgB;AACrB,UAAKC,iBAAkB;AACvB,UAAKC,gBAAiB;AACtB,UAAKC,eAAgB;;AAGvB,SAAKT,qBAAsB;YACnB;AACR,SAAKE,mBAAoB;AAGzB,OAAI,MAAKC,sBAAuB,MAAM;IACpC,MAAM,cAAc,MAAKA;AACzB,UAAKA,oBAAqB;AAE1B,SAAK,YAAY,aAAa,QAAQ,CAAC,YAAY,GAEjD;;;AAIN,SAAO;GAAE;GAAS;GAAW;GAAU;GAAS;;;;;;;;;;;;;;CAelD,sBAAsB,QAAmC;EACvD,MAAMO,SAA4B,EAAE;EACpC,MAAM,gBAAgB;EAEtB,MAAM,QAAQ,YAA2B;AAKvC,OAFmB,iBAAiB,WAAW,eAAe,SAE9C;IAGd,MAAM,UACH,QAAqC,eAAe;IACvD,MAAM,QAAS,QAAmC,aAAa;AAI/D,QAAI,EAFF,iBAAiB,WAAW,gBAAgB,OAI5C;AAIF,QAAI,kBAAkB,QAAQ,CAC5B,QAAO,KAAK,QAAQ;UAEjB;AAKL,QACE,mBAAmB,eACnB,QAAQ,MAAM,YAAY,OAE1B;AAGF,QAAI,kBAAkB,QAAQ,CAC5B,QAAO,KAAK,QAAQ;;GAKxB,MAAM,WAAW,MAAKC,4BAA6B,QAAQ;AAC3D,QAAK,MAAM,SAAS,SAClB,MAAK,MAAM;;AAIf,OAAK,MAAKb,YAAa;AACvB,SAAO;;;;;;;CAQT,6BAA6B,SAAqC;AAEhE,MAAI,QAAQ,YAAY;GACtB,MAAM,QAAQ,QAAQ,WAAW,iBAAiB,OAAO;AACzD,OAAI,MAAM,SAAS,GAAG;IACpB,MAAMc,mBAA8B,EAAE;AACtC,SAAK,MAAM,QAAQ,MACjB,kBAAiB,KAAK,GAAG,KAAK,kBAAkB,CAAC;AAGnD,SAAK,MAAM,SAAS,QAAQ,WAAW,SACrC,KAAI,MAAM,YAAY,OACpB,kBAAiB,KAAK,MAAM;AAGhC,WAAO;;;AAKX,SAAO,QAAQ;;;;;CAMjB,IAAI,cAAuB;AACzB,SAAO,MAAKV"}
|
|
1
|
+
{"version":3,"file":"FrameController.js","names":["#rootElement","#abortController","#lastRenderedTimeMs","#renderInProgress","#pendingRenderTime","#queryVisibleElements","result: FrameRenderable[]","#getChildrenIncludingSlotted","assignedElements: Element[]"],"sources":["../../src/preview/FrameController.ts"],"sourcesContent":["/**\n * FrameController: Centralized frame rendering control\n *\n * Replaces the distributed Lit Task hierarchy with a single control loop\n * that queries elements and coordinates rendering directly.\n *\n * Benefits over the previous Task-based system:\n * - Single abort controller instead of distributed abort handling\n * - Clear prepare → render phases\n * - All coordination visible in one place\n * - No Lit Task reactivity overhead\n */\n\nimport type { LitElement } from \"lit\";\n\n// ============================================================================\n// Priority Constants\n// ============================================================================\n// Lower numbers render first. Elements with dependencies should have higher\n// priority numbers than their dependencies.\n//\n// Example: Waveform depends on audio analysis data, so it renders after audio.\n// ============================================================================\n\n/**\n * Priority for video elements.\n * Video renders first as other elements may depend on video frames being ready.\n */\nexport const PRIORITY_VIDEO = 1;\n\n/**\n * Priority for captions elements.\n * Captions render after video so they can overlay correctly.\n */\nexport const PRIORITY_CAPTIONS = 2;\n\n/**\n * Priority for audio elements.\n * Audio renders after captions (no visual dependency, but keeps consistent ordering).\n */\nexport const PRIORITY_AUDIO = 3;\n\n/**\n * Priority for waveform elements.\n * Waveform renders after audio because it depends on audio analysis data.\n */\nexport const PRIORITY_WAVEFORM = 4;\n\n/**\n * Priority for image elements.\n * Images render with low priority as they're typically static.\n */\nexport const PRIORITY_IMAGE = 5;\n\n/**\n * Default priority for elements that don't specify one.\n * High number ensures custom elements render after standard elements.\n */\nexport const PRIORITY_DEFAULT = 100;\n\n/**\n * State returned by elements describing their readiness for a given time.\n */\nexport interface FrameState {\n /**\n * Whether async preparation is needed before rendering.\n * Examples: video needs to seek, captions need to load data.\n */\n needsPreparation: boolean;\n\n /**\n * Whether the element is ready to render synchronously.\n * True when all async work is complete and renderFrame() can be called.\n */\n isReady: boolean;\n\n /**\n * Rendering priority hint. Lower numbers render first.\n * Used to order render calls for elements with dependencies.\n *\n * Standard priorities:\n * - PRIORITY_VIDEO (1): Video elements\n * - PRIORITY_CAPTIONS (2): Caption overlays\n * - PRIORITY_AUDIO (3): Audio elements\n * - PRIORITY_WAVEFORM (4): Audio visualizers (depend on audio)\n * - PRIORITY_IMAGE (5): Static images\n * - PRIORITY_DEFAULT (100): Fallback for custom elements\n */\n priority: number;\n}\n\n/**\n * Interface that elements implement to participate in centralized frame rendering.\n * Elements keep their rendering logic local but expose a standardized interface.\n */\nexport interface FrameRenderable {\n /**\n * Query the element's readiness state for a given time.\n * Must be synchronous and cheap to call.\n */\n getFrameState(timeMs: number): FrameState;\n\n /**\n * Async preparation phase. Called when getFrameState().needsPreparation is true.\n * Performs any async work needed before rendering (seeking, loading, etc.).\n *\n * @param timeMs - The time to prepare for\n * @param signal - Abort signal for cancellation\n */\n prepareFrame(timeMs: number, signal: AbortSignal): Promise<void>;\n\n /**\n * Synchronous render phase. Called after all preparation is complete.\n * Performs the actual rendering (paint to canvas, update DOM, etc.).\n *\n * @param timeMs - The time to render\n */\n renderFrame(timeMs: number): void;\n}\n\n/**\n * Type guard to check if an element implements FrameRenderable.\n */\nexport function isFrameRenderable(\n element: unknown,\n): element is FrameRenderable {\n return (\n typeof element === \"object\" &&\n element !== null &&\n \"getFrameState\" in element &&\n \"prepareFrame\" in element &&\n \"renderFrame\" in element &&\n typeof (element as FrameRenderable).getFrameState === \"function\" &&\n typeof (element as FrameRenderable).prepareFrame === \"function\" &&\n typeof (element as FrameRenderable).renderFrame === \"function\"\n );\n}\n\n/**\n * Per-phase timing data returned by FrameController.renderFrame().\n * All values are in milliseconds.\n */\nexport interface RenderFrameTiming {\n queryMs: number;\n prepareMs: number;\n renderMs: number;\n animsMs: number;\n}\n\n/**\n * Options for FrameController.renderFrame()\n */\nexport interface RenderFrameOptions {\n /**\n * Whether to wait for Lit updateComplete before querying elements.\n * Default: true\n */\n waitForLitUpdate?: boolean;\n\n /**\n * Callback to update CSS animations after frame rendering completes.\n * Called with the root element after all elements have rendered.\n * This centralizes animation synchronization in one place.\n */\n onAnimationsUpdate?: (rootElement: Element) => void;\n}\n\n/**\n * Central controller for frame rendering.\n * Lives at the root timegroup and orchestrates all element rendering.\n */\nexport class FrameController {\n #rootElement: LitElement & { currentTimeMs: number };\n #abortController: AbortController | null = null;\n #renderInProgress = false;\n #pendingRenderTime: number | null = null;\n /**\n * Last successfully rendered time. Used for deduplication when multiple\n * callers (e.g., PlaybackController RAF loop and canvas render loop)\n * both try to render the same frame within one animation frame.\n */\n #lastRenderedTimeMs: number = -1;\n\n constructor(rootElement: LitElement & { currentTimeMs: number }) {\n this.#rootElement = rootElement;\n }\n\n /**\n * Cancel any in-progress render operation and reset deduplication state.\n */\n abort(): void {\n this.#abortController?.abort();\n this.#abortController = null;\n // Reset deduplication state so next render goes through even if same time\n this.#lastRenderedTimeMs = -1;\n }\n\n /**\n * Render a frame at the specified time.\n *\n * This is the main entry point for frame rendering. It:\n * 1. Cancels any previous in-progress render\n * 2. Queries all visible FrameRenderable elements\n * 3. Runs preparation in parallel for elements that need it\n * 4. Runs render in priority order\n *\n * @param timeMs - The time in milliseconds to render\n * @param options - Optional configuration\n */\n async renderFrame(\n timeMs: number,\n options: RenderFrameOptions = {},\n ): Promise<RenderFrameTiming | null> {\n const { waitForLitUpdate = true, onAnimationsUpdate } = options;\n\n // Deduplicate: skip if we just rendered this exact time.\n // This prevents double-rendering when multiple RAF loops (e.g., PlaybackController\n // and canvas render loop) both call renderFrame() for the same frame.\n if (timeMs === this.#lastRenderedTimeMs) {\n return null;\n }\n\n // If a render is in progress, queue this one\n if (this.#renderInProgress) {\n this.#pendingRenderTime = timeMs;\n return null;\n }\n\n // Cancel any previous render operation\n this.#abortController?.abort();\n this.#abortController = new AbortController();\n const signal = this.#abortController.signal;\n\n this.#renderInProgress = true;\n\n try {\n if (waitForLitUpdate) {\n await this.#rootElement.updateComplete;\n signal.throwIfAborted();\n }\n\n const tQuery = performance.now();\n const elements = this.#queryVisibleElements(timeMs);\n const queryMs = performance.now() - tQuery;\n signal.throwIfAborted();\n\n const tPrepare = performance.now();\n const elementsNeedingPreparation = elements.filter(\n (el) => el.getFrameState(timeMs).needsPreparation,\n );\n\n if (elementsNeedingPreparation.length > 0) {\n await Promise.all(\n elementsNeedingPreparation.map((el) =>\n el.prepareFrame(timeMs, signal),\n ),\n );\n signal.throwIfAborted();\n }\n const prepareMs = performance.now() - tPrepare;\n\n const tRender = performance.now();\n const sortedElements = [...elements].sort(\n (a, b) =>\n a.getFrameState(timeMs).priority - b.getFrameState(timeMs).priority,\n );\n\n for (const element of sortedElements) {\n signal.throwIfAborted();\n element.renderFrame(timeMs);\n }\n const renderMs = performance.now() - tRender;\n\n const tAnims = performance.now();\n if (onAnimationsUpdate) {\n onAnimationsUpdate(this.#rootElement);\n }\n const animsMs = performance.now() - tAnims;\n\n this.#lastRenderedTimeMs = timeMs;\n return { queryMs, prepareMs, renderMs, animsMs };\n } finally {\n this.#renderInProgress = false;\n\n // Process any queued render\n if (this.#pendingRenderTime !== null) {\n const pendingTime = this.#pendingRenderTime;\n this.#pendingRenderTime = null;\n // Don't await - fire and forget to avoid recursive waiting\n this.renderFrame(pendingTime, options).catch(() => {\n // Silently ignore errors from queued renders (likely aborted)\n });\n }\n }\n }\n\n /**\n * Query all visible FrameRenderable elements in the tree.\n * Uses temporal visibility to filter out elements not visible at current time.\n *\n * IMPORTANT: For temporal elements, we use temporal visibility (startTimeMs/endTimeMs)\n * instead of CSS visibility. This is because updateAnimations sets display:none on\n * elements outside their time range, but that CSS state is from the PREVIOUS frame.\n * When seeking, we need to evaluate visibility based on the NEW time, not stale CSS.\n *\n * @param timeMs - The time to use for visibility checks. This should be the target\n * render time, not read from root element (which may be stale).\n */\n #queryVisibleElements(timeMs: number): FrameRenderable[] {\n const result: FrameRenderable[] = [];\n const currentTimeMs = timeMs;\n\n const walk = (element: Element): void => {\n // For temporal elements (ef-timegroup, ef-video, etc.), use temporal visibility\n // instead of CSS visibility. CSS display:none may be stale from previous frame.\n const isTemporal = \"startTimeMs\" in element && \"endTimeMs\" in element;\n\n if (isTemporal) {\n // Temporal element: check time-based visibility\n // Use exclusive end (< not <=) to avoid overlap at boundaries\n const startMs =\n (element as { startTimeMs?: number }).startTimeMs ?? -Infinity;\n const endMs = (element as { endTimeMs?: number }).endTimeMs ?? Infinity;\n const isTemporallyVisible =\n currentTimeMs >= startMs && currentTimeMs < endMs;\n\n if (!isTemporallyVisible) {\n // Skip this element AND its children (children's times are relative to parent)\n return;\n }\n\n // Element is temporally visible - include if it implements FrameRenderable\n if (isFrameRenderable(element)) {\n result.push(element);\n }\n } else {\n // Non-temporal element: only check inline display style (fast path).\n // Skip getComputedStyle — it forces synchronous style recalc and is\n // unnecessary because FrameRenderable elements are always temporal.\n // We only walk non-temporal elements to reach temporal children.\n if (\n element instanceof HTMLElement &&\n element.style.display === \"none\"\n ) {\n return;\n }\n\n if (isFrameRenderable(element)) {\n result.push(element);\n }\n }\n\n // Walk children - handle both regular children and slotted content\n const children = this.#getChildrenIncludingSlotted(element);\n for (const child of children) {\n walk(child);\n }\n };\n\n walk(this.#rootElement);\n return result;\n }\n\n /**\n * Gets all child elements including slotted content for shadow DOM elements.\n * For elements with shadow DOM that contain slots, this returns the assigned\n * elements (slotted content) instead of just the shadow DOM children.\n */\n #getChildrenIncludingSlotted(element: Element): Iterable<Element> {\n // If element has shadowRoot with slots, get assigned elements\n if (element.shadowRoot) {\n const slots = element.shadowRoot.querySelectorAll(\"slot\");\n if (slots.length > 0) {\n const assignedElements: Element[] = [];\n for (const slot of slots) {\n assignedElements.push(...slot.assignedElements());\n }\n // Also include shadow DOM children that aren't slots (for mixed content)\n for (const child of element.shadowRoot.children) {\n if (child.tagName !== \"SLOT\") {\n assignedElements.push(child);\n }\n }\n return assignedElements;\n }\n }\n\n // Return HTMLCollection directly (iterable, no allocation)\n return element.children;\n }\n\n /**\n * Check if a render is currently in progress.\n */\n get isRendering(): boolean {\n return this.#renderInProgress;\n }\n}\n\n/**\n * Default frame state for elements that don't need special handling.\n * Use this for simple elements that are always ready.\n */\nexport const DEFAULT_FRAME_STATE: FrameState = {\n needsPreparation: false,\n isReady: true,\n priority: PRIORITY_DEFAULT,\n};\n\n/**\n * Helper to create a FrameRenderable mixin for elements.\n * Provides default implementations that can be overridden.\n */\nexport function createFrameRenderableMixin<\n T extends { new (...args: any[]): HTMLElement },\n>(Base: T) {\n return class FrameRenderableMixin extends Base implements FrameRenderable {\n getFrameState(_timeMs: number): FrameState {\n return DEFAULT_FRAME_STATE;\n }\n\n async prepareFrame(_timeMs: number, _signal: AbortSignal): Promise<void> {\n // Default: no preparation needed\n }\n\n renderFrame(_timeMs: number): void {\n // Default: no explicit render needed\n }\n };\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@editframe/elements",
|
|
3
|
-
"version": "0.40.
|
|
3
|
+
"version": "0.40.2",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"license": "UNLICENSED",
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@bramus/style-observer": "^1.3.0",
|
|
21
|
-
"@editframe/assets": "0.40.
|
|
21
|
+
"@editframe/assets": "0.40.2",
|
|
22
22
|
"@lit/context": "^1.1.6",
|
|
23
23
|
"@opentelemetry/api": "^1.9.0",
|
|
24
24
|
"@opentelemetry/context-zone": "^1.26.0",
|
|
@@ -111,4 +111,4 @@
|
|
|
111
111
|
},
|
|
112
112
|
"main": "./dist/index.js",
|
|
113
113
|
"module": "./dist/index.js"
|
|
114
|
-
}
|
|
114
|
+
}
|