@editframe/elements 0.40.5 → 0.40.6

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.
@@ -10,6 +10,7 @@ import { efConfigurationContext } from "./EFConfiguration.js";
10
10
  import { fetchContext } from "./fetchContext.js";
11
11
  import { focusContext } from "./focusContext.js";
12
12
  import { focusedElementContext } from "./focusedElementContext.js";
13
+ import { shouldSignUrl } from "./shouldSignUrl.js";
13
14
  import { ContextProvider, consume, createContext, provide } from "@lit/context";
14
15
  import { property, state } from "lit/decorators.js";
15
16
 
@@ -33,8 +34,7 @@ function ContextMixin(superClass) {
33
34
  init.headers ||= {};
34
35
  Object.assign(init.headers, { "Content-Type": "application/json" });
35
36
  }
36
- const isLocalEndpoint = url.startsWith("/@ef-");
37
- if (!EF_RENDERING() && this.signingURL && !isLocalEndpoint) {
37
+ if (!EF_RENDERING() && this.signingURL && shouldSignUrl(url, window.location.origin)) {
38
38
  const { cacheKey, signingPayload } = this.#getTokenCacheKey(url);
39
39
  const urlToken = await globalURLTokenDeduplicator.getToken(cacheKey, async () => {
40
40
  try {
@@ -52,7 +52,7 @@ function ContextMixin(superClass) {
52
52
  init.headers ||= {};
53
53
  Object.assign(init.headers, { authorization: `Bearer ${urlToken}` });
54
54
  } else {
55
- if (!this.#isCrossOrigin(url)) init.credentials = "include";
55
+ if (!shouldSignUrl(url, window.location.origin)) init.credentials = "include";
56
56
  if (this.#isEditframeDomain(url)) console.warn(`[Editframe] Request to ${new URL(url).hostname} has no signing URL configured. Ensure <ef-configuration signing-url="..."> is an ancestor of your <ef-preview> or <ef-workbench>.`);
57
57
  }
58
58
  try {
@@ -146,22 +146,6 @@ function ContextMixin(superClass) {
146
146
  break;
147
147
  }
148
148
  };
149
- /**
150
- * Generate a cache key for URL token based on signing strategy
151
- *
152
- * Uses unified prefix + parameter matching approach:
153
- * - For transcode URLs: signs base "/api/v1/transcode" + params like {url: "source.mp4"}
154
- * - For regular URLs: signs full URL with empty params {}
155
- * - All validation uses prefix matching + exhaustive parameter matching
156
- * - Multiple transcode segments with same source share one token (reduces round-trips)
157
- */
158
- #isCrossOrigin(url) {
159
- try {
160
- return new URL(url, window.location.origin).origin !== window.location.origin;
161
- } catch {
162
- return false;
163
- }
164
- }
165
149
  #isEditframeDomain(url) {
166
150
  try {
167
151
  const hostname = new URL(url).hostname;
@@ -1 +1 @@
1
- {"version":3,"file":"ContextMixin.js","names":["#getTokenCacheKey","#parseTokenExpiration","#isCrossOrigin","#isEditframeDomain","#apiHost","#targetTemporal","#subscribedController","#controllerSubscribed","#onControllerUpdate","#targetTemporalProvider","#loop","#playingProvider","#loopProvider","#currentTimeMsProvider","#signingURL","#collectUndefinedEFTags","#retryTemporalDiscovery","#timegroupObserver"],"sources":["../../src/gui/ContextMixin.ts"],"sourcesContent":["import { ContextProvider, consume, createContext, provide } from \"@lit/context\";\nimport type { LitElement } from \"lit\";\nimport { property, state } from \"lit/decorators.js\";\nimport { EF_RENDERING } from \"../EF_RENDERING.ts\";\nimport {\n isEFTemporal,\n type TemporalMixinInterface,\n} from \"../elements/EFTemporal.js\";\nimport { globalURLTokenDeduplicator } from \"../transcoding/cache/URLTokenDeduplicator.js\";\nimport { currentTimeContext } from \"./currentTimeContext.js\";\nimport { durationContext } from \"./durationContext.js\";\nimport {\n type EFConfiguration,\n efConfigurationContext,\n} from \"./EFConfiguration.ts\";\nimport { efContext } from \"./efContext.js\";\nimport { fetchContext } from \"./fetchContext.js\";\nimport { type FocusContext, focusContext } from \"./focusContext.js\";\nimport { focusedElementContext } from \"./focusedElementContext.js\";\nimport { loopContext, playingContext } from \"./playingContext.js\";\n\nexport const targetTemporalContext =\n createContext<TemporalMixinInterface | null>(Symbol(\"target-temporal\"));\n\nexport declare class ContextMixinInterface extends LitElement {\n signingURL?: string;\n apiHost?: string;\n rendering: boolean;\n playing: boolean;\n loop: boolean;\n currentTimeMs: number;\n focusedElement?: HTMLElement;\n targetTemporal: TemporalMixinInterface | null;\n play(): Promise<void>;\n pause(): void;\n}\n\nconst contextMixinSymbol = Symbol(\"contextMixin\");\n\nexport function isContextMixin(value: any): value is ContextMixinInterface {\n return (\n typeof value === \"object\" &&\n value !== null &&\n contextMixinSymbol in value.constructor\n );\n}\n\ntype Constructor<T = {}> = new (...args: any[]) => T;\nexport function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {\n class ContextElement extends superClass {\n static [contextMixinSymbol] = true;\n\n @consume({ context: efConfigurationContext, subscribe: true })\n efConfiguration: EFConfiguration | null = null;\n\n @provide({ context: focusContext })\n focusContext = this as FocusContext;\n\n @provide({ context: focusedElementContext })\n @state()\n focusedElement?: HTMLElement;\n\n #playingProvider!: ContextProvider<typeof playingContext>;\n #loopProvider!: ContextProvider<typeof loopContext>;\n #currentTimeMsProvider!: ContextProvider<typeof currentTimeContext>;\n #targetTemporalProvider!: ContextProvider<typeof targetTemporalContext>;\n\n #loop = false;\n\n #apiHost?: string;\n @property({ type: String, attribute: \"api-host\" })\n get apiHost() {\n return this.#apiHost ?? this.efConfiguration?.apiHost ?? \"\";\n }\n\n set apiHost(value: string) {\n this.#apiHost = value;\n }\n\n @provide({ context: efContext })\n efContext = this;\n\n #targetTemporal: TemporalMixinInterface | null = null;\n\n @state()\n get targetTemporal(): TemporalMixinInterface | null {\n return this.#targetTemporal;\n }\n #controllerSubscribed = false;\n\n /**\n * Find the first root temporal element (recursively searches through children)\n * Supports ef-timegroup, ef-video, ef-audio, and any other temporal elements\n * even when they're wrapped in non-temporal elements like divs\n */\n private findRootTemporal(): TemporalMixinInterface | null {\n const findRecursive = (\n element: Element,\n ): TemporalMixinInterface | null => {\n if (isEFTemporal(element)) {\n return element as TemporalMixinInterface & HTMLElement;\n }\n\n for (const child of element.children) {\n const found = findRecursive(child);\n if (found) return found;\n }\n\n return null;\n };\n\n for (const child of this.children) {\n const found = findRecursive(child);\n if (found) return found;\n }\n\n return null;\n }\n\n #subscribedController: any = null;\n\n set targetTemporal(value: TemporalMixinInterface | null) {\n if (\n this.#targetTemporal === value &&\n value?.playbackController === this.#subscribedController &&\n this.#controllerSubscribed\n )\n return;\n\n // Unsubscribe from old controller updates\n if (this.#subscribedController) {\n this.#subscribedController.removeListener(this.#onControllerUpdate);\n this.#controllerSubscribed = false;\n this.#subscribedController = null;\n }\n\n this.#targetTemporal = value;\n this.#targetTemporalProvider?.setValue(value);\n\n // Sync all provided contexts\n this.requestUpdate(\"targetTemporal\");\n this.requestUpdate(\"playing\");\n this.requestUpdate(\"loop\");\n this.requestUpdate(\"currentTimeMs\");\n\n // If the new targetTemporal has a playbackController, apply stored loop value immediately\n if (value?.playbackController && this.#loop) {\n value.playbackController.setLoop(this.#loop);\n }\n\n // If the new targetTemporal doesn't have a playbackController yet,\n // wait for it to complete its updates (it might be initializing)\n if (value && !value.playbackController) {\n // Wait for the temporal element to initialize\n (value as any).updateComplete?.then(() => {\n if (value === this.#targetTemporal && !this.#controllerSubscribed) {\n this.requestUpdate();\n }\n });\n }\n }\n\n #onControllerUpdate = (\n event: import(\"./PlaybackController.js\").PlaybackControllerUpdateEvent,\n ) => {\n switch (event.property) {\n case \"playing\":\n this.#playingProvider.setValue(event.value as boolean);\n break;\n case \"loop\":\n this.#loopProvider.setValue(event.value as boolean);\n break;\n case \"currentTimeMs\":\n this.#currentTimeMsProvider.setValue(event.value as number);\n break;\n }\n };\n\n // Add reactive properties that depend on the targetTemporal\n @provide({ context: durationContext })\n @property({ type: Number })\n durationMs = 0;\n\n @property({ type: Number })\n endTimeMs = 0;\n\n @provide({ context: fetchContext })\n fetch = async (url: string, init: RequestInit = {}) => {\n if (init.body) {\n init.headers ||= {};\n Object.assign(init.headers, {\n \"Content-Type\": \"application/json\",\n });\n }\n\n // Check if this is a local @ef-* endpoint that doesn't need authentication\n // These endpoints are handled by the Vite plugin locally and don't require signing\n const isLocalEndpoint = url.startsWith(\"/@ef-\");\n\n if (!EF_RENDERING() && this.signingURL && !isLocalEndpoint) {\n const { cacheKey, signingPayload } = this.#getTokenCacheKey(url);\n\n // Use global token deduplicator to share tokens across all context providers\n const urlToken = await globalURLTokenDeduplicator.getToken(\n cacheKey,\n async () => {\n try {\n const response = await fetch(this.signingURL, {\n method: \"POST\",\n body: JSON.stringify(signingPayload),\n });\n\n if (response.ok) {\n const tokenData = await response.json();\n return tokenData.token;\n }\n throw new Error(\n `Failed to sign URL: ${url}. SigningURL: ${this.signingURL} ${response.status} ${response.statusText}`,\n );\n } catch (error) {\n console.error(\"ContextMixin urlToken fetch error\", url, error);\n throw error;\n }\n },\n (token: string) => this.#parseTokenExpiration(token),\n );\n\n init.headers ||= {};\n Object.assign(init.headers, {\n authorization: `Bearer ${urlToken}`,\n });\n } else {\n // Only include credentials for same-origin requests where session cookies\n // are relevant. For cross-origin requests without a signing URL, credentials\n // cause CORS failures when the server responds with Access-Control-Allow-Origin: *\n if (!this.#isCrossOrigin(url)) {\n init.credentials = \"include\";\n }\n\n if (this.#isEditframeDomain(url)) {\n console.warn(\n `[Editframe] Request to ${new URL(url).hostname} has no signing URL configured. ` +\n `Ensure <ef-configuration signing-url=\"...\"> is an ancestor of your <ef-preview> or <ef-workbench>.`,\n );\n }\n }\n\n try {\n const fetchPromise = fetch(url, init);\n // Wrap the promise to catch rejections and log the URL\n // Return the promise chain so errors are logged but still propagate\n return fetchPromise.catch((error) => {\n // For AbortErrors, re-throw directly without modification\n // DOMException properties like 'name' are read-only\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n\n console.error(\n \"ContextMixin fetch error\",\n url,\n error,\n window.location.href,\n );\n // Create a new error with the URL in the message, preserving the original error type\n const ErrorConstructor =\n error instanceof Error ? error.constructor : Error;\n const enhancedError = new (ErrorConstructor as typeof Error)(\n `Failed to fetch: ${url}. Original error: ${error instanceof Error ? error.message : String(error)}`,\n );\n // Preserve the original error's properties (except for DOMException which has read-only properties)\n if (error instanceof Error && !(error instanceof DOMException)) {\n enhancedError.name = error.name;\n enhancedError.stack = error.stack;\n // Copy any additional properties from the original error\n Object.assign(enhancedError, error);\n }\n throw enhancedError;\n });\n } catch (error) {\n console.error(\n \"ContextMixin fetch error (synchronous)\",\n url,\n error,\n window.location.href,\n );\n throw error;\n }\n };\n\n // Note: URL token caching is now handled globally via URLTokenDeduplicator\n // Keeping these for any potential backwards compatibility, but they're no longer used\n\n /**\n * Generate a cache key for URL token based on signing strategy\n *\n * Uses unified prefix + parameter matching approach:\n * - For transcode URLs: signs base \"/api/v1/transcode\" + params like {url: \"source.mp4\"}\n * - For regular URLs: signs full URL with empty params {}\n * - All validation uses prefix matching + exhaustive parameter matching\n * - Multiple transcode segments with same source share one token (reduces round-trips)\n */\n #isCrossOrigin(url: string): boolean {\n try {\n const targetUrl = new URL(url, window.location.origin);\n return targetUrl.origin !== window.location.origin;\n } catch {\n return false;\n }\n }\n\n #isEditframeDomain(url: string): boolean {\n try {\n const hostname = new URL(url).hostname;\n return (\n hostname === \"editframe.dev\" ||\n hostname === \"editframe.com\" ||\n hostname.endsWith(\".editframe.dev\") ||\n hostname.endsWith(\".editframe.com\")\n );\n } catch {\n return false;\n }\n }\n\n #getTokenCacheKey(url: string): {\n cacheKey: string;\n signingPayload: { url: string; params?: Record<string, string> };\n } {\n try {\n const urlObj = new URL(url);\n\n // Check if this is a transcode URL pattern\n if (urlObj.pathname.includes(\"/api/v1/transcode/\")) {\n const urlParam = urlObj.searchParams.get(\"url\");\n if (urlParam) {\n // For transcode URLs, sign the base path + url parameter\n const basePath = `${urlObj.origin}/api/v1/transcode`;\n const cacheKey = `${basePath}?url=${urlParam}`;\n return {\n cacheKey,\n signingPayload: { url: basePath, params: { url: urlParam } },\n };\n }\n }\n\n // For non-transcode URLs, use full URL (existing behavior)\n return {\n cacheKey: url,\n signingPayload: { url },\n };\n } catch {\n // If URL parsing fails, fall back to full URL\n return {\n cacheKey: url,\n signingPayload: { url },\n };\n }\n }\n\n /**\n * Parse JWT token to extract safe expiration time (with buffer)\n * @param token JWT token string\n * @returns Safe expiration timestamp in milliseconds (actual expiry minus buffer), or 0 if parsing fails\n */\n #parseTokenExpiration(token: string): number {\n try {\n // JWT has 3 parts separated by dots: header.payload.signature\n const parts = token.split(\".\");\n if (parts.length !== 3) return 0;\n\n // Decode the payload (second part)\n const payload = parts[1];\n if (!payload) return 0;\n\n const decoded = atob(payload.replace(/-/g, \"+\").replace(/_/g, \"/\"));\n const parsed = JSON.parse(decoded);\n\n // Extract timestamps (in seconds)\n const exp = parsed.exp;\n const iat = parsed.iat;\n if (!exp) return 0;\n\n // Calculate token lifetime and buffer\n const lifetimeSeconds = iat ? exp - iat : 3600; // Default to 1 hour if no iat\n const tenPercentBufferMs = lifetimeSeconds * 0.1 * 1000; // 10% of lifetime in ms\n const fiveMinutesMs = 5 * 60 * 1000; // 5 minutes in ms\n\n // Use whichever buffer is smaller (more conservative)\n const bufferMs = Math.min(fiveMinutesMs, tenPercentBufferMs);\n\n // Return expiration time minus buffer\n return exp * 1000 - bufferMs;\n } catch {\n return 0;\n }\n }\n\n #signingURL?: string;\n /**\n * A URL that will be used to generated signed tokens for accessing media files from the\n * editframe API. This is used to authenticate media requests per-user.\n */\n @property({ type: String, attribute: \"signing-url\" })\n get signingURL() {\n return this.#signingURL ?? this.efConfiguration?.signingURL ?? \"\";\n }\n set signingURL(value: string) {\n this.#signingURL = value;\n }\n\n @property({ type: Boolean, reflect: true })\n get playing(): boolean {\n return this.targetTemporal?.playbackController?.playing ?? false;\n }\n set playing(value: boolean) {\n if (this.targetTemporal?.playbackController) {\n this.targetTemporal.playbackController.setPlaying(value);\n }\n }\n\n @property({ type: Boolean, reflect: true, attribute: \"loop\" })\n get loop(): boolean {\n return this.targetTemporal?.playbackController?.loop ?? this.#loop;\n }\n set loop(value: boolean) {\n const oldValue = this.#loop;\n this.#loop = value;\n if (this.targetTemporal?.playbackController) {\n this.targetTemporal.playbackController.setLoop(value);\n }\n this.requestUpdate(\"loop\", oldValue);\n }\n\n @property({ type: Boolean })\n rendering = false;\n\n @property({ type: Number })\n get currentTimeMs(): number {\n return (\n this.targetTemporal?.playbackController?.currentTimeMs ?? Number.NaN\n );\n }\n set currentTimeMs(value: number) {\n if (this.targetTemporal?.playbackController) {\n this.targetTemporal.playbackController.setCurrentTimeMs(value);\n }\n }\n\n #timegroupObserver = new MutationObserver((mutations) => {\n let shouldUpdate = false;\n const undefinedEFTags = new Set<string>();\n\n for (const mutation of mutations) {\n if (mutation.type === \"childList\") {\n const newTemporal = this.findRootTemporal();\n if (newTemporal !== this.targetTemporal) {\n this.targetTemporal = newTemporal;\n shouldUpdate = true;\n } else if (\n mutation.target instanceof Element &&\n isEFTemporal(mutation.target)\n ) {\n // Handle childList changes within existing temporal elements\n shouldUpdate = true;\n }\n\n // Collect ef-* tags from added nodes that haven't upgraded yet.\n // When React hydrates or TimelineRoot renders, the custom element\n // may be inserted before its class is defined, so isEFTemporal()\n // returns false. We need to retry after the element upgrades.\n if (!this.targetTemporal) {\n for (const node of mutation.addedNodes) {\n if (node instanceof Element) {\n this.#collectUndefinedEFTags(node, undefinedEFTags);\n }\n }\n }\n } else if (mutation.type === \"attributes\") {\n // Watch for attribute changes that might affect duration\n const durationAffectingAttributes = [\n \"duration\",\n \"mode\",\n \"trimstart\",\n \"trimend\",\n \"sourcein\",\n \"sourceout\",\n ];\n\n if (\n durationAffectingAttributes.includes(\n mutation.attributeName || \"\",\n ) ||\n (mutation.target instanceof Element &&\n isEFTemporal(mutation.target))\n ) {\n shouldUpdate = true;\n }\n }\n }\n\n if (undefinedEFTags.size > 0) {\n this.#retryTemporalDiscovery(undefinedEFTags);\n }\n\n if (shouldUpdate) {\n // Trigger an update to ensure reactive properties recalculate\n // Use a microtask to ensure DOM updates are complete\n queueMicrotask(() => {\n // Recalculate duration and endTime when temporal element changes\n this.updateDurationProperties();\n this.requestUpdate();\n // Also ensure the targetTemporal updates its computed properties\n if (this.targetTemporal) {\n (this.targetTemporal as any).requestUpdate();\n }\n });\n }\n });\n\n /**\n * Recursively collect ef-* tag names from an element tree that\n * have not yet been registered as custom elements.\n */\n #collectUndefinedEFTags(el: Element, tags: Set<string>): void {\n const tag = el.tagName.toLowerCase();\n if (tag.startsWith(\"ef-\") && !customElements.get(tag)) {\n tags.add(tag);\n }\n for (const child of el.children) {\n this.#collectUndefinedEFTags(child, tags);\n }\n }\n\n /**\n * Wait for unregistered ef-* custom elements to upgrade, then\n * retry findRootTemporal(). Mirrors the whenDefined pattern in play().\n */\n async #retryTemporalDiscovery(tags: Set<string>): Promise<void> {\n await Promise.all(\n [...tags].map((tag) => customElements.whenDefined(tag).catch(() => {})),\n );\n\n if (this.targetTemporal) return; // already found by another path\n\n const found = this.findRootTemporal();\n if (found) {\n this.targetTemporal = found;\n await (found as any).updateComplete;\n this.updateDurationProperties();\n this.requestUpdate();\n }\n }\n\n /**\n * Update duration properties when temporal element changes\n */\n updateDurationProperties(): void {\n const newDuration = this.targetTemporal?.durationMs ?? 0;\n const newEndTime = this.targetTemporal?.endTimeMs ?? 0;\n\n if (this.durationMs !== newDuration) {\n this.durationMs = newDuration;\n }\n\n if (this.endTimeMs !== newEndTime) {\n this.endTimeMs = newEndTime;\n }\n }\n\n connectedCallback(): void {\n super.connectedCallback();\n\n // Create manual context providers for playback state\n this.#playingProvider = new ContextProvider(this, {\n context: playingContext,\n initialValue: this.playing,\n });\n this.#loopProvider = new ContextProvider(this, {\n context: loopContext,\n initialValue: this.loop,\n });\n this.#currentTimeMsProvider = new ContextProvider(this, {\n context: currentTimeContext,\n initialValue: this.currentTimeMs,\n });\n this.#targetTemporalProvider = new ContextProvider(this, {\n context: targetTemporalContext,\n initialValue: this.targetTemporal,\n });\n\n // Initialize targetTemporal to first root temporal element\n this.targetTemporal = this.findRootTemporal();\n // Initialize duration properties\n this.updateDurationProperties();\n\n this.#timegroupObserver.observe(this, {\n childList: true,\n subtree: true,\n attributes: true,\n });\n }\n\n disconnectedCallback(): void {\n super.disconnectedCallback();\n this.#timegroupObserver.disconnect();\n\n // Unsubscribe from controller\n if (this.#subscribedController) {\n this.#subscribedController.removeListener(this.#onControllerUpdate);\n this.#controllerSubscribed = false;\n this.#subscribedController = null;\n }\n\n this.pause();\n }\n\n updated(changedProperties: Map<string | number | symbol, unknown>) {\n super.updated?.(changedProperties);\n\n // Subscribe to controller when it becomes available or changes\n const currentController = this.#targetTemporal?.playbackController;\n if (\n currentController &&\n (!this.#controllerSubscribed ||\n this.#subscribedController !== currentController)\n ) {\n // Unsubscribe from old controller if it changed\n if (\n this.#subscribedController &&\n this.#subscribedController !== currentController\n ) {\n this.#subscribedController.removeListener(this.#onControllerUpdate);\n }\n currentController.addListener(this.#onControllerUpdate);\n this.#controllerSubscribed = true;\n this.#subscribedController = currentController;\n\n // Apply stored loop value when playbackController becomes available\n if (this.#loop) {\n currentController.setLoop(this.#loop);\n }\n\n // Trigger initial sync of context providers\n this.#playingProvider.setValue(this.playing);\n this.#loopProvider.setValue(this.loop);\n this.#currentTimeMsProvider.setValue(this.currentTimeMs);\n }\n }\n\n async play() {\n // If targetTemporal is not set, try to find it now\n // This handles cases where the DOM may not have been fully ready during connectedCallback\n if (!this.targetTemporal) {\n // Wait for any temporal custom elements to be defined\n const potentialTemporalTags = Array.from(this.children)\n .map((el) => el.tagName.toLowerCase())\n .filter((tag) => tag.startsWith(\"ef-\"));\n\n await Promise.all(\n potentialTemporalTags.map((tag) =>\n customElements.whenDefined(tag).catch(() => {}),\n ),\n );\n\n const foundTemporal = this.findRootTemporal();\n if (foundTemporal) {\n this.targetTemporal = foundTemporal;\n // Wait for it to initialize\n await (foundTemporal as any).updateComplete;\n } else {\n console.warn(\"No temporal element found to play\");\n return;\n }\n }\n\n // If playbackController doesn't exist yet, wait for it\n if (!this.targetTemporal.playbackController) {\n await (this.targetTemporal as any).updateComplete;\n // After waiting, check again\n if (!this.targetTemporal.playbackController) {\n console.warn(\"PlaybackController not available for temporal element\");\n return;\n }\n }\n\n this.targetTemporal.playbackController.play();\n }\n\n pause() {\n if (this.targetTemporal?.playbackController) {\n this.targetTemporal.playbackController.pause();\n }\n }\n }\n\n return ContextElement as Constructor<ContextMixinInterface> & T;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ContextMixin.js","names":["#getTokenCacheKey","#parseTokenExpiration","#isEditframeDomain","#apiHost","#targetTemporal","#subscribedController","#controllerSubscribed","#onControllerUpdate","#targetTemporalProvider","#loop","#playingProvider","#loopProvider","#currentTimeMsProvider","#signingURL","#collectUndefinedEFTags","#retryTemporalDiscovery","#timegroupObserver"],"sources":["../../src/gui/ContextMixin.ts"],"sourcesContent":["import { ContextProvider, consume, createContext, provide } from \"@lit/context\";\nimport type { LitElement } from \"lit\";\nimport { property, state } from \"lit/decorators.js\";\nimport { EF_RENDERING } from \"../EF_RENDERING.ts\";\nimport {\n isEFTemporal,\n type TemporalMixinInterface,\n} from \"../elements/EFTemporal.js\";\nimport { globalURLTokenDeduplicator } from \"../transcoding/cache/URLTokenDeduplicator.js\";\nimport { currentTimeContext } from \"./currentTimeContext.js\";\nimport { durationContext } from \"./durationContext.js\";\nimport {\n type EFConfiguration,\n efConfigurationContext,\n} from \"./EFConfiguration.ts\";\nimport { efContext } from \"./efContext.js\";\nimport { fetchContext } from \"./fetchContext.js\";\nimport { type FocusContext, focusContext } from \"./focusContext.js\";\nimport { focusedElementContext } from \"./focusedElementContext.js\";\nimport { loopContext, playingContext } from \"./playingContext.js\";\nimport { shouldSignUrl } from \"./shouldSignUrl.js\";\n\nexport const targetTemporalContext =\n createContext<TemporalMixinInterface | null>(Symbol(\"target-temporal\"));\n\nexport declare class ContextMixinInterface extends LitElement {\n signingURL?: string;\n apiHost?: string;\n rendering: boolean;\n playing: boolean;\n loop: boolean;\n currentTimeMs: number;\n focusedElement?: HTMLElement;\n targetTemporal: TemporalMixinInterface | null;\n play(): Promise<void>;\n pause(): void;\n}\n\nconst contextMixinSymbol = Symbol(\"contextMixin\");\n\nexport function isContextMixin(value: any): value is ContextMixinInterface {\n return (\n typeof value === \"object\" &&\n value !== null &&\n contextMixinSymbol in value.constructor\n );\n}\n\ntype Constructor<T = {}> = new (...args: any[]) => T;\nexport function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {\n class ContextElement extends superClass {\n static [contextMixinSymbol] = true;\n\n @consume({ context: efConfigurationContext, subscribe: true })\n efConfiguration: EFConfiguration | null = null;\n\n @provide({ context: focusContext })\n focusContext = this as FocusContext;\n\n @provide({ context: focusedElementContext })\n @state()\n focusedElement?: HTMLElement;\n\n #playingProvider!: ContextProvider<typeof playingContext>;\n #loopProvider!: ContextProvider<typeof loopContext>;\n #currentTimeMsProvider!: ContextProvider<typeof currentTimeContext>;\n #targetTemporalProvider!: ContextProvider<typeof targetTemporalContext>;\n\n #loop = false;\n\n #apiHost?: string;\n @property({ type: String, attribute: \"api-host\" })\n get apiHost() {\n return this.#apiHost ?? this.efConfiguration?.apiHost ?? \"\";\n }\n\n set apiHost(value: string) {\n this.#apiHost = value;\n }\n\n @provide({ context: efContext })\n efContext = this;\n\n #targetTemporal: TemporalMixinInterface | null = null;\n\n @state()\n get targetTemporal(): TemporalMixinInterface | null {\n return this.#targetTemporal;\n }\n #controllerSubscribed = false;\n\n /**\n * Find the first root temporal element (recursively searches through children)\n * Supports ef-timegroup, ef-video, ef-audio, and any other temporal elements\n * even when they're wrapped in non-temporal elements like divs\n */\n private findRootTemporal(): TemporalMixinInterface | null {\n const findRecursive = (\n element: Element,\n ): TemporalMixinInterface | null => {\n if (isEFTemporal(element)) {\n return element as TemporalMixinInterface & HTMLElement;\n }\n\n for (const child of element.children) {\n const found = findRecursive(child);\n if (found) return found;\n }\n\n return null;\n };\n\n for (const child of this.children) {\n const found = findRecursive(child);\n if (found) return found;\n }\n\n return null;\n }\n\n #subscribedController: any = null;\n\n set targetTemporal(value: TemporalMixinInterface | null) {\n if (\n this.#targetTemporal === value &&\n value?.playbackController === this.#subscribedController &&\n this.#controllerSubscribed\n )\n return;\n\n // Unsubscribe from old controller updates\n if (this.#subscribedController) {\n this.#subscribedController.removeListener(this.#onControllerUpdate);\n this.#controllerSubscribed = false;\n this.#subscribedController = null;\n }\n\n this.#targetTemporal = value;\n this.#targetTemporalProvider?.setValue(value);\n\n // Sync all provided contexts\n this.requestUpdate(\"targetTemporal\");\n this.requestUpdate(\"playing\");\n this.requestUpdate(\"loop\");\n this.requestUpdate(\"currentTimeMs\");\n\n // If the new targetTemporal has a playbackController, apply stored loop value immediately\n if (value?.playbackController && this.#loop) {\n value.playbackController.setLoop(this.#loop);\n }\n\n // If the new targetTemporal doesn't have a playbackController yet,\n // wait for it to complete its updates (it might be initializing)\n if (value && !value.playbackController) {\n // Wait for the temporal element to initialize\n (value as any).updateComplete?.then(() => {\n if (value === this.#targetTemporal && !this.#controllerSubscribed) {\n this.requestUpdate();\n }\n });\n }\n }\n\n #onControllerUpdate = (\n event: import(\"./PlaybackController.js\").PlaybackControllerUpdateEvent,\n ) => {\n switch (event.property) {\n case \"playing\":\n this.#playingProvider.setValue(event.value as boolean);\n break;\n case \"loop\":\n this.#loopProvider.setValue(event.value as boolean);\n break;\n case \"currentTimeMs\":\n this.#currentTimeMsProvider.setValue(event.value as number);\n break;\n }\n };\n\n // Add reactive properties that depend on the targetTemporal\n @provide({ context: durationContext })\n @property({ type: Number })\n durationMs = 0;\n\n @property({ type: Number })\n endTimeMs = 0;\n\n @provide({ context: fetchContext })\n fetch = async (url: string, init: RequestInit = {}) => {\n if (init.body) {\n init.headers ||= {};\n Object.assign(init.headers, {\n \"Content-Type\": \"application/json\",\n });\n }\n\n if (\n !EF_RENDERING() &&\n this.signingURL &&\n shouldSignUrl(url, window.location.origin)\n ) {\n const { cacheKey, signingPayload } = this.#getTokenCacheKey(url);\n\n // Use global token deduplicator to share tokens across all context providers\n const urlToken = await globalURLTokenDeduplicator.getToken(\n cacheKey,\n async () => {\n try {\n const response = await fetch(this.signingURL, {\n method: \"POST\",\n body: JSON.stringify(signingPayload),\n });\n\n if (response.ok) {\n const tokenData = await response.json();\n return tokenData.token;\n }\n throw new Error(\n `Failed to sign URL: ${url}. SigningURL: ${this.signingURL} ${response.status} ${response.statusText}`,\n );\n } catch (error) {\n console.error(\"ContextMixin urlToken fetch error\", url, error);\n throw error;\n }\n },\n (token: string) => this.#parseTokenExpiration(token),\n );\n\n init.headers ||= {};\n Object.assign(init.headers, {\n authorization: `Bearer ${urlToken}`,\n });\n } else {\n // Only include credentials for same-origin requests where session cookies\n // are relevant. For cross-origin requests without a signing URL, credentials\n // cause CORS failures when the server responds with Access-Control-Allow-Origin: *\n if (!shouldSignUrl(url, window.location.origin)) {\n init.credentials = \"include\";\n }\n\n if (this.#isEditframeDomain(url)) {\n console.warn(\n `[Editframe] Request to ${new URL(url).hostname} has no signing URL configured. ` +\n `Ensure <ef-configuration signing-url=\"...\"> is an ancestor of your <ef-preview> or <ef-workbench>.`,\n );\n }\n }\n\n try {\n const fetchPromise = fetch(url, init);\n // Wrap the promise to catch rejections and log the URL\n // Return the promise chain so errors are logged but still propagate\n return fetchPromise.catch((error) => {\n // For AbortErrors, re-throw directly without modification\n // DOMException properties like 'name' are read-only\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n\n console.error(\n \"ContextMixin fetch error\",\n url,\n error,\n window.location.href,\n );\n // Create a new error with the URL in the message, preserving the original error type\n const ErrorConstructor =\n error instanceof Error ? error.constructor : Error;\n const enhancedError = new (ErrorConstructor as typeof Error)(\n `Failed to fetch: ${url}. Original error: ${error instanceof Error ? error.message : String(error)}`,\n );\n // Preserve the original error's properties (except for DOMException which has read-only properties)\n if (error instanceof Error && !(error instanceof DOMException)) {\n enhancedError.name = error.name;\n enhancedError.stack = error.stack;\n // Copy any additional properties from the original error\n Object.assign(enhancedError, error);\n }\n throw enhancedError;\n });\n } catch (error) {\n console.error(\n \"ContextMixin fetch error (synchronous)\",\n url,\n error,\n window.location.href,\n );\n throw error;\n }\n };\n\n #isEditframeDomain(url: string): boolean {\n try {\n const hostname = new URL(url).hostname;\n return (\n hostname === \"editframe.dev\" ||\n hostname === \"editframe.com\" ||\n hostname.endsWith(\".editframe.dev\") ||\n hostname.endsWith(\".editframe.com\")\n );\n } catch {\n return false;\n }\n }\n\n #getTokenCacheKey(url: string): {\n cacheKey: string;\n signingPayload: { url: string; params?: Record<string, string> };\n } {\n try {\n const urlObj = new URL(url);\n\n // Check if this is a transcode URL pattern\n if (urlObj.pathname.includes(\"/api/v1/transcode/\")) {\n const urlParam = urlObj.searchParams.get(\"url\");\n if (urlParam) {\n // For transcode URLs, sign the base path + url parameter\n const basePath = `${urlObj.origin}/api/v1/transcode`;\n const cacheKey = `${basePath}?url=${urlParam}`;\n return {\n cacheKey,\n signingPayload: { url: basePath, params: { url: urlParam } },\n };\n }\n }\n\n // For non-transcode URLs, use full URL (existing behavior)\n return {\n cacheKey: url,\n signingPayload: { url },\n };\n } catch {\n // If URL parsing fails, fall back to full URL\n return {\n cacheKey: url,\n signingPayload: { url },\n };\n }\n }\n\n /**\n * Parse JWT token to extract safe expiration time (with buffer)\n * @param token JWT token string\n * @returns Safe expiration timestamp in milliseconds (actual expiry minus buffer), or 0 if parsing fails\n */\n #parseTokenExpiration(token: string): number {\n try {\n // JWT has 3 parts separated by dots: header.payload.signature\n const parts = token.split(\".\");\n if (parts.length !== 3) return 0;\n\n // Decode the payload (second part)\n const payload = parts[1];\n if (!payload) return 0;\n\n const decoded = atob(payload.replace(/-/g, \"+\").replace(/_/g, \"/\"));\n const parsed = JSON.parse(decoded);\n\n // Extract timestamps (in seconds)\n const exp = parsed.exp;\n const iat = parsed.iat;\n if (!exp) return 0;\n\n // Calculate token lifetime and buffer\n const lifetimeSeconds = iat ? exp - iat : 3600; // Default to 1 hour if no iat\n const tenPercentBufferMs = lifetimeSeconds * 0.1 * 1000; // 10% of lifetime in ms\n const fiveMinutesMs = 5 * 60 * 1000; // 5 minutes in ms\n\n // Use whichever buffer is smaller (more conservative)\n const bufferMs = Math.min(fiveMinutesMs, tenPercentBufferMs);\n\n // Return expiration time minus buffer\n return exp * 1000 - bufferMs;\n } catch {\n return 0;\n }\n }\n\n #signingURL?: string;\n /**\n * A URL that will be used to generated signed tokens for accessing media files from the\n * editframe API. This is used to authenticate media requests per-user.\n */\n @property({ type: String, attribute: \"signing-url\" })\n get signingURL() {\n return this.#signingURL ?? this.efConfiguration?.signingURL ?? \"\";\n }\n set signingURL(value: string) {\n this.#signingURL = value;\n }\n\n @property({ type: Boolean, reflect: true })\n get playing(): boolean {\n return this.targetTemporal?.playbackController?.playing ?? false;\n }\n set playing(value: boolean) {\n if (this.targetTemporal?.playbackController) {\n this.targetTemporal.playbackController.setPlaying(value);\n }\n }\n\n @property({ type: Boolean, reflect: true, attribute: \"loop\" })\n get loop(): boolean {\n return this.targetTemporal?.playbackController?.loop ?? this.#loop;\n }\n set loop(value: boolean) {\n const oldValue = this.#loop;\n this.#loop = value;\n if (this.targetTemporal?.playbackController) {\n this.targetTemporal.playbackController.setLoop(value);\n }\n this.requestUpdate(\"loop\", oldValue);\n }\n\n @property({ type: Boolean })\n rendering = false;\n\n @property({ type: Number })\n get currentTimeMs(): number {\n return (\n this.targetTemporal?.playbackController?.currentTimeMs ?? Number.NaN\n );\n }\n set currentTimeMs(value: number) {\n if (this.targetTemporal?.playbackController) {\n this.targetTemporal.playbackController.setCurrentTimeMs(value);\n }\n }\n\n #timegroupObserver = new MutationObserver((mutations) => {\n let shouldUpdate = false;\n const undefinedEFTags = new Set<string>();\n\n for (const mutation of mutations) {\n if (mutation.type === \"childList\") {\n const newTemporal = this.findRootTemporal();\n if (newTemporal !== this.targetTemporal) {\n this.targetTemporal = newTemporal;\n shouldUpdate = true;\n } else if (\n mutation.target instanceof Element &&\n isEFTemporal(mutation.target)\n ) {\n // Handle childList changes within existing temporal elements\n shouldUpdate = true;\n }\n\n // Collect ef-* tags from added nodes that haven't upgraded yet.\n // When React hydrates or TimelineRoot renders, the custom element\n // may be inserted before its class is defined, so isEFTemporal()\n // returns false. We need to retry after the element upgrades.\n if (!this.targetTemporal) {\n for (const node of mutation.addedNodes) {\n if (node instanceof Element) {\n this.#collectUndefinedEFTags(node, undefinedEFTags);\n }\n }\n }\n } else if (mutation.type === \"attributes\") {\n // Watch for attribute changes that might affect duration\n const durationAffectingAttributes = [\n \"duration\",\n \"mode\",\n \"trimstart\",\n \"trimend\",\n \"sourcein\",\n \"sourceout\",\n ];\n\n if (\n durationAffectingAttributes.includes(\n mutation.attributeName || \"\",\n ) ||\n (mutation.target instanceof Element &&\n isEFTemporal(mutation.target))\n ) {\n shouldUpdate = true;\n }\n }\n }\n\n if (undefinedEFTags.size > 0) {\n this.#retryTemporalDiscovery(undefinedEFTags);\n }\n\n if (shouldUpdate) {\n // Trigger an update to ensure reactive properties recalculate\n // Use a microtask to ensure DOM updates are complete\n queueMicrotask(() => {\n // Recalculate duration and endTime when temporal element changes\n this.updateDurationProperties();\n this.requestUpdate();\n // Also ensure the targetTemporal updates its computed properties\n if (this.targetTemporal) {\n (this.targetTemporal as any).requestUpdate();\n }\n });\n }\n });\n\n /**\n * Recursively collect ef-* tag names from an element tree that\n * have not yet been registered as custom elements.\n */\n #collectUndefinedEFTags(el: Element, tags: Set<string>): void {\n const tag = el.tagName.toLowerCase();\n if (tag.startsWith(\"ef-\") && !customElements.get(tag)) {\n tags.add(tag);\n }\n for (const child of el.children) {\n this.#collectUndefinedEFTags(child, tags);\n }\n }\n\n /**\n * Wait for unregistered ef-* custom elements to upgrade, then\n * retry findRootTemporal(). Mirrors the whenDefined pattern in play().\n */\n async #retryTemporalDiscovery(tags: Set<string>): Promise<void> {\n await Promise.all(\n [...tags].map((tag) => customElements.whenDefined(tag).catch(() => {})),\n );\n\n if (this.targetTemporal) return; // already found by another path\n\n const found = this.findRootTemporal();\n if (found) {\n this.targetTemporal = found;\n await (found as any).updateComplete;\n this.updateDurationProperties();\n this.requestUpdate();\n }\n }\n\n /**\n * Update duration properties when temporal element changes\n */\n updateDurationProperties(): void {\n const newDuration = this.targetTemporal?.durationMs ?? 0;\n const newEndTime = this.targetTemporal?.endTimeMs ?? 0;\n\n if (this.durationMs !== newDuration) {\n this.durationMs = newDuration;\n }\n\n if (this.endTimeMs !== newEndTime) {\n this.endTimeMs = newEndTime;\n }\n }\n\n connectedCallback(): void {\n super.connectedCallback();\n\n // Create manual context providers for playback state\n this.#playingProvider = new ContextProvider(this, {\n context: playingContext,\n initialValue: this.playing,\n });\n this.#loopProvider = new ContextProvider(this, {\n context: loopContext,\n initialValue: this.loop,\n });\n this.#currentTimeMsProvider = new ContextProvider(this, {\n context: currentTimeContext,\n initialValue: this.currentTimeMs,\n });\n this.#targetTemporalProvider = new ContextProvider(this, {\n context: targetTemporalContext,\n initialValue: this.targetTemporal,\n });\n\n // Initialize targetTemporal to first root temporal element\n this.targetTemporal = this.findRootTemporal();\n // Initialize duration properties\n this.updateDurationProperties();\n\n this.#timegroupObserver.observe(this, {\n childList: true,\n subtree: true,\n attributes: true,\n });\n }\n\n disconnectedCallback(): void {\n super.disconnectedCallback();\n this.#timegroupObserver.disconnect();\n\n // Unsubscribe from controller\n if (this.#subscribedController) {\n this.#subscribedController.removeListener(this.#onControllerUpdate);\n this.#controllerSubscribed = false;\n this.#subscribedController = null;\n }\n\n this.pause();\n }\n\n updated(changedProperties: Map<string | number | symbol, unknown>) {\n super.updated?.(changedProperties);\n\n // Subscribe to controller when it becomes available or changes\n const currentController = this.#targetTemporal?.playbackController;\n if (\n currentController &&\n (!this.#controllerSubscribed ||\n this.#subscribedController !== currentController)\n ) {\n // Unsubscribe from old controller if it changed\n if (\n this.#subscribedController &&\n this.#subscribedController !== currentController\n ) {\n this.#subscribedController.removeListener(this.#onControllerUpdate);\n }\n currentController.addListener(this.#onControllerUpdate);\n this.#controllerSubscribed = true;\n this.#subscribedController = currentController;\n\n // Apply stored loop value when playbackController becomes available\n if (this.#loop) {\n currentController.setLoop(this.#loop);\n }\n\n // Trigger initial sync of context providers\n this.#playingProvider.setValue(this.playing);\n this.#loopProvider.setValue(this.loop);\n this.#currentTimeMsProvider.setValue(this.currentTimeMs);\n }\n }\n\n async play() {\n // If targetTemporal is not set, try to find it now\n // This handles cases where the DOM may not have been fully ready during connectedCallback\n if (!this.targetTemporal) {\n // Wait for any temporal custom elements to be defined\n const potentialTemporalTags = Array.from(this.children)\n .map((el) => el.tagName.toLowerCase())\n .filter((tag) => tag.startsWith(\"ef-\"));\n\n await Promise.all(\n potentialTemporalTags.map((tag) =>\n customElements.whenDefined(tag).catch(() => {}),\n ),\n );\n\n const foundTemporal = this.findRootTemporal();\n if (foundTemporal) {\n this.targetTemporal = foundTemporal;\n // Wait for it to initialize\n await (foundTemporal as any).updateComplete;\n } else {\n console.warn(\"No temporal element found to play\");\n return;\n }\n }\n\n // If playbackController doesn't exist yet, wait for it\n if (!this.targetTemporal.playbackController) {\n await (this.targetTemporal as any).updateComplete;\n // After waiting, check again\n if (!this.targetTemporal.playbackController) {\n console.warn(\"PlaybackController not available for temporal element\");\n return;\n }\n }\n\n this.targetTemporal.playbackController.play();\n }\n\n pause() {\n if (this.targetTemporal?.playbackController) {\n this.targetTemporal.playbackController.pause();\n }\n }\n }\n\n return ContextElement as Constructor<ContextMixinInterface> & T;\n}\n"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
- import * as lit33 from "lit";
1
+ import * as lit32 from "lit";
2
2
  import { LitElement } from "lit";
3
- import * as lit_html31 from "lit-html";
3
+ import * as lit_html30 from "lit-html";
4
4
 
5
5
  //#region src/gui/EFOverlayItem.d.ts
6
6
  /**
@@ -23,7 +23,7 @@ interface OverlayItemPosition {
23
23
  * ensures transforms are applied before positions are read.
24
24
  */
25
25
  declare class EFOverlayItem extends LitElement {
26
- static styles: lit33.CSSResult[];
26
+ static styles: lit32.CSSResult[];
27
27
  elementId?: string;
28
28
  target?: HTMLElement | string;
29
29
  private currentPosition;
@@ -36,7 +36,7 @@ declare class EFOverlayItem extends LitElement {
36
36
  updatePosition(): void;
37
37
  connectedCallback(): void;
38
38
  disconnectedCallback(): void;
39
- render(): lit_html31.TemplateResult<1>;
39
+ render(): lit_html30.TemplateResult<1>;
40
40
  }
41
41
  declare global {
42
42
  interface HTMLElementTagNameMap {
@@ -1,8 +1,8 @@
1
1
  import { PanZoomTransform } from "../elements/EFPanZoom.js";
2
2
  import { EFOverlayItem } from "./EFOverlayItem.js";
3
- import * as lit32 from "lit";
3
+ import * as lit31 from "lit";
4
4
  import { LitElement } from "lit";
5
- import * as lit_html30 from "lit-html";
5
+ import * as lit_html29 from "lit-html";
6
6
 
7
7
  //#region src/gui/EFOverlayLayer.d.ts
8
8
 
@@ -26,7 +26,7 @@ import * as lit_html30 from "lit-html";
26
26
  * 2. EFOverlayItem can use this rect for coordinate calculations
27
27
  */
28
28
  declare class EFOverlayLayer extends LitElement {
29
- static styles: lit32.CSSResult[];
29
+ static styles: lit31.CSSResult[];
30
30
  panZoomTransformFromContext?: PanZoomTransform;
31
31
  /**
32
32
  * Pan/zoom transform as fallback for when context or sibling PanZoom is not available.
@@ -58,7 +58,7 @@ declare class EFOverlayLayer extends LitElement {
58
58
  connectedCallback(): void;
59
59
  disconnectedCallback(): void;
60
60
  updated(): void;
61
- render(): lit_html30.TemplateResult<1>;
61
+ render(): lit_html29.TemplateResult<1>;
62
62
  }
63
63
  declare global {
64
64
  interface HTMLElementTagNameMap {
@@ -1,7 +1,7 @@
1
1
  import { TimelineState } from "./timeline/timelineStateContext.js";
2
- import * as lit34 from "lit";
2
+ import * as lit33 from "lit";
3
3
  import { LitElement } from "lit";
4
- import * as lit_html32 from "lit-html";
4
+ import * as lit_html31 from "lit-html";
5
5
 
6
6
  //#region src/gui/EFTimelineRuler.d.ts
7
7
  /**
@@ -25,7 +25,7 @@ declare function calculatePixelsPerFrame(frameIntervalMs: number, pixelsPerMs: n
25
25
  */
26
26
  declare function shouldShowFrameMarkers(pixelsPerFrame: number, minSpacing?: number): boolean;
27
27
  declare class EFTimelineRuler extends LitElement {
28
- static styles: lit34.CSSResult[];
28
+ static styles: lit33.CSSResult[];
29
29
  durationMs: number;
30
30
  contextDurationMs: number;
31
31
  timelineState?: TimelineState;
@@ -59,7 +59,7 @@ declare class EFTimelineRuler extends LitElement {
59
59
  private calculateLabelInterval;
60
60
  private getVisibleLabels;
61
61
  private renderCanvas;
62
- render(): lit_html32.TemplateResult<1>;
62
+ render(): lit_html31.TemplateResult<1>;
63
63
  }
64
64
  declare global {
65
65
  interface HTMLElementTagNameMap {
@@ -0,0 +1,23 @@
1
+ //#region src/gui/shouldSignUrl.ts
2
+ /**
3
+ * Determines whether a URL should be signed before fetching.
4
+ *
5
+ * Signing is skipped for:
6
+ * - Local vite plugin endpoints (/@ef-*)
7
+ * - Same-origin URLs (local dev server routes like /api/v1/transcode/*)
8
+ *
9
+ * @param url - The URL to evaluate
10
+ * @param currentOrigin - The current page origin (window.location.origin)
11
+ */
12
+ function shouldSignUrl(url, currentOrigin) {
13
+ if (url.startsWith("/@ef-")) return false;
14
+ try {
15
+ return new URL(url, currentOrigin).origin !== currentOrigin;
16
+ } catch {
17
+ return true;
18
+ }
19
+ }
20
+
21
+ //#endregion
22
+ export { shouldSignUrl };
23
+ //# sourceMappingURL=shouldSignUrl.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shouldSignUrl.js","names":[],"sources":["../../src/gui/shouldSignUrl.ts"],"sourcesContent":["/**\n * Determines whether a URL should be signed before fetching.\n *\n * Signing is skipped for:\n * - Local vite plugin endpoints (/@ef-*)\n * - Same-origin URLs (local dev server routes like /api/v1/transcode/*)\n *\n * @param url - The URL to evaluate\n * @param currentOrigin - The current page origin (window.location.origin)\n */\nexport function shouldSignUrl(url: string, currentOrigin: string): boolean {\n if (url.startsWith(\"/@ef-\")) return false;\n\n try {\n const targetUrl = new URL(url, currentOrigin);\n return targetUrl.origin !== currentOrigin;\n } catch {\n return true;\n }\n}\n"],"mappings":";;;;;;;;;;;AAUA,SAAgB,cAAc,KAAa,eAAgC;AACzE,KAAI,IAAI,WAAW,QAAQ,CAAE,QAAO;AAEpC,KAAI;AAEF,SADkB,IAAI,IAAI,KAAK,cAAc,CAC5B,WAAW;SACtB;AACN,SAAO"}
@@ -4,7 +4,7 @@ import { TimelineState } from "./timelineStateContext.js";
4
4
  import "./tracks/preloadTracks.js";
5
5
  import "./EFTimelineRow.js";
6
6
  import "../EFTimelineRuler.js";
7
- import * as lit35 from "lit";
7
+ import * as lit34 from "lit";
8
8
  import { LitElement, PropertyValues, TemplateResult } from "lit";
9
9
 
10
10
  //#region src/gui/timeline/EFTimeline.d.ts
@@ -17,7 +17,7 @@ declare const EFTimeline_base: typeof LitElement;
17
17
  */
18
18
  declare class EFTimeline extends EFTimeline_base {
19
19
  #private;
20
- static styles: lit35.CSSResult[];
20
+ static styles: lit34.CSSResult[];
21
21
  /**
22
22
  * Target element ID or "selection" to derive from canvas selection.
23
23
  *
@@ -1,7 +1,7 @@
1
1
  import { TimelineEditingContext } from "./timelineEditingContext.js";
2
- import * as lit31 from "lit";
2
+ import * as lit35 from "lit";
3
3
  import { LitElement } from "lit";
4
- import * as lit_html29 from "lit-html";
4
+ import * as lit_html32 from "lit-html";
5
5
 
6
6
  //#region src/gui/timeline/TrimHandles.d.ts
7
7
  interface TrimValue {
@@ -16,7 +16,7 @@ interface TrimChangeDetail {
16
16
  declare const EFTrimHandles_base: typeof LitElement;
17
17
  declare class EFTrimHandles extends EFTrimHandles_base {
18
18
  #private;
19
- static styles: lit31.CSSResult[];
19
+ static styles: lit35.CSSResult[];
20
20
  mode: "standalone" | "track";
21
21
  elementId: string;
22
22
  pixelsPerMs: number | null;
@@ -38,7 +38,7 @@ declare class EFTrimHandles extends EFTrimHandles_base {
38
38
  private handleRegionPointerDown;
39
39
  private handlePointerMove;
40
40
  private handlePointerUp;
41
- render(): lit_html29.TemplateResult<1>;
41
+ render(): lit_html32.TemplateResult<1>;
42
42
  }
43
43
  declare global {
44
44
  interface HTMLElementTagNameMap {
@@ -345,6 +345,35 @@ async function renderTimegroupToVideo(timegroup, options = {}) {
345
345
  } catch (e) {}
346
346
  if (config.benchmarkMode) return;
347
347
  await output.finalize();
348
+ if (options.telemetryToken) {
349
+ const elapsedMs = Math.round(performance.now() - renderStartTime);
350
+ const endpoint = options.telemetryEndpoint ?? "https://editframe.com";
351
+ const efMediaCount = timegroup.querySelectorAll("ef-video,ef-audio").length;
352
+ const efImageCount = timegroup.querySelectorAll("ef-image").length;
353
+ const efCaptionsCount = timegroup.querySelectorAll("ef-captions").length;
354
+ const efTextCount = timegroup.querySelectorAll("ef-text").length;
355
+ fetch(`${endpoint}/api/v1/telemetry`, {
356
+ method: "POST",
357
+ headers: {
358
+ "Content-Type": "application/json",
359
+ Authorization: `Bearer ${options.telemetryToken}`
360
+ },
361
+ body: JSON.stringify({
362
+ render_path: "client",
363
+ duration_ms: elapsedMs,
364
+ width: config.videoWidth,
365
+ height: config.videoHeight,
366
+ fps: config.fps,
367
+ feature_usage: {
368
+ efMediaCount,
369
+ efImageCount,
370
+ efCaptionsCount,
371
+ efTextCount
372
+ }
373
+ }),
374
+ keepalive: true
375
+ }).catch(() => {});
376
+ }
348
377
  if (useStreaming) return;
349
378
  else {
350
379
  const videoBuffer = target.buffer;
@@ -1 +1 @@
1
- {"version":3,"file":"renderTimegroupToVideo.js","names":["timestamps: number[]","output: Output | null","videoSource: CanvasSource | null","audioSource: AudioBufferSource | null","target: BufferTarget | StreamTarget | null","fileStream: {\n writable: WritableStream<Uint8Array>;\n close: () => Promise<void>;\n } | null","encodingCanvas: OffscreenCanvas | null","encodingCtx: OffscreenCanvasRenderingContext2D | null","videoConfig: VideoEncodingConfig","thumbCanvas: HTMLCanvasElement | null","thumbCtx: CanvasRenderingContext2D | null","pendingFrames: PendingFrame[]","entry: PendingFrame","image","image: HTMLImageElement"],"sources":["../../src/preview/renderTimegroupToVideo.ts"],"sourcesContent":["/**\n * Video rendering for timegroups using direct serialization.\n *\n * Architecture:\n * - Creates a render clone of the timeline\n * - For each frame:\n * 1. Seeks the clone to the target time\n * 2. Executes frame tasks (SVG updates, canvas draws, etc.)\n * 3. Serializes the live DOM directly to SVG+foreignObject data URI\n * 4. Renders to image and encodes to video\n *\n * RenderContext provides pixel caching across frames for performance.\n */\n\nimport { logger } from \"./logger.js\";\nimport {\n Output,\n Mp4OutputFormat,\n BufferTarget,\n StreamTarget,\n CanvasSource,\n AudioBufferSource,\n QUALITY_HIGH,\n canEncodeAudio,\n getEncodableAudioCodecs,\n type VideoEncodingConfig,\n type AudioEncodingConfig,\n type AudioCodec,\n} from \"mediabunny\";\nimport type { EFTimegroup } from \"../elements/EFTimegroup.js\";\nimport type { RenderToVideoOptions } from \"./renderTimegroupToVideo.types.js\";\nimport type { ContentReadyMode } from \"./renderTimegroupToCanvas.types.js\";\nimport {\n resetRenderState,\n waitForVideoContent,\n} from \"./renderTimegroupToCanvas.js\";\nimport { captureTimelineToDataUri } from \"./rendering/serializeTimelineDirect.js\";\nimport { renderToImageNative } from \"./rendering/renderToImageNative.js\";\nimport { isNativeCanvasApiAvailable } from \"./previewSettings.js\";\nimport { createPreviewContainer } from \"./previewTypes.js\";\nimport { RenderContext } from \"./RenderContext.js\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\n// Re-export types from type-only module (zero side effects)\nexport type {\n RenderProgress,\n RenderToVideoOptions,\n} from \"./renderTimegroupToVideo.types.js\";\n\n// ============================================================================\n// Errors\n// ============================================================================\n\nexport class NoSupportedAudioCodecError extends Error {\n constructor(requestedCodecs: AudioCodec[], availableCodecs: AudioCodec[]) {\n super(\n `No supported audio codec found. Requested: [${requestedCodecs.join(\", \")}], ` +\n `Available: [${availableCodecs.length > 0 ? availableCodecs.join(\", \") : \"none\"}]`,\n );\n this.name = \"NoSupportedAudioCodecError\";\n }\n}\n\nexport class RenderCancelledError extends Error {\n constructor() {\n super(\"Render cancelled\");\n this.name = \"RenderCancelledError\";\n }\n}\n\n// ============================================================================\n// Configuration\n// ============================================================================\n\ninterface ResolvedConfig {\n fps: number;\n codec: \"avc\" | \"hevc\" | \"vp9\" | \"av1\" | \"vp8\";\n bitrate: number;\n filename: string;\n scale: number;\n keyFrameInterval: number;\n startMs: number;\n endMs: number;\n renderDurationMs: number;\n width: number;\n height: number;\n videoWidth: number;\n videoHeight: number;\n totalFrames: number;\n frameDurationMs: number;\n frameDurationS: number;\n streaming: boolean;\n includeAudio: boolean;\n audioBitrate: number;\n contentReadyMode: ContentReadyMode;\n blockingTimeoutMs: number;\n returnBuffer: boolean;\n preferredAudioCodecs: AudioCodec[];\n benchmarkMode: boolean;\n progressPreviewInterval: number;\n canvasMode: \"native\" | \"foreignObject\";\n}\n\nfunction resolveConfig(\n timegroup: EFTimegroup,\n options: RenderToVideoOptions = {},\n): ResolvedConfig {\n const fps = options.fps ?? timegroup.effectiveFps ?? 30;\n const codec = options.codec ?? \"avc\";\n const bitrate = options.bitrate ?? 8_000_000;\n const filename = options.filename ?? \"timegroup-video.mp4\";\n const scale = options.scale ?? 1;\n const keyFrameInterval = options.keyFrameInterval ?? 2;\n const streaming = options.streaming ?? true;\n const includeAudio = options.includeAudio ?? true;\n const audioBitrate = options.audioBitrate ?? 128_000;\n const contentReadyMode = options.contentReadyMode ?? \"blocking\";\n const blockingTimeoutMs = options.blockingTimeoutMs ?? 5000;\n const returnBuffer = options.returnBuffer ?? false;\n const preferredAudioCodecs = options.preferredAudioCodecs ?? [\"aac\", \"opus\"];\n const benchmarkMode = options.benchmarkMode ?? false;\n // Preview generation now uses canvas reference (no encoding) - cheap to enable!\n // Defaults to 60 frames (every 2 seconds at 30fps). Set to 0 to disable.\n const progressPreviewInterval = options.progressPreviewInterval ?? 60;\n\n const totalDurationMs = timegroup.durationMs;\n if (!totalDurationMs || totalDurationMs <= 0) {\n throw new Error(\"Timegroup has no duration\");\n }\n\n const startMs = Math.max(0, options.fromMs ?? 0);\n const endMs =\n options.toMs !== undefined\n ? Math.min(options.toMs, totalDurationMs)\n : totalDurationMs;\n const renderDurationMs = endMs - startMs;\n\n if (renderDurationMs <= 0) {\n throw new Error(`Invalid render range: from ${startMs}ms to ${endMs}ms`);\n }\n\n // Force layout reflow before reading dimensions\n void timegroup.offsetHeight;\n\n // Try multiple sources for dimensions (offsetWidth can be 0 in headless browsers)\n let timegroupWidth = timegroup.offsetWidth;\n let timegroupHeight = timegroup.offsetHeight;\n\n if (!timegroupWidth || !timegroupHeight) {\n const rect = timegroup.getBoundingClientRect();\n if (rect.width > 0 && rect.height > 0) {\n timegroupWidth = rect.width;\n timegroupHeight = rect.height;\n }\n }\n\n if (!timegroupWidth || !timegroupHeight) {\n const computed = getComputedStyle(timegroup);\n const cw = parseFloat(computed.width);\n const ch = parseFloat(computed.height);\n if (cw > 0 && ch > 0) {\n timegroupWidth = cw;\n timegroupHeight = ch;\n }\n }\n\n if (!timegroupWidth || !timegroupHeight) {\n throw new Error(\n `Timegroup has no dimensions (${timegroupWidth}x${timegroupHeight}). ` +\n `Ensure the timegroup element is in the document and has explicit width/height styles ` +\n `(e.g., class=\"w-[1920px] h-[1080px]\")`,\n );\n }\n const width = Math.floor(timegroupWidth * scale);\n const height = Math.floor(timegroupHeight * scale);\n\n const videoWidth = width % 2 === 0 ? width : width - 1;\n const videoHeight = height % 2 === 0 ? height : height - 1;\n\n const frameDurationMs = 1000 / fps;\n const totalFrames = Math.ceil(renderDurationMs / frameDurationMs);\n const frameDurationS = frameDurationMs / 1000;\n\n // Determine effective canvas mode:\n // 1. If explicitly specified, use that (with fallback if native not available)\n // 2. If not specified, default to foreignObject for compatibility\n const canvasMode = (() => {\n const requested = options.canvasMode;\n if (!requested) return \"foreignObject\";\n if (requested === \"native\" && !isNativeCanvasApiAvailable()) {\n logger.debug(\n \"[renderTimegroupToVideo] Native canvas mode requested but not available, falling back to foreignObject\",\n );\n return \"foreignObject\";\n }\n return requested;\n })();\n\n return {\n fps,\n codec,\n bitrate,\n filename,\n scale,\n keyFrameInterval,\n startMs,\n endMs,\n renderDurationMs,\n width,\n height,\n videoWidth,\n videoHeight,\n totalFrames,\n frameDurationMs,\n frameDurationS,\n streaming,\n includeAudio,\n audioBitrate,\n contentReadyMode,\n blockingTimeoutMs,\n returnBuffer,\n preferredAudioCodecs,\n benchmarkMode,\n progressPreviewInterval,\n canvasMode,\n };\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction isFileSystemAccessSupported(): boolean {\n return typeof window !== \"undefined\" && \"showSaveFilePicker\" in window;\n}\n\nasync function getFileWritableStream(filename: string): Promise<{\n writable: WritableStream<Uint8Array>;\n close: () => Promise<void>;\n} | null> {\n if (!isFileSystemAccessSupported()) {\n return null;\n }\n\n try {\n const fileHandle = await (window as any).showSaveFilePicker({\n suggestedName: filename,\n types: [{ description: \"MP4 Video\", accept: { \"video/mp4\": [\".mp4\"] } }],\n });\n const writable = await fileHandle.createWritable();\n return {\n writable,\n close: async () => {\n await writable.close();\n },\n };\n } catch (e) {\n if ((e as Error).name !== \"AbortError\") {\n logger.warn(\"[renderToVideo] File System Access failed:\", e);\n }\n return null;\n }\n}\n\nasync function selectAudioCodec(\n preferredCodecs: AudioCodec[],\n encodingOptions: {\n numberOfChannels: number;\n sampleRate: number;\n bitrate: number;\n },\n): Promise<AudioCodec> {\n for (const codec of preferredCodecs) {\n try {\n const isSupported = await canEncodeAudio(codec, encodingOptions);\n if (isSupported) return codec;\n } catch (e) {\n logger.warn(`[selectAudioCodec] Check failed for ${codec}:`, e);\n }\n }\n const availableCodecs = await getEncodableAudioCodecs(\n undefined,\n encodingOptions,\n );\n throw new NoSupportedAudioCodecError(preferredCodecs, availableCodecs);\n}\n\nfunction downloadBlob(blob: Blob, filename: string): void {\n const url = URL.createObjectURL(blob);\n const a = document.createElement(\"a\");\n a.href = url;\n a.download = filename;\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n URL.revokeObjectURL(url);\n}\n\n// ============================================================================\n// Public API\n// ============================================================================\n\nexport async function getSupportedAudioCodecs(options?: {\n numberOfChannels?: number;\n sampleRate?: number;\n bitrate?: number;\n}): Promise<AudioCodec[]> {\n const {\n numberOfChannels = 2,\n sampleRate = 48000,\n bitrate = 128000,\n } = options ?? {};\n return getEncodableAudioCodecs(undefined, {\n numberOfChannels,\n sampleRate,\n bitrate,\n });\n}\n\n/**\n * Renders a timegroup to an MP4 video file.\n *\n * Uses the EXACT same code path as thumbnail generation (captureFromClone).\n * This ensures consistency - if thumbnails work, video export works.\n */\nexport async function renderTimegroupToVideo(\n timegroup: EFTimegroup,\n options: RenderToVideoOptions = {},\n): Promise<Uint8Array | undefined> {\n const config = resolveConfig(timegroup, options);\n const { signal, onProgress } = options;\n\n const checkCancelled = () => {\n if (signal?.aborted) throw new RenderCancelledError();\n };\n\n resetRenderState();\n\n // =========================================================================\n // Create render clone - EXACT same as captureBatch in EFTimegroup\n // =========================================================================\n const {\n clone: renderClone,\n container: cloneContainer,\n cleanup: cleanupRenderClone,\n } = await timegroup.createRenderClone();\n\n // Build timestamps array for frame loop\n const timestamps: number[] = [];\n for (let i = 0; i < config.totalFrames; i++) {\n timestamps.push(config.startMs + i * config.frameDurationMs);\n }\n\n // =========================================================================\n // Set up video encoding\n // =========================================================================\n let output: Output | null = null;\n let videoSource: CanvasSource | null = null;\n let audioSource: AudioBufferSource | null = null;\n let target: BufferTarget | StreamTarget | null = null;\n let fileStream: {\n writable: WritableStream<Uint8Array>;\n close: () => Promise<void>;\n } | null = null;\n let useStreaming = false;\n let encodingCanvas: OffscreenCanvas | null = null;\n let encodingCtx: OffscreenCanvasRenderingContext2D | null = null;\n\n if (!config.benchmarkMode) {\n // Check for custom writable stream first (for programmatic streaming)\n if (options.customWritableStream) {\n target = new StreamTarget(options.customWritableStream as any);\n output = new Output({\n format: new Mp4OutputFormat({ fastStart: \"fragmented\" }),\n target,\n });\n useStreaming = true;\n } else if (config.streaming) {\n fileStream = await getFileWritableStream(config.filename);\n useStreaming = fileStream !== null;\n\n if (useStreaming && fileStream) {\n target = new StreamTarget(fileStream.writable as any);\n output = new Output({\n format: new Mp4OutputFormat({ fastStart: \"fragmented\" }),\n target,\n });\n }\n }\n\n if (!target) {\n target = new BufferTarget();\n output = new Output({ format: new Mp4OutputFormat(), target });\n }\n\n encodingCanvas = new OffscreenCanvas(config.videoWidth, config.videoHeight);\n encodingCtx = encodingCanvas.getContext(\"2d\");\n if (!encodingCtx) {\n cleanupRenderClone();\n throw new Error(\"Failed to get encoding canvas context\");\n }\n\n if (!output) {\n throw new Error(\"Output not initialized\");\n }\n\n const videoConfig: VideoEncodingConfig = {\n codec: config.codec,\n bitrate: config.bitrate,\n keyFrameInterval: config.keyFrameInterval,\n };\n videoSource = new CanvasSource(encodingCanvas, videoConfig);\n output.addVideoTrack(videoSource);\n\n if (config.includeAudio) {\n const selectedCodec = await selectAudioCodec(\n config.preferredAudioCodecs,\n {\n numberOfChannels: 2,\n sampleRate: 48000,\n bitrate: config.audioBitrate,\n },\n );\n const audioConfig: AudioEncodingConfig = {\n codec: selectedCodec,\n bitrate: config.audioBitrate,\n };\n audioSource = new AudioBufferSource(audioConfig);\n output.addAudioTrack(audioSource);\n }\n\n await output.start();\n }\n\n // =========================================================================\n // Setup for per-frame passive structure rebuilding (like live preview)\n // =========================================================================\n // Create RenderContext for caching across all frames\n const renderContext = new RenderContext();\n\n // Create preview container with proper styling (reusable, content rebuilt each frame)\n // Use unscaled dimensions for the preview container (which holds the full-size clone)\n const containerWidth = timegroup.offsetWidth || 1920;\n const containerHeight = timegroup.offsetHeight || 1080;\n const previewContainer = createPreviewContainer({\n width: containerWidth,\n height: containerHeight,\n background: getComputedStyle(timegroup).background || \"#000\",\n });\n\n // Setup for direct serialization\n logger.debug(`[renderTimegroupToVideo] Using direct timeline serialization`);\n\n // Attach clone container (keeps renderClone in its React-managed DOM position)\n previewContainer.appendChild(cloneContainer);\n\n // Add ef-render-clone-container class for CSS selectors and debugging\n previewContainer.classList.add(\"ef-render-clone-container\");\n\n // CRITICAL: Attach container to document so getComputedStyle returns actual values\n // Without this, all computed styles are empty strings!\n // Hide the container OFF-SCREEN but do NOT use visibility:hidden because:\n // 1. visibility:hidden is inherited by all children\n // 2. seekForRender checks getComputedStyle().visibility and skips \"hidden\" subtrees\n // 3. This would cause FrameController to skip rendering all nested content\n previewContainer.style.cssText +=\n \";position:fixed;left:-99999px;top:-99999px;pointer-events:none;\";\n document.body.appendChild(previewContainer);\n\n // Force layout/reflow so getComputedStyle returns correct values\n void renderClone.offsetHeight;\n logger.debug(\n `[renderTimegroupToVideo] Attached previewContainer to document.body (off-screen) for style computation`,\n );\n\n // =========================================================================\n // Frame loop - DEEP PIPELINE: overlap encode + render + prepare\n // =========================================================================\n const renderStartTime = performance.now();\n let lastRenderedAudioEndMs = config.startMs;\n const audioChunkDurationMs = 2000;\n\n // Reusable thumbnail canvas for preview (no encoding, just draw to canvas)\n let thumbCanvas: HTMLCanvasElement | null = null;\n let thumbCtx: CanvasRenderingContext2D | null = null;\n if (onProgress && config.progressPreviewInterval > 0) {\n const previewWidth = 160;\n const previewHeight = Math.round(\n previewWidth * (config.videoHeight / config.videoWidth),\n );\n thumbCanvas = document.createElement(\"canvas\");\n thumbCanvas.width = previewWidth;\n thumbCanvas.height = previewHeight;\n thumbCtx = thumbCanvas.getContext(\"2d\");\n }\n\n try {\n // ========================================================================\n // OVERLAPPED PIPELINE: image loading runs parallel with seek+serialize\n // ========================================================================\n // The clone can only seek one frame at a time, and serialization must\n // capture the DOM before the next seek. But image loading (data URI →\n // Image) is independent of the clone and runs in the background.\n //\n // Per-frame timeline:\n // [seek(N)] → [serialize(N)] → [image.load(N) in background...]\n // └─ [seek(N+1)] → [serialize(N+1)] → ...\n // └─ encode(N) when image resolves\n\n type PendingFrame = {\n frameIndex: number;\n timeMs: number;\n timestampS: number;\n resolved: HTMLImageElement | null;\n promise: Promise<HTMLImageElement>;\n };\n\n const MAX_AHEAD = 2;\n const pendingFrames: PendingFrame[] = [];\n let nextSeekFrame = 0;\n let encodedFrames = 0;\n\n while (encodedFrames < config.totalFrames) {\n checkCancelled();\n\n // ==================================================================\n // PHASE 1: Fill pipeline — seek+serialize ahead while images load\n // ==================================================================\n while (\n nextSeekFrame < config.totalFrames &&\n pendingFrames.length < MAX_AHEAD\n ) {\n const fi = nextSeekFrame;\n const timeMs = timestamps[fi]!;\n const timestampS = (fi * config.frameDurationMs) / 1000;\n\n await renderClone.seekForRender(timeMs);\n\n const entry: PendingFrame = {\n frameIndex: fi,\n timeMs,\n timestampS,\n resolved: null,\n promise: null!,\n };\n\n // Wait for video content if using blocking mode\n if (config.contentReadyMode === \"blocking\") {\n await waitForVideoContent(\n renderClone,\n timeMs,\n config.blockingTimeoutMs,\n );\n }\n\n if (config.canvasMode === \"native\") {\n const canvas = await renderToImageNative(\n renderClone,\n config.width,\n config.height,\n {\n skipDprScaling: true,\n },\n );\n entry.resolved = canvas as any as HTMLImageElement;\n entry.promise = Promise.resolve(entry.resolved);\n } else {\n // Synchronous capture: walks DOM + snapshots canvas pixels.\n // Returns immediately — clone is free for next seek.\n // Encoding (canvas→base64, SVG assembly) and image loading\n // all resolve in the background.\n const dataUriPromise = captureTimelineToDataUri(\n renderClone,\n config.width,\n config.height,\n {\n renderContext,\n canvasScale: config.scale,\n timeMs,\n },\n );\n\n entry.promise = dataUriPromise.then((dataUri) => {\n return new Promise<HTMLImageElement>((resolve, reject) => {\n const image = new Image();\n image.onload = () => {\n entry.resolved = image;\n resolve(image);\n };\n image.onerror = (e) => {\n console.error(`[Render] frame ${fi} image load error:`, e);\n reject(new Error(`Failed to load image from data URI`));\n };\n image.src = dataUri;\n });\n });\n }\n\n pendingFrames.push(entry);\n nextSeekFrame++;\n }\n\n // ==================================================================\n // PHASE 2: Encode next frame in order (await if not yet loaded)\n // ==================================================================\n const head = pendingFrames.shift()!;\n const preloaded = head.resolved !== null;\n let image: HTMLImageElement;\n if (preloaded) {\n image = head.resolved!;\n } else {\n image = await head.promise;\n }\n\n if (\n audioSource &&\n head.timeMs >= lastRenderedAudioEndMs + audioChunkDurationMs\n ) {\n const chunkEndMs = Math.min(\n head.timeMs + audioChunkDurationMs,\n config.endMs,\n );\n try {\n const audioBuffer = await timegroup.renderAudio(\n lastRenderedAudioEndMs,\n chunkEndMs,\n signal,\n );\n if (audioBuffer && audioBuffer.length > 0) {\n await audioSource.add(audioBuffer);\n }\n } catch (e) {\n /* Audio render failures are non-fatal */\n }\n lastRenderedAudioEndMs = chunkEndMs;\n }\n\n if (videoSource && output && encodingCtx) {\n encodingCtx.drawImage(\n image,\n 0,\n 0,\n image.width,\n image.height,\n 0,\n 0,\n config.videoWidth,\n config.videoHeight,\n );\n await videoSource.add(head.timestampS, config.frameDurationS);\n }\n\n // ==================================================================\n // Progress reporting\n // ==================================================================\n encodedFrames++;\n const currentFrame = encodedFrames;\n const progress = currentFrame / config.totalFrames;\n const renderedMs = currentFrame * config.frameDurationMs;\n const elapsedMs = performance.now() - renderStartTime;\n const msPerFrame = elapsedMs / currentFrame;\n const remainingFrames = config.totalFrames - currentFrame;\n const estimatedRemainingMs = remainingFrames * msPerFrame;\n const speedMultiplier = renderedMs / elapsedMs;\n\n if (\n thumbCanvas &&\n thumbCtx &&\n head.frameIndex % config.progressPreviewInterval === 0\n ) {\n thumbCtx.drawImage(image, 0, 0, thumbCanvas.width, thumbCanvas.height);\n }\n\n onProgress?.({\n progress,\n currentFrame,\n totalFrames: config.totalFrames,\n renderedMs,\n totalDurationMs: config.renderDurationMs,\n elapsedMs,\n estimatedRemainingMs,\n speedMultiplier,\n framePreviewCanvas: thumbCanvas || undefined,\n });\n }\n\n // Render remaining audio\n if (audioSource && lastRenderedAudioEndMs < config.endMs) {\n try {\n const audioBuffer = await timegroup.renderAudio(\n lastRenderedAudioEndMs,\n config.endMs,\n signal,\n );\n if (audioBuffer && audioBuffer.length > 0) {\n await audioSource.add(audioBuffer);\n }\n } catch (e) {\n /* Audio render failures are non-fatal */\n }\n }\n\n if (config.benchmarkMode) {\n return undefined;\n }\n\n await output!.finalize();\n\n if (useStreaming) {\n // Streaming mode: chunks already sent via customWritableStream or file stream\n return undefined;\n } else {\n const bufferTarget = target as BufferTarget;\n const videoBuffer = bufferTarget.buffer;\n if (!videoBuffer) {\n throw new Error(\"Video encoding failed: no buffer produced\");\n }\n\n if (config.returnBuffer) {\n return new Uint8Array(videoBuffer);\n }\n\n const videoBlob = new Blob([videoBuffer], { type: \"video/mp4\" });\n downloadBlob(videoBlob, config.filename);\n return undefined;\n }\n } finally {\n renderContext.dispose();\n // Remove previewContainer first — renderClone was moved into it, so it must be\n // detached before cleanupRenderClone() unmounts the React root that owns renderClone.\n if (previewContainer.parentNode) {\n previewContainer.parentNode.removeChild(previewContainer);\n }\n cleanupRenderClone();\n }\n}\n\nexport { QUALITY_HIGH };\nexport type { AudioCodec };\n"],"mappings":"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"}
1
+ {"version":3,"file":"renderTimegroupToVideo.js","names":["timestamps: number[]","output: Output | null","videoSource: CanvasSource | null","audioSource: AudioBufferSource | null","target: BufferTarget | StreamTarget | null","fileStream: {\n writable: WritableStream<Uint8Array>;\n close: () => Promise<void>;\n } | null","encodingCanvas: OffscreenCanvas | null","encodingCtx: OffscreenCanvasRenderingContext2D | null","videoConfig: VideoEncodingConfig","thumbCanvas: HTMLCanvasElement | null","thumbCtx: CanvasRenderingContext2D | null","pendingFrames: PendingFrame[]","entry: PendingFrame","image","image: HTMLImageElement"],"sources":["../../src/preview/renderTimegroupToVideo.ts"],"sourcesContent":["/**\n * Video rendering for timegroups using direct serialization.\n *\n * Architecture:\n * - Creates a render clone of the timeline\n * - For each frame:\n * 1. Seeks the clone to the target time\n * 2. Executes frame tasks (SVG updates, canvas draws, etc.)\n * 3. Serializes the live DOM directly to SVG+foreignObject data URI\n * 4. Renders to image and encodes to video\n *\n * RenderContext provides pixel caching across frames for performance.\n */\n\nimport { logger } from \"./logger.js\";\nimport {\n Output,\n Mp4OutputFormat,\n BufferTarget,\n StreamTarget,\n CanvasSource,\n AudioBufferSource,\n QUALITY_HIGH,\n canEncodeAudio,\n getEncodableAudioCodecs,\n type VideoEncodingConfig,\n type AudioEncodingConfig,\n type AudioCodec,\n} from \"mediabunny\";\nimport type { EFTimegroup } from \"../elements/EFTimegroup.js\";\nimport type { RenderToVideoOptions } from \"./renderTimegroupToVideo.types.js\";\nimport type { ContentReadyMode } from \"./renderTimegroupToCanvas.types.js\";\nimport {\n resetRenderState,\n waitForVideoContent,\n} from \"./renderTimegroupToCanvas.js\";\nimport { captureTimelineToDataUri } from \"./rendering/serializeTimelineDirect.js\";\nimport { renderToImageNative } from \"./rendering/renderToImageNative.js\";\nimport { isNativeCanvasApiAvailable } from \"./previewSettings.js\";\nimport { createPreviewContainer } from \"./previewTypes.js\";\nimport { RenderContext } from \"./RenderContext.js\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\n// Re-export types from type-only module (zero side effects)\nexport type {\n RenderProgress,\n RenderToVideoOptions,\n} from \"./renderTimegroupToVideo.types.js\";\n\n// ============================================================================\n// Errors\n// ============================================================================\n\nexport class NoSupportedAudioCodecError extends Error {\n constructor(requestedCodecs: AudioCodec[], availableCodecs: AudioCodec[]) {\n super(\n `No supported audio codec found. Requested: [${requestedCodecs.join(\", \")}], ` +\n `Available: [${availableCodecs.length > 0 ? availableCodecs.join(\", \") : \"none\"}]`,\n );\n this.name = \"NoSupportedAudioCodecError\";\n }\n}\n\nexport class RenderCancelledError extends Error {\n constructor() {\n super(\"Render cancelled\");\n this.name = \"RenderCancelledError\";\n }\n}\n\n// ============================================================================\n// Configuration\n// ============================================================================\n\ninterface ResolvedConfig {\n fps: number;\n codec: \"avc\" | \"hevc\" | \"vp9\" | \"av1\" | \"vp8\";\n bitrate: number;\n filename: string;\n scale: number;\n keyFrameInterval: number;\n startMs: number;\n endMs: number;\n renderDurationMs: number;\n width: number;\n height: number;\n videoWidth: number;\n videoHeight: number;\n totalFrames: number;\n frameDurationMs: number;\n frameDurationS: number;\n streaming: boolean;\n includeAudio: boolean;\n audioBitrate: number;\n contentReadyMode: ContentReadyMode;\n blockingTimeoutMs: number;\n returnBuffer: boolean;\n preferredAudioCodecs: AudioCodec[];\n benchmarkMode: boolean;\n progressPreviewInterval: number;\n canvasMode: \"native\" | \"foreignObject\";\n}\n\nfunction resolveConfig(\n timegroup: EFTimegroup,\n options: RenderToVideoOptions = {},\n): ResolvedConfig {\n const fps = options.fps ?? timegroup.effectiveFps ?? 30;\n const codec = options.codec ?? \"avc\";\n const bitrate = options.bitrate ?? 8_000_000;\n const filename = options.filename ?? \"timegroup-video.mp4\";\n const scale = options.scale ?? 1;\n const keyFrameInterval = options.keyFrameInterval ?? 2;\n const streaming = options.streaming ?? true;\n const includeAudio = options.includeAudio ?? true;\n const audioBitrate = options.audioBitrate ?? 128_000;\n const contentReadyMode = options.contentReadyMode ?? \"blocking\";\n const blockingTimeoutMs = options.blockingTimeoutMs ?? 5000;\n const returnBuffer = options.returnBuffer ?? false;\n const preferredAudioCodecs = options.preferredAudioCodecs ?? [\"aac\", \"opus\"];\n const benchmarkMode = options.benchmarkMode ?? false;\n // Preview generation now uses canvas reference (no encoding) - cheap to enable!\n // Defaults to 60 frames (every 2 seconds at 30fps). Set to 0 to disable.\n const progressPreviewInterval = options.progressPreviewInterval ?? 60;\n\n const totalDurationMs = timegroup.durationMs;\n if (!totalDurationMs || totalDurationMs <= 0) {\n throw new Error(\"Timegroup has no duration\");\n }\n\n const startMs = Math.max(0, options.fromMs ?? 0);\n const endMs =\n options.toMs !== undefined\n ? Math.min(options.toMs, totalDurationMs)\n : totalDurationMs;\n const renderDurationMs = endMs - startMs;\n\n if (renderDurationMs <= 0) {\n throw new Error(`Invalid render range: from ${startMs}ms to ${endMs}ms`);\n }\n\n // Force layout reflow before reading dimensions\n void timegroup.offsetHeight;\n\n // Try multiple sources for dimensions (offsetWidth can be 0 in headless browsers)\n let timegroupWidth = timegroup.offsetWidth;\n let timegroupHeight = timegroup.offsetHeight;\n\n if (!timegroupWidth || !timegroupHeight) {\n const rect = timegroup.getBoundingClientRect();\n if (rect.width > 0 && rect.height > 0) {\n timegroupWidth = rect.width;\n timegroupHeight = rect.height;\n }\n }\n\n if (!timegroupWidth || !timegroupHeight) {\n const computed = getComputedStyle(timegroup);\n const cw = parseFloat(computed.width);\n const ch = parseFloat(computed.height);\n if (cw > 0 && ch > 0) {\n timegroupWidth = cw;\n timegroupHeight = ch;\n }\n }\n\n if (!timegroupWidth || !timegroupHeight) {\n throw new Error(\n `Timegroup has no dimensions (${timegroupWidth}x${timegroupHeight}). ` +\n `Ensure the timegroup element is in the document and has explicit width/height styles ` +\n `(e.g., class=\"w-[1920px] h-[1080px]\")`,\n );\n }\n const width = Math.floor(timegroupWidth * scale);\n const height = Math.floor(timegroupHeight * scale);\n\n const videoWidth = width % 2 === 0 ? width : width - 1;\n const videoHeight = height % 2 === 0 ? height : height - 1;\n\n const frameDurationMs = 1000 / fps;\n const totalFrames = Math.ceil(renderDurationMs / frameDurationMs);\n const frameDurationS = frameDurationMs / 1000;\n\n // Determine effective canvas mode:\n // 1. If explicitly specified, use that (with fallback if native not available)\n // 2. If not specified, default to foreignObject for compatibility\n const canvasMode = (() => {\n const requested = options.canvasMode;\n if (!requested) return \"foreignObject\";\n if (requested === \"native\" && !isNativeCanvasApiAvailable()) {\n logger.debug(\n \"[renderTimegroupToVideo] Native canvas mode requested but not available, falling back to foreignObject\",\n );\n return \"foreignObject\";\n }\n return requested;\n })();\n\n return {\n fps,\n codec,\n bitrate,\n filename,\n scale,\n keyFrameInterval,\n startMs,\n endMs,\n renderDurationMs,\n width,\n height,\n videoWidth,\n videoHeight,\n totalFrames,\n frameDurationMs,\n frameDurationS,\n streaming,\n includeAudio,\n audioBitrate,\n contentReadyMode,\n blockingTimeoutMs,\n returnBuffer,\n preferredAudioCodecs,\n benchmarkMode,\n progressPreviewInterval,\n canvasMode,\n };\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction isFileSystemAccessSupported(): boolean {\n return typeof window !== \"undefined\" && \"showSaveFilePicker\" in window;\n}\n\nasync function getFileWritableStream(filename: string): Promise<{\n writable: WritableStream<Uint8Array>;\n close: () => Promise<void>;\n} | null> {\n if (!isFileSystemAccessSupported()) {\n return null;\n }\n\n try {\n const fileHandle = await (window as any).showSaveFilePicker({\n suggestedName: filename,\n types: [{ description: \"MP4 Video\", accept: { \"video/mp4\": [\".mp4\"] } }],\n });\n const writable = await fileHandle.createWritable();\n return {\n writable,\n close: async () => {\n await writable.close();\n },\n };\n } catch (e) {\n if ((e as Error).name !== \"AbortError\") {\n logger.warn(\"[renderToVideo] File System Access failed:\", e);\n }\n return null;\n }\n}\n\nasync function selectAudioCodec(\n preferredCodecs: AudioCodec[],\n encodingOptions: {\n numberOfChannels: number;\n sampleRate: number;\n bitrate: number;\n },\n): Promise<AudioCodec> {\n for (const codec of preferredCodecs) {\n try {\n const isSupported = await canEncodeAudio(codec, encodingOptions);\n if (isSupported) return codec;\n } catch (e) {\n logger.warn(`[selectAudioCodec] Check failed for ${codec}:`, e);\n }\n }\n const availableCodecs = await getEncodableAudioCodecs(\n undefined,\n encodingOptions,\n );\n throw new NoSupportedAudioCodecError(preferredCodecs, availableCodecs);\n}\n\nfunction downloadBlob(blob: Blob, filename: string): void {\n const url = URL.createObjectURL(blob);\n const a = document.createElement(\"a\");\n a.href = url;\n a.download = filename;\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n URL.revokeObjectURL(url);\n}\n\n// ============================================================================\n// Public API\n// ============================================================================\n\nexport async function getSupportedAudioCodecs(options?: {\n numberOfChannels?: number;\n sampleRate?: number;\n bitrate?: number;\n}): Promise<AudioCodec[]> {\n const {\n numberOfChannels = 2,\n sampleRate = 48000,\n bitrate = 128000,\n } = options ?? {};\n return getEncodableAudioCodecs(undefined, {\n numberOfChannels,\n sampleRate,\n bitrate,\n });\n}\n\n/**\n * Renders a timegroup to an MP4 video file.\n *\n * Uses the EXACT same code path as thumbnail generation (captureFromClone).\n * This ensures consistency - if thumbnails work, video export works.\n */\nexport async function renderTimegroupToVideo(\n timegroup: EFTimegroup,\n options: RenderToVideoOptions = {},\n): Promise<Uint8Array | undefined> {\n const config = resolveConfig(timegroup, options);\n const { signal, onProgress } = options;\n\n const checkCancelled = () => {\n if (signal?.aborted) throw new RenderCancelledError();\n };\n\n resetRenderState();\n\n // =========================================================================\n // Create render clone - EXACT same as captureBatch in EFTimegroup\n // =========================================================================\n const {\n clone: renderClone,\n container: cloneContainer,\n cleanup: cleanupRenderClone,\n } = await timegroup.createRenderClone();\n\n // Build timestamps array for frame loop\n const timestamps: number[] = [];\n for (let i = 0; i < config.totalFrames; i++) {\n timestamps.push(config.startMs + i * config.frameDurationMs);\n }\n\n // =========================================================================\n // Set up video encoding\n // =========================================================================\n let output: Output | null = null;\n let videoSource: CanvasSource | null = null;\n let audioSource: AudioBufferSource | null = null;\n let target: BufferTarget | StreamTarget | null = null;\n let fileStream: {\n writable: WritableStream<Uint8Array>;\n close: () => Promise<void>;\n } | null = null;\n let useStreaming = false;\n let encodingCanvas: OffscreenCanvas | null = null;\n let encodingCtx: OffscreenCanvasRenderingContext2D | null = null;\n\n if (!config.benchmarkMode) {\n // Check for custom writable stream first (for programmatic streaming)\n if (options.customWritableStream) {\n target = new StreamTarget(options.customWritableStream as any);\n output = new Output({\n format: new Mp4OutputFormat({ fastStart: \"fragmented\" }),\n target,\n });\n useStreaming = true;\n } else if (config.streaming) {\n fileStream = await getFileWritableStream(config.filename);\n useStreaming = fileStream !== null;\n\n if (useStreaming && fileStream) {\n target = new StreamTarget(fileStream.writable as any);\n output = new Output({\n format: new Mp4OutputFormat({ fastStart: \"fragmented\" }),\n target,\n });\n }\n }\n\n if (!target) {\n target = new BufferTarget();\n output = new Output({ format: new Mp4OutputFormat(), target });\n }\n\n encodingCanvas = new OffscreenCanvas(config.videoWidth, config.videoHeight);\n encodingCtx = encodingCanvas.getContext(\"2d\");\n if (!encodingCtx) {\n cleanupRenderClone();\n throw new Error(\"Failed to get encoding canvas context\");\n }\n\n if (!output) {\n throw new Error(\"Output not initialized\");\n }\n\n const videoConfig: VideoEncodingConfig = {\n codec: config.codec,\n bitrate: config.bitrate,\n keyFrameInterval: config.keyFrameInterval,\n };\n videoSource = new CanvasSource(encodingCanvas, videoConfig);\n output.addVideoTrack(videoSource);\n\n if (config.includeAudio) {\n const selectedCodec = await selectAudioCodec(\n config.preferredAudioCodecs,\n {\n numberOfChannels: 2,\n sampleRate: 48000,\n bitrate: config.audioBitrate,\n },\n );\n const audioConfig: AudioEncodingConfig = {\n codec: selectedCodec,\n bitrate: config.audioBitrate,\n };\n audioSource = new AudioBufferSource(audioConfig);\n output.addAudioTrack(audioSource);\n }\n\n await output.start();\n }\n\n // =========================================================================\n // Setup for per-frame passive structure rebuilding (like live preview)\n // =========================================================================\n // Create RenderContext for caching across all frames\n const renderContext = new RenderContext();\n\n // Create preview container with proper styling (reusable, content rebuilt each frame)\n // Use unscaled dimensions for the preview container (which holds the full-size clone)\n const containerWidth = timegroup.offsetWidth || 1920;\n const containerHeight = timegroup.offsetHeight || 1080;\n const previewContainer = createPreviewContainer({\n width: containerWidth,\n height: containerHeight,\n background: getComputedStyle(timegroup).background || \"#000\",\n });\n\n // Setup for direct serialization\n logger.debug(`[renderTimegroupToVideo] Using direct timeline serialization`);\n\n // Attach clone container (keeps renderClone in its React-managed DOM position)\n previewContainer.appendChild(cloneContainer);\n\n // Add ef-render-clone-container class for CSS selectors and debugging\n previewContainer.classList.add(\"ef-render-clone-container\");\n\n // CRITICAL: Attach container to document so getComputedStyle returns actual values\n // Without this, all computed styles are empty strings!\n // Hide the container OFF-SCREEN but do NOT use visibility:hidden because:\n // 1. visibility:hidden is inherited by all children\n // 2. seekForRender checks getComputedStyle().visibility and skips \"hidden\" subtrees\n // 3. This would cause FrameController to skip rendering all nested content\n previewContainer.style.cssText +=\n \";position:fixed;left:-99999px;top:-99999px;pointer-events:none;\";\n document.body.appendChild(previewContainer);\n\n // Force layout/reflow so getComputedStyle returns correct values\n void renderClone.offsetHeight;\n logger.debug(\n `[renderTimegroupToVideo] Attached previewContainer to document.body (off-screen) for style computation`,\n );\n\n // =========================================================================\n // Frame loop - DEEP PIPELINE: overlap encode + render + prepare\n // =========================================================================\n const renderStartTime = performance.now();\n let lastRenderedAudioEndMs = config.startMs;\n const audioChunkDurationMs = 2000;\n\n // Reusable thumbnail canvas for preview (no encoding, just draw to canvas)\n let thumbCanvas: HTMLCanvasElement | null = null;\n let thumbCtx: CanvasRenderingContext2D | null = null;\n if (onProgress && config.progressPreviewInterval > 0) {\n const previewWidth = 160;\n const previewHeight = Math.round(\n previewWidth * (config.videoHeight / config.videoWidth),\n );\n thumbCanvas = document.createElement(\"canvas\");\n thumbCanvas.width = previewWidth;\n thumbCanvas.height = previewHeight;\n thumbCtx = thumbCanvas.getContext(\"2d\");\n }\n\n try {\n // ========================================================================\n // OVERLAPPED PIPELINE: image loading runs parallel with seek+serialize\n // ========================================================================\n // The clone can only seek one frame at a time, and serialization must\n // capture the DOM before the next seek. But image loading (data URI →\n // Image) is independent of the clone and runs in the background.\n //\n // Per-frame timeline:\n // [seek(N)] → [serialize(N)] → [image.load(N) in background...]\n // └─ [seek(N+1)] → [serialize(N+1)] → ...\n // └─ encode(N) when image resolves\n\n type PendingFrame = {\n frameIndex: number;\n timeMs: number;\n timestampS: number;\n resolved: HTMLImageElement | null;\n promise: Promise<HTMLImageElement>;\n };\n\n const MAX_AHEAD = 2;\n const pendingFrames: PendingFrame[] = [];\n let nextSeekFrame = 0;\n let encodedFrames = 0;\n\n while (encodedFrames < config.totalFrames) {\n checkCancelled();\n\n // ==================================================================\n // PHASE 1: Fill pipeline — seek+serialize ahead while images load\n // ==================================================================\n while (\n nextSeekFrame < config.totalFrames &&\n pendingFrames.length < MAX_AHEAD\n ) {\n const fi = nextSeekFrame;\n const timeMs = timestamps[fi]!;\n const timestampS = (fi * config.frameDurationMs) / 1000;\n\n await renderClone.seekForRender(timeMs);\n\n const entry: PendingFrame = {\n frameIndex: fi,\n timeMs,\n timestampS,\n resolved: null,\n promise: null!,\n };\n\n // Wait for video content if using blocking mode\n if (config.contentReadyMode === \"blocking\") {\n await waitForVideoContent(\n renderClone,\n timeMs,\n config.blockingTimeoutMs,\n );\n }\n\n if (config.canvasMode === \"native\") {\n const canvas = await renderToImageNative(\n renderClone,\n config.width,\n config.height,\n {\n skipDprScaling: true,\n },\n );\n entry.resolved = canvas as any as HTMLImageElement;\n entry.promise = Promise.resolve(entry.resolved);\n } else {\n // Synchronous capture: walks DOM + snapshots canvas pixels.\n // Returns immediately — clone is free for next seek.\n // Encoding (canvas→base64, SVG assembly) and image loading\n // all resolve in the background.\n const dataUriPromise = captureTimelineToDataUri(\n renderClone,\n config.width,\n config.height,\n {\n renderContext,\n canvasScale: config.scale,\n timeMs,\n },\n );\n\n entry.promise = dataUriPromise.then((dataUri) => {\n return new Promise<HTMLImageElement>((resolve, reject) => {\n const image = new Image();\n image.onload = () => {\n entry.resolved = image;\n resolve(image);\n };\n image.onerror = (e) => {\n console.error(`[Render] frame ${fi} image load error:`, e);\n reject(new Error(`Failed to load image from data URI`));\n };\n image.src = dataUri;\n });\n });\n }\n\n pendingFrames.push(entry);\n nextSeekFrame++;\n }\n\n // ==================================================================\n // PHASE 2: Encode next frame in order (await if not yet loaded)\n // ==================================================================\n const head = pendingFrames.shift()!;\n const preloaded = head.resolved !== null;\n let image: HTMLImageElement;\n if (preloaded) {\n image = head.resolved!;\n } else {\n image = await head.promise;\n }\n\n if (\n audioSource &&\n head.timeMs >= lastRenderedAudioEndMs + audioChunkDurationMs\n ) {\n const chunkEndMs = Math.min(\n head.timeMs + audioChunkDurationMs,\n config.endMs,\n );\n try {\n const audioBuffer = await timegroup.renderAudio(\n lastRenderedAudioEndMs,\n chunkEndMs,\n signal,\n );\n if (audioBuffer && audioBuffer.length > 0) {\n await audioSource.add(audioBuffer);\n }\n } catch (e) {\n /* Audio render failures are non-fatal */\n }\n lastRenderedAudioEndMs = chunkEndMs;\n }\n\n if (videoSource && output && encodingCtx) {\n encodingCtx.drawImage(\n image,\n 0,\n 0,\n image.width,\n image.height,\n 0,\n 0,\n config.videoWidth,\n config.videoHeight,\n );\n await videoSource.add(head.timestampS, config.frameDurationS);\n }\n\n // ==================================================================\n // Progress reporting\n // ==================================================================\n encodedFrames++;\n const currentFrame = encodedFrames;\n const progress = currentFrame / config.totalFrames;\n const renderedMs = currentFrame * config.frameDurationMs;\n const elapsedMs = performance.now() - renderStartTime;\n const msPerFrame = elapsedMs / currentFrame;\n const remainingFrames = config.totalFrames - currentFrame;\n const estimatedRemainingMs = remainingFrames * msPerFrame;\n const speedMultiplier = renderedMs / elapsedMs;\n\n if (\n thumbCanvas &&\n thumbCtx &&\n head.frameIndex % config.progressPreviewInterval === 0\n ) {\n thumbCtx.drawImage(image, 0, 0, thumbCanvas.width, thumbCanvas.height);\n }\n\n onProgress?.({\n progress,\n currentFrame,\n totalFrames: config.totalFrames,\n renderedMs,\n totalDurationMs: config.renderDurationMs,\n elapsedMs,\n estimatedRemainingMs,\n speedMultiplier,\n framePreviewCanvas: thumbCanvas || undefined,\n });\n }\n\n // Render remaining audio\n if (audioSource && lastRenderedAudioEndMs < config.endMs) {\n try {\n const audioBuffer = await timegroup.renderAudio(\n lastRenderedAudioEndMs,\n config.endMs,\n signal,\n );\n if (audioBuffer && audioBuffer.length > 0) {\n await audioSource.add(audioBuffer);\n }\n } catch (e) {\n /* Audio render failures are non-fatal */\n }\n }\n\n if (config.benchmarkMode) {\n return undefined;\n }\n\n await output!.finalize();\n\n // Report telemetry (fire-and-forget)\n if (options.telemetryToken) {\n const elapsedMs = Math.round(performance.now() - renderStartTime);\n const endpoint = options.telemetryEndpoint ?? \"https://editframe.com\";\n const efMediaCount =\n timegroup.querySelectorAll(\"ef-video,ef-audio\").length;\n const efImageCount = timegroup.querySelectorAll(\"ef-image\").length;\n const efCaptionsCount = timegroup.querySelectorAll(\"ef-captions\").length;\n const efTextCount = timegroup.querySelectorAll(\"ef-text\").length;\n fetch(`${endpoint}/api/v1/telemetry`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${options.telemetryToken}`,\n },\n body: JSON.stringify({\n render_path: \"client\",\n duration_ms: elapsedMs,\n width: config.videoWidth,\n height: config.videoHeight,\n fps: config.fps,\n feature_usage: {\n efMediaCount,\n efImageCount,\n efCaptionsCount,\n efTextCount,\n },\n }),\n keepalive: true,\n }).catch(() => {\n // Telemetry errors must never surface to users.\n });\n }\n\n if (useStreaming) {\n // Streaming mode: chunks already sent via customWritableStream or file stream\n return undefined;\n } else {\n const bufferTarget = target as BufferTarget;\n const videoBuffer = bufferTarget.buffer;\n if (!videoBuffer) {\n throw new Error(\"Video encoding failed: no buffer produced\");\n }\n\n if (config.returnBuffer) {\n return new Uint8Array(videoBuffer);\n }\n\n const videoBlob = new Blob([videoBuffer], { type: \"video/mp4\" });\n downloadBlob(videoBlob, config.filename);\n return undefined;\n }\n } finally {\n renderContext.dispose();\n // Remove previewContainer first — renderClone was moved into it, so it must be\n // detached before cleanupRenderClone() unmounts the React root that owns renderClone.\n if (previewContainer.parentNode) {\n previewContainer.parentNode.removeChild(previewContainer);\n }\n cleanupRenderClone();\n }\n}\n\nexport { QUALITY_HIGH };\nexport type { AudioCodec };\n"],"mappings":"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"}
@@ -36,6 +36,15 @@ interface RenderToVideoOptions {
36
36
  customWritableStream?: WritableStream<Uint8Array>;
37
37
  progressPreviewInterval?: number;
38
38
  canvasMode?: "native" | "foreignObject";
39
+ /**
40
+ * API token (`ef_...`) for telemetry reporting. Omitting this skips the
41
+ * client-side telemetry beacon, which is a violation of the Editframe SDK
42
+ * License Agreement. Not required when called from the CLI render path,
43
+ * which handles telemetry from Node.js.
44
+ */
45
+ telemetryToken?: string;
46
+ /** Override the telemetry endpoint (defaults to https://editframe.com). */
47
+ telemetryEndpoint?: string;
39
48
  }
40
49
  //#endregion
41
50
  export { RenderProgress, RenderToVideoOptions };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@editframe/elements",
3
- "version": "0.40.5",
3
+ "version": "0.40.6",
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.5",
21
+ "@editframe/assets": "0.40.6",
22
22
  "@lit/context": "^1.1.6",
23
23
  "@opentelemetry/api": "^1.9.0",
24
24
  "@opentelemetry/context-zone": "^1.26.0",