@editframe/elements 0.35.0-beta → 0.36.1-beta
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/canvas/EFCanvas.d.ts +4 -4
- package/dist/elements/EFAudio.d.ts +4 -4
- package/dist/elements/EFCaptions.d.ts +0 -4
- package/dist/elements/EFCaptions.js +12 -32
- package/dist/elements/EFCaptions.js.map +1 -1
- package/dist/elements/EFImage.js +11 -2
- package/dist/elements/EFImage.js.map +1 -1
- package/dist/elements/EFPanZoom.d.ts +4 -4
- package/dist/elements/EFSurface.d.ts +4 -4
- package/dist/elements/EFTemporal.js +1 -0
- package/dist/elements/EFTemporal.js.map +1 -1
- package/dist/elements/EFText.d.ts +4 -4
- package/dist/elements/EFTextSegment.d.ts +4 -4
- package/dist/elements/EFThumbnailStrip.d.ts +4 -4
- package/dist/elements/EFTimegroup.d.ts +40 -6
- package/dist/elements/EFTimegroup.js +127 -8
- package/dist/elements/EFTimegroup.js.map +1 -1
- package/dist/elements/EFVideo.d.ts +6 -6
- package/dist/elements/EFWaveform.d.ts +4 -4
- package/dist/elements/updateAnimations.js +113 -15
- package/dist/elements/updateAnimations.js.map +1 -1
- package/dist/gui/EFActiveRootTemporal.d.ts +4 -4
- package/dist/gui/EFConfiguration.d.ts +4 -4
- package/dist/gui/EFControls.d.ts +2 -2
- package/dist/gui/EFDial.d.ts +4 -4
- package/dist/gui/EFFilmstrip.d.ts +2 -2
- package/dist/gui/EFFitScale.d.ts +3 -3
- package/dist/gui/EFFocusOverlay.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/EFResizableBox.d.ts +4 -4
- package/dist/gui/EFScrubber.d.ts +4 -4
- package/dist/gui/EFTimeDisplay.d.ts +4 -4
- package/dist/gui/EFToggleLoop.d.ts +4 -4
- package/dist/gui/EFTogglePlay.d.ts +4 -4
- package/dist/gui/EFTransformHandles.d.ts +4 -4
- package/dist/gui/EFWorkbench.d.ts +6 -6
- package/dist/gui/EFWorkbench.js +38 -12
- package/dist/gui/EFWorkbench.js.map +1 -1
- package/dist/gui/TWMixin.js +1 -1
- package/dist/gui/TWMixin.js.map +1 -1
- package/dist/gui/hierarchy/EFHierarchy.d.ts +4 -4
- package/dist/gui/hierarchy/EFHierarchyItem.d.ts +2 -2
- package/dist/gui/timeline/tracks/ImageTrack.d.ts +2 -2
- package/dist/gui/timeline/tracks/TimegroupTrack.d.ts +5 -5
- package/dist/gui/timeline/tracks/VideoTrack.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/FrameController.js +6 -1
- package/dist/preview/FrameController.js.map +1 -1
- package/dist/preview/encoding/canvasEncoder.js.map +1 -1
- package/dist/preview/encoding/mainThreadEncoder.js +3 -0
- package/dist/preview/encoding/mainThreadEncoder.js.map +1 -1
- package/dist/preview/renderTimegroupPreview.js +57 -55
- package/dist/preview/renderTimegroupPreview.js.map +1 -1
- package/dist/preview/renderTimegroupToCanvas.js +22 -23
- package/dist/preview/renderTimegroupToCanvas.js.map +1 -1
- package/dist/preview/renderTimegroupToVideo.d.ts +2 -1
- package/dist/preview/renderTimegroupToVideo.js +77 -40
- package/dist/preview/renderTimegroupToVideo.js.map +1 -1
- package/dist/preview/rendering/renderToImage.d.ts +1 -0
- package/dist/preview/rendering/renderToImage.js +1 -26
- package/dist/preview/rendering/renderToImage.js.map +1 -1
- package/dist/preview/rendering/renderToImageForeignObject.js +34 -6
- package/dist/preview/rendering/renderToImageForeignObject.js.map +1 -1
- package/dist/preview/rendering/serializeTimelineDirect.js +379 -0
- package/dist/preview/rendering/serializeTimelineDirect.js.map +1 -0
- package/dist/render/EFRenderAPI.js +45 -0
- package/dist/render/EFRenderAPI.js.map +1 -1
- package/dist/style.css +45 -0
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"renderTimegroupToCanvas.js","names":["timeMs: number","timeoutMs: number","blankVideos: string[]","renderState: RenderState","scaleColors: Record<number, string>","image: HTMLCanvasElement | HTMLImageElement","options: CanvasPreviewOptions","pendingResolutionScale: number | null"],"sources":["../../src/preview/renderTimegroupToCanvas.ts"],"sourcesContent":["import type { EFTimegroup } from \"../elements/EFTimegroup.js\";\nimport {\n buildCloneStructure,\n syncStyles,\n collectDocumentStyles,\n overrideRootCloneStyles,\n removeHiddenNodesForSerialization,\n restoreHiddenNodes,\n type SyncState,\n} from \"./renderTimegroupPreview.js\";\nimport { getEffectiveRenderMode } from \"./renderers.js\";\nimport { RenderContext } from \"./RenderContext.js\";\nimport { FrameController } from \"./FrameController.js\";\n\n// Re-export renderer types for external use\nexport type { RenderOptions, RenderResult, Renderer } from \"./renderers.js\";\nexport { getEffectiveRenderMode, isCanvas, isImage } from \"./renderers.js\";\nimport {\n type TemporalElement,\n isVisibleAtTime,\n DEFAULT_WIDTH,\n DEFAULT_HEIGHT,\n DEFAULT_THUMBNAIL_SCALE,\n DEFAULT_BLOCKING_TIMEOUT_MS,\n createPreviewContainer,\n} from \"./previewTypes.js\";\nimport { defaultProfiler } from \"./RenderProfiler.js\";\nimport { logger } from \"./logger.js\";\n\n// Import rendering modules\nimport {\n renderToImage,\n renderToImageDirect,\n prepareFrameDataUri,\n loadImageFromDataUri,\n} from \"./rendering/renderToImage.js\";\nimport { renderToImageNative, createDprCanvas } from \"./rendering/renderToImageNative.js\";\nimport { clearInlineImageCache, getInlineImageCacheSize } from \"./rendering/inlineImages.js\";\n\n// Re-export rendering types and functions for external use\nexport type {\n NativeRenderOptions,\n ForeignObjectRenderOptions,\n} from \"./rendering/types.js\";\nexport {\n renderToImage,\n renderToImageNative,\n renderToImageDirect,\n prepareFrameDataUri,\n loadImageFromDataUri,\n};\n\n// ============================================================================\n// Constants (module-specific, not shared)\n// ============================================================================\n\n/** Number of rows to sample when checking canvas content */\nconst CANVAS_SAMPLE_STRIP_HEIGHT = 4;\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Content readiness strategy for capture operations.\n * - \"immediate\": Capture NOW, skip all waits. May have blank video frames.\n * - \"blocking\": Wait for video content to be ready. Throws on timeout.\n */\nexport type ContentReadyMode = \"immediate\" | \"blocking\";\n\n/**\n * Options for capturing a timegroup frame.\n */\nexport interface CaptureOptions {\n /** Time to capture at in milliseconds (required) */\n timeMs: number;\n /** Scale factor (default: 0.25 for captureTimegroupAtTime) */\n scale?: number;\n /** Skip restoring original time after capture (for batch operations) */\n skipRestore?: boolean;\n /** Content readiness strategy (default: \"immediate\") */\n contentReadyMode?: ContentReadyMode;\n /** Max wait time for blocking mode before throwing (default: 5000ms) */\n blockingTimeoutMs?: number;\n}\n\n/**\n * Options for batch capture operations, excluding timeMs which is provided per-timestamp.\n */\nexport interface CaptureBatchOptions {\n /** Scale factor for thumbnails (default: 0.25) */\n scale?: number;\n /** Content readiness strategy (default: \"immediate\") */\n contentReadyMode?: ContentReadyMode;\n /** Max wait time for blocking mode before throwing (default: 5000ms) */\n blockingTimeoutMs?: number;\n}\n\n/**\n * Error thrown when video content is not ready within the blocking timeout.\n */\nexport class ContentNotReadyError extends Error {\n constructor(\n public readonly timeMs: number,\n public readonly timeoutMs: number,\n public readonly blankVideos: string[],\n ) {\n super(`Video content not ready at ${timeMs}ms after ${timeoutMs}ms timeout. Blank videos: ${blankVideos.join(', ')}`);\n this.name = 'ContentNotReadyError';\n }\n}\n\n// ============================================================================\n// Module State (reset via resetRenderState)\n// ============================================================================\n\n/**\n * Module-level render state including caches and reusable objects.\n */\ninterface RenderState {\n inlineImageCache: Map<string, string>;\n layoutInitializedCanvases: WeakSet<HTMLCanvasElement>;\n xmlSerializer: XMLSerializer | null;\n textEncoder: TextEncoder;\n metrics: {\n inlineImageCacheHits: number;\n inlineImageCacheMisses: number;\n inlineImageCacheEvictions: number;\n };\n}\n\n/**\n * Module-level state for render operations.\n * Note: xmlSerializer is lazy-initialized for Node.js compatibility\n */\nconst renderState: RenderState = {\n inlineImageCache: new Map(),\n layoutInitializedCanvases: new WeakSet(),\n xmlSerializer: null, // Lazy-initialized in browser context\n textEncoder: new TextEncoder(),\n metrics: {\n inlineImageCacheHits: 0,\n inlineImageCacheMisses: 0,\n inlineImageCacheEvictions: 0,\n },\n};\n\n/**\n * Get the current render state for testing and debugging.\n * @returns The module-level render state object\n */\nexport function getRenderState(): RenderState {\n return renderState;\n}\n\n/**\n * Get cache metrics for monitoring performance.\n * @returns Object with cache hit/miss/eviction counts\n */\nexport function getCacheMetrics(): RenderState['metrics'] {\n return { ...renderState.metrics };\n}\n\n/**\n * Reset cache metrics to zero.\n */\nexport function resetCacheMetrics(): void {\n renderState.metrics.inlineImageCacheHits = 0;\n renderState.metrics.inlineImageCacheMisses = 0;\n renderState.metrics.inlineImageCacheEvictions = 0;\n}\n\n/**\n * Reset all module state including profiling counters, caches, and logging flags.\n * Call at the start of export sessions to ensure clean state.\n */\nexport function resetRenderState(): void {\n defaultProfiler.reset();\n clearInlineImageCache();\n resetCacheMetrics();\n}\n\n// Re-export cache management functions\nexport { clearInlineImageCache, getInlineImageCacheSize };\n\n// ============================================================================\n// Internal Helpers\n// ============================================================================\n\n/**\n * Create a debug label for showing render info.\n */\nfunction createDebugLabel(): HTMLDivElement {\n const debugLabel = document.createElement(\"div\");\n debugLabel.style.cssText = `\n position: absolute;\n top: -24px;\n left: 0;\n padding: 2px 8px;\n font: bold 12px monospace;\n background: rgba(0, 0, 0, 0.8);\n border-radius: 3px;\n white-space: nowrap;\n z-index: 1000;\n pointer-events: none;\n `;\n return debugLabel;\n}\n\n/**\n * Update debug label with resolution info.\n */\nfunction updateDebugLabel(\n label: HTMLDivElement,\n renderWidth: number,\n renderHeight: number,\n resolutionScale: number,\n): void {\n const scaleColors: Record<number, string> = {\n 1: \"#00ff00\",\n 0.75: \"#ffff00\",\n 0.5: \"#ff8800\",\n 0.25: \"#ff0000\",\n };\n label.style.color = scaleColors[resolutionScale] || \"#ffffff\";\n label.textContent = `Render: ${renderWidth}x${renderHeight} (${Math.round(resolutionScale * 100)}%)`;\n}\n\n/**\n * Wait for next animation frame (allows browser to complete layout)\n */\nfunction waitForFrame(): Promise<void> {\n return new Promise(resolve => requestAnimationFrame(() => resolve()));\n}\n\n/**\n * Check if a canvas has any rendered content (not all transparent/uninitialized).\n * Returns true if there's ANY non-transparent pixel.\n */\nfunction canvasHasContent(canvas: HTMLCanvasElement): boolean {\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return false;\n \n try {\n const width = canvas.width;\n const height = canvas.height;\n if (width === 0 || height === 0) return false;\n \n // Sample a horizontal strip across the middle of the canvas\n // This catches most video content even if edges are black\n const stripY = Math.floor(height / 2);\n const imageData = ctx.getImageData(0, stripY, width, CANVAS_SAMPLE_STRIP_HEIGHT);\n const data = imageData.data;\n \n // Check if ANY pixel has non-zero alpha (is not transparent)\n // A truly blank/uninitialized canvas has all pixels at [0,0,0,0]\n // A black video frame would have pixels at [0,0,0,255] (opaque black)\n for (let i = 3; i < data.length; i += 4) {\n if (data[i] !== 0) {\n return true;\n }\n }\n \n return false;\n } catch {\n // Canvas might be tainted, assume it has content\n return true;\n }\n}\n\ninterface WaitForVideoContentResult {\n ready: boolean;\n blankVideos: string[];\n}\n\n/**\n * Wait for video canvases within a timegroup to have content.\n * Only checks videos that should be visible at the current time.\n * Returns result with ready status and list of blank video names.\n */\nasync function waitForVideoContent(\n timegroup: EFTimegroup,\n timeMs: number,\n maxWaitMs: number,\n): Promise<WaitForVideoContentResult> {\n const startTime = performance.now();\n \n // Find all video elements in the timegroup (including nested)\n const allVideos = timegroup.querySelectorAll(\"ef-video\");\n if (allVideos.length === 0) return { ready: true, blankVideos: [] };\n \n // Filter to only videos that should be visible at this time\n const visibleVideos = Array.from(allVideos).filter(video => {\n // Check if video itself is in time range\n if (!isVisibleAtTime(video, timeMs)) return false;\n \n // Check if all ancestor timegroups are in time range\n let parent = video.parentElement;\n while (parent && parent !== timegroup) {\n if (parent.tagName === 'EF-TIMEGROUP' && !isVisibleAtTime(parent, timeMs)) {\n return false;\n }\n parent = parent.parentElement;\n }\n return true;\n });\n \n if (visibleVideos.length === 0) return { ready: true, blankVideos: [] };\n \n const getBlankVideoNames = () => visibleVideos\n .filter(video => {\n const shadowCanvas = video.shadowRoot?.querySelector(\"canvas\");\n return shadowCanvas && !canvasHasContent(shadowCanvas);\n })\n .map(v => (v as TemporalElement).src || v.id || 'unnamed');\n \n while (performance.now() - startTime < maxWaitMs) {\n let allHaveContent = true;\n \n for (const video of visibleVideos) {\n const shadowCanvas = video.shadowRoot?.querySelector(\"canvas\");\n if (shadowCanvas && shadowCanvas.width > 0 && shadowCanvas.height > 0) {\n if (!canvasHasContent(shadowCanvas)) {\n allHaveContent = false;\n break;\n }\n }\n }\n \n if (allHaveContent) return { ready: true, blankVideos: [] };\n \n // Wait a bit and check again\n await waitForFrame();\n }\n \n return { ready: false, blankVideos: getBlankVideoNames() };\n}\n\n/**\n * Options for capturing from an existing render clone.\n */\nexport interface CaptureFromCloneOptions {\n /** Scale factor for the output canvas (default: 0.25) */\n scale?: number;\n /** Content readiness strategy (default: \"immediate\") */\n contentReadyMode?: ContentReadyMode;\n /** Max wait time for blocking mode before throwing (default: 5000ms) */\n blockingTimeoutMs?: number;\n /** Original timegroup (for dimension and background reference) */\n originalTimegroup?: EFTimegroup;\n}\n\n/**\n * Captures a frame from an already-seeked render clone.\n * Used internally by captureBatch for efficiency (reuses one clone across all captures).\n * \n * @param renderClone - A render clone that has already been seeked to the target time\n * @param renderContainer - The container holding the render clone (from createRenderClone)\n * @param options - Capture options\n * @returns Canvas with the rendered frame\n */\nexport async function captureFromClone(\n renderClone: EFTimegroup,\n renderContainer: HTMLElement,\n options: CaptureFromCloneOptions = {},\n): Promise<HTMLCanvasElement> {\n const {\n scale = DEFAULT_THUMBNAIL_SCALE,\n contentReadyMode = \"immediate\",\n blockingTimeoutMs = DEFAULT_BLOCKING_TIMEOUT_MS,\n originalTimegroup,\n } = options;\n\n // Use original timegroup dimensions if available, otherwise clone dimensions\n const sourceForDimensions = originalTimegroup ?? renderClone;\n const width = sourceForDimensions.offsetWidth || DEFAULT_WIDTH;\n const height = sourceForDimensions.offsetHeight || DEFAULT_HEIGHT;\n\n // Create canvas at scaled size\n const dpr = window.devicePixelRatio || 1;\n const canvas = document.createElement(\"canvas\");\n canvas.width = Math.floor(width * scale * dpr);\n canvas.height = Math.floor(height * scale * dpr);\n canvas.style.width = `${Math.floor(width * scale)}px`;\n canvas.style.height = `${Math.floor(height * scale)}px`;\n\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) {\n throw new Error(\"Failed to get canvas 2d context\");\n }\n\n // Handle content readiness based on mode\n const timeMs = renderClone.currentTimeMs;\n \n // Use FrameController to ensure all FrameRenderable elements are ready\n // This coordinates prepare → render phases for video, audio, captions, etc.\n const frameController = new FrameController(renderClone);\n await frameController.renderFrame(timeMs, { waitForLitUpdate: false });\n \n if (contentReadyMode === \"blocking\") {\n const result = await waitForVideoContent(renderClone, timeMs, blockingTimeoutMs);\n if (!result.ready) {\n throw new ContentNotReadyError(timeMs, blockingTimeoutMs, result.blankVideos);\n }\n }\n\n // Create RenderContext for caching during this capture operation\n const renderContext = new RenderContext();\n \n try {\n let image: HTMLCanvasElement | HTMLImageElement;\n const renderMode = getEffectiveRenderMode();\n \n if (renderMode === \"native\") {\n // NATIVE PATH: Render the seeked renderClone directly from live DOM\n // The clone is already at the correct time, so drawElementImage captures its current\n // visual state including video frames at the correct position.\n // \n // Position render container properly for capture\n renderContainer.style.cssText = `\n position: fixed;\n left: 0;\n top: 0;\n width: ${width}px;\n height: ${height}px;\n pointer-events: none;\n overflow: hidden;\n `;\n \n // OPTIMIZATION: Always skip DPR scaling for captures (thumbnails and video export).\n // Retina quality isn't needed for captured frames, and DPR=2 means 4x more pixels.\n // Live preview uses a different code path (renderTimegroupToCanvas) which handles DPR properly.\n image = await renderToImageNative(renderContainer, width, height, { skipDprScaling: true });\n } else {\n // FOREIGNOBJECT PATH: Build passive structure from the SEEKED render clone\n // The clone is already at the correct time, so getComputedStyle captures the right values.\n // Styles are synced during clone building in a single pass.\n const t0 = performance.now();\n const { container, syncState } = buildCloneStructure(renderClone, timeMs);\n const buildTime = performance.now() - t0;\n\n // Create wrapper using shared helper\n const bgSource = originalTimegroup ?? renderClone;\n const previewContainer = createPreviewContainer({\n width,\n height,\n background: getComputedStyle(bgSource).background || \"#000\",\n });\n \n const t1 = performance.now();\n const styleEl = document.createElement(\"style\");\n styleEl.textContent = collectDocumentStyles();\n const stylesTime = performance.now() - t1;\n previewContainer.appendChild(styleEl);\n previewContainer.appendChild(container);\n \n // Ensure clone root is visible\n overrideRootCloneStyles(syncState, true);\n\n // Render using foreignObject serialization\n // Pass scale, renderContext, and sourceMap for caching optimization\n const t2 = performance.now();\n image = await renderToImage(previewContainer, width, height, { \n canvasScale: scale,\n renderContext,\n sourceMap: syncState.canvasSourceMap,\n });\n const renderTime = performance.now() - t2;\n \n logger.debug(`[captureFromClone] build=${buildTime.toFixed(0)}ms, styles=${stylesTime.toFixed(0)}ms, render=${renderTime.toFixed(0)}ms (canvasScale=${scale})`);\n }\n\n // Draw to canvas (may need scaling for native path which is at DPR)\n const srcWidth = image.width;\n const srcHeight = image.height;\n ctx.drawImage(\n image,\n 0, 0, srcWidth, srcHeight,\n 0, 0, canvas.width, canvas.height\n );\n\n return canvas;\n } finally {\n // Ensure RenderContext is disposed even if an error occurs\n renderContext.dispose();\n }\n}\n\n/**\n * Captures a single frame from a timegroup at a specific time.\n * \n * CLONE-TIMELINE ARCHITECTURE:\n * Creates an independent render clone, seeks it to the target time, and captures.\n * Prime-timeline is NEVER seeked - user can continue previewing/editing during capture.\n * \n * @param timegroup - The source timegroup\n * @param options - Capture options including timeMs, scale, contentReadyMode\n * @returns Canvas with the rendered frame\n * @throws ContentNotReadyError if blocking mode times out waiting for video content\n */\nexport async function captureTimegroupAtTime(\n timegroup: EFTimegroup,\n options: CaptureOptions,\n): Promise<HTMLCanvasElement> {\n const {\n timeMs,\n scale = DEFAULT_THUMBNAIL_SCALE,\n // skipRestore is deprecated with Clone-timeline (Prime is never seeked)\n contentReadyMode = \"immediate\",\n blockingTimeoutMs = DEFAULT_BLOCKING_TIMEOUT_MS,\n } = options;\n\n // CLONE-TIMELINE: Create a short-lived render clone for this capture\n // Prime-timeline is NEVER seeked - clone is fully independent\n const { clone: renderClone, container: renderContainer, cleanup: cleanupRenderClone } = \n await timegroup.createRenderClone();\n \n try {\n // Seek the clone to target time (Prime stays at user position)\n // Use seekForRender which bypasses duration clamping - render clones may have\n // zero duration initially until media durations are computed, but we still\n // want to seek to the requested time for capture purposes.\n await renderClone.seekForRender(timeMs);\n \n // Use the shared capture helper\n return await captureFromClone(renderClone, renderContainer, {\n scale,\n contentReadyMode,\n blockingTimeoutMs,\n originalTimegroup: timegroup,\n });\n } finally {\n // Clean up the render clone\n cleanupRenderClone();\n }\n}\n\n/** Epsilon for comparing time values (ms) - times within this are considered equal */\nconst TIME_EPSILON_MS = 1;\n\n/** Default scale for preview rendering */\nconst DEFAULT_PREVIEW_SCALE = 1;\n\n/** Default resolution scale (full resolution) */\nconst DEFAULT_RESOLUTION_SCALE = 1;\n\n/**\n * Convert relative time to absolute time for a timegroup.\n * Nested timegroup children have ABSOLUTE startTimeMs values,\n * so relative capture times must be converted for temporal culling.\n */\nfunction toAbsoluteTime(timegroup: EFTimegroup, relativeTimeMs: number): number {\n return relativeTimeMs + (timegroup.startTimeMs ?? 0);\n}\n\nexport interface CanvasPreviewResult {\n /**\n * Wrapper container holding the canvas and debug label.\n * Append this to your DOM - the canvas inside will receive transforms.\n */\n container: HTMLDivElement;\n canvas: HTMLCanvasElement;\n /**\n * Call this to re-render the timegroup to canvas at current visual state.\n * Returns a promise that resolves when rendering is complete.\n */\n refresh: () => Promise<void>;\n syncState: SyncState;\n /**\n * Dynamically change the resolution scale without rebuilding the clone structure.\n * This is nearly instant - just updates CSS and internal variables.\n * The next refresh() call will render at the new resolution.\n */\n setResolutionScale: (scale: number) => void;\n /**\n * Get the current resolution scale.\n */\n getResolutionScale: () => number;\n /**\n * Dispose the preview and release resources.\n * Call this when the preview is no longer needed.\n */\n dispose: () => void;\n}\n\n/**\n * Options for canvas preview rendering.\n */\nexport interface CanvasPreviewOptions {\n /**\n * Output scale factor (default: 1).\n * Scales the final canvas size.\n */\n scale?: number;\n \n /**\n * Resolution scale for internal rendering (default: 1).\n * Reduces the internal render resolution for better performance.\n * The canvas CSS size remains the same (browser upscales).\n * - 1: Full resolution\n * - 0.75: 3/4 resolution\n * - 0.5: Half resolution\n * - 0.25: Quarter resolution\n */\n resolutionScale?: number;\n}\n\n/**\n * Renders a timegroup preview to a canvas using SVG foreignObject.\n * \n * Optimized with:\n * - Persistent clone structure (built once)\n * - Temporal bucketing for time-based culling\n * - Property split (static vs animated)\n * - Parent index for O(1) visibility checks\n * - Resolution scaling for performance (renders at lower resolution, CSS upscales)\n *\n * @param timegroup - The source timegroup to preview\n * @param scaleOrOptions - Scale factor (default 1) or options object\n * @returns Object with canvas and refresh function\n */\nexport function renderTimegroupToCanvas(\n timegroup: EFTimegroup,\n scaleOrOptions: number | CanvasPreviewOptions = DEFAULT_PREVIEW_SCALE,\n): CanvasPreviewResult {\n // Normalize options\n const options: CanvasPreviewOptions = typeof scaleOrOptions === \"number\"\n ? { scale: scaleOrOptions }\n : scaleOrOptions;\n \n const scale = options.scale ?? DEFAULT_PREVIEW_SCALE;\n // These are mutable to support dynamic resolution changes\n let currentResolutionScale = options.resolutionScale ?? DEFAULT_RESOLUTION_SCALE;\n \n const width = timegroup.offsetWidth || DEFAULT_WIDTH;\n const height = timegroup.offsetHeight || DEFAULT_HEIGHT;\n const dpr = window.devicePixelRatio || 1;\n \n // Calculate effective render dimensions (internal resolution) - mutable\n let renderWidth = Math.floor(width * currentResolutionScale);\n let renderHeight = Math.floor(height * currentResolutionScale);\n\n // Create canvas with proper DPR handling\n const canvas = createDprCanvas({\n renderWidth,\n renderHeight,\n scale,\n fullWidth: width,\n fullHeight: height,\n dpr,\n });\n \n // Create wrapper container with debug label\n const wrapperContainer = document.createElement(\"div\");\n wrapperContainer.style.cssText = \"position: relative; display: inline-block;\";\n const debugLabel = createDebugLabel();\n wrapperContainer.appendChild(debugLabel);\n wrapperContainer.appendChild(canvas);\n\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) {\n throw new Error(\"Failed to get canvas 2d context\");\n }\n\n // Build clone structure ONCE with optimized sync state\n // Initial sync happens during clone building in a single pass\n const initialTimeMs = toAbsoluteTime(timegroup, timegroup.currentTimeMs ?? 0);\n const { container, syncState } = buildCloneStructure(timegroup, initialTimeMs);\n\n // Create a wrapper div with scaled dimensions\n // When resolutionScale < 1, we render at a smaller size and CSS transform scales the content\n const previewContainer = createPreviewContainer({\n width: renderWidth,\n height: renderHeight,\n background: getComputedStyle(timegroup).background || \"#000\",\n });\n \n // Apply CSS transform to scale down the content within the container\n // This makes the clone render at reduced complexity\n if (currentResolutionScale < 1) {\n container.style.transform = `scale(${currentResolutionScale})`;\n container.style.transformOrigin = \"top left\";\n }\n \n // Inject document styles so CSS rules work in SVG foreignObject\n const styleEl = document.createElement(\"style\");\n styleEl.textContent = collectDocumentStyles();\n previewContainer.appendChild(styleEl);\n \n previewContainer.appendChild(container);\n overrideRootCloneStyles(syncState);\n\n // Track render state\n let rendering = false;\n let lastTimeMs = -1;\n let disposed = false;\n\n // Create RenderContext for caching across refresh calls\n const renderContext = new RenderContext();\n \n // Create FrameController for coordinating element rendering\n // Cached for the lifetime of this preview instance\n const frameController = new FrameController(timegroup);\n\n // Log resolution scale on first render for debugging\n let hasLoggedScale = false;\n \n // Pending resolution change - applied at start of next refresh to avoid blanking\n let pendingResolutionScale: number | null = null;\n \n /**\n * Apply pending resolution scale changes.\n * Called at the start of refresh() before rendering, so the old content\n * stays visible until new content is ready to be drawn.\n */\n const applyPendingResolutionChange = (): void => {\n if (pendingResolutionScale === null) return;\n \n const newScale = pendingResolutionScale;\n pendingResolutionScale = null;\n \n currentResolutionScale = newScale;\n renderWidth = Math.floor(width * currentResolutionScale);\n renderHeight = Math.floor(height * currentResolutionScale);\n \n // Update previewContainer dimensions (affects what renderToImage produces)\n previewContainer.style.width = `${renderWidth}px`;\n previewContainer.style.height = `${renderHeight}px`;\n \n // Update clone transform\n if (currentResolutionScale < 1) {\n container.style.transform = `scale(${currentResolutionScale})`;\n container.style.transformOrigin = \"top left\";\n } else {\n container.style.transform = \"\";\n }\n \n // Canvas dimensions will be updated right before drawing (in refresh)\n // to avoid clearing the canvas until new content is ready\n };\n \n /**\n * Dynamically change resolution scale without rebuilding clone structure.\n * The actual change is deferred until next refresh() to avoid blanking -\n * old content stays visible until new content is ready.\n */\n const setResolutionScale = (newScale: number): void => {\n // Clamp to valid range\n newScale = Math.max(0.1, Math.min(1, newScale));\n \n if (newScale === currentResolutionScale && pendingResolutionScale === null) return;\n \n // Queue the change - will be applied at start of next refresh\n pendingResolutionScale = newScale;\n \n // Force re-render on next refresh by invalidating lastTimeMs\n lastTimeMs = -1;\n };\n \n const getResolutionScale = (): number => pendingResolutionScale ?? currentResolutionScale;\n \n const refresh = async (): Promise<void> => {\n if (rendering || disposed) return;\n // Clone-timeline: captures use separate clones, Prime-timeline is never locked\n \n const sourceTimeMs = timegroup.currentTimeMs ?? 0;\n const userTimeMs = timegroup.userTimeMs ?? 0;\n if (Math.abs(sourceTimeMs - userTimeMs) > TIME_EPSILON_MS) return;\n \n if (userTimeMs === lastTimeMs) return;\n lastTimeMs = userTimeMs;\n \n rendering = true;\n \n // Apply any pending resolution changes before rendering\n // This updates previewContainer and clone transform, but NOT canvas dimensions yet\n applyPendingResolutionChange();\n \n // Log scale info once per initialization\n if (!hasLoggedScale) {\n hasLoggedScale = true;\n const mode = getEffectiveRenderMode();\n logger.debug(`[renderTimegroupToCanvas] Resolution scale: ${currentResolutionScale} (${width}x${height} → ${renderWidth}x${renderHeight}), canvas buffer: ${canvas.width}x${canvas.height}, CSS size: ${canvas.style.width}x${canvas.style.height}, renderMode: ${mode}`);\n }\n\n try {\n // Use FrameController to ensure all FrameRenderable elements are ready\n // This coordinates prepare → render phases before we capture their state\n await frameController.renderFrame(userTimeMs);\n \n syncStyles(syncState, toAbsoluteTime(timegroup, userTimeMs));\n overrideRootCloneStyles(syncState);\n\n // Remove hidden nodes from DOM for serialization - they won't be serialized\n // or have their canvases encoded. This is a significant optimization.\n const removedNodes = removeHiddenNodesForSerialization(syncState);\n\n // Render at scaled dimensions with canvas scaling for internal video frames\n // Pass renderContext and sourceMap for caching optimization\n const t0 = performance.now();\n const image = await renderToImage(previewContainer, renderWidth, renderHeight, {\n canvasScale: currentResolutionScale,\n renderContext,\n sourceMap: syncState.canvasSourceMap,\n });\n const renderTime = performance.now() - t0;\n \n // Restore hidden nodes for next frame's delta tracking\n restoreHiddenNodes(removedNodes);\n\n // Update canvas buffer dimensions NOW, right before drawing\n // This clears the canvas, but we immediately draw new content\n const targetWidth = Math.floor(renderWidth * scale * dpr);\n const targetHeight = Math.floor(renderHeight * scale * dpr);\n if (canvas.width !== targetWidth || canvas.height !== targetHeight) {\n canvas.width = targetWidth;\n canvas.height = targetHeight;\n } else {\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n }\n \n ctx.save();\n ctx.scale(dpr * scale, dpr * scale);\n ctx.drawImage(image, 0, 0);\n ctx.restore();\n \n // Log render time periodically (every 60 frames)\n defaultProfiler.incrementRenderCount();\n if (defaultProfiler.shouldLogByFrameCount(60)) {\n logger.debug(`[renderTimegroupToCanvas] Frame render: ${renderTime.toFixed(1)}ms (resolutionScale=${currentResolutionScale}, image=${image.width}x${image.height})`);\n }\n \n // Update debug label\n updateDebugLabel(debugLabel, renderWidth, renderHeight, currentResolutionScale);\n } catch (e) {\n logger.error(\"Canvas preview render failed:\", e);\n } finally {\n rendering = false;\n }\n };\n\n /**\n * Dispose the preview and release resources.\n */\n const dispose = (): void => {\n if (disposed) return;\n disposed = true;\n frameController.abort();\n renderContext.dispose();\n };\n\n // Do initial render\n refresh();\n\n return { container: wrapperContainer, canvas, refresh, syncState, setResolutionScale, getResolutionScale, dispose };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"renderTimegroupToCanvas.js","names":["timeMs: number","timeoutMs: number","blankVideos: string[]","renderState: RenderState","scaleColors: Record<number, string>","image: HTMLCanvasElement | HTMLImageElement","container","options: CanvasPreviewOptions","pendingResolutionScale: number | null"],"sources":["../../src/preview/renderTimegroupToCanvas.ts"],"sourcesContent":["import type { EFTimegroup } from \"../elements/EFTimegroup.js\";\nimport {\n buildCloneStructure,\n syncStyles,\n collectDocumentStyles,\n overrideRootCloneStyles,\n removeHiddenNodesForSerialization,\n restoreHiddenNodes,\n type SyncState,\n} from \"./renderTimegroupPreview.js\";\nimport { getEffectiveRenderMode } from \"./renderers.js\";\nimport { RenderContext } from \"./RenderContext.js\";\nimport { FrameController } from \"./FrameController.js\";\n\n// Re-export renderer types for external use\nexport type { RenderOptions, RenderResult, Renderer } from \"./renderers.js\";\nexport { getEffectiveRenderMode, isCanvas, isImage } from \"./renderers.js\";\nimport {\n type TemporalElement,\n isVisibleAtTime,\n DEFAULT_WIDTH,\n DEFAULT_HEIGHT,\n DEFAULT_THUMBNAIL_SCALE,\n DEFAULT_BLOCKING_TIMEOUT_MS,\n createPreviewContainer,\n} from \"./previewTypes.js\";\nimport { defaultProfiler } from \"./RenderProfiler.js\";\nimport { logger } from \"./logger.js\";\n\n// Import rendering modules\nimport {\n renderToImage,\n renderToImageDirect,\n prepareFrameDataUri,\n loadImageFromDataUri,\n} from \"./rendering/renderToImage.js\";\nimport { renderToImageNative, createDprCanvas } from \"./rendering/renderToImageNative.js\";\nimport { clearInlineImageCache, getInlineImageCacheSize } from \"./rendering/inlineImages.js\";\n\n// Re-export rendering types and functions for external use\nexport type {\n NativeRenderOptions,\n ForeignObjectRenderOptions,\n} from \"./rendering/types.js\";\nexport {\n renderToImage,\n renderToImageNative,\n renderToImageDirect,\n prepareFrameDataUri,\n loadImageFromDataUri,\n};\n\n// ============================================================================\n// Constants (module-specific, not shared)\n// ============================================================================\n\n/** Number of rows to sample when checking canvas content */\nconst CANVAS_SAMPLE_STRIP_HEIGHT = 4;\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Content readiness strategy for capture operations.\n * - \"immediate\": Capture NOW, skip all waits. May have blank video frames.\n * - \"blocking\": Wait for video content to be ready. Throws on timeout.\n */\nexport type ContentReadyMode = \"immediate\" | \"blocking\";\n\n/**\n * Options for capturing a timegroup frame.\n */\nexport interface CaptureOptions {\n /** Time to capture at in milliseconds (required) */\n timeMs: number;\n /** Scale factor (default: 0.25 for captureTimegroupAtTime) */\n scale?: number;\n /** Skip restoring original time after capture (for batch operations) */\n skipRestore?: boolean;\n /** Content readiness strategy (default: \"immediate\") */\n contentReadyMode?: ContentReadyMode;\n /** Max wait time for blocking mode before throwing (default: 5000ms) */\n blockingTimeoutMs?: number;\n}\n\n/**\n * Options for batch capture operations, excluding timeMs which is provided per-timestamp.\n */\nexport interface CaptureBatchOptions {\n /** Scale factor for thumbnails (default: 0.25) */\n scale?: number;\n /** Content readiness strategy (default: \"immediate\") */\n contentReadyMode?: ContentReadyMode;\n /** Max wait time for blocking mode before throwing (default: 5000ms) */\n blockingTimeoutMs?: number;\n}\n\n/**\n * Error thrown when video content is not ready within the blocking timeout.\n */\nexport class ContentNotReadyError extends Error {\n constructor(\n public readonly timeMs: number,\n public readonly timeoutMs: number,\n public readonly blankVideos: string[],\n ) {\n super(`Video content not ready at ${timeMs}ms after ${timeoutMs}ms timeout. Blank videos: ${blankVideos.join(', ')}`);\n this.name = 'ContentNotReadyError';\n }\n}\n\n// ============================================================================\n// Module State (reset via resetRenderState)\n// ============================================================================\n\n/**\n * Module-level render state including caches and reusable objects.\n */\ninterface RenderState {\n inlineImageCache: Map<string, string>;\n layoutInitializedCanvases: WeakSet<HTMLCanvasElement>;\n xmlSerializer: XMLSerializer | null;\n textEncoder: TextEncoder;\n metrics: {\n inlineImageCacheHits: number;\n inlineImageCacheMisses: number;\n inlineImageCacheEvictions: number;\n };\n}\n\n/**\n * Module-level state for render operations.\n * Note: xmlSerializer is lazy-initialized for Node.js compatibility\n */\nconst renderState: RenderState = {\n inlineImageCache: new Map(),\n layoutInitializedCanvases: new WeakSet(),\n xmlSerializer: null, // Lazy-initialized in browser context\n textEncoder: new TextEncoder(),\n metrics: {\n inlineImageCacheHits: 0,\n inlineImageCacheMisses: 0,\n inlineImageCacheEvictions: 0,\n },\n};\n\n/**\n * Get the current render state for testing and debugging.\n * @returns The module-level render state object\n */\nexport function getRenderState(): RenderState {\n return renderState;\n}\n\n/**\n * Get cache metrics for monitoring performance.\n * @returns Object with cache hit/miss/eviction counts\n */\nexport function getCacheMetrics(): RenderState['metrics'] {\n return { ...renderState.metrics };\n}\n\n/**\n * Reset cache metrics to zero.\n */\nexport function resetCacheMetrics(): void {\n renderState.metrics.inlineImageCacheHits = 0;\n renderState.metrics.inlineImageCacheMisses = 0;\n renderState.metrics.inlineImageCacheEvictions = 0;\n}\n\n/**\n * Reset all module state including profiling counters, caches, and logging flags.\n * Call at the start of export sessions to ensure clean state.\n */\nexport function resetRenderState(): void {\n defaultProfiler.reset();\n clearInlineImageCache();\n resetCacheMetrics();\n}\n\n// Re-export cache management functions\nexport { clearInlineImageCache, getInlineImageCacheSize };\n\n// ============================================================================\n// Internal Helpers\n// ============================================================================\n\n/**\n * Create a debug label for showing render info.\n */\nfunction createDebugLabel(): HTMLDivElement {\n const debugLabel = document.createElement(\"div\");\n debugLabel.style.cssText = `\n position: absolute;\n top: -24px;\n left: 0;\n padding: 2px 8px;\n font: bold 12px monospace;\n background: rgba(0, 0, 0, 0.8);\n border-radius: 3px;\n white-space: nowrap;\n z-index: 1000;\n pointer-events: none;\n `;\n return debugLabel;\n}\n\n/**\n * Update debug label with resolution info.\n */\nfunction updateDebugLabel(\n label: HTMLDivElement,\n renderWidth: number,\n renderHeight: number,\n resolutionScale: number,\n): void {\n const scaleColors: Record<number, string> = {\n 1: \"#00ff00\",\n 0.75: \"#ffff00\",\n 0.5: \"#ff8800\",\n 0.25: \"#ff0000\",\n };\n label.style.color = scaleColors[resolutionScale] || \"#ffffff\";\n label.textContent = `Render: ${renderWidth}x${renderHeight} (${Math.round(resolutionScale * 100)}%)`;\n}\n\n/**\n * Wait for next animation frame (allows browser to complete layout)\n */\nfunction waitForFrame(): Promise<void> {\n return new Promise(resolve => requestAnimationFrame(() => resolve()));\n}\n\n/**\n * Check if a canvas has any rendered content (not all transparent/uninitialized).\n * Returns true if there's ANY non-transparent pixel.\n */\nfunction canvasHasContent(canvas: HTMLCanvasElement): boolean {\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return false;\n \n try {\n const width = canvas.width;\n const height = canvas.height;\n if (width === 0 || height === 0) return false;\n \n // Sample a horizontal strip across the middle of the canvas\n // This catches most video content even if edges are black\n const stripY = Math.floor(height / 2);\n const imageData = ctx.getImageData(0, stripY, width, CANVAS_SAMPLE_STRIP_HEIGHT);\n const data = imageData.data;\n \n // Check if ANY pixel has non-zero alpha (is not transparent)\n // A truly blank/uninitialized canvas has all pixels at [0,0,0,0]\n // A black video frame would have pixels at [0,0,0,255] (opaque black)\n for (let i = 3; i < data.length; i += 4) {\n if (data[i] !== 0) {\n return true;\n }\n }\n \n return false;\n } catch {\n // Canvas might be tainted, assume it has content\n return true;\n }\n}\n\ninterface WaitForVideoContentResult {\n ready: boolean;\n blankVideos: string[];\n}\n\n/**\n * Wait for video canvases within a timegroup to have content.\n * Only checks videos that should be visible at the current time.\n * Returns result with ready status and list of blank video names.\n */\nasync function waitForVideoContent(\n timegroup: EFTimegroup,\n timeMs: number,\n maxWaitMs: number,\n): Promise<WaitForVideoContentResult> {\n const startTime = performance.now();\n \n // Find all video elements in the timegroup (including nested)\n const allVideos = timegroup.querySelectorAll(\"ef-video\");\n if (allVideos.length === 0) return { ready: true, blankVideos: [] };\n \n // Filter to only videos that should be visible at this time\n const visibleVideos = Array.from(allVideos).filter(video => {\n // Check if video itself is in time range\n if (!isVisibleAtTime(video, timeMs)) return false;\n \n // Check if all ancestor timegroups are in time range\n let parent = video.parentElement;\n while (parent && parent !== timegroup) {\n if (parent.tagName === 'EF-TIMEGROUP' && !isVisibleAtTime(parent, timeMs)) {\n return false;\n }\n parent = parent.parentElement;\n }\n return true;\n });\n \n if (visibleVideos.length === 0) return { ready: true, blankVideos: [] };\n \n const getBlankVideoNames = () => visibleVideos\n .filter(video => {\n const shadowCanvas = video.shadowRoot?.querySelector(\"canvas\");\n return shadowCanvas && !canvasHasContent(shadowCanvas);\n })\n .map(v => (v as TemporalElement).src || v.id || 'unnamed');\n \n while (performance.now() - startTime < maxWaitMs) {\n let allHaveContent = true;\n \n for (const video of visibleVideos) {\n const shadowCanvas = video.shadowRoot?.querySelector(\"canvas\");\n if (shadowCanvas && shadowCanvas.width > 0 && shadowCanvas.height > 0) {\n if (!canvasHasContent(shadowCanvas)) {\n allHaveContent = false;\n break;\n }\n }\n }\n \n if (allHaveContent) return { ready: true, blankVideos: [] };\n \n // Wait a bit and check again\n await waitForFrame();\n }\n \n return { ready: false, blankVideos: getBlankVideoNames() };\n}\n\n/**\n * Options for capturing from an existing render clone.\n */\nexport interface CaptureFromCloneOptions {\n /** Scale factor for the output canvas (default: 0.25) */\n scale?: number;\n /** Content readiness strategy (default: \"immediate\") */\n contentReadyMode?: ContentReadyMode;\n /** Max wait time for blocking mode before throwing (default: 5000ms) */\n blockingTimeoutMs?: number;\n /** Original timegroup (for dimension and background reference) */\n originalTimegroup?: EFTimegroup;\n}\n\n/**\n * Captures a frame from an already-seeked render clone.\n * Used internally by captureBatch for efficiency (reuses one clone across all captures).\n * \n * @param renderClone - A render clone that has already been seeked to the target time\n * @param renderContainer - The container holding the render clone (from createRenderClone)\n * @param options - Capture options\n * @returns Canvas with the rendered frame\n */\nexport async function captureFromClone(\n renderClone: EFTimegroup,\n renderContainer: HTMLElement,\n options: CaptureFromCloneOptions = {},\n): Promise<HTMLCanvasElement> {\n const {\n scale = DEFAULT_THUMBNAIL_SCALE,\n contentReadyMode = \"immediate\",\n blockingTimeoutMs = DEFAULT_BLOCKING_TIMEOUT_MS,\n originalTimegroup,\n } = options;\n\n // Use original timegroup dimensions if available, otherwise clone dimensions\n const sourceForDimensions = originalTimegroup ?? renderClone;\n const width = sourceForDimensions.offsetWidth || DEFAULT_WIDTH;\n const height = sourceForDimensions.offsetHeight || DEFAULT_HEIGHT;\n\n // Create canvas at scaled size\n const dpr = window.devicePixelRatio || 1;\n const canvas = document.createElement(\"canvas\");\n canvas.width = Math.floor(width * scale * dpr);\n canvas.height = Math.floor(height * scale * dpr);\n canvas.style.width = `${Math.floor(width * scale)}px`;\n canvas.style.height = `${Math.floor(height * scale)}px`;\n\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) {\n throw new Error(\"Failed to get canvas 2d context\");\n }\n\n // Handle content readiness based on mode\n const timeMs = renderClone.currentTimeMs;\n \n // NOTE: seekForRender() has already:\n // 1. Called frameController.renderFrame() to coordinate FrameRenderable elements\n // 2. Awaited #executeCustomFrameTasks() so frame tasks are complete\n // No need to call frameController.renderFrame() again - it would fire tasks redundantly\n \n if (contentReadyMode === \"blocking\") {\n const result = await waitForVideoContent(renderClone, timeMs, blockingTimeoutMs);\n if (!result.ready) {\n throw new ContentNotReadyError(timeMs, blockingTimeoutMs, result.blankVideos);\n }\n }\n\n // Create RenderContext for caching during this capture operation\n const renderContext = new RenderContext();\n \n try {\n let image: HTMLCanvasElement | HTMLImageElement;\n const renderMode = getEffectiveRenderMode();\n \n if (renderMode === \"native\") {\n // NATIVE PATH: Render the seeked renderClone directly from live DOM\n // The clone is already at the correct time, so drawElementImage captures its current\n // visual state including video frames at the correct position.\n // \n // Position render container properly for capture\n renderContainer.style.cssText = `\n position: fixed;\n left: 0;\n top: 0;\n width: ${width}px;\n height: ${height}px;\n pointer-events: none;\n overflow: hidden;\n `;\n \n // OPTIMIZATION: Always skip DPR scaling for captures (thumbnails and video export).\n // Retina quality isn't needed for captured frames, and DPR=2 means 4x more pixels.\n // Live preview uses a different code path (renderTimegroupToCanvas) which handles DPR properly.\n image = await renderToImageNative(renderContainer, width, height, { skipDprScaling: true });\n } else {\n // FOREIGNOBJECT PATH: Build passive structure from the SEEKED render clone\n // The clone is already at the correct time, so getComputedStyle captures the right values.\n // Styles are synced during clone building in a single pass.\n const t0 = performance.now();\n const { container, syncState } = buildCloneStructure(renderClone, timeMs);\n const buildTime = performance.now() - t0;\n\n // Create wrapper using shared helper\n const bgSource = originalTimegroup ?? renderClone;\n const previewContainer = createPreviewContainer({\n width,\n height,\n background: getComputedStyle(bgSource).background || \"#000\",\n });\n \n const t1 = performance.now();\n const styleEl = document.createElement(\"style\");\n styleEl.textContent = collectDocumentStyles();\n const stylesTime = performance.now() - t1;\n previewContainer.appendChild(styleEl);\n previewContainer.appendChild(container);\n \n // Ensure clone root is visible\n overrideRootCloneStyles(syncState, true);\n\n // Render using foreignObject serialization\n // Pass scale, renderContext, and sourceMap for caching optimization\n const t2 = performance.now();\n image = await renderToImage(previewContainer, width, height, { \n canvasScale: scale,\n renderContext,\n sourceMap: syncState.canvasSourceMap,\n });\n const renderTime = performance.now() - t2;\n \n logger.debug(`[captureFromClone] build=${buildTime.toFixed(0)}ms, styles=${stylesTime.toFixed(0)}ms, render=${renderTime.toFixed(0)}ms (canvasScale=${scale})`);\n }\n\n // Draw to canvas (may need scaling for native path which is at DPR)\n const srcWidth = image.width;\n const srcHeight = image.height;\n ctx.drawImage(\n image,\n 0, 0, srcWidth, srcHeight,\n 0, 0, canvas.width, canvas.height\n );\n\n return canvas;\n } finally {\n // Ensure RenderContext is disposed even if an error occurs\n renderContext.dispose();\n }\n}\n\n/**\n * Captures a single frame from a timegroup at a specific time.\n * \n * CLONE-TIMELINE ARCHITECTURE:\n * Creates an independent render clone, seeks it to the target time, and captures.\n * Prime-timeline is NEVER seeked - user can continue previewing/editing during capture.\n * \n * @param timegroup - The source timegroup\n * @param options - Capture options including timeMs, scale, contentReadyMode\n * @returns Canvas with the rendered frame\n * @throws ContentNotReadyError if blocking mode times out waiting for video content\n */\nexport async function captureTimegroupAtTime(\n timegroup: EFTimegroup,\n options: CaptureOptions,\n): Promise<HTMLCanvasElement> {\n const {\n timeMs,\n scale = DEFAULT_THUMBNAIL_SCALE,\n // skipRestore is deprecated with Clone-timeline (Prime is never seeked)\n contentReadyMode = \"immediate\",\n blockingTimeoutMs = DEFAULT_BLOCKING_TIMEOUT_MS,\n } = options;\n\n // CLONE-TIMELINE: Create a short-lived render clone for this capture\n // Prime-timeline is NEVER seeked - clone is fully independent\n const { clone: renderClone, container: renderContainer, cleanup: cleanupRenderClone } = \n await timegroup.createRenderClone();\n \n try {\n // Seek the clone to target time (Prime stays at user position)\n // Use seekForRender which bypasses duration clamping - render clones may have\n // zero duration initially until media durations are computed, but we still\n // want to seek to the requested time for capture purposes.\n await renderClone.seekForRender(timeMs);\n \n // Use the shared capture helper\n return await captureFromClone(renderClone, renderContainer, {\n scale,\n contentReadyMode,\n blockingTimeoutMs,\n originalTimegroup: timegroup,\n });\n } finally {\n // Clean up the render clone\n cleanupRenderClone();\n }\n}\n\n/** Epsilon for comparing time values (ms) - times within this are considered equal */\nconst TIME_EPSILON_MS = 1;\n\n/** Default scale for preview rendering */\nconst DEFAULT_PREVIEW_SCALE = 1;\n\n/** Default resolution scale (full resolution) */\nconst DEFAULT_RESOLUTION_SCALE = 1;\n\n/**\n * Convert relative time to absolute time for a timegroup.\n * Nested timegroup children have ABSOLUTE startTimeMs values,\n * so relative capture times must be converted for temporal culling.\n */\nfunction toAbsoluteTime(timegroup: EFTimegroup, relativeTimeMs: number): number {\n return relativeTimeMs + (timegroup.startTimeMs ?? 0);\n}\n\nexport interface CanvasPreviewResult {\n /**\n * Wrapper container holding the canvas and debug label.\n * Append this to your DOM - the canvas inside will receive transforms.\n */\n container: HTMLDivElement;\n canvas: HTMLCanvasElement;\n /**\n * Call this to re-render the timegroup to canvas at current visual state.\n * Returns a promise that resolves when rendering is complete.\n */\n refresh: () => Promise<void>;\n /**\n * Dynamically change the resolution scale without rebuilding the clone structure.\n * This is nearly instant - just updates CSS and internal variables.\n * The next refresh() call will render at the new resolution.\n */\n setResolutionScale: (scale: number) => void;\n /**\n * Get the current resolution scale.\n */\n getResolutionScale: () => number;\n /**\n * Dispose the preview and release resources.\n * Call this when the preview is no longer needed.\n */\n dispose: () => void;\n}\n\n/**\n * Options for canvas preview rendering.\n */\nexport interface CanvasPreviewOptions {\n /**\n * Output scale factor (default: 1).\n * Scales the final canvas size.\n */\n scale?: number;\n \n /**\n * Resolution scale for internal rendering (default: 1).\n * Reduces the internal render resolution for better performance.\n * The canvas CSS size remains the same (browser upscales).\n * - 1: Full resolution\n * - 0.75: 3/4 resolution\n * - 0.5: Half resolution\n * - 0.25: Quarter resolution\n */\n resolutionScale?: number;\n}\n\n/**\n * Renders a timegroup preview to a canvas using SVG foreignObject.\n * \n * Captures the prime timeline's current visual state including DOM changes\n * from frame tasks (SVG paths, canvas content, text updates, etc.).\n * \n * Optimized with:\n * - Passive clone structure rebuilt each frame from prime's current state\n * - Temporal bucketing for time-based culling\n * - RenderContext for canvas pixel caching across frames\n * - Resolution scaling for performance (renders at lower resolution, CSS upscales)\n *\n * @param timegroup - The source timegroup to preview (prime timeline)\n * @param scaleOrOptions - Scale factor (default 1) or options object\n * @returns Object with canvas and refresh function\n */\nexport function renderTimegroupToCanvas(\n timegroup: EFTimegroup,\n scaleOrOptions: number | CanvasPreviewOptions = DEFAULT_PREVIEW_SCALE,\n): CanvasPreviewResult {\n // Normalize options\n const options: CanvasPreviewOptions = typeof scaleOrOptions === \"number\"\n ? { scale: scaleOrOptions }\n : scaleOrOptions;\n \n const scale = options.scale ?? DEFAULT_PREVIEW_SCALE;\n // These are mutable to support dynamic resolution changes\n let currentResolutionScale = options.resolutionScale ?? DEFAULT_RESOLUTION_SCALE;\n \n const width = timegroup.offsetWidth || DEFAULT_WIDTH;\n const height = timegroup.offsetHeight || DEFAULT_HEIGHT;\n const dpr = window.devicePixelRatio || 1;\n \n // Calculate effective render dimensions (internal resolution) - mutable\n let renderWidth = Math.floor(width * currentResolutionScale);\n let renderHeight = Math.floor(height * currentResolutionScale);\n\n // Create canvas with proper DPR handling\n const canvas = createDprCanvas({\n renderWidth,\n renderHeight,\n scale,\n fullWidth: width,\n fullHeight: height,\n dpr,\n });\n \n // Create wrapper container with debug label\n const wrapperContainer = document.createElement(\"div\");\n wrapperContainer.style.cssText = \"position: relative; display: inline-block;\";\n const debugLabel = createDebugLabel();\n wrapperContainer.appendChild(debugLabel);\n wrapperContainer.appendChild(canvas);\n\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) {\n throw new Error(\"Failed to get canvas 2d context\");\n }\n\n // Track render state\n let rendering = false;\n let lastTimeMs = -1;\n let disposed = false;\n\n // Create RenderContext for caching across refresh calls\n const renderContext = new RenderContext();\n \n // Create FrameController for coordinating element rendering\n // Cached for the lifetime of this preview instance\n const frameController = new FrameController(timegroup);\n \n // Reusable preview container and style element\n // Container gets new content each frame but CSS styles are cached\n const previewContainer = createPreviewContainer({\n width: renderWidth,\n height: renderHeight,\n background: getComputedStyle(timegroup).background || \"#000\",\n });\n \n const styleEl = document.createElement(\"style\");\n styleEl.textContent = collectDocumentStyles();\n previewContainer.appendChild(styleEl);\n\n // Log resolution scale on first render for debugging\n let hasLoggedScale = false;\n \n // Pending resolution change - applied at start of next refresh to avoid blanking\n let pendingResolutionScale: number | null = null;\n \n /**\n * Apply pending resolution scale changes.\n * Called at the start of refresh() before rendering, so the old content\n * stays visible until new content is ready to be drawn.\n */\n const applyPendingResolutionChange = (): void => {\n if (pendingResolutionScale === null) return;\n \n const newScale = pendingResolutionScale;\n pendingResolutionScale = null;\n \n currentResolutionScale = newScale;\n renderWidth = Math.floor(width * currentResolutionScale);\n renderHeight = Math.floor(height * currentResolutionScale);\n \n // Update previewContainer dimensions (affects what renderToImage produces)\n previewContainer.style.width = `${renderWidth}px`;\n previewContainer.style.height = `${renderHeight}px`;\n \n // Update clone transform\n if (currentResolutionScale < 1) {\n container.style.transform = `scale(${currentResolutionScale})`;\n container.style.transformOrigin = \"top left\";\n } else {\n container.style.transform = \"\";\n }\n \n // Canvas dimensions will be updated right before drawing (in refresh)\n // to avoid clearing the canvas until new content is ready\n };\n \n /**\n * Dynamically change resolution scale without rebuilding clone structure.\n * The actual change is deferred until next refresh() to avoid blanking -\n * old content stays visible until new content is ready.\n */\n const setResolutionScale = (newScale: number): void => {\n // Clamp to valid range\n newScale = Math.max(0.1, Math.min(1, newScale));\n \n if (newScale === currentResolutionScale && pendingResolutionScale === null) return;\n \n // Queue the change - will be applied at start of next refresh\n pendingResolutionScale = newScale;\n \n // Force re-render on next refresh by invalidating lastTimeMs\n lastTimeMs = -1;\n };\n \n const getResolutionScale = (): number => pendingResolutionScale ?? currentResolutionScale;\n \n const refresh = async (): Promise<void> => {\n if (rendering || disposed) return;\n // Clone-timeline: captures use separate clones, Prime-timeline is never locked\n \n const sourceTimeMs = timegroup.currentTimeMs ?? 0;\n const userTimeMs = timegroup.userTimeMs ?? 0;\n if (Math.abs(sourceTimeMs - userTimeMs) > TIME_EPSILON_MS) return;\n \n if (userTimeMs === lastTimeMs) return;\n lastTimeMs = userTimeMs;\n \n rendering = true;\n \n // Apply any pending resolution changes before rendering\n // This updates previewContainer and clone transform, but NOT canvas dimensions yet\n applyPendingResolutionChange();\n \n // Log scale info once per initialization\n if (!hasLoggedScale) {\n hasLoggedScale = true;\n const mode = getEffectiveRenderMode();\n logger.debug(`[renderTimegroupToCanvas] Resolution scale: ${currentResolutionScale} (${width}x${height} → ${renderWidth}x${renderHeight}), canvas buffer: ${canvas.width}x${canvas.height}, CSS size: ${canvas.style.width}x${canvas.style.height}, renderMode: ${mode}`);\n }\n\n try {\n // Use FrameController to ensure all FrameRenderable elements are ready\n // This coordinates prepare → render phases before we capture their state\n await frameController.renderFrame(userTimeMs);\n \n // Build passive structure from prime timeline's CURRENT state\n // Frame tasks have already run above, so DOM changes are captured\n const absoluteTimeMs = toAbsoluteTime(timegroup, userTimeMs);\n const { container, syncState } = buildCloneStructure(timegroup, absoluteTimeMs);\n \n // Apply CSS transform to scale down the content within the container\n // This makes the clone render at reduced complexity\n if (currentResolutionScale < 1) {\n container.style.transform = `scale(${currentResolutionScale})`;\n container.style.transformOrigin = \"top left\";\n }\n \n // Clear previous container content and add new structure\n while (previewContainer.firstChild !== styleEl && previewContainer.firstChild) {\n previewContainer.removeChild(previewContainer.firstChild);\n }\n previewContainer.appendChild(container);\n overrideRootCloneStyles(syncState);\n\n // Remove hidden nodes from DOM for serialization - they won't be serialized\n // or have their canvases encoded. This is a significant optimization.\n const removedNodes = removeHiddenNodesForSerialization(syncState);\n\n // Render at scaled dimensions with canvas scaling for internal video frames\n // Pass renderContext and sourceMap for caching optimization\n const t0 = performance.now();\n const image = await renderToImage(previewContainer, renderWidth, renderHeight, {\n canvasScale: currentResolutionScale,\n renderContext,\n sourceMap: syncState.canvasSourceMap,\n });\n const renderTime = performance.now() - t0;\n \n // Restore hidden nodes for next frame's delta tracking\n restoreHiddenNodes(removedNodes);\n\n // Update canvas buffer dimensions NOW, right before drawing\n // This clears the canvas, but we immediately draw new content\n const targetWidth = Math.floor(renderWidth * scale * dpr);\n const targetHeight = Math.floor(renderHeight * scale * dpr);\n if (canvas.width !== targetWidth || canvas.height !== targetHeight) {\n canvas.width = targetWidth;\n canvas.height = targetHeight;\n } else {\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n }\n \n ctx.save();\n ctx.scale(dpr * scale, dpr * scale);\n ctx.drawImage(image, 0, 0);\n ctx.restore();\n \n // Log render time periodically (every 60 frames)\n defaultProfiler.incrementRenderCount();\n if (defaultProfiler.shouldLogByFrameCount(60)) {\n logger.debug(`[renderTimegroupToCanvas] Frame render: ${renderTime.toFixed(1)}ms (resolutionScale=${currentResolutionScale}, image=${image.width}x${image.height})`);\n }\n \n // Update debug label\n updateDebugLabel(debugLabel, renderWidth, renderHeight, currentResolutionScale);\n } catch (e) {\n logger.error(\"Canvas preview render failed:\", e);\n } finally {\n rendering = false;\n }\n };\n\n /**\n * Dispose the preview and release resources.\n */\n const dispose = (): void => {\n if (disposed) return;\n disposed = true;\n frameController.abort();\n renderContext.dispose();\n };\n\n // Do initial render\n refresh();\n\n return { container: wrapperContainer, canvas, refresh, setResolutionScale, getResolutionScale, dispose };\n}\n"],"mappings":"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"}
|
|
@@ -13,7 +13,7 @@ interface RenderProgress {
|
|
|
13
13
|
elapsedMs: number;
|
|
14
14
|
estimatedRemainingMs: number;
|
|
15
15
|
speedMultiplier: number;
|
|
16
|
-
|
|
16
|
+
framePreviewCanvas?: HTMLCanvasElement;
|
|
17
17
|
}
|
|
18
18
|
interface RenderToVideoOptions {
|
|
19
19
|
fps?: number;
|
|
@@ -35,6 +35,7 @@ interface RenderToVideoOptions {
|
|
|
35
35
|
preferredAudioCodecs?: AudioCodec[];
|
|
36
36
|
benchmarkMode?: boolean;
|
|
37
37
|
customWritableStream?: WritableStream<Uint8Array>;
|
|
38
|
+
progressPreviewInterval?: number;
|
|
38
39
|
}
|
|
39
40
|
//#endregion
|
|
40
41
|
export { RenderProgress, RenderToVideoOptions };
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import { FrameController } from "./FrameController.js";
|
|
2
1
|
import { logger } from "./logger.js";
|
|
3
2
|
import { createPreviewContainer } from "./previewTypes.js";
|
|
4
|
-
import {
|
|
5
|
-
import { inlineImages } from "./rendering/inlineImages.js";
|
|
6
|
-
import { renderToImageDirect } from "./rendering/renderToImage.js";
|
|
3
|
+
import { RenderContext } from "./RenderContext.js";
|
|
7
4
|
import { resetRenderState } from "./renderTimegroupToCanvas.js";
|
|
5
|
+
import { serializeTimelineToDataUri } from "./rendering/serializeTimelineDirect.js";
|
|
8
6
|
import { AudioBufferSource, BufferTarget, CanvasSource, Mp4OutputFormat, Output, StreamTarget, canEncodeAudio, getEncodableAudioCodecs } from "mediabunny";
|
|
9
7
|
|
|
10
8
|
//#region src/preview/renderTimegroupToVideo.ts
|
|
@@ -35,14 +33,20 @@ function resolveConfig(timegroup, options) {
|
|
|
35
33
|
const returnBuffer = options.returnBuffer ?? false;
|
|
36
34
|
const preferredAudioCodecs = options.preferredAudioCodecs ?? ["aac", "opus"];
|
|
37
35
|
const benchmarkMode = options.benchmarkMode ?? false;
|
|
36
|
+
const progressPreviewInterval = options.progressPreviewInterval ?? 60;
|
|
38
37
|
const totalDurationMs = timegroup.durationMs;
|
|
39
38
|
if (!totalDurationMs || totalDurationMs <= 0) throw new Error("Timegroup has no duration");
|
|
40
39
|
const startMs = Math.max(0, options.fromMs ?? 0);
|
|
41
40
|
const endMs = options.toMs !== void 0 ? Math.min(options.toMs, totalDurationMs) : totalDurationMs;
|
|
42
41
|
const renderDurationMs = endMs - startMs;
|
|
43
42
|
if (renderDurationMs <= 0) throw new Error(`Invalid render range: from ${startMs}ms to ${endMs}ms`);
|
|
44
|
-
|
|
45
|
-
const
|
|
43
|
+
timegroup.offsetHeight;
|
|
44
|
+
const timegroupWidth = timegroup.offsetWidth;
|
|
45
|
+
const timegroupHeight = timegroup.offsetHeight;
|
|
46
|
+
console.log(`[renderTimegroupToVideo] Timegroup dimensions: ${timegroupWidth}x${timegroupHeight}`);
|
|
47
|
+
console.log(`[renderTimegroupToVideo] Computed style:`, getComputedStyle(timegroup).width, getComputedStyle(timegroup).height);
|
|
48
|
+
console.log(`[renderTimegroupToVideo] BoundingClientRect:`, timegroup.getBoundingClientRect());
|
|
49
|
+
if (!timegroupWidth || !timegroupHeight) throw new Error(`Timegroup has no dimensions (${timegroupWidth}x${timegroupHeight}). Ensure the timegroup element is in the document and has explicit width/height styles (e.g., class="w-[1920px] h-[1080px]")`);
|
|
46
50
|
const width = Math.floor(timegroupWidth * scale);
|
|
47
51
|
const height = Math.floor(timegroupHeight * scale);
|
|
48
52
|
const videoWidth = width % 2 === 0 ? width : width - 1;
|
|
@@ -58,6 +62,8 @@ function resolveConfig(timegroup, options) {
|
|
|
58
62
|
startMs,
|
|
59
63
|
endMs,
|
|
60
64
|
renderDurationMs,
|
|
65
|
+
width,
|
|
66
|
+
height,
|
|
61
67
|
videoWidth,
|
|
62
68
|
videoHeight,
|
|
63
69
|
totalFrames: Math.ceil(renderDurationMs / frameDurationMs),
|
|
@@ -70,7 +76,8 @@ function resolveConfig(timegroup, options) {
|
|
|
70
76
|
blockingTimeoutMs,
|
|
71
77
|
returnBuffer,
|
|
72
78
|
preferredAudioCodecs,
|
|
73
|
-
benchmarkMode
|
|
79
|
+
benchmarkMode,
|
|
80
|
+
progressPreviewInterval
|
|
74
81
|
};
|
|
75
82
|
}
|
|
76
83
|
function isFileSystemAccessSupported() {
|
|
@@ -198,26 +205,32 @@ async function renderTimegroupToVideo(timegroup, options = {}) {
|
|
|
198
205
|
}
|
|
199
206
|
await output.start();
|
|
200
207
|
}
|
|
201
|
-
const
|
|
202
|
-
await renderClone.seekForRender(initialTimeMs);
|
|
203
|
-
const { container: cloneContainer, syncState } = buildCloneStructure(renderClone, initialTimeMs);
|
|
204
|
-
const frameController = new FrameController(renderClone);
|
|
205
|
-
const width = timegroup.offsetWidth || 1920;
|
|
206
|
-
const height = timegroup.offsetHeight || 1080;
|
|
208
|
+
const renderContext = new RenderContext();
|
|
207
209
|
const previewContainer = createPreviewContainer({
|
|
208
|
-
width,
|
|
209
|
-
height,
|
|
210
|
+
width: timegroup.offsetWidth || 1920,
|
|
211
|
+
height: timegroup.offsetHeight || 1080,
|
|
210
212
|
background: getComputedStyle(timegroup).background || "#000"
|
|
211
213
|
});
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
previewContainer.
|
|
215
|
-
previewContainer.
|
|
216
|
-
|
|
214
|
+
console.log(`[renderTimegroupToVideo] Using direct timeline serialization`);
|
|
215
|
+
previewContainer.appendChild(renderClone);
|
|
216
|
+
previewContainer.classList.add("ef-render-clone-container");
|
|
217
|
+
previewContainer.style.cssText += ";position:fixed;left:-99999px;top:-99999px;pointer-events:none;";
|
|
218
|
+
document.body.appendChild(previewContainer);
|
|
219
|
+
renderClone.offsetHeight;
|
|
220
|
+
console.log(`[renderTimegroupToVideo] Attached previewContainer to document.body (off-screen) for style computation`);
|
|
217
221
|
const renderStartTime = performance.now();
|
|
218
|
-
let lastFramePreviewUrl;
|
|
219
222
|
let lastRenderedAudioEndMs = config.startMs;
|
|
220
223
|
const audioChunkDurationMs = 2e3;
|
|
224
|
+
let thumbCanvas = null;
|
|
225
|
+
let thumbCtx = null;
|
|
226
|
+
if (onProgress && config.progressPreviewInterval > 0) {
|
|
227
|
+
const previewWidth = 160;
|
|
228
|
+
const previewHeight = Math.round(previewWidth * (config.videoHeight / config.videoWidth));
|
|
229
|
+
thumbCanvas = document.createElement("canvas");
|
|
230
|
+
thumbCanvas.width = previewWidth;
|
|
231
|
+
thumbCanvas.height = previewHeight;
|
|
232
|
+
thumbCtx = thumbCanvas.getContext("2d");
|
|
233
|
+
}
|
|
221
234
|
let totalSeekMs = 0;
|
|
222
235
|
let totalSyncMs = 0;
|
|
223
236
|
let totalRenderMs = 0;
|
|
@@ -226,10 +239,9 @@ async function renderTimegroupToVideo(timegroup, options = {}) {
|
|
|
226
239
|
const seekQueue = [];
|
|
227
240
|
const renderTasks = [];
|
|
228
241
|
const MAX_SEEK = 1;
|
|
229
|
-
const MAX_RENDER =
|
|
242
|
+
const MAX_RENDER = 4;
|
|
230
243
|
let nextSeekFrame = 0;
|
|
231
244
|
let nextRenderFrame = 0;
|
|
232
|
-
await inlineImages(previewContainer);
|
|
233
245
|
for (let completedFrames = 0; completedFrames < config.totalFrames; completedFrames++) {
|
|
234
246
|
checkCancelled();
|
|
235
247
|
const frameIndex = completedFrames;
|
|
@@ -249,14 +261,29 @@ async function renderTimegroupToVideo(timegroup, options = {}) {
|
|
|
249
261
|
const renderTimeMs = timestamps[renderFrameIndex];
|
|
250
262
|
const renderTimestampS = renderFrameIndex * config.frameDurationMs / 1e3;
|
|
251
263
|
const renderPromise = seekQueue.shift().then(async () => {
|
|
252
|
-
await frameController.renderFrame(renderTimeMs, { waitForLitUpdate: false });
|
|
253
264
|
const syncStart = performance.now();
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
265
|
+
const dataUri = await serializeTimelineToDataUri(renderClone, config.width, config.height, {
|
|
266
|
+
renderContext,
|
|
267
|
+
canvasScale: config.scale,
|
|
268
|
+
timeMs: renderTimeMs
|
|
269
|
+
});
|
|
270
|
+
const syncTime = performance.now() - syncStart;
|
|
271
|
+
totalSyncMs += syncTime;
|
|
257
272
|
const renderStart = performance.now();
|
|
258
|
-
const image$1 =
|
|
259
|
-
|
|
273
|
+
const image$1 = new Image();
|
|
274
|
+
await new Promise((resolve, reject) => {
|
|
275
|
+
image$1.onload = () => resolve();
|
|
276
|
+
image$1.onerror = (e) => {
|
|
277
|
+
console.error(`[Frame ${renderFrameIndex}] Image load error:`, e);
|
|
278
|
+
console.error(`[Frame ${renderFrameIndex}] Data URI preview:`, dataUri.substring(0, 200) + "...");
|
|
279
|
+
reject(/* @__PURE__ */ new Error(`Failed to load image from data URI`));
|
|
280
|
+
};
|
|
281
|
+
image$1.src = dataUri;
|
|
282
|
+
});
|
|
283
|
+
const renderTime = performance.now() - renderStart;
|
|
284
|
+
totalRenderMs += renderTime;
|
|
285
|
+
if (renderFrameIndex % 30 === 0) console.log(`[Frame ${renderFrameIndex}] Image loaded: ${image$1.width}x${image$1.height}`);
|
|
286
|
+
if (renderFrameIndex % 30 === 0) console.log(`[Frame ${renderFrameIndex}] serialize=${syncTime.toFixed(1)}ms`);
|
|
260
287
|
return image$1;
|
|
261
288
|
});
|
|
262
289
|
renderTasks.push({
|
|
@@ -292,15 +319,7 @@ async function renderTimegroupToVideo(timegroup, options = {}) {
|
|
|
292
319
|
const msPerFrame = elapsedMs / currentFrame;
|
|
293
320
|
const estimatedRemainingMs = (config.totalFrames - currentFrame) * msPerFrame;
|
|
294
321
|
const speedMultiplier = renderedMs / elapsedMs;
|
|
295
|
-
if (
|
|
296
|
-
const previewWidth = 160;
|
|
297
|
-
const previewHeight = Math.round(previewWidth * (config.videoHeight / config.videoWidth));
|
|
298
|
-
const thumbCanvas = document.createElement("canvas");
|
|
299
|
-
thumbCanvas.width = previewWidth;
|
|
300
|
-
thumbCanvas.height = previewHeight;
|
|
301
|
-
thumbCanvas.getContext("2d").drawImage(image, 0, 0, previewWidth, previewHeight);
|
|
302
|
-
lastFramePreviewUrl = thumbCanvas.toDataURL("image/jpeg", .7);
|
|
303
|
-
}
|
|
322
|
+
if (thumbCanvas && thumbCtx && frameIndex % config.progressPreviewInterval === 0) thumbCtx.drawImage(image, 0, 0, thumbCanvas.width, thumbCanvas.height);
|
|
304
323
|
onProgress?.({
|
|
305
324
|
progress,
|
|
306
325
|
currentFrame,
|
|
@@ -310,7 +329,7 @@ async function renderTimegroupToVideo(timegroup, options = {}) {
|
|
|
310
329
|
elapsedMs,
|
|
311
330
|
estimatedRemainingMs,
|
|
312
331
|
speedMultiplier,
|
|
313
|
-
|
|
332
|
+
framePreviewCanvas: thumbCanvas || void 0
|
|
314
333
|
});
|
|
315
334
|
}
|
|
316
335
|
if (audioSource && lastRenderedAudioEndMs < config.endMs) try {
|
|
@@ -318,6 +337,23 @@ async function renderTimegroupToVideo(timegroup, options = {}) {
|
|
|
318
337
|
if (audioBuffer && audioBuffer.length > 0) await audioSource.add(audioBuffer);
|
|
319
338
|
} catch (e) {}
|
|
320
339
|
const totalTime = performance.now() - renderStartTime;
|
|
340
|
+
const avgSeek = totalSeekMs / config.totalFrames;
|
|
341
|
+
const avgSync = totalSyncMs / config.totalFrames;
|
|
342
|
+
const avgRender = totalRenderMs / config.totalFrames;
|
|
343
|
+
const avgEncode = totalEncodeMs / config.totalFrames;
|
|
344
|
+
const avgTotal = totalTime / config.totalFrames;
|
|
345
|
+
const untracked = totalTime - (totalSeekMs + totalSyncMs + totalRenderMs + totalEncodeMs);
|
|
346
|
+
console.log(`\n=== Video Export Performance Breakdown ===`);
|
|
347
|
+
console.log(`Mode: Direct Serialization`);
|
|
348
|
+
console.log(`Total frames: ${config.totalFrames}`);
|
|
349
|
+
console.log(`Total time: ${totalTime.toFixed(0)}ms (${avgTotal.toFixed(1)}ms/frame)`);
|
|
350
|
+
console.log(`\nPer-stage totals:`);
|
|
351
|
+
console.log(` Seek: ${totalSeekMs.toFixed(0)}ms (${(totalSeekMs / totalTime * 100).toFixed(1)}%) - avg ${avgSeek.toFixed(1)}ms/frame`);
|
|
352
|
+
console.log(` Serialize: ${totalSyncMs.toFixed(0)}ms (${(totalSyncMs / totalTime * 100).toFixed(1)}%) - avg ${avgSync.toFixed(1)}ms/frame`);
|
|
353
|
+
console.log(` Render: ${totalRenderMs.toFixed(0)}ms (${(totalRenderMs / totalTime * 100).toFixed(1)}%) - avg ${avgRender.toFixed(1)}ms/frame`);
|
|
354
|
+
console.log(` Encode: ${totalEncodeMs.toFixed(0)}ms (${(totalEncodeMs / totalTime * 100).toFixed(1)}%) - avg ${avgEncode.toFixed(1)}ms/frame`);
|
|
355
|
+
console.log(` Other: ${untracked.toFixed(0)}ms (${(untracked / totalTime * 100).toFixed(1)}%)`);
|
|
356
|
+
console.log(`==========================================\n`);
|
|
321
357
|
logger.debug(`[renderTimegroupToVideo] ${config.totalFrames} frames: seek=${totalSeekMs.toFixed(0)}ms, sync=${totalSyncMs.toFixed(0)}ms, render=${totalRenderMs.toFixed(0)}ms, encode=${totalEncodeMs.toFixed(0)}ms, total=${totalTime.toFixed(0)}ms`);
|
|
322
358
|
if (config.benchmarkMode) return;
|
|
323
359
|
await output.finalize();
|
|
@@ -330,8 +366,9 @@ async function renderTimegroupToVideo(timegroup, options = {}) {
|
|
|
330
366
|
return;
|
|
331
367
|
}
|
|
332
368
|
} finally {
|
|
333
|
-
|
|
369
|
+
renderContext.dispose();
|
|
334
370
|
cleanupRenderClone();
|
|
371
|
+
if (previewContainer.parentNode) previewContainer.parentNode.removeChild(previewContainer);
|
|
335
372
|
}
|
|
336
373
|
}
|
|
337
374
|
|
|
@@ -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: { writable: WritableStream<Uint8Array>; close: () => Promise<void> } | null","encodingCanvas: OffscreenCanvas | null","encodingCtx: OffscreenCanvasRenderingContext2D | null","videoConfig: VideoEncodingConfig","lastFramePreviewUrl: string | undefined","seekQueue: Promise<void>[]","renderTasks: RenderTask[]","image"],"sources":["../../src/preview/renderTimegroupToVideo.ts"],"sourcesContent":["/**\n * Video rendering for timegroups.\n * \n * Uses the EXACT same rendering path as thumbnail generation (captureFromClone),\n * ensuring consistency between preview thumbnails and exported video.\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 { EFVideo } from \"../elements/EFVideo.js\";\nimport {\n resetRenderState,\n type ContentReadyMode,\n} from \"./renderTimegroupToCanvas.js\";\nimport {\n buildCloneStructure,\n syncStyles,\n collectDocumentStyles,\n overrideRootCloneStyles,\n // NOTE: Video export does NOT use removeHiddenNodesForSerialization because the\n // concurrent pipeline has multiple frames in flight sharing the same container.\n // If frame N removes node X and frame N+1 needs X, N+1's serialization would be wrong.\n // Instead, hidden nodes get display:none which is sufficient for correctness.\n // Live preview (renderTimegroupToCanvas) uses the full remove/restore optimization.\n} from \"./renderTimegroupPreview.js\";\nimport { renderToImageDirect } from \"./rendering/renderToImage.js\";\nimport { createPreviewContainer } from \"./previewTypes.js\";\nimport { inlineImages } from \"./rendering/inlineImages.js\";\nimport { FrameController } from \"./FrameController.js\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface RenderProgress {\n progress: number;\n currentFrame: number;\n totalFrames: number;\n renderedMs: number;\n totalDurationMs: number;\n elapsedMs: number;\n estimatedRemainingMs: number;\n speedMultiplier: number;\n framePreviewUrl?: string;\n}\n\nexport interface RenderToVideoOptions {\n fps?: number;\n codec?: \"avc\" | \"hevc\" | \"vp9\" | \"av1\" | \"vp8\";\n bitrate?: number;\n filename?: string;\n scale?: number;\n keyFrameInterval?: number;\n fromMs?: number;\n toMs?: number;\n onProgress?: (progress: RenderProgress) => void;\n streaming?: boolean;\n signal?: AbortSignal;\n includeAudio?: boolean;\n audioBitrate?: number;\n contentReadyMode?: ContentReadyMode;\n blockingTimeoutMs?: number;\n returnBuffer?: boolean;\n preferredAudioCodecs?: AudioCodec[];\n benchmarkMode?: boolean;\n customWritableStream?: WritableStream<Uint8Array>; // For programmatic streaming (CLI/Playwright)\n}\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 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}\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\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 = options.toMs !== undefined ? Math.min(options.toMs, totalDurationMs) : 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 const timegroupWidth = timegroup.offsetWidth || 1920;\n const timegroupHeight = timegroup.offsetHeight || 1080;\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 return {\n fps,\n codec,\n bitrate,\n filename,\n scale,\n keyFrameInterval,\n startMs,\n endMs,\n renderDurationMs,\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 };\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction isFileSystemAccessSupported(): boolean {\n return typeof window !== \"undefined\" && \"showSaveFilePicker\" in window;\n}\n\nasync function getFileWritableStream(\n filename: string,\n): Promise<{ writable: WritableStream<Uint8Array>; close: () => Promise<void> } | 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 { writable, close: async () => { await writable.close(); } };\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: { numberOfChannels: number; sampleRate: number; bitrate: number },\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(undefined, encodingOptions);\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 { numberOfChannels = 2, sampleRate = 48000, bitrate = 128000 } = options ?? {};\n return getEncodableAudioCodecs(undefined, { numberOfChannels, sampleRate, bitrate });\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 { clone: renderClone, cleanup: cleanupRenderClone } =\n await timegroup.createRenderClone();\n \n // Pre-fetch main video segments for all timestamps\n // This ensures all segments are cached before rendering starts,\n // avoiding network delays during the 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 const videoElements = renderClone.querySelectorAll(\"ef-video\");\n if (videoElements.length > 0) {\n logger.debug(`[renderTimegroupToVideo] Prefetching main video segments for ${videoElements.length} video(s)...`);\n await Promise.all(\n Array.from(videoElements).map((video) =>\n (video as EFVideo).prefetchMainVideoSegments(timestamps),\n ),\n );\n logger.debug(`[renderTimegroupToVideo] Prefetch complete`);\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: { writable: WritableStream<Uint8Array>; close: () => Promise<void> } | 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(config.preferredAudioCodecs, {\n numberOfChannels: 2,\n sampleRate: 48000,\n bitrate: config.audioBitrate,\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 // Build clone structure ONCE - reuse like live preview does\n // =========================================================================\n const initialTimeMs = config.startMs;\n await renderClone.seekForRender(initialTimeMs);\n const { container: cloneContainer, syncState } = buildCloneStructure(renderClone, initialTimeMs);\n \n // Create FrameController for coordinating element rendering\n const frameController = new FrameController(renderClone);\n \n // Create preview container with proper styling\n const width = timegroup.offsetWidth || 1920;\n const height = timegroup.offsetHeight || 1080;\n const previewContainer = createPreviewContainer({\n width,\n height,\n background: getComputedStyle(timegroup).background || \"#000\",\n });\n \n // Inject document styles\n const styleEl = document.createElement(\"style\");\n styleEl.textContent = collectDocumentStyles();\n previewContainer.appendChild(styleEl);\n previewContainer.appendChild(cloneContainer);\n overrideRootCloneStyles(syncState, true);\n \n // =========================================================================\n // Frame loop - DEEP PIPELINE: overlap encode + render + prepare\n // =========================================================================\n const renderStartTime = performance.now();\n let lastFramePreviewUrl: string | undefined;\n let lastRenderedAudioEndMs = config.startMs;\n const audioChunkDurationMs = 2000;\n \n let totalSeekMs = 0;\n let totalSyncMs = 0;\n let totalRenderMs = 0;\n let totalEncodeMs = 0;\n \n try {\n // ========================================================================\n // DEEP PIPELINE: 3-4 frames ahead with operation queues\n // ========================================================================\n // Maintain queues of in-flight work (like the reference architecture)\n type RenderTask = { frameIndex: number; timeMs: number; timestampS: number; promise: Promise<HTMLImageElement> };\n const seekQueue: Promise<void>[] = [];\n const renderTasks: RenderTask[] = [];\n \n // Pipeline depth configuration\n // NOTE: Set to 1 for correctness - parallel seeks cause duplicate frames\n // TODO: Investigate why parallel seeks don't work with the clone structure\n const MAX_SEEK = 1;\n const MAX_RENDER = 1;\n \n let nextSeekFrame = 0;\n let nextRenderFrame = 0;\n \n // Inline external images once (they're the same for all frames)\n await inlineImages(previewContainer);\n \n for (let completedFrames = 0; completedFrames < config.totalFrames; completedFrames++) {\n checkCancelled();\n \n const frameIndex = completedFrames;\n const timeMs = timestamps[frameIndex]!;\n const timestampS = (frameIndex * config.frameDurationMs) / 1000;\n \n // =====================================================================\n // STAGE 1: Fill seek queue (don't block!)\n // =====================================================================\n while (seekQueue.length < MAX_SEEK && nextSeekFrame < config.totalFrames) {\n const seekFrameIndex = nextSeekFrame;\n const seekTimeMs = timestamps[seekFrameIndex]!;\n \n const seekStart = performance.now();\n const seekPromise = renderClone.seekForRender(seekTimeMs).then(() => {\n totalSeekMs += performance.now() - seekStart;\n });\n seekQueue.push(seekPromise);\n nextSeekFrame++;\n }\n \n // =====================================================================\n // STAGE 2: Fill render queue (don't block!)\n // =====================================================================\n while (renderTasks.length < MAX_RENDER && seekQueue.length > 0 && nextRenderFrame < config.totalFrames) {\n const renderFrameIndex = nextRenderFrame;\n const renderTimeMs = timestamps[renderFrameIndex]!;\n const renderTimestampS = (renderFrameIndex * config.frameDurationMs) / 1000;\n const seekPromise = seekQueue.shift()!;\n \n const renderPromise = seekPromise.then(async () => {\n // Ensure all FrameRenderable elements are ready before capturing state\n await frameController.renderFrame(renderTimeMs, { waitForLitUpdate: false });\n \n const syncStart = performance.now();\n syncStyles(syncState, renderTimeMs);\n overrideRootCloneStyles(syncState, true);\n totalSyncMs += performance.now() - syncStart;\n \n const renderStart = performance.now();\n const image = await renderToImageDirect(previewContainer, width, height);\n totalRenderMs += performance.now() - renderStart;\n \n return image;\n });\n \n renderTasks.push({\n frameIndex: renderFrameIndex,\n timeMs: renderTimeMs,\n timestampS: renderTimestampS,\n promise: renderPromise,\n });\n nextRenderFrame++;\n }\n \n // =====================================================================\n // STAGE 3: Await the render for THIS frame (in strict order)\n // =====================================================================\n const taskIndex = renderTasks.findIndex((t) => t.frameIndex === frameIndex);\n if (taskIndex === -1) {\n throw new Error(`No render task found for frame ${frameIndex}`);\n }\n \n const task = renderTasks[taskIndex]!;\n const image = await task.promise;\n renderTasks.splice(taskIndex, 1);\n \n // =====================================================================\n // STAGE 4: Render audio chunk if needed\n // =====================================================================\n if (audioSource && timeMs >= lastRenderedAudioEndMs + audioChunkDurationMs) {\n const chunkEndMs = Math.min(timeMs + audioChunkDurationMs, config.endMs);\n try {\n const audioBuffer = await timegroup.renderAudio(lastRenderedAudioEndMs, chunkEndMs);\n if (audioBuffer && audioBuffer.length > 0) {\n await audioSource.add(audioBuffer);\n }\n } catch (e) { /* Audio render failures are non-fatal */ }\n lastRenderedAudioEndMs = chunkEndMs;\n }\n \n // =====================================================================\n // STAGE 5: Encode frame (sequential, maintains order)\n // =====================================================================\n if (videoSource && output && encodingCtx) {\n const encodeStart = performance.now();\n encodingCtx.drawImage(\n image,\n 0, 0, image.width, image.height,\n 0, 0, config.videoWidth, config.videoHeight,\n );\n await videoSource.add(timestampS, config.frameDurationS);\n totalEncodeMs += performance.now() - encodeStart;\n }\n \n // =====================================================================\n // STAGE 6: Progress reporting\n // =====================================================================\n const currentFrame = frameIndex + 1;\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 (onProgress && frameIndex % 10 === 0) {\n const previewWidth = 160;\n const previewHeight = Math.round(previewWidth * (config.videoHeight / config.videoWidth));\n const thumbCanvas = document.createElement(\"canvas\");\n thumbCanvas.width = previewWidth;\n thumbCanvas.height = previewHeight;\n const thumbCtx = thumbCanvas.getContext(\"2d\")!;\n thumbCtx.drawImage(image, 0, 0, previewWidth, previewHeight);\n lastFramePreviewUrl = thumbCanvas.toDataURL(\"image/jpeg\", 0.7);\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 framePreviewUrl: lastFramePreviewUrl,\n });\n }\n \n // Render remaining audio\n if (audioSource && lastRenderedAudioEndMs < config.endMs) {\n try {\n const audioBuffer = await timegroup.renderAudio(lastRenderedAudioEndMs, config.endMs);\n if (audioBuffer && audioBuffer.length > 0) {\n await audioSource.add(audioBuffer);\n }\n } catch (e) { /* Audio render failures are non-fatal */ }\n }\n \n const totalTime = performance.now() - renderStartTime;\n logger.debug(\n `[renderTimegroupToVideo] ${config.totalFrames} frames: ` +\n `seek=${totalSeekMs.toFixed(0)}ms, sync=${totalSyncMs.toFixed(0)}ms, ` +\n `render=${totalRenderMs.toFixed(0)}ms, encode=${totalEncodeMs.toFixed(0)}ms, ` +\n `total=${totalTime.toFixed(0)}ms`\n );\n \n if (config.benchmarkMode) {\n return undefined;\n }\n \n await output!.finalize();\n \n if (useStreaming) {\n // Streaming mode: chunks already sent via customWritableStream or file stream\n return undefined;\n } else {\n const bufferTarget = target as BufferTarget;\n const videoBuffer = bufferTarget.buffer;\n if (!videoBuffer) {\n throw new Error(\"Video encoding failed: no buffer produced\");\n }\n \n if (config.returnBuffer) {\n return new Uint8Array(videoBuffer);\n }\n \n const videoBlob = new Blob([videoBuffer], { type: \"video/mp4\" });\n downloadBlob(videoBlob, config.filename);\n return undefined;\n }\n \n } finally {\n frameController.abort();\n cleanupRenderClone();\n }\n}\n\nexport { QUALITY_HIGH };\nexport type { AudioCodec };\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"renderTimegroupToVideo.js","names":["timestamps: number[]","output: Output | null","videoSource: CanvasSource | null","audioSource: AudioBufferSource | null","target: BufferTarget | StreamTarget | null","fileStream: { writable: WritableStream<Uint8Array>; close: () => Promise<void> } | null","encodingCanvas: OffscreenCanvas | null","encodingCtx: OffscreenCanvasRenderingContext2D | null","videoConfig: VideoEncodingConfig","thumbCanvas: HTMLCanvasElement | null","thumbCtx: CanvasRenderingContext2D | null","seekQueue: Promise<void>[]","renderTasks: RenderTask[]","image"],"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 { EFVideo } from \"../elements/EFVideo.js\";\nimport {\n resetRenderState,\n type ContentReadyMode,\n} from \"./renderTimegroupToCanvas.js\";\nimport { serializeTimelineToDataUri } from \"./rendering/serializeTimelineDirect.js\";\nimport { createPreviewContainer } from \"./previewTypes.js\";\nimport { RenderContext } from \"./RenderContext.js\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface RenderProgress {\n progress: number;\n currentFrame: number;\n totalFrames: number;\n renderedMs: number;\n totalDurationMs: number;\n elapsedMs: number;\n estimatedRemainingMs: number;\n speedMultiplier: number;\n framePreviewCanvas?: HTMLCanvasElement; // Canvas with current frame (updated async, no encoding cost)\n}\n\nexport interface RenderToVideoOptions {\n fps?: number;\n codec?: \"avc\" | \"hevc\" | \"vp9\" | \"av1\" | \"vp8\";\n bitrate?: number;\n filename?: string;\n scale?: number;\n keyFrameInterval?: number;\n fromMs?: number;\n toMs?: number;\n onProgress?: (progress: RenderProgress) => void;\n streaming?: boolean;\n signal?: AbortSignal;\n includeAudio?: boolean;\n audioBitrate?: number;\n contentReadyMode?: ContentReadyMode;\n blockingTimeoutMs?: number;\n returnBuffer?: boolean;\n preferredAudioCodecs?: AudioCodec[];\n benchmarkMode?: boolean;\n customWritableStream?: WritableStream<Uint8Array>; // For programmatic streaming (CLI/Playwright)\n progressPreviewInterval?: number; // How often to generate preview thumbnails (default: 60 frames, 0 = disabled)\n}\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}\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 = options.toMs !== undefined ? Math.min(options.toMs, totalDurationMs) : 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 const timegroupWidth = timegroup.offsetWidth;\n const timegroupHeight = timegroup.offsetHeight;\n \n console.log(`[renderTimegroupToVideo] Timegroup dimensions: ${timegroupWidth}x${timegroupHeight}`);\n console.log(`[renderTimegroupToVideo] Computed style:`, getComputedStyle(timegroup).width, getComputedStyle(timegroup).height);\n console.log(`[renderTimegroupToVideo] BoundingClientRect:`, timegroup.getBoundingClientRect());\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 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 };\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction isFileSystemAccessSupported(): boolean {\n return typeof window !== \"undefined\" && \"showSaveFilePicker\" in window;\n}\n\nasync function getFileWritableStream(\n filename: string,\n): Promise<{ writable: WritableStream<Uint8Array>; close: () => Promise<void> } | 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 { writable, close: async () => { await writable.close(); } };\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: { numberOfChannels: number; sampleRate: number; bitrate: number },\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(undefined, encodingOptions);\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 { numberOfChannels = 2, sampleRate = 48000, bitrate = 128000 } = options ?? {};\n return getEncodableAudioCodecs(undefined, { numberOfChannels, sampleRate, bitrate });\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 { clone: renderClone, cleanup: cleanupRenderClone } =\n await timegroup.createRenderClone();\n \n // Pre-fetch main video segments for all timestamps\n // This ensures all segments are cached before rendering starts,\n // avoiding network delays during the 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 const videoElements = renderClone.querySelectorAll(\"ef-video\");\n if (videoElements.length > 0) {\n logger.debug(`[renderTimegroupToVideo] Prefetching main video segments for ${videoElements.length} video(s)...`);\n await Promise.all(\n Array.from(videoElements).map((video) =>\n (video as EFVideo).prefetchMainVideoSegments(timestamps),\n ),\n );\n logger.debug(`[renderTimegroupToVideo] Prefetch complete`);\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: { writable: WritableStream<Uint8Array>; close: () => Promise<void> } | 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(config.preferredAudioCodecs, {\n numberOfChannels: 2,\n sampleRate: 48000,\n bitrate: config.audioBitrate,\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 console.log(`[renderTimegroupToVideo] Using direct timeline serialization`);\n \n // Attach renderClone to container\n previewContainer.appendChild(renderClone);\n \n // CRITICAL: Add ef-render-clone-container class so isRenderClone() returns true\n // This affects animation tracking - without it, the animation system treats the clone\n // as the prime timeline, which causes incorrect behavior\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 += ';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 console.log(`[renderTimegroupToVideo] Attached previewContainer to document.body (off-screen) for style computation`);\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(previewWidth * (config.videoHeight / config.videoWidth));\n thumbCanvas = document.createElement(\"canvas\");\n thumbCanvas.width = previewWidth;\n thumbCanvas.height = previewHeight;\n thumbCtx = thumbCanvas.getContext(\"2d\");\n }\n \n let totalSeekMs = 0;\n let totalSyncMs = 0;\n let totalRenderMs = 0;\n let totalEncodeMs = 0;\n \n try {\n // ========================================================================\n // DEEP PIPELINE: 3-4 frames ahead with operation queues\n // ========================================================================\n // Maintain queues of in-flight work (like the reference architecture)\n type RenderTask = { frameIndex: number; timeMs: number; timestampS: number; promise: Promise<HTMLImageElement> };\n const seekQueue: Promise<void>[] = [];\n const renderTasks: RenderTask[] = [];\n \n // Pipeline depth configuration\n // MAX_SEEK must be 1: Only one clone exists, so seeks must be sequential\n // MAX_RENDER can be higher: serializeElement captures DOM state synchronously,\n // then canvas encoding and image loading happen async and don't touch the clone\n const MAX_SEEK = 1;\n const MAX_RENDER = 4; // Allow 4 frames to encode/load in parallel (seek, serialize, encode, load)\n \n let nextSeekFrame = 0;\n let nextRenderFrame = 0;\n \n for (let completedFrames = 0; completedFrames < config.totalFrames; completedFrames++) {\n checkCancelled();\n \n const frameIndex = completedFrames;\n const timeMs = timestamps[frameIndex]!;\n const timestampS = (frameIndex * config.frameDurationMs) / 1000;\n \n // =====================================================================\n // STAGE 1: Fill seek queue (don't block!)\n // =====================================================================\n while (seekQueue.length < MAX_SEEK && nextSeekFrame < config.totalFrames) {\n const seekFrameIndex = nextSeekFrame;\n const seekTimeMs = timestamps[seekFrameIndex]!;\n \n const seekStart = performance.now();\n const seekPromise = renderClone.seekForRender(seekTimeMs).then(() => {\n totalSeekMs += performance.now() - seekStart;\n });\n seekQueue.push(seekPromise);\n nextSeekFrame++;\n }\n \n // =====================================================================\n // STAGE 2: Fill render queue (don't block!)\n // =====================================================================\n while (renderTasks.length < MAX_RENDER && seekQueue.length > 0 && nextRenderFrame < config.totalFrames) {\n const renderFrameIndex = nextRenderFrame;\n const renderTimeMs = timestamps[renderFrameIndex]!;\n const renderTimestampS = (renderFrameIndex * config.frameDurationMs) / 1000;\n const seekPromise = seekQueue.shift()!;\n \n const renderPromise = seekPromise.then(async () => {\n // NOTE: seekForRender() has already:\n // 1. Called frameController.renderFrame() to coordinate FrameRenderable elements\n // 2. Awaited #executeCustomFrameTasks() so frame tasks are complete\n // Clone's DOM now reflects all changes from frame tasks\n \n // Direct serialization: serialize timeline to data URI in one pass\n const syncStart = performance.now();\n const dataUri = await serializeTimelineToDataUri(renderClone, config.width, config.height, {\n renderContext,\n canvasScale: config.scale,\n timeMs: renderTimeMs,\n });\n const syncTime = performance.now() - syncStart;\n totalSyncMs += syncTime;\n \n // Create image from data URI\n const renderStart = performance.now();\n const image = new Image();\n await new Promise<void>((resolve, reject) => {\n image.onload = () => resolve();\n image.onerror = (e) => {\n console.error(`[Frame ${renderFrameIndex}] Image load error:`, e);\n console.error(`[Frame ${renderFrameIndex}] Data URI preview:`, dataUri.substring(0, 200) + '...');\n reject(new Error(`Failed to load image from data URI`));\n };\n image.src = dataUri;\n });\n const renderTime = performance.now() - renderStart;\n totalRenderMs += renderTime;\n \n if (renderFrameIndex % 30 === 0) {\n console.log(`[Frame ${renderFrameIndex}] Image loaded: ${image.width}x${image.height}`);\n }\n \n // Log detailed timing every 30 frames to see breakdown\n if (renderFrameIndex % 30 === 0) {\n console.log(`[Frame ${renderFrameIndex}] serialize=${syncTime.toFixed(1)}ms`);\n }\n \n return image;\n });\n \n renderTasks.push({\n frameIndex: renderFrameIndex,\n timeMs: renderTimeMs,\n timestampS: renderTimestampS,\n promise: renderPromise,\n });\n nextRenderFrame++;\n }\n \n // =====================================================================\n // STAGE 3: Await the render for THIS frame (in strict order)\n // =====================================================================\n const taskIndex = renderTasks.findIndex((t) => t.frameIndex === frameIndex);\n if (taskIndex === -1) {\n throw new Error(`No render task found for frame ${frameIndex}`);\n }\n \n const task = renderTasks[taskIndex]!;\n const image = await task.promise;\n renderTasks.splice(taskIndex, 1);\n \n // =====================================================================\n // STAGE 4: Render audio chunk if needed\n // =====================================================================\n if (audioSource && timeMs >= lastRenderedAudioEndMs + audioChunkDurationMs) {\n const chunkEndMs = Math.min(timeMs + audioChunkDurationMs, config.endMs);\n try {\n const audioBuffer = await timegroup.renderAudio(lastRenderedAudioEndMs, chunkEndMs);\n if (audioBuffer && audioBuffer.length > 0) {\n await audioSource.add(audioBuffer);\n }\n } catch (e) { /* Audio render failures are non-fatal */ }\n lastRenderedAudioEndMs = chunkEndMs;\n }\n \n // =====================================================================\n // STAGE 5: Encode frame (sequential, maintains order)\n // =====================================================================\n if (videoSource && output && encodingCtx) {\n const encodeStart = performance.now();\n encodingCtx.drawImage(\n image,\n 0, 0, image.width, image.height,\n 0, 0, config.videoWidth, config.videoHeight,\n );\n await videoSource.add(timestampS, config.frameDurationS);\n totalEncodeMs += performance.now() - encodeStart;\n }\n \n // =====================================================================\n // STAGE 6: Progress reporting\n // =====================================================================\n const currentFrame = frameIndex + 1;\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 // Update preview canvas if enabled (just draw, no encoding - super fast!)\n // The canvas reference is passed to onProgress and can be displayed directly in UI\n if (thumbCanvas && thumbCtx && frameIndex % config.progressPreviewInterval === 0) {\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, // Pass canvas reference (no encoding!)\n });\n }\n \n // Render remaining audio\n if (audioSource && lastRenderedAudioEndMs < config.endMs) {\n try {\n const audioBuffer = await timegroup.renderAudio(lastRenderedAudioEndMs, config.endMs);\n if (audioBuffer && audioBuffer.length > 0) {\n await audioSource.add(audioBuffer);\n }\n } catch (e) { /* Audio render failures are non-fatal */ }\n }\n \n const totalTime = performance.now() - renderStartTime;\n \n // Calculate percentages and averages for performance analysis\n const avgSeek = totalSeekMs / config.totalFrames;\n const avgSync = totalSyncMs / config.totalFrames;\n const avgRender = totalRenderMs / config.totalFrames;\n const avgEncode = totalEncodeMs / config.totalFrames;\n const avgTotal = totalTime / config.totalFrames;\n \n const tracked = totalSeekMs + totalSyncMs + totalRenderMs + totalEncodeMs;\n const untracked = totalTime - tracked;\n \n console.log(`\\n=== Video Export Performance Breakdown ===`);\n console.log(`Mode: Direct Serialization`);\n console.log(`Total frames: ${config.totalFrames}`);\n console.log(`Total time: ${totalTime.toFixed(0)}ms (${avgTotal.toFixed(1)}ms/frame)`);\n console.log(`\\nPer-stage totals:`);\n console.log(` Seek: ${totalSeekMs.toFixed(0)}ms (${(totalSeekMs/totalTime*100).toFixed(1)}%) - avg ${avgSeek.toFixed(1)}ms/frame`);\n console.log(` Serialize: ${totalSyncMs.toFixed(0)}ms (${(totalSyncMs/totalTime*100).toFixed(1)}%) - avg ${avgSync.toFixed(1)}ms/frame`);\n console.log(` Render: ${totalRenderMs.toFixed(0)}ms (${(totalRenderMs/totalTime*100).toFixed(1)}%) - avg ${avgRender.toFixed(1)}ms/frame`);\n console.log(` Encode: ${totalEncodeMs.toFixed(0)}ms (${(totalEncodeMs/totalTime*100).toFixed(1)}%) - avg ${avgEncode.toFixed(1)}ms/frame`);\n console.log(` Other: ${untracked.toFixed(0)}ms (${(untracked/totalTime*100).toFixed(1)}%)`);\n console.log(`==========================================\\n`);\n \n logger.debug(\n `[renderTimegroupToVideo] ${config.totalFrames} frames: ` +\n `seek=${totalSeekMs.toFixed(0)}ms, sync=${totalSyncMs.toFixed(0)}ms, ` +\n `render=${totalRenderMs.toFixed(0)}ms, encode=${totalEncodeMs.toFixed(0)}ms, ` +\n `total=${totalTime.toFixed(0)}ms`\n );\n \n if (config.benchmarkMode) {\n return undefined;\n }\n \n await output!.finalize();\n \n if (useStreaming) {\n // Streaming mode: chunks already sent via customWritableStream or file stream\n return undefined;\n } else {\n const bufferTarget = target as BufferTarget;\n const videoBuffer = bufferTarget.buffer;\n if (!videoBuffer) {\n throw new Error(\"Video encoding failed: no buffer produced\");\n }\n \n if (config.returnBuffer) {\n return new Uint8Array(videoBuffer);\n }\n \n const videoBlob = new Blob([videoBuffer], { type: \"video/mp4\" });\n downloadBlob(videoBlob, config.filename);\n return undefined;\n }\n \n } finally {\n renderContext.dispose();\n cleanupRenderClone();\n // Remove preview container if it was attached to document\n if (previewContainer.parentNode) {\n previewContainer.parentNode.removeChild(previewContainer);\n }\n }\n}\n\nexport { QUALITY_HIGH };\nexport type { AudioCodec };\n"],"mappings":"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"}
|