@editframe/elements 0.42.6 → 0.42.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/elements/EFCaptions.d.ts +4 -4
- package/dist/elements/EFImage.d.ts +4 -4
- package/dist/elements/EFMedia.d.ts +2 -2
- package/dist/elements/EFText.d.ts +4 -4
- package/dist/elements/EFTextSegment.d.ts +4 -4
- package/dist/elements/EFTimegroup.js +1 -1
- package/dist/elements/EFTimegroup.js.map +1 -1
- package/dist/elements/EFWaveform.d.ts +4 -4
- package/dist/gui/EFActiveRootTemporal.d.ts +4 -4
- package/dist/gui/EFConfiguration.d.ts +4 -4
- package/dist/gui/EFDial.d.ts +4 -4
- package/dist/gui/EFFilmstrip.d.ts +4 -4
- package/dist/gui/EFOverlayItem.d.ts +4 -4
- package/dist/gui/EFOverlayLayer.d.ts +4 -4
- package/dist/gui/EFPause.d.ts +4 -4
- package/dist/gui/EFPlay.d.ts +4 -4
- package/dist/gui/EFPreview.d.ts +4 -4
- package/dist/gui/EFScrubber.d.ts +4 -4
- package/dist/gui/EFTimeDisplay.d.ts +4 -4
- package/dist/gui/EFTimelineRuler.d.ts +4 -4
- package/dist/gui/EFToggleLoop.d.ts +4 -4
- package/dist/gui/EFTogglePlay.d.ts +4 -4
- package/dist/gui/EFWorkbench.d.ts +4 -4
- package/dist/gui/hierarchy/EFHierarchy.d.ts +4 -4
- package/dist/gui/hierarchy/EFHierarchyItem.d.ts +2 -2
- package/dist/gui/timeline/EFTimeline.d.ts +2 -2
- package/dist/gui/timeline/TrimHandles.d.ts +4 -4
- package/dist/gui/timeline/tracks/EFThumbnailStrip.d.ts +4 -4
- package/dist/gui/tree/EFTree.d.ts +4 -4
- package/dist/gui/tree/EFTreeItem.d.ts +4 -4
- package/dist/preview/renderTimegroupToVideo.js +1 -1
- package/dist/preview/renderTimegroupToVideo.js.map +1 -1
- package/dist/preview/rendering/serializeTimelineDirect.js +2 -1
- package/dist/preview/rendering/serializeTimelineDirect.js.map +1 -1
- package/package.json +2 -2
|
@@ -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","headers: Record<string, string>"],"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 (\n typeof process !== \"undefined\" &&\n process.env.EF_TELEMETRY_ENABLED === \"true\"\n ) {\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 const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n };\n if (options.telemetryToken) {\n headers[\"Authorization\"] = `Bearer ${options.telemetryToken}`;\n }\n fetch(`${endpoint}/api/v1/telemetry`, {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n event_type: \"render\",\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":";;;;;;;;;;AAwDA,IAAa,6BAAb,cAAgD,MAAM;CACpD,YAAY,iBAA+B,iBAA+B;AACxE,QACE,+CAA+C,gBAAgB,KAAK,KAAK,CAAC,iBACzD,gBAAgB,SAAS,IAAI,gBAAgB,KAAK,KAAK,GAAG,OAAO,GACnF;AACD,OAAK,OAAO;;;AAIhB,IAAa,uBAAb,cAA0C,MAAM;CAC9C,cAAc;AACZ,QAAM,mBAAmB;AACzB,OAAK,OAAO;;;AAqChB,SAAS,cACP,WACA,UAAgC,EAAE,EAClB;CAChB,MAAM,MAAM,QAAQ,OAAO,UAAU,gBAAgB;CACrD,MAAM,QAAQ,QAAQ,SAAS;CAC/B,MAAM,UAAU,QAAQ,WAAW;CACnC,MAAM,WAAW,QAAQ,YAAY;CACrC,MAAM,QAAQ,QAAQ,SAAS;CAC/B,MAAM,mBAAmB,QAAQ,oBAAoB;CACrD,MAAM,YAAY,QAAQ,aAAa;CACvC,MAAM,eAAe,QAAQ,gBAAgB;CAC7C,MAAM,eAAe,QAAQ,gBAAgB;CAC7C,MAAM,mBAAmB,QAAQ,oBAAoB;CACrD,MAAM,oBAAoB,QAAQ,qBAAqB;CACvD,MAAM,eAAe,QAAQ,gBAAgB;CAC7C,MAAM,uBAAuB,QAAQ,wBAAwB,CAAC,OAAO,OAAO;CAC5E,MAAM,gBAAgB,QAAQ,iBAAiB;CAG/C,MAAM,0BAA0B,QAAQ,2BAA2B;CAEnE,MAAM,kBAAkB,UAAU;AAClC,KAAI,CAAC,mBAAmB,mBAAmB,EACzC,OAAM,IAAI,MAAM,4BAA4B;CAG9C,MAAM,UAAU,KAAK,IAAI,GAAG,QAAQ,UAAU,EAAE;CAChD,MAAM,QACJ,QAAQ,SAAS,SACb,KAAK,IAAI,QAAQ,MAAM,gBAAgB,GACvC;CACN,MAAM,mBAAmB,QAAQ;AAEjC,KAAI,oBAAoB,EACtB,OAAM,IAAI,MAAM,8BAA8B,QAAQ,QAAQ,MAAM,IAAI;AAI1E,CAAK,UAAU;CAGf,IAAI,iBAAiB,UAAU;CAC/B,IAAI,kBAAkB,UAAU;AAEhC,KAAI,CAAC,kBAAkB,CAAC,iBAAiB;EACvC,MAAM,OAAO,UAAU,uBAAuB;AAC9C,MAAI,KAAK,QAAQ,KAAK,KAAK,SAAS,GAAG;AACrC,oBAAiB,KAAK;AACtB,qBAAkB,KAAK;;;AAI3B,KAAI,CAAC,kBAAkB,CAAC,iBAAiB;EACvC,MAAM,WAAW,iBAAiB,UAAU;EAC5C,MAAM,KAAK,WAAW,SAAS,MAAM;EACrC,MAAM,KAAK,WAAW,SAAS,OAAO;AACtC,MAAI,KAAK,KAAK,KAAK,GAAG;AACpB,oBAAiB;AACjB,qBAAkB;;;AAItB,KAAI,CAAC,kBAAkB,CAAC,gBACtB,OAAM,IAAI,MACR,gCAAgC,eAAe,GAAG,gBAAgB,+HAGnE;CAEH,MAAM,QAAQ,KAAK,MAAM,iBAAiB,MAAM;CAChD,MAAM,SAAS,KAAK,MAAM,kBAAkB,MAAM;CAElD,MAAM,aAAa,QAAQ,MAAM,IAAI,QAAQ,QAAQ;CACrD,MAAM,cAAc,SAAS,MAAM,IAAI,SAAS,SAAS;CAEzD,MAAM,kBAAkB,MAAO;AAmB/B,QAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,aAhCkB,KAAK,KAAK,mBAAmB,gBAAgB;EAiC/D;EACA,gBAjCqB,kBAAkB;EAkCvC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,mBAtCwB;GACxB,MAAM,YAAY,QAAQ;AAC1B,OAAI,CAAC,UAAW,QAAO;AACvB,OAAI,cAAc,YAAY,CAAC,4BAA4B,EAAE;AAC3D,WAAO,MACL,yGACD;AACD,WAAO;;AAET,UAAO;MACL;EA6BH;;AAOH,SAAS,8BAAuC;AAC9C,QAAO,OAAO,WAAW,eAAe,wBAAwB;;AAGlE,eAAe,sBAAsB,UAG3B;AACR,KAAI,CAAC,6BAA6B,CAChC,QAAO;AAGT,KAAI;EAKF,MAAM,WAAW,OAJE,MAAO,OAAe,mBAAmB;GAC1D,eAAe;GACf,OAAO,CAAC;IAAE,aAAa;IAAa,QAAQ,EAAE,aAAa,CAAC,OAAO,EAAE;IAAE,CAAC;GACzE,CAAC,EACgC,gBAAgB;AAClD,SAAO;GACL;GACA,OAAO,YAAY;AACjB,UAAM,SAAS,OAAO;;GAEzB;UACM,GAAG;AACV,MAAK,EAAY,SAAS,aACxB,QAAO,KAAK,8CAA8C,EAAE;AAE9D,SAAO;;;AAIX,eAAe,iBACb,iBACA,iBAKqB;AACrB,MAAK,MAAM,SAAS,gBAClB,KAAI;AAEF,MADoB,MAAM,eAAe,OAAO,gBAAgB,CAC/C,QAAO;UACjB,GAAG;AACV,SAAO,KAAK,uCAAuC,MAAM,IAAI,EAAE;;AAOnE,OAAM,IAAI,2BAA2B,iBAJb,MAAM,wBAC5B,QACA,gBACD,CACqE;;AAGxE,SAAS,aAAa,MAAY,UAAwB;CACxD,MAAM,MAAM,IAAI,gBAAgB,KAAK;CACrC,MAAM,IAAI,SAAS,cAAc,IAAI;AACrC,GAAE,OAAO;AACT,GAAE,WAAW;AACb,UAAS,KAAK,YAAY,EAAE;AAC5B,GAAE,OAAO;AACT,UAAS,KAAK,YAAY,EAAE;AAC5B,KAAI,gBAAgB,IAAI;;AAO1B,eAAsB,wBAAwB,SAIpB;CACxB,MAAM,EACJ,mBAAmB,GACnB,aAAa,MACb,UAAU,UACR,WAAW,EAAE;AACjB,QAAO,wBAAwB,QAAW;EACxC;EACA;EACA;EACD,CAAC;;;;;;;;AASJ,eAAsB,uBACpB,WACA,UAAgC,EAAE,EACD;CACjC,MAAM,SAAS,cAAc,WAAW,QAAQ;CAChD,MAAM,EAAE,QAAQ,eAAe;CAE/B,MAAM,uBAAuB;AAC3B,MAAI,QAAQ,QAAS,OAAM,IAAI,sBAAsB;;AAGvD,mBAAkB;CAKlB,MAAM,EACJ,OAAO,aACP,WAAW,gBACX,SAAS,uBACP,MAAM,UAAU,mBAAmB;CAGvC,MAAMA,aAAuB,EAAE;AAC/B,MAAK,IAAI,IAAI,GAAG,IAAI,OAAO,aAAa,IACtC,YAAW,KAAK,OAAO,UAAU,IAAI,OAAO,gBAAgB;CAM9D,IAAIC,SAAwB;CAC5B,IAAIC,cAAmC;CACvC,IAAIC,cAAwC;CAC5C,IAAIC,SAA6C;CACjD,IAAIC,aAGO;CACX,IAAI,eAAe;CACnB,IAAIC,iBAAyC;CAC7C,IAAIC,cAAwD;AAE5D,KAAI,CAAC,OAAO,eAAe;AAEzB,MAAI,QAAQ,sBAAsB;AAChC,YAAS,IAAI,aAAa,QAAQ,qBAA4B;AAC9D,YAAS,IAAI,OAAO;IAClB,QAAQ,IAAI,gBAAgB,EAAE,WAAW,cAAc,CAAC;IACxD;IACD,CAAC;AACF,kBAAe;aACN,OAAO,WAAW;AAC3B,gBAAa,MAAM,sBAAsB,OAAO,SAAS;AACzD,kBAAe,eAAe;AAE9B,OAAI,gBAAgB,YAAY;AAC9B,aAAS,IAAI,aAAa,WAAW,SAAgB;AACrD,aAAS,IAAI,OAAO;KAClB,QAAQ,IAAI,gBAAgB,EAAE,WAAW,cAAc,CAAC;KACxD;KACD,CAAC;;;AAIN,MAAI,CAAC,QAAQ;AACX,YAAS,IAAI,cAAc;AAC3B,YAAS,IAAI,OAAO;IAAE,QAAQ,IAAI,iBAAiB;IAAE;IAAQ,CAAC;;AAGhE,mBAAiB,IAAI,gBAAgB,OAAO,YAAY,OAAO,YAAY;AAC3E,gBAAc,eAAe,WAAW,KAAK;AAC7C,MAAI,CAAC,aAAa;AAChB,uBAAoB;AACpB,SAAM,IAAI,MAAM,wCAAwC;;AAG1D,MAAI,CAAC,OACH,OAAM,IAAI,MAAM,yBAAyB;EAG3C,MAAMC,cAAmC;GACvC,OAAO,OAAO;GACd,SAAS,OAAO;GAChB,kBAAkB,OAAO;GAC1B;AACD,gBAAc,IAAI,aAAa,gBAAgB,YAAY;AAC3D,SAAO,cAAc,YAAY;AAEjC,MAAI,OAAO,cAAc;AAavB,iBAAc,IAAI,kBAJuB;IACvC,OAToB,MAAM,iBAC1B,OAAO,sBACP;KACE,kBAAkB;KAClB,YAAY;KACZ,SAAS,OAAO;KACjB,CACF;IAGC,SAAS,OAAO;IACjB,CAC+C;AAChD,UAAO,cAAc,YAAY;;AAGnC,QAAM,OAAO,OAAO;;CAOtB,MAAM,gBAAgB,IAAI,eAAe;CAMzC,MAAM,mBAAmB,uBAAuB;EAC9C,OAHqB,UAAU,eAAe;EAI9C,QAHsB,UAAU,gBAAgB;EAIhD,YAAY,iBAAiB,UAAU,CAAC,cAAc;EACvD,CAAC;AAGF,QAAO,MAAM,+DAA+D;AAG5E,kBAAiB,YAAY,eAAe;AAG5C,kBAAiB,UAAU,IAAI,4BAA4B;AAQ3D,kBAAiB,MAAM,WACrB;AACF,UAAS,KAAK,YAAY,iBAAiB;AAG3C,CAAK,YAAY;AACjB,QAAO,MACL,yGACD;CAKD,MAAM,kBAAkB,YAAY,KAAK;CACzC,IAAI,yBAAyB,OAAO;CACpC,MAAM,uBAAuB;CAG7B,IAAIC,cAAwC;CAC5C,IAAIC,WAA4C;AAChD,KAAI,cAAc,OAAO,0BAA0B,GAAG;EACpD,MAAM,eAAe;EACrB,MAAM,gBAAgB,KAAK,MACzB,gBAAgB,OAAO,cAAc,OAAO,YAC7C;AACD,gBAAc,SAAS,cAAc,SAAS;AAC9C,cAAY,QAAQ;AACpB,cAAY,SAAS;AACrB,aAAW,YAAY,WAAW,KAAK;;AAGzC,KAAI;EAqBF,MAAM,YAAY;EAClB,MAAMC,gBAAgC,EAAE;EACxC,IAAI,gBAAgB;EACpB,IAAI,gBAAgB;AAEpB,SAAO,gBAAgB,OAAO,aAAa;AACzC,mBAAgB;AAKhB,UACE,gBAAgB,OAAO,eACvB,cAAc,SAAS,WACvB;IACA,MAAM,KAAK;IACX,MAAM,SAAS,WAAW;IAC1B,MAAM,aAAc,KAAK,OAAO,kBAAmB;AAEnD,UAAM,YAAY,cAAc,OAAO;IAEvC,MAAMC,QAAsB;KAC1B,YAAY;KACZ;KACA;KACA,UAAU;KACV,SAAS;KACV;AAGD,QAAI,OAAO,qBAAqB,WAC9B,OAAM,oBACJ,aACA,QACA,OAAO,kBACR;AAGH,QAAI,OAAO,eAAe,UAAU;AASlC,WAAM,WARS,MAAM,oBACnB,aACA,OAAO,OACP,OAAO,QACP,EACE,gBAAgB,MACjB,CACF;AAED,WAAM,UAAU,QAAQ,QAAQ,MAAM,SAAS;UAiB/C,OAAM,UAXiB,yBACrB,aACA,OAAO,OACP,OAAO,QACP;KACE;KACA,aAAa,OAAO;KACpB;KACD,CACF,CAE8B,MAAM,YAAY;AAC/C,YAAO,IAAI,SAA2B,SAAS,WAAW;MACxD,MAAMC,UAAQ,IAAI,OAAO;AACzB,cAAM,eAAe;AACnB,aAAM,WAAWA;AACjB,eAAQA,QAAM;;AAEhB,cAAM,WAAW,MAAM;AACrB,eAAQ,MAAM,kBAAkB,GAAG,qBAAqB,EAAE;AAC1D,8BAAO,IAAI,MAAM,qCAAqC,CAAC;;AAEzD,cAAM,MAAM;OACZ;MACF;AAGJ,kBAAc,KAAK,MAAM;AACzB;;GAMF,MAAM,OAAO,cAAc,OAAO;GAClC,MAAM,YAAY,KAAK,aAAa;GACpC,IAAIC;AACJ,OAAI,UACF,SAAQ,KAAK;OAEb,SAAQ,MAAM,KAAK;AAGrB,OACE,eACA,KAAK,UAAU,yBAAyB,sBACxC;IACA,MAAM,aAAa,KAAK,IACtB,KAAK,SAAS,sBACd,OAAO,MACR;AACD,QAAI;KACF,MAAM,cAAc,MAAM,UAAU,YAClC,wBACA,YACA,OACD;AACD,SAAI,eAAe,YAAY,SAAS,EACtC,OAAM,YAAY,IAAI,YAAY;aAE7B,GAAG;AAGZ,6BAAyB;;AAG3B,OAAI,eAAe,UAAU,aAAa;AACxC,gBAAY,UACV,OACA,GACA,GACA,MAAM,OACN,MAAM,QACN,GACA,GACA,OAAO,YACP,OAAO,YACR;AACD,UAAM,YAAY,IAAI,KAAK,YAAY,OAAO,eAAe;;AAM/D;GACA,MAAM,eAAe;GACrB,MAAM,WAAW,eAAe,OAAO;GACvC,MAAM,aAAa,eAAe,OAAO;GACzC,MAAM,YAAY,YAAY,KAAK,GAAG;GACtC,MAAM,aAAa,YAAY;GAE/B,MAAM,wBADkB,OAAO,cAAc,gBACE;GAC/C,MAAM,kBAAkB,aAAa;AAErC,OACE,eACA,YACA,KAAK,aAAa,OAAO,4BAA4B,EAErD,UAAS,UAAU,OAAO,GAAG,GAAG,YAAY,OAAO,YAAY,OAAO;AAGxE,gBAAa;IACX;IACA;IACA,aAAa,OAAO;IACpB;IACA,iBAAiB,OAAO;IACxB;IACA;IACA;IACA,oBAAoB,eAAe;IACpC,CAAC;;AAIJ,MAAI,eAAe,yBAAyB,OAAO,MACjD,KAAI;GACF,MAAM,cAAc,MAAM,UAAU,YAClC,wBACA,OAAO,OACP,OACD;AACD,OAAI,eAAe,YAAY,SAAS,EACtC,OAAM,YAAY,IAAI,YAAY;WAE7B,GAAG;AAKd,MAAI,OAAO,cACT;AAGF,QAAM,OAAQ,UAAU;AAExB,MACE,OAAO,YAAY,eACnB,QAAQ,IAAI,yBAAyB,QACrC;GACA,MAAM,YAAY,KAAK,MAAM,YAAY,KAAK,GAAG,gBAAgB;GACjE,MAAM,WAAW,QAAQ,qBAAqB;GAC9C,MAAM,eACJ,UAAU,iBAAiB,oBAAoB,CAAC;GAClD,MAAM,eAAe,UAAU,iBAAiB,WAAW,CAAC;GAC5D,MAAM,kBAAkB,UAAU,iBAAiB,cAAc,CAAC;GAClE,MAAM,cAAc,UAAU,iBAAiB,UAAU,CAAC;GAC1D,MAAMC,UAAkC,EACtC,gBAAgB,oBACjB;AACD,OAAI,QAAQ,eACV,SAAQ,mBAAmB,UAAU,QAAQ;AAE/C,SAAM,GAAG,SAAS,oBAAoB;IACpC,QAAQ;IACR;IACA,MAAM,KAAK,UAAU;KACnB,YAAY;KACZ,aAAa;KACb,aAAa;KACb,OAAO,OAAO;KACd,QAAQ,OAAO;KACf,KAAK,OAAO;KACZ,eAAe;MACb;MACA;MACA;MACA;MACD;KACF,CAAC;IACF,WAAW;IACZ,CAAC,CAAC,YAAY,GAEb;;AAGJ,MAAI,aAEF;OACK;GAEL,MAAM,cADe,OACY;AACjC,OAAI,CAAC,YACH,OAAM,IAAI,MAAM,4CAA4C;AAG9D,OAAI,OAAO,aACT,QAAO,IAAI,WAAW,YAAY;AAIpC,gBADkB,IAAI,KAAK,CAAC,YAAY,EAAE,EAAE,MAAM,aAAa,CAAC,EACxC,OAAO,SAAS;AACxC;;WAEM;AACR,gBAAc,SAAS;AAGvB,MAAI,iBAAiB,WACnB,kBAAiB,WAAW,YAAY,iBAAiB;AAE3D,sBAAoB"}
|
|
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","headers: Record<string, string>"],"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 (\n typeof __EF_TELEMETRY_ENABLED__ !== \"undefined\" &&\n __EF_TELEMETRY_ENABLED__\n ) {\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 const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n };\n if (options.telemetryToken) {\n headers[\"Authorization\"] = `Bearer ${options.telemetryToken}`;\n }\n fetch(`${endpoint}/api/v1/telemetry`, {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n event_type: \"render\",\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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"}
|
|
@@ -106,6 +106,7 @@ const SERIALIZED_STYLE_PROPERTIES = [
|
|
|
106
106
|
"fontVariant",
|
|
107
107
|
"textAlign",
|
|
108
108
|
"textDecoration",
|
|
109
|
+
"textShadow",
|
|
109
110
|
"textTransform",
|
|
110
111
|
"letterSpacing",
|
|
111
112
|
"wordSpacing",
|
|
@@ -184,7 +185,7 @@ function serializeComputedStyles(element, styles) {
|
|
|
184
185
|
if (value === "none" && !isCaptionChild) finalValue = resolveNaturalDisplay(element);
|
|
185
186
|
}
|
|
186
187
|
if (prop === "visibility") finalValue = "visible";
|
|
187
|
-
if (prop === "clipPath") continue;
|
|
188
|
+
if (prop === "clipPath" && value === "inset(100%)") continue;
|
|
188
189
|
if (prop === "width" && !hasExplicitWidth) continue;
|
|
189
190
|
if (prop === "height" && !hasExplicitHeight) continue;
|
|
190
191
|
styleParts.push(`${kebab}:${finalValue}`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serializeTimelineDirect.js","names":["rules: string[]","styleParts: string[]","styleStr","parts: Array<string | Promise<string>>","canvasJobs: CanvasJob[]"],"sources":["../../../src/preview/rendering/serializeTimelineDirect.ts"],"sourcesContent":["/**\n * Direct timeline serialization - no intermediate passive structure.\n *\n * Walks the timeline DOM once and builds XML string directly with promise parts\n * for async canvas encoding. 3x faster than DOM creation + XMLSerializer.\n *\n * Architecture:\n * 1. Walk timeline recursively\n * 2. Build array of string parts (some are promises for canvas encoding)\n * 3. Handle shadow DOM by serializing shadow content instead of light DOM\n * 4. Await all promises\n * 5. Join parts into final XML\n */\n\nimport { encodeCanvasesInParallel } from \"../encoding/canvasEncoder.js\";\nimport type { RenderContext } from \"../RenderContext.js\";\nimport { isTemporal, isVisibleAtTime } from \"../previewTypes.js\";\nimport { ScaleConfig } from \"./ScaleConfig.js\";\n\n/**\n * Collect document styles for shadow DOM injection.\n */\nfunction collectDocumentStyles(): string {\n const rules: string[] = [];\n try {\n for (const sheet of document.styleSheets) {\n try {\n if (sheet.cssRules) {\n for (const rule of sheet.cssRules) {\n rules.push(rule.cssText);\n }\n }\n } catch {\n // Expected: cross-origin stylesheets block cssRules access\n }\n }\n } catch (e) {\n console.warn(\n \"[collectDocumentStyles] Failed to access document.styleSheets:\",\n e,\n );\n }\n return rules.join(\"\\n\");\n}\n\n/**\n * Elements to skip entirely when serializing.\n * NOTE: SLOT is NOT skipped - it's handled specially to serialize light DOM children.\n */\nconst SKIP_TAGS = new Set([\n \"EF-AUDIO\",\n \"EF-THUMBNAIL-STRIP\",\n \"EF-FILMSTRIP\",\n \"EF-TIMELINE\",\n \"EF-WORKBENCH\",\n \"SCRIPT\",\n \"STYLE\",\n \"TEMPLATE\",\n]);\n\n/**\n * HTML void elements - these cannot have children and must be self-closing in XHTML.\n * Using `<br />` instead of `<br></br>`.\n */\nconst VOID_ELEMENTS = new Set([\n \"area\",\n \"base\",\n \"br\",\n \"col\",\n \"embed\",\n \"hr\",\n \"img\",\n \"input\",\n \"link\",\n \"meta\",\n \"param\",\n \"source\",\n \"track\",\n \"wbr\",\n]);\n\n/**\n * CSS properties to serialize as inline styles.\n */\nconst SERIALIZED_STYLE_PROPERTIES = [\n \"display\",\n \"visibility\",\n \"opacity\",\n \"position\",\n \"top\",\n \"right\",\n \"bottom\",\n \"left\",\n \"zIndex\",\n \"width\",\n \"height\",\n \"minWidth\",\n \"minHeight\",\n \"maxWidth\",\n \"maxHeight\",\n \"flexGrow\",\n \"flexShrink\",\n \"flexBasis\",\n \"flexDirection\",\n \"flexWrap\",\n \"justifyContent\",\n \"alignItems\",\n \"alignContent\",\n \"alignSelf\",\n \"gap\",\n \"gridTemplate\",\n \"gridColumn\",\n \"gridRow\",\n \"gridArea\",\n \"margin\",\n \"padding\",\n \"boxSizing\",\n \"border\",\n \"borderTop\",\n \"borderRight\",\n \"borderBottom\",\n \"borderLeft\",\n \"borderRadius\",\n \"background\",\n \"color\",\n \"boxShadow\",\n \"filter\",\n \"backdropFilter\",\n \"clipPath\",\n \"fontFamily\",\n \"fontSize\",\n \"fontWeight\",\n \"fontStyle\",\n \"fontVariant\",\n \"textAlign\",\n \"textDecoration\",\n \"textTransform\",\n \"letterSpacing\",\n \"wordSpacing\",\n \"whiteSpace\",\n \"textOverflow\",\n \"lineHeight\",\n \"verticalAlign\",\n \"transform\",\n \"transformOrigin\",\n \"transformStyle\",\n \"perspective\",\n \"perspectiveOrigin\",\n \"backfaceVisibility\",\n \"cursor\",\n \"pointerEvents\",\n \"userSelect\",\n \"overflow\",\n \"objectFit\",\n \"objectPosition\",\n] as const;\n\n/**\n * Caption child elements that should preserve display:none.\n * These use display:none for content visibility, not temporal visibility.\n */\nconst CAPTION_CHILD_TAGS = new Set([\n \"EF-CAPTIONS-ACTIVE-WORD\",\n \"EF-CAPTIONS-BEFORE-ACTIVE-WORD\",\n \"EF-CAPTIONS-AFTER-ACTIVE-WORD\",\n \"EF-CAPTIONS-SEGMENT\",\n]);\n\ninterface SerializationOptions {\n renderContext?: RenderContext;\n canvasScale: number;\n timeMs: number;\n}\n\ninterface InternalSerializationOptions {\n renderContext?: RenderContext;\n timeMs: number;\n scaleConfig: ScaleConfig;\n sourceMap: WeakMap<HTMLCanvasElement, Element>;\n}\n\ninterface CanvasJob {\n canvas: HTMLCanvasElement;\n sourceElement: Element;\n promiseIndex: number;\n}\n\n/**\n * Escape special XML characters.\n */\nfunction escapeXML(str: string): string {\n return str\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\");\n}\n\n/**\n * Resolve the natural display value for an element that has display:none\n * set as an inline style (e.g., from temporal visibility via updateAnimations).\n *\n * Temporarily removes the inline display override so getComputedStyle falls\n * through to the element's stylesheet rules (including shadow DOM :host styles),\n * reads the natural value, then restores the override.\n */\nfunction resolveNaturalDisplay(element: Element): string {\n const htmlEl = element as HTMLElement;\n const inlineDisplay = htmlEl.style?.getPropertyValue(\"display\");\n if (inlineDisplay === \"none\" && htmlEl.style) {\n htmlEl.style.removeProperty(\"display\");\n const natural = getComputedStyle(element).getPropertyValue(\"display\");\n htmlEl.style.setProperty(\"display\", \"none\");\n return natural || \"block\";\n }\n return \"block\";\n}\n\n/**\n * Serialize computed styles as inline style string.\n * Handles display:none recovery for non-caption elements by resolving\n * the element's natural display value from its stylesheet rules.\n * @param element - The element to serialize styles for\n * @param styles - Optional pre-computed CSSStyleDeclaration (avoids redundant getComputedStyle calls)\n */\nfunction serializeComputedStyles(\n element: Element,\n styles?: CSSStyleDeclaration,\n): string {\n const computed = styles ?? getComputedStyle(element);\n const styleParts: string[] = [];\n const tagName = element.tagName;\n const isCaptionChild = CAPTION_CHILD_TAGS.has(tagName);\n\n // Check if the element has explicit width/height in its inline style.\n // For elements that auto-size to content (inline, inline-block text),\n // serializing the computed \"used\" pixel width/height would lock them to\n // exact dimensions that may not match the foreignObject rendering context,\n // causing text wrapping when font metrics differ slightly.\n const htmlEl = element as HTMLElement;\n const hasExplicitWidth = !!htmlEl.style?.getPropertyValue(\"width\");\n const hasExplicitHeight = !!htmlEl.style?.getPropertyValue(\"height\");\n\n for (const prop of SERIALIZED_STYLE_PROPERTIES) {\n // Convert camelCase to kebab-case first\n const kebab = prop.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);\n const value = computed.getPropertyValue(kebab);\n\n // Skip only truly empty values\n if (!value || value === \"\") {\n continue;\n }\n\n // Handle display property specially\n let finalValue = value;\n if (prop === \"display\") {\n // For non-caption elements, recover the natural display value when display:none\n // was set by the temporal visibility system (updateAnimations). This prevents\n // inline elements (like ef-text-segment) from being serialized as display:block.\n if (value === \"none\" && !isCaptionChild) {\n finalValue = resolveNaturalDisplay(element);\n }\n }\n\n // Force visibility:visible - the source container may have visibility:hidden\n // for off-screen rendering, but we want the serialized output to be visible\n if (prop === \"visibility\") {\n finalValue = \"visible\";\n }\n\n // Skip clipPath - clones always render without clip-path\n // (source may have clip-path: inset(100%) from proxy mode)\n if (prop === \"clipPath\") {\n continue;\n }\n\n // Skip width/height when not explicitly set on the element.\n // getComputedStyle returns \"used\" pixel values for width/height even when\n // the specified value is auto. Serializing these pixel values locks\n // content-sized elements (text segments, inline-block spans) to exact\n // dimensions, which breaks when the foreignObject context renders text\n // with different font metrics.\n if (prop === \"width\" && !hasExplicitWidth) {\n continue;\n }\n if (prop === \"height\" && !hasExplicitHeight) {\n continue;\n }\n\n styleParts.push(`${kebab}:${finalValue}`);\n }\n\n // Disable animations/transitions to prevent re-animation\n styleParts.push(\"animation:none\", \"transition:none\");\n\n return styleParts.join(\";\");\n}\n\n/**\n * Serialize element attributes (excluding style, id, xmlns, event handlers).\n */\nfunction serializeAttributes(\n element: Element,\n parts: Array<string | Promise<string>>,\n): void {\n for (const attr of element.attributes) {\n const name = attr.name.toLowerCase();\n // Skip: id, style, xmlns (namespace handled separately), event handlers\n if (\n name === \"id\" ||\n name === \"style\" ||\n name === \"xmlns\" ||\n name.startsWith(\"on\")\n ) {\n continue;\n }\n parts.push(` ${attr.name}=\"${escapeXML(attr.value)}\"`);\n }\n}\n\n/**\n * Check if a canvas element should preserve alpha channel.\n * EF-WAVEFORM always needs alpha, EF-IMAGE checks hasAlpha property.\n * EF-SURFACE needs alpha because:\n * 1. Without a target, its canvas is transparent and CSS background should show through\n * 2. The target element may have transparent content\n * Raw canvas elements must preserve alpha - we don't know what they contain.\n */\nfunction shouldPreserveAlpha(sourceElement: Element): boolean {\n const tagName = sourceElement.tagName;\n if (tagName === \"EF-WAVEFORM\") {\n return true;\n }\n if (tagName === \"EF-SURFACE\") {\n // Surface needs alpha to allow CSS background to show through empty areas\n return true;\n }\n if (tagName === \"EF-IMAGE\") {\n return (\n \"hasAlpha\" in sourceElement && (sourceElement as any).hasAlpha === true\n );\n }\n // Raw canvas elements must preserve alpha\n if (sourceElement instanceof HTMLCanvasElement) {\n return true;\n }\n return false;\n}\n\n/**\n * Find the capture proxy canvas for an offscreen-rendered canvas.\n * When a canvas is transferred to offscreen via transferControlToOffscreen(),\n * the main thread can no longer read pixels from it. OffscreenCompositionCanvas\n * creates a hidden capture canvas (marked with data-offscreen-capture) that\n * receives ImageBitmap frames from the worker.\n */\nfunction findCaptureProxy(canvas: HTMLCanvasElement): HTMLCanvasElement | null {\n const container = canvas.parentElement;\n if (!container) return null;\n return container.querySelector('canvas[data-offscreen-capture=\"true\"]');\n}\n\n/**\n * Read pixels directly from a WebGL canvas's drawing buffer via gl.readPixels().\n *\n * drawImage(webglCanvas) reads from the compositor's \"presented\" surface, which\n * is only refreshed during requestAnimationFrame / compositing cycles. In hidden\n * browser tabs, compositing is suspended, so drawImage returns stale pixels even\n * though gl.render() produced new content in the drawing buffer.\n *\n * readPixels() reads from the drawing buffer directly, bypassing the compositor.\n *\n * Returns null for non-WebGL canvases (getContext returns null when a different\n * context type is already active).\n */\nfunction readWebGLPixels(canvas: HTMLCanvasElement): Uint8ClampedArray | null {\n const gl = (canvas.getContext(\"webgl2\") ??\n canvas.getContext(\"webgl\")) as WebGLRenderingContext | null;\n if (!gl) return null;\n\n const width = canvas.width;\n const height = canvas.height;\n if (width === 0 || height === 0) return null;\n\n // Ensure we read from the drawing buffer, not a leftover FBO\n gl.bindFramebuffer(gl.FRAMEBUFFER, null);\n\n const pixels = new Uint8Array(width * height * 4);\n gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);\n\n // readPixels returns rows bottom-to-top; flip to top-to-bottom for ImageData\n const rowSize = width * 4;\n const halfHeight = Math.floor(height / 2);\n const temp = new Uint8Array(rowSize);\n for (let y = 0; y < halfHeight; y++) {\n const topOffset = y * rowSize;\n const bottomOffset = (height - 1 - y) * rowSize;\n temp.set(pixels.subarray(topOffset, topOffset + rowSize));\n pixels.set(\n pixels.subarray(bottomOffset, bottomOffset + rowSize),\n topOffset,\n );\n pixels.set(temp, bottomOffset);\n }\n\n return new Uint8ClampedArray(pixels.buffer);\n}\n\n/**\n * Create a snapshot copy of a canvas's current pixels.\n * This captures the pixels synchronously before any async encoding,\n * preventing race conditions where the source canvas is modified.\n *\n * For WebGL canvases, uses gl.readPixels() to bypass the compositor's\n * presentation layer (which is suspended in hidden browser tabs).\n *\n * For offscreen-rendered canvases, this automatically uses the capture proxy\n * canvas instead of the transferred display canvas.\n */\nfunction snapshotCanvas(\n canvas: HTMLCanvasElement,\n scale: number,\n preserveAlpha: boolean,\n): HTMLCanvasElement {\n // If this canvas was transferred to offscreen, use its capture proxy\n const captureProxy = findCaptureProxy(canvas);\n const sourceCanvas = captureProxy ?? canvas;\n\n const targetWidth = Math.max(1, Math.floor(sourceCanvas.width * scale));\n const targetHeight = Math.max(1, Math.floor(sourceCanvas.height * scale));\n\n const copy = document.createElement(\"canvas\");\n copy.width = targetWidth;\n copy.height = targetHeight;\n\n if (preserveAlpha) {\n copy.dataset.preserveAlpha = \"true\";\n }\n\n const ctx = copy.getContext(\"2d\");\n if (ctx && sourceCanvas.width > 0 && sourceCanvas.height > 0) {\n // Try reading directly from WebGL drawing buffer (bypasses compositor)\n // Only needed when page is hidden - compositor is suspended in hidden tabs\n const useGlBypass = document.hidden;\n const glPixels = useGlBypass ? readWebGLPixels(sourceCanvas) : null;\n if (glPixels) {\n const srcW = sourceCanvas.width;\n const srcH = sourceCanvas.height;\n const imageData = new ImageData(\n glPixels as unknown as Uint8ClampedArray<ArrayBuffer>,\n srcW,\n srcH,\n );\n\n if (targetWidth === srcW && targetHeight === srcH) {\n ctx.putImageData(imageData, 0, 0);\n } else {\n // putImageData doesn't scale — bounce through a temp canvas\n const temp = document.createElement(\"canvas\");\n temp.width = srcW;\n temp.height = srcH;\n temp.getContext(\"2d\")!.putImageData(imageData, 0, 0);\n ctx.drawImage(temp, 0, 0, targetWidth, targetHeight);\n }\n } else {\n // Non-WebGL canvas: drawImage is synchronous and correct\n ctx.drawImage(sourceCanvas, 0, 0, targetWidth, targetHeight);\n }\n }\n\n return copy;\n}\n\n/**\n * Serialize a canvas element as an <img> with base64 data URL.\n * Creates a snapshot of current pixels before async encoding to prevent race conditions.\n *\n * OPTIMIZATION: Calculate optimal encoding resolution based on:\n * 1. CSS display size (how big it actually appears)\n * 2. Video export scale (output resolution multiplier)\n * 3. Quality multiplier (for sharpness, default 1.5x)\n */\nfunction serializeCanvas(\n sourceElement: Element,\n canvas: HTMLCanvasElement,\n parts: Array<string | Promise<string>>,\n canvasJobs: CanvasJob[],\n options: InternalSerializationOptions,\n): void {\n // If this canvas was transferred to offscreen, use its capture proxy\n const captureProxy = findCaptureProxy(canvas);\n const sourceCanvas = captureProxy ?? canvas;\n\n // Use intrinsic canvas dimensions, not computed styles (which may be zoom-affected)\n const width = sourceCanvas.width;\n const height = sourceCanvas.height;\n\n // Skip empty canvases\n if (width === 0 || height === 0) {\n return;\n }\n\n // Get computed style once and reuse\n const computedStyle = getComputedStyle(sourceElement);\n const styleStr = serializeComputedStyles(sourceElement, computedStyle);\n\n // Get computed dimensions from source element (respects CSS like w-[420px])\n const computedWidth = computedStyle.width;\n const computedHeight = computedStyle.height;\n\n // Preserve the source element's object-fit and object-position for correct scaling.\n // These CSS properties control how the canvas content fits its container and must be\n // carried through to the serialized <img> to maintain visual fidelity.\n const styleParts = styleStr\n ? styleStr.split(\";\").filter((s) => s.trim())\n : [];\n\n // Remove width/height from computed styles (we'll set them explicitly from computed dimensions)\n const filteredParts = styleParts.filter((s) => {\n const trimmed = s.trim();\n return !trimmed.startsWith(\"width:\") && !trimmed.startsWith(\"height:\");\n });\n\n // Use host element dimensions if available, otherwise fall back to canvas natural dimensions\n const displayWidth = computedWidth || `${width}px`;\n const displayHeight = computedHeight || `${height}px`;\n\n filteredParts.push(`width:${displayWidth}`);\n filteredParts.push(`height:${displayHeight}`);\n filteredParts.push(`display:block`);\n\n const finalStyle = filteredParts.join(\";\");\n\n // Check if we need to preserve alpha channel\n const preserveAlpha = shouldPreserveAlpha(sourceElement);\n\n // CRITICAL: Calculate optimal encoding scale BEFORE creating snapshot.\n // This prevents encoding at full resolution when CSS display size is much smaller.\n let optimalScale = options.scaleConfig.exportScale; // Start with export scale as fallback\n\n try {\n const cssWidth = parseFloat(computedWidth) || sourceCanvas.width;\n const cssHeight = parseFloat(computedHeight) || sourceCanvas.height;\n\n // Use ScaleConfig to compute optimal canvas scale\n optimalScale = options.scaleConfig.computeCanvasScale({\n naturalWidth: sourceCanvas.width,\n naturalHeight: sourceCanvas.height,\n displayWidth: cssWidth,\n displayHeight: cssHeight,\n });\n } catch (e) {\n // Fallback to export scale if we can't get computed style\n console.warn(\n `[serializeCanvas] Failed to get computed style for ${sourceElement.tagName}:`,\n e,\n );\n }\n\n // CRITICAL: Create a snapshot of canvas pixels SYNCHRONOUSLY before any async work.\n // This prevents race conditions where concurrent renders overwrite the shared\n // shadow canvas while encoding is in progress.\n // Note: snapshotCanvas already handles finding the capture proxy internally\n const snapshot = snapshotCanvas(canvas, optimalScale, preserveAlpha);\n\n // Open img tag with all styles from source element\n parts.push(`<img style=\"${escapeXML(finalStyle)}\" src=\"`);\n\n // Kick off async encoding of the SNAPSHOT (not the live canvas)\n const promiseIndex = parts.length;\n options.sourceMap.set(snapshot, sourceElement);\n\n // Snapshot is already scaled, so encode at 1.0 scale\n const encodePromise = encodeCanvasesInParallel([snapshot], {\n scale: 1.0,\n renderContext: options.renderContext,\n sourceMap: options.sourceMap,\n }).then((results) => results[0]?.dataUrl || \"\");\n\n parts.push(encodePromise);\n canvasJobs.push({ canvas: snapshot, sourceElement, promiseIndex });\n\n // Close img tag\n parts.push('\" />');\n}\n\n/**\n * Serialize an image element as a canvas (for shadow DOM img elements).\n */\nfunction serializeImageAsCanvas(\n sourceElement: Element,\n img: HTMLImageElement,\n parts: Array<string | Promise<string>>,\n canvasJobs: CanvasJob[],\n options: InternalSerializationOptions,\n): void {\n // Convert img to canvas for serialization\n const canvas = document.createElement(\"canvas\");\n canvas.width = img.naturalWidth;\n canvas.height = img.naturalHeight;\n\n const ctx = canvas.getContext(\"2d\");\n if (ctx) {\n try {\n ctx.drawImage(img, 0, 0);\n } catch (e) {\n // Cross-origin image - skip\n return;\n }\n }\n\n serializeCanvas(sourceElement, canvas, parts, canvasJobs, options);\n}\n\n/**\n * Serialize slotted light DOM children of a host element.\n */\nfunction serializeSlottedContent(\n slotHost: Element,\n parts: Array<string | Promise<string>>,\n canvasJobs: CanvasJob[],\n options: InternalSerializationOptions,\n parentIsSVG: boolean,\n): void {\n for (const slottedChild of slotHost.childNodes) {\n if (slottedChild.nodeType === Node.TEXT_NODE) {\n const text = slottedChild.textContent;\n if (text && text.length > 0) {\n parts.push(escapeXML(text));\n }\n } else if (slottedChild.nodeType === Node.ELEMENT_NODE) {\n serializeElement(\n slottedChild as Element,\n parts,\n canvasJobs,\n options,\n parentIsSVG,\n null,\n );\n }\n }\n}\n\n/**\n * Recursively serialize an element and its children to XML parts.\n * @param slotHost - When serializing inside shadow DOM, the custom element whose light DOM children should be serialized for slots\n */\nfunction serializeElement(\n element: Element,\n parts: Array<string | Promise<string>>,\n canvasJobs: CanvasJob[],\n options: InternalSerializationOptions,\n parentIsSVG = false,\n slotHost: Element | null = null,\n): void {\n // Skip certain elements\n if (SKIP_TAGS.has(element.tagName)) {\n return;\n }\n\n // Handle SLOT elements - serialize light DOM children of the slot host\n if (element.tagName === \"SLOT\" && slotHost) {\n serializeSlottedContent(slotHost, parts, canvasJobs, options, parentIsSVG);\n return;\n }\n\n // Check temporal visibility - skip elements outside their time bounds\n // This is non-destructive (doesn't modify DOM)\n // NOTE: Ancestor checking is unnecessary - serializeElement walks top-down,\n // so if a parent is temporally invisible, its children are never visited\n if (!isVisibleAtTime(element, options.timeMs)) {\n return;\n }\n\n // Respect updateAnimations' visibility decision for temporal elements.\n // isVisibleAtTime uses inclusive end bounds, but updateAnimations uses\n // exclusive end for mid-composition elements (VisibilityPolicy). When\n // updateAnimations has set display:none, that is the authoritative decision.\n if (\n isTemporal(element) &&\n (element as HTMLElement).style?.getPropertyValue(\"display\") === \"none\"\n ) {\n return;\n }\n\n // Custom element with shadow DOM?\n const isCustom = element.tagName.includes(\"-\");\n if (isCustom && element.shadowRoot) {\n const shadowCanvas = element.shadowRoot.querySelector(\"canvas\");\n if (shadowCanvas) {\n serializeCanvas(element, shadowCanvas, parts, canvasJobs, options);\n return;\n }\n\n const shadowImg = element.shadowRoot.querySelector(\"img\");\n if (shadowImg?.complete && shadowImg.naturalWidth > 0) {\n serializeImageAsCanvas(element, shadowImg, parts, canvasJobs, options);\n return;\n }\n\n // Serialize custom element with its styles, then shadow DOM content inside\n // Use span for inline/inline-block/inline-flex elements to preserve inline behavior\n const computedStyle = getComputedStyle(element);\n let computedDisplay = computedStyle.display;\n // If display:none was set by temporal visibility, resolve the natural display\n // to determine the correct container tag (span vs div)\n if (computedDisplay === \"none\") {\n computedDisplay = resolveNaturalDisplay(element);\n }\n const isInline =\n computedDisplay === \"inline\" ||\n computedDisplay === \"inline-block\" ||\n computedDisplay === \"inline-flex\";\n const containerTag = isInline ? \"span\" : \"div\";\n\n let styleStr = serializeComputedStyles(element, computedStyle);\n\n parts.push(`<${containerTag}`);\n\n // Copy data attributes and class from custom element\n for (const attr of element.attributes) {\n const name = attr.name.toLowerCase();\n if (name === \"class\" || name.startsWith(\"data-\")) {\n parts.push(` ${attr.name}=\"${escapeXML(attr.value)}\"`);\n }\n }\n\n if (styleStr) {\n parts.push(` style=\"${escapeXML(styleStr)}\"`);\n }\n parts.push(\">\");\n\n // Serialize shadow DOM content with this element as the slot host\n for (const child of element.shadowRoot.childNodes) {\n if (child.nodeType === Node.TEXT_NODE) {\n const text = child.textContent;\n if (text && text.length > 0) {\n parts.push(escapeXML(text));\n }\n } else if (child.nodeType === Node.ELEMENT_NODE) {\n // Pass this element as slotHost so nested SLOTs can access light DOM children\n serializeElement(\n child as Element,\n parts,\n canvasJobs,\n options,\n parentIsSVG,\n element,\n );\n }\n }\n\n parts.push(`</${containerTag}>`);\n return;\n }\n\n // Raw canvas in light DOM\n if (element instanceof HTMLCanvasElement) {\n serializeCanvas(element, element, parts, canvasJobs, options);\n return;\n }\n\n // Standard element - serialize to XHTML\n const tagName = element.tagName.toLowerCase();\n const isSVG = element instanceof SVGElement;\n const isVoid = VOID_ELEMENTS.has(tagName);\n\n // Open tag with namespace (only add xmlns for root SVG elements, not children)\n if (isSVG && !parentIsSVG) {\n // Root SVG element - needs xmlns declaration\n parts.push(`<${tagName} xmlns=\"http://www.w3.org/2000/svg\"`);\n } else {\n parts.push(`<${tagName}`);\n }\n\n // Attributes\n serializeAttributes(element, parts);\n\n // Computed styles as inline style attribute\n const styleStr = serializeComputedStyles(element);\n if (styleStr) {\n parts.push(` style=\"${escapeXML(styleStr)}\"`);\n }\n\n // Void elements: self-close with /> (XHTML requirement)\n if (isVoid) {\n parts.push(\" />\");\n return;\n }\n\n parts.push(\">\");\n\n // Children (shadow or light)\n const children = element.shadowRoot?.childNodes || element.childNodes;\n for (const child of children) {\n if (child.nodeType === Node.TEXT_NODE) {\n const text = child.textContent;\n if (text && text.length > 0) {\n parts.push(escapeXML(text));\n }\n } else if (child.nodeType === Node.ELEMENT_NODE) {\n // Preserve slotHost when recursing into standard elements inside shadow DOM\n serializeElement(\n child as Element,\n parts,\n canvasJobs,\n options,\n isSVG,\n slotHost,\n );\n }\n }\n\n // Close tag\n parts.push(`</${tagName}>`);\n}\n\n/**\n * TextEncoder instance for SVG-to-base64 encoding.\n * encode() converts to UTF-8 bytes in a single native call, then we\n * base64-encode the bytes. ~33% overhead vs ~200% for percent-encoding.\n */\nconst textEncoder = new TextEncoder();\n\n/**\n * Synchronous DOM capture phase. Walks the element tree, snapshots canvas\n * pixels, and kicks off async encoding. Returns parts array containing\n * string fragments and encoding promises.\n *\n * After this function returns, the source element's DOM is no longer\n * referenced — the clone can safely be seeked to the next frame.\n *\n * SCALING ARCHITECTURE (unified via ScaleConfig):\n *\n * ScaleConfig centralizes all scaling logic and provides:\n * 1. Output SVG dimensions (width * exportScale, height * exportScale)\n * 2. DOM scaling wrapper (CSS transform:scale when exportScale < 1)\n * 3. Per-canvas optimal encoding scale via computeCanvasScale()\n *\n * Canvas scaling is independent from DOM scaling because:\n * - Canvas elements have intrinsic pixel dimensions and can be downsampled\n * efficiently before encoding (prevents encoding 1920px at full resolution\n * when displayed at 420px)\n * - DOM content has no intrinsic resolution and must be scaled via CSS\n * transforms, which the browser handles during SVG foreignObject rendering\n *\n * Example: 1920x1080 @ 0.5 export scale\n * - Output SVG: 960x540\n * - DOM wrapper: transform:scale(0.5) on 1920x1080 content\n * - Canvas (1920px displayed at 420px): encoded at ~0.16x (315px)\n * via computeCanvasScale(420/1920 * 0.5 * 1.5 quality = 0.164)\n */\nexport function captureElementParts(\n element: Element,\n width: number,\n height: number,\n options: SerializationOptions,\n): Array<string | Promise<string>> {\n const parts: Array<string | Promise<string>> = [];\n const canvasJobs: CanvasJob[] = [];\n const sourceMap = new WeakMap<HTMLCanvasElement, Element>();\n\n // Create ScaleConfig to centralize all scaling logic\n const scaleConfig = ScaleConfig.fromOptions(\n width,\n height,\n options.canvasScale,\n );\n\n const documentStyles =\n options.renderContext?.getCachedDocumentStyles() ?? collectDocumentStyles();\n if (options.renderContext && documentStyles) {\n options.renderContext.setCachedDocumentStyles(documentStyles);\n }\n\n parts.push(\n `<div xmlns=\"http://www.w3.org/1999/xhtml\" ` +\n `style=\"width:${scaleConfig.outputWidth}px;height:${scaleConfig.outputHeight}px;overflow:hidden;position:relative;\">`,\n );\n\n if (documentStyles) {\n parts.push(`<style type=\"text/css\"><![CDATA[${documentStyles}]]></style>`);\n }\n\n // Apply DOM scaling wrapper if needed\n const domTransform = scaleConfig.getDOMTransform();\n if (domTransform) {\n const wrapperDims = scaleConfig.getDOMWrapperDimensions();\n parts.push(\n `<div style=\"transform:${domTransform};transform-origin:0 0;` +\n `width:${wrapperDims.width}px;height:${wrapperDims.height}px;\">`,\n );\n }\n\n // Create internal options with ScaleConfig\n const internalOptions: InternalSerializationOptions = {\n renderContext: options.renderContext,\n timeMs: options.timeMs,\n scaleConfig,\n sourceMap,\n };\n\n serializeElement(element, parts, canvasJobs, internalOptions);\n\n if (domTransform) {\n parts.push(\"</div>\");\n }\n\n parts.push(\"</div>\");\n\n return parts;\n}\n\n/**\n * Serialize any element directly to XHTML string.\n *\n * @param element - The element to serialize (timegroup, temporal element, or plain DOM)\n * @param width - Output width\n * @param height - Output height\n * @param options - Serialization options (renderContext, canvasScale, timeMs)\n * @returns XHTML string with all canvases encoded as base64 data URLs\n */\nexport async function serializeElementToXHTML(\n element: Element,\n width: number,\n height: number,\n options: SerializationOptions,\n): Promise<string> {\n const parts = captureElementParts(element, width, height, options);\n const resolvedParts = await Promise.all(parts);\n return resolvedParts.join(\"\");\n}\n\n/**\n * Synchronous capture with deferred data URI encoding.\n *\n * Walks the DOM and snapshots canvas pixels synchronously, then returns\n * a promise that resolves to the SVG data URI once async canvas-to-base64\n * encoding completes. The source element is NOT referenced after this\n * function returns — the caller can immediately mutate/seek the clone.\n */\nexport function captureTimelineToDataUri(\n element: Element,\n width: number,\n height: number,\n options: SerializationOptions,\n): Promise<string> {\n // Create ScaleConfig to compute scaled dimensions\n const scaleConfig = ScaleConfig.fromOptions(\n width,\n height,\n options.canvasScale,\n );\n\n const parts = captureElementParts(element, width, height, options);\n\n return Promise.all(parts).then((resolvedParts) => {\n const xhtml = resolvedParts.join(\"\");\n const svg =\n `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${scaleConfig.outputWidth}\" height=\"${scaleConfig.outputHeight}\">` +\n `<foreignObject x=\"0\" y=\"0\" width=\"${scaleConfig.outputWidth}\" height=\"${scaleConfig.outputHeight}\">${xhtml}</foreignObject>` +\n `</svg>`;\n // Encode SVG to base64 data URI inline (avoids module-level function reference issues)\n const bytes = textEncoder.encode(svg);\n let binary = \"\";\n for (let i = 0; i < bytes.length; i += 8192) {\n binary += String.fromCharCode.apply(\n null,\n bytes.subarray(i, i + 8192) as unknown as number[],\n );\n }\n return `data:image/svg+xml;base64,${btoa(binary)}`;\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"serializeTimelineDirect.js","names":["rules: string[]","styleParts: string[]","styleStr","parts: Array<string | Promise<string>>","canvasJobs: CanvasJob[]"],"sources":["../../../src/preview/rendering/serializeTimelineDirect.ts"],"sourcesContent":["/**\n * Direct timeline serialization - no intermediate passive structure.\n *\n * Walks the timeline DOM once and builds XML string directly with promise parts\n * for async canvas encoding. 3x faster than DOM creation + XMLSerializer.\n *\n * Architecture:\n * 1. Walk timeline recursively\n * 2. Build array of string parts (some are promises for canvas encoding)\n * 3. Handle shadow DOM by serializing shadow content instead of light DOM\n * 4. Await all promises\n * 5. Join parts into final XML\n */\n\nimport { encodeCanvasesInParallel } from \"../encoding/canvasEncoder.js\";\nimport type { RenderContext } from \"../RenderContext.js\";\nimport { isTemporal, isVisibleAtTime } from \"../previewTypes.js\";\nimport { ScaleConfig } from \"./ScaleConfig.js\";\n\n/**\n * Collect document styles for shadow DOM injection.\n */\nfunction collectDocumentStyles(): string {\n const rules: string[] = [];\n try {\n for (const sheet of document.styleSheets) {\n try {\n if (sheet.cssRules) {\n for (const rule of sheet.cssRules) {\n rules.push(rule.cssText);\n }\n }\n } catch {\n // Expected: cross-origin stylesheets block cssRules access\n }\n }\n } catch (e) {\n console.warn(\n \"[collectDocumentStyles] Failed to access document.styleSheets:\",\n e,\n );\n }\n return rules.join(\"\\n\");\n}\n\n/**\n * Elements to skip entirely when serializing.\n * NOTE: SLOT is NOT skipped - it's handled specially to serialize light DOM children.\n */\nconst SKIP_TAGS = new Set([\n \"EF-AUDIO\",\n \"EF-THUMBNAIL-STRIP\",\n \"EF-FILMSTRIP\",\n \"EF-TIMELINE\",\n \"EF-WORKBENCH\",\n \"SCRIPT\",\n \"STYLE\",\n \"TEMPLATE\",\n]);\n\n/**\n * HTML void elements - these cannot have children and must be self-closing in XHTML.\n * Using `<br />` instead of `<br></br>`.\n */\nconst VOID_ELEMENTS = new Set([\n \"area\",\n \"base\",\n \"br\",\n \"col\",\n \"embed\",\n \"hr\",\n \"img\",\n \"input\",\n \"link\",\n \"meta\",\n \"param\",\n \"source\",\n \"track\",\n \"wbr\",\n]);\n\n/**\n * CSS properties to serialize as inline styles.\n */\nconst SERIALIZED_STYLE_PROPERTIES = [\n \"display\",\n \"visibility\",\n \"opacity\",\n \"position\",\n \"top\",\n \"right\",\n \"bottom\",\n \"left\",\n \"zIndex\",\n \"width\",\n \"height\",\n \"minWidth\",\n \"minHeight\",\n \"maxWidth\",\n \"maxHeight\",\n \"flexGrow\",\n \"flexShrink\",\n \"flexBasis\",\n \"flexDirection\",\n \"flexWrap\",\n \"justifyContent\",\n \"alignItems\",\n \"alignContent\",\n \"alignSelf\",\n \"gap\",\n \"gridTemplate\",\n \"gridColumn\",\n \"gridRow\",\n \"gridArea\",\n \"margin\",\n \"padding\",\n \"boxSizing\",\n \"border\",\n \"borderTop\",\n \"borderRight\",\n \"borderBottom\",\n \"borderLeft\",\n \"borderRadius\",\n \"background\",\n \"color\",\n \"boxShadow\",\n \"filter\",\n \"backdropFilter\",\n \"clipPath\",\n \"fontFamily\",\n \"fontSize\",\n \"fontWeight\",\n \"fontStyle\",\n \"fontVariant\",\n \"textAlign\",\n \"textDecoration\",\n \"textShadow\",\n \"textTransform\",\n \"letterSpacing\",\n \"wordSpacing\",\n \"whiteSpace\",\n \"textOverflow\",\n \"lineHeight\",\n \"verticalAlign\",\n \"transform\",\n \"transformOrigin\",\n \"transformStyle\",\n \"perspective\",\n \"perspectiveOrigin\",\n \"backfaceVisibility\",\n \"cursor\",\n \"pointerEvents\",\n \"userSelect\",\n \"overflow\",\n \"objectFit\",\n \"objectPosition\",\n] as const;\n\n/**\n * Caption child elements that should preserve display:none.\n * These use display:none for content visibility, not temporal visibility.\n */\nconst CAPTION_CHILD_TAGS = new Set([\n \"EF-CAPTIONS-ACTIVE-WORD\",\n \"EF-CAPTIONS-BEFORE-ACTIVE-WORD\",\n \"EF-CAPTIONS-AFTER-ACTIVE-WORD\",\n \"EF-CAPTIONS-SEGMENT\",\n]);\n\ninterface SerializationOptions {\n renderContext?: RenderContext;\n canvasScale: number;\n timeMs: number;\n}\n\ninterface InternalSerializationOptions {\n renderContext?: RenderContext;\n timeMs: number;\n scaleConfig: ScaleConfig;\n sourceMap: WeakMap<HTMLCanvasElement, Element>;\n}\n\ninterface CanvasJob {\n canvas: HTMLCanvasElement;\n sourceElement: Element;\n promiseIndex: number;\n}\n\n/**\n * Escape special XML characters.\n */\nfunction escapeXML(str: string): string {\n return str\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\");\n}\n\n/**\n * Resolve the natural display value for an element that has display:none\n * set as an inline style (e.g., from temporal visibility via updateAnimations).\n *\n * Temporarily removes the inline display override so getComputedStyle falls\n * through to the element's stylesheet rules (including shadow DOM :host styles),\n * reads the natural value, then restores the override.\n */\nfunction resolveNaturalDisplay(element: Element): string {\n const htmlEl = element as HTMLElement;\n const inlineDisplay = htmlEl.style?.getPropertyValue(\"display\");\n if (inlineDisplay === \"none\" && htmlEl.style) {\n htmlEl.style.removeProperty(\"display\");\n const natural = getComputedStyle(element).getPropertyValue(\"display\");\n htmlEl.style.setProperty(\"display\", \"none\");\n return natural || \"block\";\n }\n return \"block\";\n}\n\n/**\n * Serialize computed styles as inline style string.\n * Handles display:none recovery for non-caption elements by resolving\n * the element's natural display value from its stylesheet rules.\n * @param element - The element to serialize styles for\n * @param styles - Optional pre-computed CSSStyleDeclaration (avoids redundant getComputedStyle calls)\n */\nfunction serializeComputedStyles(\n element: Element,\n styles?: CSSStyleDeclaration,\n): string {\n const computed = styles ?? getComputedStyle(element);\n const styleParts: string[] = [];\n const tagName = element.tagName;\n const isCaptionChild = CAPTION_CHILD_TAGS.has(tagName);\n\n // Check if the element has explicit width/height in its inline style.\n // For elements that auto-size to content (inline, inline-block text),\n // serializing the computed \"used\" pixel width/height would lock them to\n // exact dimensions that may not match the foreignObject rendering context,\n // causing text wrapping when font metrics differ slightly.\n const htmlEl = element as HTMLElement;\n const hasExplicitWidth = !!htmlEl.style?.getPropertyValue(\"width\");\n const hasExplicitHeight = !!htmlEl.style?.getPropertyValue(\"height\");\n\n for (const prop of SERIALIZED_STYLE_PROPERTIES) {\n // Convert camelCase to kebab-case first\n const kebab = prop.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);\n const value = computed.getPropertyValue(kebab);\n\n // Skip only truly empty values\n if (!value || value === \"\") {\n continue;\n }\n\n // Handle display property specially\n let finalValue = value;\n if (prop === \"display\") {\n // For non-caption elements, recover the natural display value when display:none\n // was set by the temporal visibility system (updateAnimations). This prevents\n // inline elements (like ef-text-segment) from being serialized as display:block.\n if (value === \"none\" && !isCaptionChild) {\n finalValue = resolveNaturalDisplay(element);\n }\n }\n\n // Force visibility:visible - the source container may have visibility:hidden\n // for off-screen rendering, but we want the serialized output to be visible\n if (prop === \"visibility\") {\n finalValue = \"visible\";\n }\n\n // Skip the proxy-mode sentinel value: inset(100%) hides the element\n // off-screen while keeping it in the layout. All other clip-path values\n // are legitimate visual effects and must be preserved.\n if (prop === \"clipPath\" && value === \"inset(100%)\") {\n continue;\n }\n\n // Skip width/height when not explicitly set on the element.\n // getComputedStyle returns \"used\" pixel values for width/height even when\n // the specified value is auto. Serializing these pixel values locks\n // content-sized elements (text segments, inline-block spans) to exact\n // dimensions, which breaks when the foreignObject context renders text\n // with different font metrics.\n if (prop === \"width\" && !hasExplicitWidth) {\n continue;\n }\n if (prop === \"height\" && !hasExplicitHeight) {\n continue;\n }\n\n styleParts.push(`${kebab}:${finalValue}`);\n }\n\n // Disable animations/transitions to prevent re-animation\n styleParts.push(\"animation:none\", \"transition:none\");\n\n return styleParts.join(\";\");\n}\n\n/**\n * Serialize element attributes (excluding style, id, xmlns, event handlers).\n */\nfunction serializeAttributes(\n element: Element,\n parts: Array<string | Promise<string>>,\n): void {\n for (const attr of element.attributes) {\n const name = attr.name.toLowerCase();\n // Skip: id, style, xmlns (namespace handled separately), event handlers\n if (\n name === \"id\" ||\n name === \"style\" ||\n name === \"xmlns\" ||\n name.startsWith(\"on\")\n ) {\n continue;\n }\n parts.push(` ${attr.name}=\"${escapeXML(attr.value)}\"`);\n }\n}\n\n/**\n * Check if a canvas element should preserve alpha channel.\n * EF-WAVEFORM always needs alpha, EF-IMAGE checks hasAlpha property.\n * EF-SURFACE needs alpha because:\n * 1. Without a target, its canvas is transparent and CSS background should show through\n * 2. The target element may have transparent content\n * Raw canvas elements must preserve alpha - we don't know what they contain.\n */\nfunction shouldPreserveAlpha(sourceElement: Element): boolean {\n const tagName = sourceElement.tagName;\n if (tagName === \"EF-WAVEFORM\") {\n return true;\n }\n if (tagName === \"EF-SURFACE\") {\n // Surface needs alpha to allow CSS background to show through empty areas\n return true;\n }\n if (tagName === \"EF-IMAGE\") {\n return (\n \"hasAlpha\" in sourceElement && (sourceElement as any).hasAlpha === true\n );\n }\n // Raw canvas elements must preserve alpha\n if (sourceElement instanceof HTMLCanvasElement) {\n return true;\n }\n return false;\n}\n\n/**\n * Find the capture proxy canvas for an offscreen-rendered canvas.\n * When a canvas is transferred to offscreen via transferControlToOffscreen(),\n * the main thread can no longer read pixels from it. OffscreenCompositionCanvas\n * creates a hidden capture canvas (marked with data-offscreen-capture) that\n * receives ImageBitmap frames from the worker.\n */\nfunction findCaptureProxy(canvas: HTMLCanvasElement): HTMLCanvasElement | null {\n const container = canvas.parentElement;\n if (!container) return null;\n return container.querySelector('canvas[data-offscreen-capture=\"true\"]');\n}\n\n/**\n * Read pixels directly from a WebGL canvas's drawing buffer via gl.readPixels().\n *\n * drawImage(webglCanvas) reads from the compositor's \"presented\" surface, which\n * is only refreshed during requestAnimationFrame / compositing cycles. In hidden\n * browser tabs, compositing is suspended, so drawImage returns stale pixels even\n * though gl.render() produced new content in the drawing buffer.\n *\n * readPixels() reads from the drawing buffer directly, bypassing the compositor.\n *\n * Returns null for non-WebGL canvases (getContext returns null when a different\n * context type is already active).\n */\nfunction readWebGLPixels(canvas: HTMLCanvasElement): Uint8ClampedArray | null {\n const gl = (canvas.getContext(\"webgl2\") ??\n canvas.getContext(\"webgl\")) as WebGLRenderingContext | null;\n if (!gl) return null;\n\n const width = canvas.width;\n const height = canvas.height;\n if (width === 0 || height === 0) return null;\n\n // Ensure we read from the drawing buffer, not a leftover FBO\n gl.bindFramebuffer(gl.FRAMEBUFFER, null);\n\n const pixels = new Uint8Array(width * height * 4);\n gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);\n\n // readPixels returns rows bottom-to-top; flip to top-to-bottom for ImageData\n const rowSize = width * 4;\n const halfHeight = Math.floor(height / 2);\n const temp = new Uint8Array(rowSize);\n for (let y = 0; y < halfHeight; y++) {\n const topOffset = y * rowSize;\n const bottomOffset = (height - 1 - y) * rowSize;\n temp.set(pixels.subarray(topOffset, topOffset + rowSize));\n pixels.set(\n pixels.subarray(bottomOffset, bottomOffset + rowSize),\n topOffset,\n );\n pixels.set(temp, bottomOffset);\n }\n\n return new Uint8ClampedArray(pixels.buffer);\n}\n\n/**\n * Create a snapshot copy of a canvas's current pixels.\n * This captures the pixels synchronously before any async encoding,\n * preventing race conditions where the source canvas is modified.\n *\n * For WebGL canvases, uses gl.readPixels() to bypass the compositor's\n * presentation layer (which is suspended in hidden browser tabs).\n *\n * For offscreen-rendered canvases, this automatically uses the capture proxy\n * canvas instead of the transferred display canvas.\n */\nfunction snapshotCanvas(\n canvas: HTMLCanvasElement,\n scale: number,\n preserveAlpha: boolean,\n): HTMLCanvasElement {\n // If this canvas was transferred to offscreen, use its capture proxy\n const captureProxy = findCaptureProxy(canvas);\n const sourceCanvas = captureProxy ?? canvas;\n\n const targetWidth = Math.max(1, Math.floor(sourceCanvas.width * scale));\n const targetHeight = Math.max(1, Math.floor(sourceCanvas.height * scale));\n\n const copy = document.createElement(\"canvas\");\n copy.width = targetWidth;\n copy.height = targetHeight;\n\n if (preserveAlpha) {\n copy.dataset.preserveAlpha = \"true\";\n }\n\n const ctx = copy.getContext(\"2d\");\n if (ctx && sourceCanvas.width > 0 && sourceCanvas.height > 0) {\n // Try reading directly from WebGL drawing buffer (bypasses compositor)\n // Only needed when page is hidden - compositor is suspended in hidden tabs\n const useGlBypass = document.hidden;\n const glPixels = useGlBypass ? readWebGLPixels(sourceCanvas) : null;\n if (glPixels) {\n const srcW = sourceCanvas.width;\n const srcH = sourceCanvas.height;\n const imageData = new ImageData(\n glPixels as unknown as Uint8ClampedArray<ArrayBuffer>,\n srcW,\n srcH,\n );\n\n if (targetWidth === srcW && targetHeight === srcH) {\n ctx.putImageData(imageData, 0, 0);\n } else {\n // putImageData doesn't scale — bounce through a temp canvas\n const temp = document.createElement(\"canvas\");\n temp.width = srcW;\n temp.height = srcH;\n temp.getContext(\"2d\")!.putImageData(imageData, 0, 0);\n ctx.drawImage(temp, 0, 0, targetWidth, targetHeight);\n }\n } else {\n // Non-WebGL canvas: drawImage is synchronous and correct\n ctx.drawImage(sourceCanvas, 0, 0, targetWidth, targetHeight);\n }\n }\n\n return copy;\n}\n\n/**\n * Serialize a canvas element as an <img> with base64 data URL.\n * Creates a snapshot of current pixels before async encoding to prevent race conditions.\n *\n * OPTIMIZATION: Calculate optimal encoding resolution based on:\n * 1. CSS display size (how big it actually appears)\n * 2. Video export scale (output resolution multiplier)\n * 3. Quality multiplier (for sharpness, default 1.5x)\n */\nfunction serializeCanvas(\n sourceElement: Element,\n canvas: HTMLCanvasElement,\n parts: Array<string | Promise<string>>,\n canvasJobs: CanvasJob[],\n options: InternalSerializationOptions,\n): void {\n // If this canvas was transferred to offscreen, use its capture proxy\n const captureProxy = findCaptureProxy(canvas);\n const sourceCanvas = captureProxy ?? canvas;\n\n // Use intrinsic canvas dimensions, not computed styles (which may be zoom-affected)\n const width = sourceCanvas.width;\n const height = sourceCanvas.height;\n\n // Skip empty canvases\n if (width === 0 || height === 0) {\n return;\n }\n\n // Get computed style once and reuse\n const computedStyle = getComputedStyle(sourceElement);\n const styleStr = serializeComputedStyles(sourceElement, computedStyle);\n\n // Get computed dimensions from source element (respects CSS like w-[420px])\n const computedWidth = computedStyle.width;\n const computedHeight = computedStyle.height;\n\n // Preserve the source element's object-fit and object-position for correct scaling.\n // These CSS properties control how the canvas content fits its container and must be\n // carried through to the serialized <img> to maintain visual fidelity.\n const styleParts = styleStr\n ? styleStr.split(\";\").filter((s) => s.trim())\n : [];\n\n // Remove width/height from computed styles (we'll set them explicitly from computed dimensions)\n const filteredParts = styleParts.filter((s) => {\n const trimmed = s.trim();\n return !trimmed.startsWith(\"width:\") && !trimmed.startsWith(\"height:\");\n });\n\n // Use host element dimensions if available, otherwise fall back to canvas natural dimensions\n const displayWidth = computedWidth || `${width}px`;\n const displayHeight = computedHeight || `${height}px`;\n\n filteredParts.push(`width:${displayWidth}`);\n filteredParts.push(`height:${displayHeight}`);\n filteredParts.push(`display:block`);\n\n const finalStyle = filteredParts.join(\";\");\n\n // Check if we need to preserve alpha channel\n const preserveAlpha = shouldPreserveAlpha(sourceElement);\n\n // CRITICAL: Calculate optimal encoding scale BEFORE creating snapshot.\n // This prevents encoding at full resolution when CSS display size is much smaller.\n let optimalScale = options.scaleConfig.exportScale; // Start with export scale as fallback\n\n try {\n const cssWidth = parseFloat(computedWidth) || sourceCanvas.width;\n const cssHeight = parseFloat(computedHeight) || sourceCanvas.height;\n\n // Use ScaleConfig to compute optimal canvas scale\n optimalScale = options.scaleConfig.computeCanvasScale({\n naturalWidth: sourceCanvas.width,\n naturalHeight: sourceCanvas.height,\n displayWidth: cssWidth,\n displayHeight: cssHeight,\n });\n } catch (e) {\n // Fallback to export scale if we can't get computed style\n console.warn(\n `[serializeCanvas] Failed to get computed style for ${sourceElement.tagName}:`,\n e,\n );\n }\n\n // CRITICAL: Create a snapshot of canvas pixels SYNCHRONOUSLY before any async work.\n // This prevents race conditions where concurrent renders overwrite the shared\n // shadow canvas while encoding is in progress.\n // Note: snapshotCanvas already handles finding the capture proxy internally\n const snapshot = snapshotCanvas(canvas, optimalScale, preserveAlpha);\n\n // Open img tag with all styles from source element\n parts.push(`<img style=\"${escapeXML(finalStyle)}\" src=\"`);\n\n // Kick off async encoding of the SNAPSHOT (not the live canvas)\n const promiseIndex = parts.length;\n options.sourceMap.set(snapshot, sourceElement);\n\n // Snapshot is already scaled, so encode at 1.0 scale\n const encodePromise = encodeCanvasesInParallel([snapshot], {\n scale: 1.0,\n renderContext: options.renderContext,\n sourceMap: options.sourceMap,\n }).then((results) => results[0]?.dataUrl || \"\");\n\n parts.push(encodePromise);\n canvasJobs.push({ canvas: snapshot, sourceElement, promiseIndex });\n\n // Close img tag\n parts.push('\" />');\n}\n\n/**\n * Serialize an image element as a canvas (for shadow DOM img elements).\n */\nfunction serializeImageAsCanvas(\n sourceElement: Element,\n img: HTMLImageElement,\n parts: Array<string | Promise<string>>,\n canvasJobs: CanvasJob[],\n options: InternalSerializationOptions,\n): void {\n // Convert img to canvas for serialization\n const canvas = document.createElement(\"canvas\");\n canvas.width = img.naturalWidth;\n canvas.height = img.naturalHeight;\n\n const ctx = canvas.getContext(\"2d\");\n if (ctx) {\n try {\n ctx.drawImage(img, 0, 0);\n } catch (e) {\n // Cross-origin image - skip\n return;\n }\n }\n\n serializeCanvas(sourceElement, canvas, parts, canvasJobs, options);\n}\n\n/**\n * Serialize slotted light DOM children of a host element.\n */\nfunction serializeSlottedContent(\n slotHost: Element,\n parts: Array<string | Promise<string>>,\n canvasJobs: CanvasJob[],\n options: InternalSerializationOptions,\n parentIsSVG: boolean,\n): void {\n for (const slottedChild of slotHost.childNodes) {\n if (slottedChild.nodeType === Node.TEXT_NODE) {\n const text = slottedChild.textContent;\n if (text && text.length > 0) {\n parts.push(escapeXML(text));\n }\n } else if (slottedChild.nodeType === Node.ELEMENT_NODE) {\n serializeElement(\n slottedChild as Element,\n parts,\n canvasJobs,\n options,\n parentIsSVG,\n null,\n );\n }\n }\n}\n\n/**\n * Recursively serialize an element and its children to XML parts.\n * @param slotHost - When serializing inside shadow DOM, the custom element whose light DOM children should be serialized for slots\n */\nfunction serializeElement(\n element: Element,\n parts: Array<string | Promise<string>>,\n canvasJobs: CanvasJob[],\n options: InternalSerializationOptions,\n parentIsSVG = false,\n slotHost: Element | null = null,\n): void {\n // Skip certain elements\n if (SKIP_TAGS.has(element.tagName)) {\n return;\n }\n\n // Handle SLOT elements - serialize light DOM children of the slot host\n if (element.tagName === \"SLOT\" && slotHost) {\n serializeSlottedContent(slotHost, parts, canvasJobs, options, parentIsSVG);\n return;\n }\n\n // Check temporal visibility - skip elements outside their time bounds\n // This is non-destructive (doesn't modify DOM)\n // NOTE: Ancestor checking is unnecessary - serializeElement walks top-down,\n // so if a parent is temporally invisible, its children are never visited\n if (!isVisibleAtTime(element, options.timeMs)) {\n return;\n }\n\n // Respect updateAnimations' visibility decision for temporal elements.\n // isVisibleAtTime uses inclusive end bounds, but updateAnimations uses\n // exclusive end for mid-composition elements (VisibilityPolicy). When\n // updateAnimations has set display:none, that is the authoritative decision.\n if (\n isTemporal(element) &&\n (element as HTMLElement).style?.getPropertyValue(\"display\") === \"none\"\n ) {\n return;\n }\n\n // Custom element with shadow DOM?\n const isCustom = element.tagName.includes(\"-\");\n if (isCustom && element.shadowRoot) {\n const shadowCanvas = element.shadowRoot.querySelector(\"canvas\");\n if (shadowCanvas) {\n serializeCanvas(element, shadowCanvas, parts, canvasJobs, options);\n return;\n }\n\n const shadowImg = element.shadowRoot.querySelector(\"img\");\n if (shadowImg?.complete && shadowImg.naturalWidth > 0) {\n serializeImageAsCanvas(element, shadowImg, parts, canvasJobs, options);\n return;\n }\n\n // Serialize custom element with its styles, then shadow DOM content inside\n // Use span for inline/inline-block/inline-flex elements to preserve inline behavior\n const computedStyle = getComputedStyle(element);\n let computedDisplay = computedStyle.display;\n // If display:none was set by temporal visibility, resolve the natural display\n // to determine the correct container tag (span vs div)\n if (computedDisplay === \"none\") {\n computedDisplay = resolveNaturalDisplay(element);\n }\n const isInline =\n computedDisplay === \"inline\" ||\n computedDisplay === \"inline-block\" ||\n computedDisplay === \"inline-flex\";\n const containerTag = isInline ? \"span\" : \"div\";\n\n let styleStr = serializeComputedStyles(element, computedStyle);\n\n parts.push(`<${containerTag}`);\n\n // Copy data attributes and class from custom element\n for (const attr of element.attributes) {\n const name = attr.name.toLowerCase();\n if (name === \"class\" || name.startsWith(\"data-\")) {\n parts.push(` ${attr.name}=\"${escapeXML(attr.value)}\"`);\n }\n }\n\n if (styleStr) {\n parts.push(` style=\"${escapeXML(styleStr)}\"`);\n }\n parts.push(\">\");\n\n // Serialize shadow DOM content with this element as the slot host\n for (const child of element.shadowRoot.childNodes) {\n if (child.nodeType === Node.TEXT_NODE) {\n const text = child.textContent;\n if (text && text.length > 0) {\n parts.push(escapeXML(text));\n }\n } else if (child.nodeType === Node.ELEMENT_NODE) {\n // Pass this element as slotHost so nested SLOTs can access light DOM children\n serializeElement(\n child as Element,\n parts,\n canvasJobs,\n options,\n parentIsSVG,\n element,\n );\n }\n }\n\n parts.push(`</${containerTag}>`);\n return;\n }\n\n // Raw canvas in light DOM\n if (element instanceof HTMLCanvasElement) {\n serializeCanvas(element, element, parts, canvasJobs, options);\n return;\n }\n\n // Standard element - serialize to XHTML\n const tagName = element.tagName.toLowerCase();\n const isSVG = element instanceof SVGElement;\n const isVoid = VOID_ELEMENTS.has(tagName);\n\n // Open tag with namespace (only add xmlns for root SVG elements, not children)\n if (isSVG && !parentIsSVG) {\n // Root SVG element - needs xmlns declaration\n parts.push(`<${tagName} xmlns=\"http://www.w3.org/2000/svg\"`);\n } else {\n parts.push(`<${tagName}`);\n }\n\n // Attributes\n serializeAttributes(element, parts);\n\n // Computed styles as inline style attribute\n const styleStr = serializeComputedStyles(element);\n if (styleStr) {\n parts.push(` style=\"${escapeXML(styleStr)}\"`);\n }\n\n // Void elements: self-close with /> (XHTML requirement)\n if (isVoid) {\n parts.push(\" />\");\n return;\n }\n\n parts.push(\">\");\n\n // Children (shadow or light)\n const children = element.shadowRoot?.childNodes || element.childNodes;\n for (const child of children) {\n if (child.nodeType === Node.TEXT_NODE) {\n const text = child.textContent;\n if (text && text.length > 0) {\n parts.push(escapeXML(text));\n }\n } else if (child.nodeType === Node.ELEMENT_NODE) {\n // Preserve slotHost when recursing into standard elements inside shadow DOM\n serializeElement(\n child as Element,\n parts,\n canvasJobs,\n options,\n isSVG,\n slotHost,\n );\n }\n }\n\n // Close tag\n parts.push(`</${tagName}>`);\n}\n\n/**\n * TextEncoder instance for SVG-to-base64 encoding.\n * encode() converts to UTF-8 bytes in a single native call, then we\n * base64-encode the bytes. ~33% overhead vs ~200% for percent-encoding.\n */\nconst textEncoder = new TextEncoder();\n\n/**\n * Synchronous DOM capture phase. Walks the element tree, snapshots canvas\n * pixels, and kicks off async encoding. Returns parts array containing\n * string fragments and encoding promises.\n *\n * After this function returns, the source element's DOM is no longer\n * referenced — the clone can safely be seeked to the next frame.\n *\n * SCALING ARCHITECTURE (unified via ScaleConfig):\n *\n * ScaleConfig centralizes all scaling logic and provides:\n * 1. Output SVG dimensions (width * exportScale, height * exportScale)\n * 2. DOM scaling wrapper (CSS transform:scale when exportScale < 1)\n * 3. Per-canvas optimal encoding scale via computeCanvasScale()\n *\n * Canvas scaling is independent from DOM scaling because:\n * - Canvas elements have intrinsic pixel dimensions and can be downsampled\n * efficiently before encoding (prevents encoding 1920px at full resolution\n * when displayed at 420px)\n * - DOM content has no intrinsic resolution and must be scaled via CSS\n * transforms, which the browser handles during SVG foreignObject rendering\n *\n * Example: 1920x1080 @ 0.5 export scale\n * - Output SVG: 960x540\n * - DOM wrapper: transform:scale(0.5) on 1920x1080 content\n * - Canvas (1920px displayed at 420px): encoded at ~0.16x (315px)\n * via computeCanvasScale(420/1920 * 0.5 * 1.5 quality = 0.164)\n */\nexport function captureElementParts(\n element: Element,\n width: number,\n height: number,\n options: SerializationOptions,\n): Array<string | Promise<string>> {\n const parts: Array<string | Promise<string>> = [];\n const canvasJobs: CanvasJob[] = [];\n const sourceMap = new WeakMap<HTMLCanvasElement, Element>();\n\n // Create ScaleConfig to centralize all scaling logic\n const scaleConfig = ScaleConfig.fromOptions(\n width,\n height,\n options.canvasScale,\n );\n\n const documentStyles =\n options.renderContext?.getCachedDocumentStyles() ?? collectDocumentStyles();\n if (options.renderContext && documentStyles) {\n options.renderContext.setCachedDocumentStyles(documentStyles);\n }\n\n parts.push(\n `<div xmlns=\"http://www.w3.org/1999/xhtml\" ` +\n `style=\"width:${scaleConfig.outputWidth}px;height:${scaleConfig.outputHeight}px;overflow:hidden;position:relative;\">`,\n );\n\n if (documentStyles) {\n parts.push(`<style type=\"text/css\"><![CDATA[${documentStyles}]]></style>`);\n }\n\n // Apply DOM scaling wrapper if needed\n const domTransform = scaleConfig.getDOMTransform();\n if (domTransform) {\n const wrapperDims = scaleConfig.getDOMWrapperDimensions();\n parts.push(\n `<div style=\"transform:${domTransform};transform-origin:0 0;` +\n `width:${wrapperDims.width}px;height:${wrapperDims.height}px;\">`,\n );\n }\n\n // Create internal options with ScaleConfig\n const internalOptions: InternalSerializationOptions = {\n renderContext: options.renderContext,\n timeMs: options.timeMs,\n scaleConfig,\n sourceMap,\n };\n\n serializeElement(element, parts, canvasJobs, internalOptions);\n\n if (domTransform) {\n parts.push(\"</div>\");\n }\n\n parts.push(\"</div>\");\n\n return parts;\n}\n\n/**\n * Serialize any element directly to XHTML string.\n *\n * @param element - The element to serialize (timegroup, temporal element, or plain DOM)\n * @param width - Output width\n * @param height - Output height\n * @param options - Serialization options (renderContext, canvasScale, timeMs)\n * @returns XHTML string with all canvases encoded as base64 data URLs\n */\nexport async function serializeElementToXHTML(\n element: Element,\n width: number,\n height: number,\n options: SerializationOptions,\n): Promise<string> {\n const parts = captureElementParts(element, width, height, options);\n const resolvedParts = await Promise.all(parts);\n return resolvedParts.join(\"\");\n}\n\n/**\n * Synchronous capture with deferred data URI encoding.\n *\n * Walks the DOM and snapshots canvas pixels synchronously, then returns\n * a promise that resolves to the SVG data URI once async canvas-to-base64\n * encoding completes. The source element is NOT referenced after this\n * function returns — the caller can immediately mutate/seek the clone.\n */\nexport function captureTimelineToDataUri(\n element: Element,\n width: number,\n height: number,\n options: SerializationOptions,\n): Promise<string> {\n // Create ScaleConfig to compute scaled dimensions\n const scaleConfig = ScaleConfig.fromOptions(\n width,\n height,\n options.canvasScale,\n );\n\n const parts = captureElementParts(element, width, height, options);\n\n return Promise.all(parts).then((resolvedParts) => {\n const xhtml = resolvedParts.join(\"\");\n const svg =\n `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${scaleConfig.outputWidth}\" height=\"${scaleConfig.outputHeight}\">` +\n `<foreignObject x=\"0\" y=\"0\" width=\"${scaleConfig.outputWidth}\" height=\"${scaleConfig.outputHeight}\">${xhtml}</foreignObject>` +\n `</svg>`;\n // Encode SVG to base64 data URI inline (avoids module-level function reference issues)\n const bytes = textEncoder.encode(svg);\n let binary = \"\";\n for (let i = 0; i < bytes.length; i += 8192) {\n binary += String.fromCharCode.apply(\n null,\n bytes.subarray(i, i + 8192) as unknown as number[],\n );\n }\n return `data:image/svg+xml;base64,${btoa(binary)}`;\n });\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@editframe/elements",
|
|
3
|
-
"version": "0.42.
|
|
3
|
+
"version": "0.42.8",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@bramus/style-observer": "^1.3.0",
|
|
26
|
-
"@editframe/assets": "0.42.
|
|
26
|
+
"@editframe/assets": "0.42.8",
|
|
27
27
|
"@lit/context": "^1.1.6",
|
|
28
28
|
"@opentelemetry/api": "^1.9.0",
|
|
29
29
|
"@opentelemetry/context-zone": "^1.26.0",
|