@editframe/elements 0.38.1 → 0.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/elements/EFCaptions.js +1 -1
- package/dist/elements/EFCaptions.js.map +1 -1
- package/dist/elements/EFImage.js +3 -4
- package/dist/elements/EFImage.js.map +1 -1
- package/dist/elements/EFMedia/BufferedSeekingInput.js +1 -1
- package/dist/elements/EFMedia/CachedFetcher.js +99 -0
- package/dist/elements/EFMedia/CachedFetcher.js.map +1 -0
- package/dist/elements/EFMedia/MediaEngine.d.ts +19 -0
- package/dist/elements/EFMedia/MediaEngine.js +129 -0
- package/dist/elements/EFMedia/MediaEngine.js.map +1 -0
- package/dist/elements/EFMedia/SegmentIndex.d.ts +32 -0
- package/dist/elements/EFMedia/SegmentIndex.js +185 -0
- package/dist/elements/EFMedia/SegmentIndex.js.map +1 -0
- package/dist/elements/EFMedia/SegmentTransport.d.ts +12 -0
- package/dist/elements/EFMedia/SegmentTransport.js +69 -0
- package/dist/elements/EFMedia/SegmentTransport.js.map +1 -0
- package/dist/elements/EFMedia/TimingModel.d.ts +10 -0
- package/dist/elements/EFMedia/TimingModel.js +28 -0
- package/dist/elements/EFMedia/TimingModel.js.map +1 -0
- package/dist/elements/EFMedia/shared/AudioSpanUtils.js +7 -6
- package/dist/elements/EFMedia/shared/AudioSpanUtils.js.map +1 -1
- package/dist/elements/EFMedia/shared/ThumbnailExtractor.js +13 -34
- package/dist/elements/EFMedia/shared/ThumbnailExtractor.js.map +1 -1
- package/dist/elements/EFMedia.d.ts +2 -1
- package/dist/elements/EFMedia.js +14 -31
- package/dist/elements/EFMedia.js.map +1 -1
- package/dist/elements/EFSourceMixin.js +1 -1
- package/dist/elements/EFSourceMixin.js.map +1 -1
- package/dist/elements/EFTemporal.js +2 -1
- package/dist/elements/EFTemporal.js.map +1 -1
- package/dist/elements/EFTimegroup.js +2 -1
- package/dist/elements/EFTimegroup.js.map +1 -1
- package/dist/elements/EFVideo.js +204 -187
- package/dist/elements/EFVideo.js.map +1 -1
- package/dist/gui/EFConfiguration.d.ts +0 -7
- package/dist/gui/EFConfiguration.js +0 -5
- package/dist/gui/EFConfiguration.js.map +1 -1
- package/dist/gui/EFWorkbench.d.ts +2 -0
- package/dist/gui/EFWorkbench.js +68 -1
- package/dist/gui/EFWorkbench.js.map +1 -1
- package/dist/gui/PlaybackController.d.ts +2 -0
- package/dist/gui/PlaybackController.js +11 -1
- package/dist/gui/PlaybackController.js.map +1 -1
- package/dist/gui/ef-theme.css +11 -0
- package/dist/gui/timeline/tracks/AudioTrack.js +28 -30
- package/dist/gui/timeline/tracks/AudioTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/EFThumbnailStrip.d.ts +1 -0
- package/dist/gui/timeline/tracks/EFThumbnailStrip.js +41 -8
- package/dist/gui/timeline/tracks/EFThumbnailStrip.js.map +1 -1
- package/dist/gui/timeline/tracks/VideoTrack.js +2 -2
- package/dist/gui/timeline/tracks/VideoTrack.js.map +1 -1
- package/dist/gui/timeline/tracks/waveformUtils.js +19 -19
- package/dist/gui/timeline/tracks/waveformUtils.js.map +1 -1
- package/dist/preview/QualityUpgradeScheduler.d.ts +8 -0
- package/dist/preview/QualityUpgradeScheduler.js +13 -1
- package/dist/preview/QualityUpgradeScheduler.js.map +1 -1
- package/dist/preview/renderTimegroupToVideo.js +3 -3
- package/dist/preview/renderTimegroupToVideo.js.map +1 -1
- package/dist/preview/renderVideoToVideo.js +5 -6
- package/dist/preview/renderVideoToVideo.js.map +1 -1
- package/dist/transcoding/types/index.d.ts +6 -94
- package/dist/transcoding/utils/UrlGenerator.d.ts +3 -12
- package/dist/transcoding/utils/UrlGenerator.js +3 -29
- package/dist/transcoding/utils/UrlGenerator.js.map +1 -1
- package/package.json +2 -2
- package/test/setup.ts +1 -1
- package/test/useAssetMSW.ts +0 -100
- package/dist/elements/EFMedia/AssetMediaEngine.js +0 -284
- package/dist/elements/EFMedia/AssetMediaEngine.js.map +0 -1
- package/dist/elements/EFMedia/BaseMediaEngine.js +0 -200
- package/dist/elements/EFMedia/BaseMediaEngine.js.map +0 -1
- package/dist/elements/EFMedia/FileMediaEngine.js +0 -122
- package/dist/elements/EFMedia/FileMediaEngine.js.map +0 -1
- package/dist/elements/EFMedia/JitMediaEngine.js +0 -157
- package/dist/elements/EFMedia/JitMediaEngine.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"renderTimegroupToVideo.js","names":["timestamps: number[]","output: Output | null","videoSource: CanvasSource | null","audioSource: AudioBufferSource | null","target: BufferTarget | StreamTarget | null","fileStream: {\n writable: WritableStream<Uint8Array>;\n close: () => Promise<void>;\n } | null","encodingCanvas: OffscreenCanvas | null","encodingCtx: OffscreenCanvasRenderingContext2D | null","videoConfig: VideoEncodingConfig","thumbCanvas: HTMLCanvasElement | null","thumbCtx: CanvasRenderingContext2D | null","pendingFrames: PendingFrame[]","entry: PendingFrame","image","image: HTMLImageElement"],"sources":["../../src/preview/renderTimegroupToVideo.ts"],"sourcesContent":["/**\n * Video rendering for timegroups using direct serialization.\n *\n * Architecture:\n * - Creates a render clone of the timeline\n * - For each frame:\n * 1. Seeks the clone to the target time\n * 2. Executes frame tasks (SVG updates, canvas draws, etc.)\n * 3. Serializes the live DOM directly to SVG+foreignObject data URI\n * 4. Renders to image and encodes to video\n *\n * RenderContext provides pixel caching across frames for performance.\n */\n\nimport { logger } from \"./logger.js\";\nimport {\n Output,\n Mp4OutputFormat,\n BufferTarget,\n StreamTarget,\n CanvasSource,\n AudioBufferSource,\n QUALITY_HIGH,\n canEncodeAudio,\n getEncodableAudioCodecs,\n type VideoEncodingConfig,\n type AudioEncodingConfig,\n type AudioCodec,\n} from \"mediabunny\";\nimport type { EFTimegroup } from \"../elements/EFTimegroup.js\";\nimport type { RenderToVideoOptions } from \"./renderTimegroupToVideo.types.js\";\nimport type { ContentReadyMode } from \"./renderTimegroupToCanvas.types.js\";\nimport {\n resetRenderState,\n waitForVideoContent,\n} from \"./renderTimegroupToCanvas.js\";\nimport { captureTimelineToDataUri } from \"./rendering/serializeTimelineDirect.js\";\nimport { renderToImageNative } from \"./rendering/renderToImageNative.js\";\nimport { isNativeCanvasApiAvailable } from \"./previewSettings.js\";\nimport { createPreviewContainer } from \"./previewTypes.js\";\nimport { RenderContext } from \"./RenderContext.js\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\n// Re-export types from type-only module (zero side effects)\nexport type {\n RenderProgress,\n RenderToVideoOptions,\n} from \"./renderTimegroupToVideo.types.js\";\n\n// ============================================================================\n// Errors\n// ============================================================================\n\nexport class NoSupportedAudioCodecError extends Error {\n constructor(requestedCodecs: AudioCodec[], availableCodecs: AudioCodec[]) {\n super(\n `No supported audio codec found. Requested: [${requestedCodecs.join(\", \")}], ` +\n `Available: [${availableCodecs.length > 0 ? availableCodecs.join(\", \") : \"none\"}]`,\n );\n this.name = \"NoSupportedAudioCodecError\";\n }\n}\n\nexport class RenderCancelledError extends Error {\n constructor() {\n super(\"Render cancelled\");\n this.name = \"RenderCancelledError\";\n }\n}\n\n// ============================================================================\n// Configuration\n// ============================================================================\n\ninterface ResolvedConfig {\n fps: number;\n codec: \"avc\" | \"hevc\" | \"vp9\" | \"av1\" | \"vp8\";\n bitrate: number;\n filename: string;\n scale: number;\n keyFrameInterval: number;\n startMs: number;\n endMs: number;\n renderDurationMs: number;\n width: number;\n height: number;\n videoWidth: number;\n videoHeight: number;\n totalFrames: number;\n frameDurationMs: number;\n frameDurationS: number;\n streaming: boolean;\n includeAudio: boolean;\n audioBitrate: number;\n contentReadyMode: ContentReadyMode;\n blockingTimeoutMs: number;\n returnBuffer: boolean;\n preferredAudioCodecs: AudioCodec[];\n benchmarkMode: boolean;\n progressPreviewInterval: number;\n canvasMode: \"native\" | \"foreignObject\";\n}\n\nfunction resolveConfig(\n timegroup: EFTimegroup,\n options: RenderToVideoOptions = {},\n): ResolvedConfig {\n const fps = options.fps ?? timegroup.effectiveFps ?? 30;\n const codec = options.codec ?? \"avc\";\n const bitrate = options.bitrate ?? 8_000_000;\n const filename = options.filename ?? \"timegroup-video.mp4\";\n const scale = options.scale ?? 1;\n const keyFrameInterval = options.keyFrameInterval ?? 2;\n const streaming = options.streaming ?? true;\n const includeAudio = options.includeAudio ?? true;\n const audioBitrate = options.audioBitrate ?? 128_000;\n const contentReadyMode = options.contentReadyMode ?? \"blocking\";\n const blockingTimeoutMs = options.blockingTimeoutMs ?? 5000;\n const returnBuffer = options.returnBuffer ?? false;\n const preferredAudioCodecs = options.preferredAudioCodecs ?? [\"aac\", \"opus\"];\n const benchmarkMode = options.benchmarkMode ?? false;\n // Preview generation now uses canvas reference (no encoding) - cheap to enable!\n // Defaults to 60 frames (every 2 seconds at 30fps). Set to 0 to disable.\n const progressPreviewInterval = options.progressPreviewInterval ?? 60;\n\n const totalDurationMs = timegroup.durationMs;\n if (!totalDurationMs || totalDurationMs <= 0) {\n throw new Error(\"Timegroup has no duration\");\n }\n\n const startMs = Math.max(0, options.fromMs ?? 0);\n const endMs =\n options.toMs !== undefined\n ? Math.min(options.toMs, totalDurationMs)\n : totalDurationMs;\n const renderDurationMs = endMs - startMs;\n\n if (renderDurationMs <= 0) {\n throw new Error(`Invalid render range: from ${startMs}ms to ${endMs}ms`);\n }\n\n // Force layout reflow before reading dimensions\n void timegroup.offsetHeight;\n\n // Try multiple sources for dimensions (offsetWidth can be 0 in headless browsers)\n let timegroupWidth = timegroup.offsetWidth;\n let timegroupHeight = timegroup.offsetHeight;\n\n if (!timegroupWidth || !timegroupHeight) {\n const rect = timegroup.getBoundingClientRect();\n if (rect.width > 0 && rect.height > 0) {\n timegroupWidth = rect.width;\n timegroupHeight = rect.height;\n }\n }\n\n if (!timegroupWidth || !timegroupHeight) {\n const computed = getComputedStyle(timegroup);\n const cw = parseFloat(computed.width);\n const ch = parseFloat(computed.height);\n if (cw > 0 && ch > 0) {\n timegroupWidth = cw;\n timegroupHeight = ch;\n }\n }\n\n if (!timegroupWidth || !timegroupHeight) {\n throw new Error(\n `Timegroup has no dimensions (${timegroupWidth}x${timegroupHeight}). ` +\n `Ensure the timegroup element is in the document and has explicit width/height styles ` +\n `(e.g., class=\"w-[1920px] h-[1080px]\")`,\n );\n }\n const width = Math.floor(timegroupWidth * scale);\n const height = Math.floor(timegroupHeight * scale);\n\n const videoWidth = width % 2 === 0 ? width : width - 1;\n const videoHeight = height % 2 === 0 ? height : height - 1;\n\n const frameDurationMs = 1000 / fps;\n const totalFrames = Math.ceil(renderDurationMs / frameDurationMs);\n const frameDurationS = frameDurationMs / 1000;\n\n // Determine effective canvas mode:\n // 1. If explicitly specified, use that (with fallback if native not available)\n // 2. If not specified, default to foreignObject for compatibility\n const canvasMode = (() => {\n const requested = options.canvasMode;\n if (!requested) return \"foreignObject\";\n if (requested === \"native\" && !isNativeCanvasApiAvailable()) {\n logger.debug(\n \"[renderTimegroupToVideo] Native canvas mode requested but not available, falling back to foreignObject\",\n );\n return \"foreignObject\";\n }\n return requested;\n })();\n\n return {\n fps,\n codec,\n bitrate,\n filename,\n scale,\n keyFrameInterval,\n startMs,\n endMs,\n renderDurationMs,\n width,\n height,\n videoWidth,\n videoHeight,\n totalFrames,\n frameDurationMs,\n frameDurationS,\n streaming,\n includeAudio,\n audioBitrate,\n contentReadyMode,\n blockingTimeoutMs,\n returnBuffer,\n preferredAudioCodecs,\n benchmarkMode,\n progressPreviewInterval,\n canvasMode,\n };\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction isFileSystemAccessSupported(): boolean {\n return typeof window !== \"undefined\" && \"showSaveFilePicker\" in window;\n}\n\nasync function getFileWritableStream(filename: string): Promise<{\n writable: WritableStream<Uint8Array>;\n close: () => Promise<void>;\n} | null> {\n if (!isFileSystemAccessSupported()) {\n return null;\n }\n\n try {\n const fileHandle = await (window as any).showSaveFilePicker({\n suggestedName: filename,\n types: [{ description: \"MP4 Video\", accept: { \"video/mp4\": [\".mp4\"] } }],\n });\n const writable = await fileHandle.createWritable();\n return {\n writable,\n close: async () => {\n await writable.close();\n },\n };\n } catch (e) {\n if ((e as Error).name !== \"AbortError\") {\n logger.warn(\"[renderToVideo] File System Access failed:\", e);\n }\n return null;\n }\n}\n\nasync function selectAudioCodec(\n preferredCodecs: AudioCodec[],\n encodingOptions: {\n numberOfChannels: number;\n sampleRate: number;\n bitrate: number;\n },\n): Promise<AudioCodec> {\n for (const codec of preferredCodecs) {\n try {\n const isSupported = await canEncodeAudio(codec, encodingOptions);\n if (isSupported) return codec;\n } catch (e) {\n logger.warn(`[selectAudioCodec] Check failed for ${codec}:`, e);\n }\n }\n const availableCodecs = await getEncodableAudioCodecs(\n undefined,\n encodingOptions,\n );\n throw new NoSupportedAudioCodecError(preferredCodecs, availableCodecs);\n}\n\nfunction downloadBlob(blob: Blob, filename: string): void {\n const url = URL.createObjectURL(blob);\n const a = document.createElement(\"a\");\n a.href = url;\n a.download = filename;\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n URL.revokeObjectURL(url);\n}\n\n// ============================================================================\n// Public API\n// ============================================================================\n\nexport async function getSupportedAudioCodecs(options?: {\n numberOfChannels?: number;\n sampleRate?: number;\n bitrate?: number;\n}): Promise<AudioCodec[]> {\n const {\n numberOfChannels = 2,\n sampleRate = 48000,\n bitrate = 128000,\n } = options ?? {};\n return getEncodableAudioCodecs(undefined, {\n numberOfChannels,\n sampleRate,\n bitrate,\n });\n}\n\n/**\n * Renders a timegroup to an MP4 video file.\n *\n * Uses the EXACT same code path as thumbnail generation (captureFromClone).\n * This ensures consistency - if thumbnails work, video export works.\n */\nexport async function renderTimegroupToVideo(\n timegroup: EFTimegroup,\n options: RenderToVideoOptions = {},\n): Promise<Uint8Array | undefined> {\n const config = resolveConfig(timegroup, options);\n const { signal, onProgress } = options;\n\n const checkCancelled = () => {\n if (signal?.aborted) throw new RenderCancelledError();\n };\n\n resetRenderState();\n\n // =========================================================================\n // Create render clone - EXACT same as captureBatch in EFTimegroup\n // =========================================================================\n const { clone: renderClone, cleanup: cleanupRenderClone } =\n await timegroup.createRenderClone();\n\n // Build timestamps array for frame loop\n const timestamps: number[] = [];\n for (let i = 0; i < config.totalFrames; i++) {\n timestamps.push(config.startMs + i * config.frameDurationMs);\n }\n\n // =========================================================================\n // Set up video encoding\n // =========================================================================\n let output: Output | null = null;\n let videoSource: CanvasSource | null = null;\n let audioSource: AudioBufferSource | null = null;\n let target: BufferTarget | StreamTarget | null = null;\n let fileStream: {\n writable: WritableStream<Uint8Array>;\n close: () => Promise<void>;\n } | null = null;\n let useStreaming = false;\n let encodingCanvas: OffscreenCanvas | null = null;\n let encodingCtx: OffscreenCanvasRenderingContext2D | null = null;\n\n if (!config.benchmarkMode) {\n // Check for custom writable stream first (for programmatic streaming)\n if (options.customWritableStream) {\n target = new StreamTarget(options.customWritableStream as any);\n output = new Output({\n format: new Mp4OutputFormat({ fastStart: \"fragmented\" }),\n target,\n });\n useStreaming = true;\n } else if (config.streaming) {\n fileStream = await getFileWritableStream(config.filename);\n useStreaming = fileStream !== null;\n\n if (useStreaming && fileStream) {\n target = new StreamTarget(fileStream.writable as any);\n output = new Output({\n format: new Mp4OutputFormat({ fastStart: \"fragmented\" }),\n target,\n });\n }\n }\n\n if (!target) {\n target = new BufferTarget();\n output = new Output({ format: new Mp4OutputFormat(), target });\n }\n\n encodingCanvas = new OffscreenCanvas(config.videoWidth, config.videoHeight);\n encodingCtx = encodingCanvas.getContext(\"2d\");\n if (!encodingCtx) {\n cleanupRenderClone();\n throw new Error(\"Failed to get encoding canvas context\");\n }\n\n if (!output) {\n throw new Error(\"Output not initialized\");\n }\n\n const videoConfig: VideoEncodingConfig = {\n codec: config.codec,\n bitrate: config.bitrate,\n keyFrameInterval: config.keyFrameInterval,\n };\n videoSource = new CanvasSource(encodingCanvas, videoConfig);\n output.addVideoTrack(videoSource);\n\n if (config.includeAudio) {\n const selectedCodec = await selectAudioCodec(\n config.preferredAudioCodecs,\n {\n numberOfChannels: 2,\n sampleRate: 48000,\n bitrate: config.audioBitrate,\n },\n );\n const audioConfig: AudioEncodingConfig = {\n codec: selectedCodec,\n bitrate: config.audioBitrate,\n };\n audioSource = new AudioBufferSource(audioConfig);\n output.addAudioTrack(audioSource);\n }\n\n await output.start();\n }\n\n // =========================================================================\n // Setup for per-frame passive structure rebuilding (like live preview)\n // =========================================================================\n // Create RenderContext for caching across all frames\n const renderContext = new RenderContext();\n\n // Create preview container with proper styling (reusable, content rebuilt each frame)\n // Use unscaled dimensions for the preview container (which holds the full-size clone)\n const containerWidth = timegroup.offsetWidth || 1920;\n const containerHeight = timegroup.offsetHeight || 1080;\n const previewContainer = createPreviewContainer({\n width: containerWidth,\n height: containerHeight,\n background: getComputedStyle(timegroup).background || \"#000\",\n });\n\n // Setup for direct serialization\n logger.debug(`[renderTimegroupToVideo] Using direct timeline serialization`);\n\n // Attach renderClone to container\n previewContainer.appendChild(renderClone);\n\n // Add ef-render-clone-container class for CSS selectors and debugging\n previewContainer.classList.add(\"ef-render-clone-container\");\n\n // CRITICAL: Attach container to document so getComputedStyle returns actual values\n // Without this, all computed styles are empty strings!\n // Hide the container OFF-SCREEN but do NOT use visibility:hidden because:\n // 1. visibility:hidden is inherited by all children\n // 2. seekForRender checks getComputedStyle().visibility and skips \"hidden\" subtrees\n // 3. This would cause FrameController to skip rendering all nested content\n previewContainer.style.cssText +=\n \";position:fixed;left:-99999px;top:-99999px;pointer-events:none;\";\n document.body.appendChild(previewContainer);\n\n // Force layout/reflow so getComputedStyle returns correct values\n void renderClone.offsetHeight;\n logger.debug(\n `[renderTimegroupToVideo] Attached previewContainer to document.body (off-screen) for style computation`,\n );\n\n // =========================================================================\n // Frame loop - DEEP PIPELINE: overlap encode + render + prepare\n // =========================================================================\n const renderStartTime = performance.now();\n let lastRenderedAudioEndMs = config.startMs;\n const audioChunkDurationMs = 2000;\n\n // Reusable thumbnail canvas for preview (no encoding, just draw to canvas)\n let thumbCanvas: HTMLCanvasElement | null = null;\n let thumbCtx: CanvasRenderingContext2D | null = null;\n if (onProgress && config.progressPreviewInterval > 0) {\n const previewWidth = 160;\n const previewHeight = Math.round(\n previewWidth * (config.videoHeight / config.videoWidth),\n );\n thumbCanvas = document.createElement(\"canvas\");\n thumbCanvas.width = previewWidth;\n thumbCanvas.height = previewHeight;\n thumbCtx = thumbCanvas.getContext(\"2d\");\n }\n\n try {\n // ========================================================================\n // OVERLAPPED PIPELINE: image loading runs parallel with seek+serialize\n // ========================================================================\n // The clone can only seek one frame at a time, and serialization must\n // capture the DOM before the next seek. But image loading (data URI →\n // Image) is independent of the clone and runs in the background.\n //\n // Per-frame timeline:\n // [seek(N)] → [serialize(N)] → [image.load(N) in background...]\n // └─ [seek(N+1)] → [serialize(N+1)] → ...\n // └─ encode(N) when image resolves\n\n type PendingFrame = {\n frameIndex: number;\n timeMs: number;\n timestampS: number;\n resolved: HTMLImageElement | null;\n promise: Promise<HTMLImageElement>;\n };\n\n const MAX_AHEAD = 2;\n const pendingFrames: PendingFrame[] = [];\n let nextSeekFrame = 0;\n let encodedFrames = 0;\n\n while (encodedFrames < config.totalFrames) {\n checkCancelled();\n\n // ==================================================================\n // PHASE 1: Fill pipeline — seek+serialize ahead while images load\n // ==================================================================\n while (\n nextSeekFrame < config.totalFrames &&\n pendingFrames.length < MAX_AHEAD\n ) {\n const fi = nextSeekFrame;\n const timeMs = timestamps[fi]!;\n const timestampS = (fi * config.frameDurationMs) / 1000;\n\n await renderClone.seekForRender(timeMs);\n\n const entry: PendingFrame = {\n frameIndex: fi,\n timeMs,\n timestampS,\n resolved: null,\n promise: null!,\n };\n\n // Wait for video content if using blocking mode\n if (config.contentReadyMode === \"blocking\") {\n await waitForVideoContent(\n renderClone,\n timeMs,\n config.blockingTimeoutMs,\n );\n }\n\n if (config.canvasMode === \"native\") {\n const canvas = await renderToImageNative(\n renderClone,\n config.width,\n config.height,\n {\n skipDprScaling: true,\n },\n );\n entry.resolved = canvas as any as HTMLImageElement;\n entry.promise = Promise.resolve(entry.resolved);\n } else {\n // Synchronous capture: walks DOM + snapshots canvas pixels.\n // Returns immediately — clone is free for next seek.\n // Encoding (canvas→base64, SVG assembly) and image loading\n // all resolve in the background.\n const dataUriPromise = captureTimelineToDataUri(\n renderClone,\n config.width,\n config.height,\n {\n renderContext,\n canvasScale: config.scale,\n timeMs,\n },\n );\n\n entry.promise = dataUriPromise.then((dataUri) => {\n return new Promise<HTMLImageElement>((resolve, reject) => {\n const image = new Image();\n image.onload = () => {\n entry.resolved = image;\n resolve(image);\n };\n image.onerror = (e) => {\n console.error(`[Render] frame ${fi} image load error:`, e);\n reject(new Error(`Failed to load image from data URI`));\n };\n image.src = dataUri;\n });\n });\n }\n\n pendingFrames.push(entry);\n nextSeekFrame++;\n }\n\n // ==================================================================\n // PHASE 2: Encode next frame in order (await if not yet loaded)\n // ==================================================================\n const head = pendingFrames.shift()!;\n const preloaded = head.resolved !== null;\n let image: HTMLImageElement;\n if (preloaded) {\n image = head.resolved!;\n } else {\n image = await head.promise;\n }\n\n if (\n audioSource &&\n head.timeMs >= lastRenderedAudioEndMs + audioChunkDurationMs\n ) {\n const chunkEndMs = Math.min(\n head.timeMs + audioChunkDurationMs,\n config.endMs,\n );\n try {\n const audioBuffer = await timegroup.renderAudio(\n lastRenderedAudioEndMs,\n chunkEndMs,\n signal,\n );\n if (audioBuffer && audioBuffer.length > 0) {\n await audioSource.add(audioBuffer);\n }\n } catch (e) {\n /* Audio render failures are non-fatal */\n }\n lastRenderedAudioEndMs = chunkEndMs;\n }\n\n if (videoSource && output && encodingCtx) {\n encodingCtx.drawImage(\n image,\n 0,\n 0,\n image.width,\n image.height,\n 0,\n 0,\n config.videoWidth,\n config.videoHeight,\n );\n await videoSource.add(head.timestampS, config.frameDurationS);\n }\n\n // ==================================================================\n // Progress reporting\n // ==================================================================\n encodedFrames++;\n const currentFrame = encodedFrames;\n const progress = currentFrame / config.totalFrames;\n const renderedMs = currentFrame * config.frameDurationMs;\n const elapsedMs = performance.now() - renderStartTime;\n const msPerFrame = elapsedMs / currentFrame;\n const remainingFrames = config.totalFrames - currentFrame;\n const estimatedRemainingMs = remainingFrames * msPerFrame;\n const speedMultiplier = renderedMs / elapsedMs;\n\n if (\n thumbCanvas &&\n thumbCtx &&\n head.frameIndex % config.progressPreviewInterval === 0\n ) {\n thumbCtx.drawImage(image, 0, 0, thumbCanvas.width, thumbCanvas.height);\n }\n\n onProgress?.({\n progress,\n currentFrame,\n totalFrames: config.totalFrames,\n renderedMs,\n totalDurationMs: config.renderDurationMs,\n elapsedMs,\n estimatedRemainingMs,\n speedMultiplier,\n framePreviewCanvas: thumbCanvas || undefined,\n });\n }\n\n // Render remaining audio\n if (audioSource && lastRenderedAudioEndMs < config.endMs) {\n try {\n const audioBuffer = await timegroup.renderAudio(\n lastRenderedAudioEndMs,\n config.endMs,\n signal,\n );\n if (audioBuffer && audioBuffer.length > 0) {\n await audioSource.add(audioBuffer);\n }\n } catch (e) {\n /* Audio render failures are non-fatal */\n }\n }\n\n if (config.benchmarkMode) {\n return undefined;\n }\n\n await output!.finalize();\n\n if (useStreaming) {\n // Streaming mode: chunks already sent via customWritableStream or file stream\n return undefined;\n } else {\n const bufferTarget = target as BufferTarget;\n const videoBuffer = bufferTarget.buffer;\n if (!videoBuffer) {\n throw new Error(\"Video encoding failed: no buffer produced\");\n }\n\n if (config.returnBuffer) {\n return new Uint8Array(videoBuffer);\n }\n\n const videoBlob = new Blob([videoBuffer], { type: \"video/mp4\" });\n downloadBlob(videoBlob, config.filename);\n return undefined;\n }\n } finally {\n renderContext.dispose();\n 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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"}
|
|
1
|
+
{"version":3,"file":"renderTimegroupToVideo.js","names":["timestamps: number[]","output: Output | null","videoSource: CanvasSource | null","audioSource: AudioBufferSource | null","target: BufferTarget | StreamTarget | null","fileStream: {\n writable: WritableStream<Uint8Array>;\n close: () => Promise<void>;\n } | null","encodingCanvas: OffscreenCanvas | null","encodingCtx: OffscreenCanvasRenderingContext2D | null","videoConfig: VideoEncodingConfig","thumbCanvas: HTMLCanvasElement | null","thumbCtx: CanvasRenderingContext2D | null","pendingFrames: PendingFrame[]","entry: PendingFrame","image","image: HTMLImageElement"],"sources":["../../src/preview/renderTimegroupToVideo.ts"],"sourcesContent":["/**\n * Video rendering for timegroups using direct serialization.\n *\n * Architecture:\n * - Creates a render clone of the timeline\n * - For each frame:\n * 1. Seeks the clone to the target time\n * 2. Executes frame tasks (SVG updates, canvas draws, etc.)\n * 3. Serializes the live DOM directly to SVG+foreignObject data URI\n * 4. Renders to image and encodes to video\n *\n * RenderContext provides pixel caching across frames for performance.\n */\n\nimport { logger } from \"./logger.js\";\nimport {\n Output,\n Mp4OutputFormat,\n BufferTarget,\n StreamTarget,\n CanvasSource,\n AudioBufferSource,\n QUALITY_HIGH,\n canEncodeAudio,\n getEncodableAudioCodecs,\n type VideoEncodingConfig,\n type AudioEncodingConfig,\n type AudioCodec,\n} from \"mediabunny\";\nimport type { EFTimegroup } from \"../elements/EFTimegroup.js\";\nimport type { RenderToVideoOptions } from \"./renderTimegroupToVideo.types.js\";\nimport type { ContentReadyMode } from \"./renderTimegroupToCanvas.types.js\";\nimport {\n resetRenderState,\n waitForVideoContent,\n} from \"./renderTimegroupToCanvas.js\";\nimport { captureTimelineToDataUri } from \"./rendering/serializeTimelineDirect.js\";\nimport { renderToImageNative } from \"./rendering/renderToImageNative.js\";\nimport { isNativeCanvasApiAvailable } from \"./previewSettings.js\";\nimport { createPreviewContainer } from \"./previewTypes.js\";\nimport { RenderContext } from \"./RenderContext.js\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\n// Re-export types from type-only module (zero side effects)\nexport type {\n RenderProgress,\n RenderToVideoOptions,\n} from \"./renderTimegroupToVideo.types.js\";\n\n// ============================================================================\n// Errors\n// ============================================================================\n\nexport class NoSupportedAudioCodecError extends Error {\n constructor(requestedCodecs: AudioCodec[], availableCodecs: AudioCodec[]) {\n super(\n `No supported audio codec found. Requested: [${requestedCodecs.join(\", \")}], ` +\n `Available: [${availableCodecs.length > 0 ? availableCodecs.join(\", \") : \"none\"}]`,\n );\n this.name = \"NoSupportedAudioCodecError\";\n }\n}\n\nexport class RenderCancelledError extends Error {\n constructor() {\n super(\"Render cancelled\");\n this.name = \"RenderCancelledError\";\n }\n}\n\n// ============================================================================\n// Configuration\n// ============================================================================\n\ninterface ResolvedConfig {\n fps: number;\n codec: \"avc\" | \"hevc\" | \"vp9\" | \"av1\" | \"vp8\";\n bitrate: number;\n filename: string;\n scale: number;\n keyFrameInterval: number;\n startMs: number;\n endMs: number;\n renderDurationMs: number;\n width: number;\n height: number;\n videoWidth: number;\n videoHeight: number;\n totalFrames: number;\n frameDurationMs: number;\n frameDurationS: number;\n streaming: boolean;\n includeAudio: boolean;\n audioBitrate: number;\n contentReadyMode: ContentReadyMode;\n blockingTimeoutMs: number;\n returnBuffer: boolean;\n preferredAudioCodecs: AudioCodec[];\n benchmarkMode: boolean;\n progressPreviewInterval: number;\n canvasMode: \"native\" | \"foreignObject\";\n}\n\nfunction resolveConfig(\n timegroup: EFTimegroup,\n options: RenderToVideoOptions = {},\n): ResolvedConfig {\n const fps = options.fps ?? timegroup.effectiveFps ?? 30;\n const codec = options.codec ?? \"avc\";\n const bitrate = options.bitrate ?? 8_000_000;\n const filename = options.filename ?? \"timegroup-video.mp4\";\n const scale = options.scale ?? 1;\n const keyFrameInterval = options.keyFrameInterval ?? 2;\n const streaming = options.streaming ?? true;\n const includeAudio = options.includeAudio ?? true;\n const audioBitrate = options.audioBitrate ?? 128_000;\n const contentReadyMode = options.contentReadyMode ?? \"blocking\";\n const blockingTimeoutMs = options.blockingTimeoutMs ?? 5000;\n const returnBuffer = options.returnBuffer ?? false;\n const preferredAudioCodecs = options.preferredAudioCodecs ?? [\"aac\", \"opus\"];\n const benchmarkMode = options.benchmarkMode ?? false;\n // Preview generation now uses canvas reference (no encoding) - cheap to enable!\n // Defaults to 60 frames (every 2 seconds at 30fps). Set to 0 to disable.\n const progressPreviewInterval = options.progressPreviewInterval ?? 60;\n\n const totalDurationMs = timegroup.durationMs;\n if (!totalDurationMs || totalDurationMs <= 0) {\n throw new Error(\"Timegroup has no duration\");\n }\n\n const startMs = Math.max(0, options.fromMs ?? 0);\n const endMs =\n options.toMs !== undefined\n ? Math.min(options.toMs, totalDurationMs)\n : totalDurationMs;\n const renderDurationMs = endMs - startMs;\n\n if (renderDurationMs <= 0) {\n throw new Error(`Invalid render range: from ${startMs}ms to ${endMs}ms`);\n }\n\n // Force layout reflow before reading dimensions\n void timegroup.offsetHeight;\n\n // Try multiple sources for dimensions (offsetWidth can be 0 in headless browsers)\n let timegroupWidth = timegroup.offsetWidth;\n let timegroupHeight = timegroup.offsetHeight;\n\n if (!timegroupWidth || !timegroupHeight) {\n const rect = timegroup.getBoundingClientRect();\n if (rect.width > 0 && rect.height > 0) {\n timegroupWidth = rect.width;\n timegroupHeight = rect.height;\n }\n }\n\n if (!timegroupWidth || !timegroupHeight) {\n const computed = getComputedStyle(timegroup);\n const cw = parseFloat(computed.width);\n const ch = parseFloat(computed.height);\n if (cw > 0 && ch > 0) {\n timegroupWidth = cw;\n timegroupHeight = ch;\n }\n }\n\n if (!timegroupWidth || !timegroupHeight) {\n throw new Error(\n `Timegroup has no dimensions (${timegroupWidth}x${timegroupHeight}). ` +\n `Ensure the timegroup element is in the document and has explicit width/height styles ` +\n `(e.g., class=\"w-[1920px] h-[1080px]\")`,\n );\n }\n const width = Math.floor(timegroupWidth * scale);\n const height = Math.floor(timegroupHeight * scale);\n\n const videoWidth = width % 2 === 0 ? width : width - 1;\n const videoHeight = height % 2 === 0 ? height : height - 1;\n\n const frameDurationMs = 1000 / fps;\n const totalFrames = Math.ceil(renderDurationMs / frameDurationMs);\n const frameDurationS = frameDurationMs / 1000;\n\n // Determine effective canvas mode:\n // 1. If explicitly specified, use that (with fallback if native not available)\n // 2. If not specified, default to foreignObject for compatibility\n const canvasMode = (() => {\n const requested = options.canvasMode;\n if (!requested) return \"foreignObject\";\n if (requested === \"native\" && !isNativeCanvasApiAvailable()) {\n logger.debug(\n \"[renderTimegroupToVideo] Native canvas mode requested but not available, falling back to foreignObject\",\n );\n return \"foreignObject\";\n }\n return requested;\n })();\n\n return {\n fps,\n codec,\n bitrate,\n filename,\n scale,\n keyFrameInterval,\n startMs,\n endMs,\n renderDurationMs,\n width,\n height,\n videoWidth,\n videoHeight,\n totalFrames,\n frameDurationMs,\n frameDurationS,\n streaming,\n includeAudio,\n audioBitrate,\n contentReadyMode,\n blockingTimeoutMs,\n returnBuffer,\n preferredAudioCodecs,\n benchmarkMode,\n progressPreviewInterval,\n canvasMode,\n };\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction isFileSystemAccessSupported(): boolean {\n return typeof window !== \"undefined\" && \"showSaveFilePicker\" in window;\n}\n\nasync function getFileWritableStream(filename: string): Promise<{\n writable: WritableStream<Uint8Array>;\n close: () => Promise<void>;\n} | null> {\n if (!isFileSystemAccessSupported()) {\n return null;\n }\n\n try {\n const fileHandle = await (window as any).showSaveFilePicker({\n suggestedName: filename,\n types: [{ description: \"MP4 Video\", accept: { \"video/mp4\": [\".mp4\"] } }],\n });\n const writable = await fileHandle.createWritable();\n return {\n writable,\n close: async () => {\n await writable.close();\n },\n };\n } catch (e) {\n if ((e as Error).name !== \"AbortError\") {\n logger.warn(\"[renderToVideo] File System Access failed:\", e);\n }\n return null;\n }\n}\n\nasync function selectAudioCodec(\n preferredCodecs: AudioCodec[],\n encodingOptions: {\n numberOfChannels: number;\n sampleRate: number;\n bitrate: number;\n },\n): Promise<AudioCodec> {\n for (const codec of preferredCodecs) {\n try {\n const isSupported = await canEncodeAudio(codec, encodingOptions);\n if (isSupported) return codec;\n } catch (e) {\n logger.warn(`[selectAudioCodec] Check failed for ${codec}:`, e);\n }\n }\n const availableCodecs = await getEncodableAudioCodecs(\n undefined,\n encodingOptions,\n );\n throw new NoSupportedAudioCodecError(preferredCodecs, availableCodecs);\n}\n\nfunction downloadBlob(blob: Blob, filename: string): void {\n const url = URL.createObjectURL(blob);\n const a = document.createElement(\"a\");\n a.href = url;\n a.download = filename;\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n URL.revokeObjectURL(url);\n}\n\n// ============================================================================\n// Public API\n// ============================================================================\n\nexport async function getSupportedAudioCodecs(options?: {\n numberOfChannels?: number;\n sampleRate?: number;\n bitrate?: number;\n}): Promise<AudioCodec[]> {\n const {\n numberOfChannels = 2,\n sampleRate = 48000,\n bitrate = 128000,\n } = options ?? {};\n return getEncodableAudioCodecs(undefined, {\n numberOfChannels,\n sampleRate,\n bitrate,\n });\n}\n\n/**\n * Renders a timegroup to an MP4 video file.\n *\n * Uses the EXACT same code path as thumbnail generation (captureFromClone).\n * This ensures consistency - if thumbnails work, video export works.\n */\nexport async function renderTimegroupToVideo(\n timegroup: EFTimegroup,\n options: RenderToVideoOptions = {},\n): Promise<Uint8Array | undefined> {\n const config = resolveConfig(timegroup, options);\n const { signal, onProgress } = options;\n\n const checkCancelled = () => {\n if (signal?.aborted) throw new RenderCancelledError();\n };\n\n resetRenderState();\n\n // =========================================================================\n // Create render clone - EXACT same as captureBatch in EFTimegroup\n // =========================================================================\n const {\n clone: renderClone,\n container: cloneContainer,\n cleanup: cleanupRenderClone,\n } = await timegroup.createRenderClone();\n\n // Build timestamps array for frame loop\n const timestamps: number[] = [];\n for (let i = 0; i < config.totalFrames; i++) {\n timestamps.push(config.startMs + i * config.frameDurationMs);\n }\n\n // =========================================================================\n // Set up video encoding\n // =========================================================================\n let output: Output | null = null;\n let videoSource: CanvasSource | null = null;\n let audioSource: AudioBufferSource | null = null;\n let target: BufferTarget | StreamTarget | null = null;\n let fileStream: {\n writable: WritableStream<Uint8Array>;\n close: () => Promise<void>;\n } | null = null;\n let useStreaming = false;\n let encodingCanvas: OffscreenCanvas | null = null;\n let encodingCtx: OffscreenCanvasRenderingContext2D | null = null;\n\n if (!config.benchmarkMode) {\n // Check for custom writable stream first (for programmatic streaming)\n if (options.customWritableStream) {\n target = new StreamTarget(options.customWritableStream as any);\n output = new Output({\n format: new Mp4OutputFormat({ fastStart: \"fragmented\" }),\n target,\n });\n useStreaming = true;\n } else if (config.streaming) {\n fileStream = await getFileWritableStream(config.filename);\n useStreaming = fileStream !== null;\n\n if (useStreaming && fileStream) {\n target = new StreamTarget(fileStream.writable as any);\n output = new Output({\n format: new Mp4OutputFormat({ fastStart: \"fragmented\" }),\n target,\n });\n }\n }\n\n if (!target) {\n target = new BufferTarget();\n output = new Output({ format: new Mp4OutputFormat(), target });\n }\n\n encodingCanvas = new OffscreenCanvas(config.videoWidth, config.videoHeight);\n encodingCtx = encodingCanvas.getContext(\"2d\");\n if (!encodingCtx) {\n cleanupRenderClone();\n throw new Error(\"Failed to get encoding canvas context\");\n }\n\n if (!output) {\n throw new Error(\"Output not initialized\");\n }\n\n const videoConfig: VideoEncodingConfig = {\n codec: config.codec,\n bitrate: config.bitrate,\n keyFrameInterval: config.keyFrameInterval,\n };\n videoSource = new CanvasSource(encodingCanvas, videoConfig);\n output.addVideoTrack(videoSource);\n\n if (config.includeAudio) {\n const selectedCodec = await selectAudioCodec(\n config.preferredAudioCodecs,\n {\n numberOfChannels: 2,\n sampleRate: 48000,\n bitrate: config.audioBitrate,\n },\n );\n const audioConfig: AudioEncodingConfig = {\n codec: selectedCodec,\n bitrate: config.audioBitrate,\n };\n audioSource = new AudioBufferSource(audioConfig);\n output.addAudioTrack(audioSource);\n }\n\n await output.start();\n }\n\n // =========================================================================\n // Setup for per-frame passive structure rebuilding (like live preview)\n // =========================================================================\n // Create RenderContext for caching across all frames\n const renderContext = new RenderContext();\n\n // Create preview container with proper styling (reusable, content rebuilt each frame)\n // Use unscaled dimensions for the preview container (which holds the full-size clone)\n const containerWidth = timegroup.offsetWidth || 1920;\n const containerHeight = timegroup.offsetHeight || 1080;\n const previewContainer = createPreviewContainer({\n width: containerWidth,\n height: containerHeight,\n background: getComputedStyle(timegroup).background || \"#000\",\n });\n\n // Setup for direct serialization\n logger.debug(`[renderTimegroupToVideo] Using direct timeline serialization`);\n\n // Attach clone container (keeps renderClone in its React-managed DOM position)\n previewContainer.appendChild(cloneContainer);\n\n // Add ef-render-clone-container class for CSS selectors and debugging\n previewContainer.classList.add(\"ef-render-clone-container\");\n\n // CRITICAL: Attach container to document so getComputedStyle returns actual values\n // Without this, all computed styles are empty strings!\n // Hide the container OFF-SCREEN but do NOT use visibility:hidden because:\n // 1. visibility:hidden is inherited by all children\n // 2. seekForRender checks getComputedStyle().visibility and skips \"hidden\" subtrees\n // 3. This would cause FrameController to skip rendering all nested content\n previewContainer.style.cssText +=\n \";position:fixed;left:-99999px;top:-99999px;pointer-events:none;\";\n document.body.appendChild(previewContainer);\n\n // Force layout/reflow so getComputedStyle returns correct values\n void renderClone.offsetHeight;\n logger.debug(\n `[renderTimegroupToVideo] Attached previewContainer to document.body (off-screen) for style computation`,\n );\n\n // =========================================================================\n // Frame loop - DEEP PIPELINE: overlap encode + render + prepare\n // =========================================================================\n const renderStartTime = performance.now();\n let lastRenderedAudioEndMs = config.startMs;\n const audioChunkDurationMs = 2000;\n\n // Reusable thumbnail canvas for preview (no encoding, just draw to canvas)\n let thumbCanvas: HTMLCanvasElement | null = null;\n let thumbCtx: CanvasRenderingContext2D | null = null;\n if (onProgress && config.progressPreviewInterval > 0) {\n const previewWidth = 160;\n const previewHeight = Math.round(\n previewWidth * (config.videoHeight / config.videoWidth),\n );\n thumbCanvas = document.createElement(\"canvas\");\n thumbCanvas.width = previewWidth;\n thumbCanvas.height = previewHeight;\n thumbCtx = thumbCanvas.getContext(\"2d\");\n }\n\n try {\n // ========================================================================\n // OVERLAPPED PIPELINE: image loading runs parallel with seek+serialize\n // ========================================================================\n // The clone can only seek one frame at a time, and serialization must\n // capture the DOM before the next seek. But image loading (data URI →\n // Image) is independent of the clone and runs in the background.\n //\n // Per-frame timeline:\n // [seek(N)] → [serialize(N)] → [image.load(N) in background...]\n // └─ [seek(N+1)] → [serialize(N+1)] → ...\n // └─ encode(N) when image resolves\n\n type PendingFrame = {\n frameIndex: number;\n timeMs: number;\n timestampS: number;\n resolved: HTMLImageElement | null;\n promise: Promise<HTMLImageElement>;\n };\n\n const MAX_AHEAD = 2;\n const pendingFrames: PendingFrame[] = [];\n let nextSeekFrame = 0;\n let encodedFrames = 0;\n\n while (encodedFrames < config.totalFrames) {\n checkCancelled();\n\n // ==================================================================\n // PHASE 1: Fill pipeline — seek+serialize ahead while images load\n // ==================================================================\n while (\n nextSeekFrame < config.totalFrames &&\n pendingFrames.length < MAX_AHEAD\n ) {\n const fi = nextSeekFrame;\n const timeMs = timestamps[fi]!;\n const timestampS = (fi * config.frameDurationMs) / 1000;\n\n await renderClone.seekForRender(timeMs);\n\n const entry: PendingFrame = {\n frameIndex: fi,\n timeMs,\n timestampS,\n resolved: null,\n promise: null!,\n };\n\n // Wait for video content if using blocking mode\n if (config.contentReadyMode === \"blocking\") {\n await waitForVideoContent(\n renderClone,\n timeMs,\n config.blockingTimeoutMs,\n );\n }\n\n if (config.canvasMode === \"native\") {\n const canvas = await renderToImageNative(\n renderClone,\n config.width,\n config.height,\n {\n skipDprScaling: true,\n },\n );\n entry.resolved = canvas as any as HTMLImageElement;\n entry.promise = Promise.resolve(entry.resolved);\n } else {\n // Synchronous capture: walks DOM + snapshots canvas pixels.\n // Returns immediately — clone is free for next seek.\n // Encoding (canvas→base64, SVG assembly) and image loading\n // all resolve in the background.\n const dataUriPromise = captureTimelineToDataUri(\n renderClone,\n config.width,\n config.height,\n {\n renderContext,\n canvasScale: config.scale,\n timeMs,\n },\n );\n\n entry.promise = dataUriPromise.then((dataUri) => {\n return new Promise<HTMLImageElement>((resolve, reject) => {\n const image = new Image();\n image.onload = () => {\n entry.resolved = image;\n resolve(image);\n };\n image.onerror = (e) => {\n console.error(`[Render] frame ${fi} image load error:`, e);\n reject(new Error(`Failed to load image from data URI`));\n };\n image.src = dataUri;\n });\n });\n }\n\n pendingFrames.push(entry);\n nextSeekFrame++;\n }\n\n // ==================================================================\n // PHASE 2: Encode next frame in order (await if not yet loaded)\n // ==================================================================\n const head = pendingFrames.shift()!;\n const preloaded = head.resolved !== null;\n let image: HTMLImageElement;\n if (preloaded) {\n image = head.resolved!;\n } else {\n image = await head.promise;\n }\n\n if (\n audioSource &&\n head.timeMs >= lastRenderedAudioEndMs + audioChunkDurationMs\n ) {\n const chunkEndMs = Math.min(\n head.timeMs + audioChunkDurationMs,\n config.endMs,\n );\n try {\n const audioBuffer = await timegroup.renderAudio(\n lastRenderedAudioEndMs,\n chunkEndMs,\n signal,\n );\n if (audioBuffer && audioBuffer.length > 0) {\n await audioSource.add(audioBuffer);\n }\n } catch (e) {\n /* Audio render failures are non-fatal */\n }\n lastRenderedAudioEndMs = chunkEndMs;\n }\n\n if (videoSource && output && encodingCtx) {\n encodingCtx.drawImage(\n image,\n 0,\n 0,\n image.width,\n image.height,\n 0,\n 0,\n config.videoWidth,\n config.videoHeight,\n );\n await videoSource.add(head.timestampS, config.frameDurationS);\n }\n\n // ==================================================================\n // Progress reporting\n // ==================================================================\n encodedFrames++;\n const currentFrame = encodedFrames;\n const progress = currentFrame / config.totalFrames;\n const renderedMs = currentFrame * config.frameDurationMs;\n const elapsedMs = performance.now() - renderStartTime;\n const msPerFrame = elapsedMs / currentFrame;\n const remainingFrames = config.totalFrames - currentFrame;\n const estimatedRemainingMs = remainingFrames * msPerFrame;\n const speedMultiplier = renderedMs / elapsedMs;\n\n if (\n thumbCanvas &&\n thumbCtx &&\n head.frameIndex % config.progressPreviewInterval === 0\n ) {\n thumbCtx.drawImage(image, 0, 0, thumbCanvas.width, thumbCanvas.height);\n }\n\n onProgress?.({\n progress,\n currentFrame,\n totalFrames: config.totalFrames,\n renderedMs,\n totalDurationMs: config.renderDurationMs,\n elapsedMs,\n estimatedRemainingMs,\n speedMultiplier,\n framePreviewCanvas: thumbCanvas || undefined,\n });\n }\n\n // Render remaining audio\n if (audioSource && lastRenderedAudioEndMs < config.endMs) {\n try {\n const audioBuffer = await timegroup.renderAudio(\n lastRenderedAudioEndMs,\n config.endMs,\n signal,\n );\n if (audioBuffer && audioBuffer.length > 0) {\n await audioSource.add(audioBuffer);\n }\n } catch (e) {\n /* Audio render failures are non-fatal */\n }\n }\n\n if (config.benchmarkMode) {\n return undefined;\n }\n\n await output!.finalize();\n\n if (useStreaming) {\n // Streaming mode: chunks already sent via customWritableStream or file stream\n return undefined;\n } else {\n const bufferTarget = target as BufferTarget;\n const videoBuffer = bufferTarget.buffer;\n if (!videoBuffer) {\n throw new Error(\"Video encoding failed: no buffer produced\");\n }\n\n if (config.returnBuffer) {\n return new Uint8Array(videoBuffer);\n }\n\n const videoBlob = new Blob([videoBuffer], { type: \"video/mp4\" });\n downloadBlob(videoBlob, config.filename);\n return undefined;\n }\n } finally {\n renderContext.dispose();\n // Remove previewContainer first — renderClone was moved into it, so it must be\n // detached before cleanupRenderClone() unmounts the React root that owns renderClone.\n if (previewContainer.parentNode) {\n previewContainer.parentNode.removeChild(previewContainer);\n }\n cleanupRenderClone();\n }\n}\n\nexport { QUALITY_HIGH };\nexport type { AudioCodec };\n"],"mappings":"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"}
|
|
@@ -26,14 +26,9 @@ async function resolveVideoConfig(video, options = {}) {
|
|
|
26
26
|
const endMs = options.toMs !== void 0 ? Math.min(options.toMs, effectiveDurationMs) : effectiveDurationMs;
|
|
27
27
|
const renderDurationMs = endMs - startMs;
|
|
28
28
|
if (renderDurationMs <= 0) throw new Error(`Invalid render range: from ${startMs}ms to ${endMs}ms`);
|
|
29
|
-
const mediaEngine = await video.getMediaEngine();
|
|
30
29
|
let width;
|
|
31
30
|
let height;
|
|
32
|
-
|
|
33
|
-
if (videoRendition?.width && videoRendition?.height) {
|
|
34
|
-
width = videoRendition.width;
|
|
35
|
-
height = videoRendition.height;
|
|
36
|
-
} else {
|
|
31
|
+
{
|
|
37
32
|
const firstFrame = await video.getVideoFrameAtSourceTime(trimStartMs, { quality: "main" });
|
|
38
33
|
try {
|
|
39
34
|
width = firstFrame.displayWidth;
|
|
@@ -128,6 +123,7 @@ async function renderVideoToVideo(video, options = {}) {
|
|
|
128
123
|
await video.waitForMediaDurations(signal);
|
|
129
124
|
checkCancelled();
|
|
130
125
|
const config = await resolveVideoConfig(video, options);
|
|
126
|
+
const pc = video.playbackController;
|
|
131
127
|
const computedStyle = getComputedStyle(video);
|
|
132
128
|
const cssFilter = computedStyle.filter;
|
|
133
129
|
const cssOpacity = parseFloat(computedStyle.opacity);
|
|
@@ -206,6 +202,7 @@ async function renderVideoToVideo(video, options = {}) {
|
|
|
206
202
|
let totalSeekMs = 0;
|
|
207
203
|
let totalDrawMs = 0;
|
|
208
204
|
let totalEncodeMs = 0;
|
|
205
|
+
pc?.suspendSelfRender();
|
|
209
206
|
try {
|
|
210
207
|
for (let frameIndex = 0; frameIndex < config.totalFrames; frameIndex++) {
|
|
211
208
|
checkCancelled();
|
|
@@ -278,6 +275,8 @@ async function renderVideoToVideo(video, options = {}) {
|
|
|
278
275
|
await output?.finalize();
|
|
279
276
|
} catch {}
|
|
280
277
|
throw error;
|
|
278
|
+
} finally {
|
|
279
|
+
pc?.resumeSelfRender();
|
|
281
280
|
}
|
|
282
281
|
}
|
|
283
282
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"renderVideoToVideo.js","names":["width: number","height: number","output: Output | null","videoSource: CanvasSource | null","audioSource: AudioBufferSource | null","target: BufferTarget | StreamTarget | null","fileStream: {\n writable: WritableStream<Uint8Array>;\n close: () => Promise<void>;\n } | null","thumbCanvas: HTMLCanvasElement | null","thumbCtx: CanvasRenderingContext2D | null"],"sources":["../../src/preview/renderVideoToVideo.ts"],"sourcesContent":["/**\n * Direct video-to-video rendering — fast path for single video elements.\n *\n * Bypasses the full DOM serialization pipeline (foreignObject/native canvas)\n * by decoding frames directly from the media engine and re-encoding to MP4.\n *\n * Supports CSS effects via canvas 2D context:\n * - filter (ctx.filter)\n * - opacity (ctx.globalAlpha)\n */\n\nimport {\n Output,\n Mp4OutputFormat,\n BufferTarget,\n StreamTarget,\n CanvasSource,\n AudioBufferSource,\n canEncodeAudio,\n getEncodableAudioCodecs,\n type VideoEncodingConfig,\n type AudioEncodingConfig,\n type AudioCodec,\n} from \"mediabunny\";\nimport type { EFVideo } from \"../elements/EFVideo.js\";\nimport {\n NoSupportedAudioCodecError,\n RenderCancelledError,\n} from \"./renderTimegroupToVideo.js\";\nimport type { RenderToVideoOptions } from \"./renderTimegroupToVideo.types.js\";\nimport { logger } from \"./logger.js\";\n\n// ============================================================================\n// Configuration\n// ============================================================================\n\ninterface ResolvedVideoConfig {\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 returnBuffer: boolean;\n preferredAudioCodecs: AudioCodec[];\n progressPreviewInterval: number;\n trimStartMs: number;\n}\n\nasync function resolveVideoConfig(\n video: EFVideo,\n options: RenderToVideoOptions = {},\n): Promise<ResolvedVideoConfig> {\n const fps = options.fps ?? 30;\n const codec = options.codec ?? \"avc\";\n const bitrate = options.bitrate ?? 8_000_000;\n const filename = options.filename ?? \"video-export.mp4\";\n const scale = options.scale ?? 1;\n const keyFrameInterval = options.keyFrameInterval ?? 2;\n const streaming = options.streaming ?? false;\n const includeAudio = options.includeAudio ?? true;\n const audioBitrate = options.audioBitrate ?? 128_000;\n const returnBuffer = options.returnBuffer ?? false;\n const preferredAudioCodecs = options.preferredAudioCodecs ?? [\"aac\", \"opus\"];\n const progressPreviewInterval = options.progressPreviewInterval ?? 60;\n\n const trimStartMs = video.trimStartMs ?? 0;\n const trimEndMs = video.trimEndMs ?? 0;\n const intrinsicDurationMs = video.intrinsicDurationMs;\n\n if (!intrinsicDurationMs || intrinsicDurationMs <= 0) {\n throw new Error(\n \"Video has no intrinsic duration. Ensure the media engine is loaded.\",\n );\n }\n\n const effectiveDurationMs = intrinsicDurationMs - trimStartMs - trimEndMs;\n if (effectiveDurationMs <= 0) {\n throw new Error(\n `Invalid trim range: trimStart=${trimStartMs}ms, trimEnd=${trimEndMs}ms, ` +\n `intrinsicDuration=${intrinsicDurationMs}ms leaves no content.`,\n );\n }\n\n const startMs =\n options.fromMs !== undefined ? Math.max(0, options.fromMs) : 0;\n const endMs =\n options.toMs !== undefined\n ? Math.min(options.toMs, effectiveDurationMs)\n : effectiveDurationMs;\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 // Determine video dimensions from the media engine rendition metadata\n const mediaEngine = await video.getMediaEngine();\n let width: number;\n let height: number;\n\n const videoRendition =\n mediaEngine?.getVideoRendition?.() || (mediaEngine as any)?.videoRendition;\n if (videoRendition?.width && videoRendition?.height) {\n width = videoRendition.width;\n height = videoRendition.height;\n } else {\n // Fall back: decode first frame to get dimensions\n const firstFrame = await video.getVideoFrameAtSourceTime(trimStartMs, {\n quality: \"main\",\n });\n try {\n width = firstFrame.displayWidth;\n height = firstFrame.displayHeight;\n } finally {\n firstFrame.close();\n }\n }\n\n const videoWidth = Math.floor(width * scale);\n const videoHeight = Math.floor(height * scale);\n // Ensure even dimensions for video encoding\n const evenWidth = videoWidth % 2 === 0 ? videoWidth : videoWidth - 1;\n const evenHeight = videoHeight % 2 === 0 ? videoHeight : videoHeight - 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: evenWidth,\n videoHeight: evenHeight,\n totalFrames,\n frameDurationMs,\n frameDurationS,\n streaming,\n includeAudio,\n audioBitrate,\n returnBuffer,\n preferredAudioCodecs,\n progressPreviewInterval,\n trimStartMs,\n };\n}\n\n// ============================================================================\n// Utilities (same as renderTimegroupToVideo — not exported from there)\n// ============================================================================\n\nfunction isFileSystemAccessSupported(): boolean {\n return typeof window !== \"undefined\" && \"showSaveFilePicker\" in window;\n}\n\nasync function getFileWritableStream(filename: string): Promise<{\n writable: WritableStream<Uint8Array>;\n close: () => Promise<void>;\n} | null> {\n if (!isFileSystemAccessSupported()) {\n return null;\n }\n\n try {\n const fileHandle = await (window as any).showSaveFilePicker({\n suggestedName: filename,\n types: [{ description: \"MP4 Video\", accept: { \"video/mp4\": [\".mp4\"] } }],\n });\n const writable = await fileHandle.createWritable();\n return {\n writable,\n close: async () => {\n await writable.close();\n },\n };\n } catch (e) {\n if ((e as Error).name !== \"AbortError\") {\n logger.warn(\"[renderVideoToVideo] File System Access failed:\", e);\n }\n return null;\n }\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\nasync function selectAudioCodec(\n preferredCodecs: AudioCodec[],\n encodingOptions: {\n numberOfChannels: number;\n sampleRate: number;\n bitrate: number;\n },\n): Promise<AudioCodec> {\n for (const codec of preferredCodecs) {\n try {\n const isSupported = await canEncodeAudio(codec, encodingOptions);\n if (isSupported) return codec;\n } catch (e) {\n logger.warn(`[selectAudioCodec] Check failed for ${codec}:`, e);\n }\n }\n const availableCodecs = await getEncodableAudioCodecs(\n undefined,\n encodingOptions,\n );\n throw new NoSupportedAudioCodecError(preferredCodecs, availableCodecs);\n}\n\n// ============================================================================\n// Main render function\n// ============================================================================\n\n/**\n * Render a single EFVideo element directly to MP4.\n *\n * This is the fast path: frames are decoded from the media engine,\n * drawn to an encoding canvas (with CSS filter/opacity applied),\n * and encoded to video. No DOM serialization involved.\n */\nexport async function renderVideoToVideo(\n video: EFVideo,\n options: RenderToVideoOptions = {},\n): Promise<Uint8Array | undefined> {\n const { signal, onProgress } = options;\n\n const checkCancelled = () => {\n if (signal?.aborted) throw new RenderCancelledError();\n };\n\n // Ensure media engine is loaded\n await video.waitForMediaDurations(signal);\n checkCancelled();\n\n const config = await resolveVideoConfig(video, options);\n\n // Read CSS effects once before the frame loop (values don't change during rendering)\n const computedStyle = getComputedStyle(video);\n const cssFilter = computedStyle.filter;\n const cssOpacity = parseFloat(computedStyle.opacity);\n const hasFilter = cssFilter && cssFilter !== \"none\";\n const hasOpacity = cssOpacity < 1;\n\n logger.debug(\n `[renderVideoToVideo] starting: ${config.totalFrames} frames, ` +\n `${config.videoWidth}x${config.videoHeight} @ ${config.fps}fps, ` +\n `trim=[${config.trimStartMs}, -${video.trimEndMs ?? 0}], ` +\n `css: filter=${hasFilter ? cssFilter : \"none\"}, opacity=${cssOpacity}`,\n );\n\n // =========================================================================\n // Set up video encoding\n // =========================================================================\n let output: Output | null = null;\n let videoSource: CanvasSource | null = null;\n let audioSource: AudioBufferSource | null = null;\n let target: BufferTarget | StreamTarget | null = null;\n let fileStream: {\n writable: WritableStream<Uint8Array>;\n close: () => Promise<void>;\n } | null = null;\n let useStreaming = false;\n\n const encodingCanvas = new OffscreenCanvas(\n config.videoWidth,\n config.videoHeight,\n );\n const encodingCtx = encodingCanvas.getContext(\n \"2d\",\n hasFilter || hasOpacity ? { willReadFrequently: true } : undefined,\n );\n if (!encodingCtx) {\n throw new Error(\"Failed to get encoding canvas context\");\n }\n\n if (hasFilter) {\n encodingCtx.filter = cssFilter;\n }\n if (hasOpacity) {\n encodingCtx.globalAlpha = cssOpacity;\n }\n\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 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\n // Use CanvasSource directly - filter and opacity don't require special handling\n videoSource = new CanvasSource(encodingCanvas, videoConfig);\n output.addVideoTrack(videoSource);\n\n if (config.includeAudio) {\n try {\n const selectedCodec = await selectAudioCodec(\n config.preferredAudioCodecs,\n {\n numberOfChannels: 2,\n sampleRate: 48000,\n bitrate: config.audioBitrate,\n },\n );\n const audioConfig: AudioEncodingConfig = {\n codec: selectedCodec,\n bitrate: config.audioBitrate,\n };\n audioSource = new AudioBufferSource(audioConfig);\n output.addAudioTrack(audioSource);\n } catch (e) {\n logger.warn(\n \"[renderVideoToVideo] Audio codec selection failed, rendering without audio:\",\n e,\n );\n }\n }\n\n await output.start();\n\n // =========================================================================\n // Frame loop\n // =========================================================================\n const renderStartTime = performance.now();\n let lastRenderedAudioEndMs = config.startMs;\n const audioChunkDurationMs = 2000;\n\n let thumbCanvas: HTMLCanvasElement | null = null;\n let thumbCtx: CanvasRenderingContext2D | null = null;\n\n if (config.progressPreviewInterval > 0) {\n const thumbScale = 160 / config.videoWidth;\n thumbCanvas = document.createElement(\"canvas\");\n thumbCanvas.width = Math.round(config.videoWidth * thumbScale);\n thumbCanvas.height = Math.round(config.videoHeight * thumbScale);\n thumbCtx = thumbCanvas.getContext(\"2d\");\n }\n\n let totalSeekMs = 0;\n let totalDrawMs = 0;\n let totalEncodeMs = 0;\n\n try {\n for (let frameIndex = 0; frameIndex < config.totalFrames; frameIndex++) {\n checkCancelled();\n\n const timelineTimeMs =\n config.startMs + frameIndex * config.frameDurationMs;\n const sourceTimeMs = timelineTimeMs + config.trimStartMs;\n const timestampS = (frameIndex * config.frameDurationMs) / 1000;\n\n // Decode frame\n const seekStart = performance.now();\n const videoFrame = await video.getVideoFrameAtSourceTime(sourceTimeMs, {\n quality: \"main\",\n signal,\n });\n totalSeekMs += performance.now() - seekStart;\n\n try {\n const drawStart = performance.now();\n\n encodingCtx.drawImage(\n videoFrame,\n 0,\n 0,\n videoFrame.displayWidth,\n videoFrame.displayHeight,\n 0,\n 0,\n config.videoWidth,\n config.videoHeight,\n );\n\n totalDrawMs += performance.now() - drawStart;\n } finally {\n videoFrame.close();\n }\n\n // Encode frame\n const encodeStart = performance.now();\n await videoSource!.add(timestampS, config.frameDurationS);\n totalEncodeMs += performance.now() - encodeStart;\n\n // Render audio in chunks\n if (\n audioSource &&\n timelineTimeMs >= lastRenderedAudioEndMs + audioChunkDurationMs\n ) {\n const chunkEndMs = Math.min(\n timelineTimeMs + audioChunkDurationMs,\n config.endMs,\n );\n try {\n const audioBuffer = await video.renderAudio(\n lastRenderedAudioEndMs,\n chunkEndMs,\n );\n if (audioBuffer && audioBuffer.length > 0) {\n await audioSource.add(audioBuffer);\n }\n } catch (e) {\n // Audio render failures are non-fatal\n }\n lastRenderedAudioEndMs = chunkEndMs;\n }\n\n // Progress preview thumbnail\n if (\n thumbCanvas &&\n thumbCtx &&\n frameIndex % config.progressPreviewInterval === 0\n ) {\n thumbCtx.drawImage(\n encodingCanvas as any,\n 0,\n 0,\n thumbCanvas.width,\n thumbCanvas.height,\n );\n }\n\n // Progress reporting\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 onProgress?.({\n progress,\n currentFrame,\n totalFrames: config.totalFrames,\n renderedMs,\n totalDurationMs: config.renderDurationMs,\n elapsedMs,\n estimatedRemainingMs,\n speedMultiplier,\n framePreviewCanvas: thumbCanvas || undefined,\n });\n }\n\n // Render remaining audio\n if (audioSource && lastRenderedAudioEndMs < config.endMs) {\n try {\n const audioBuffer = await video.renderAudio(\n lastRenderedAudioEndMs,\n config.endMs,\n );\n if (audioBuffer && audioBuffer.length > 0) {\n await audioSource.add(audioBuffer);\n }\n } catch (e) {\n // Audio render failures are non-fatal\n }\n }\n\n // =========================================================================\n // Finalize\n // =========================================================================\n const totalElapsed = performance.now() - renderStartTime;\n logger.debug(\n `[renderVideoToVideo] complete: ${config.totalFrames} frames in ${totalElapsed.toFixed(0)}ms ` +\n `(seek=${totalSeekMs.toFixed(0)}ms, draw=${totalDrawMs.toFixed(0)}ms, encode=${totalEncodeMs.toFixed(0)}ms) ` +\n `speed=${(config.renderDurationMs / totalElapsed).toFixed(1)}x`,\n );\n\n await output.finalize();\n\n if (useStreaming) {\n if (fileStream) {\n await fileStream.close();\n }\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 } catch (error) {\n // Clean up output on failure\n try {\n await output?.finalize();\n } catch {\n // Ignore finalize errors during cleanup\n }\n throw error;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"renderVideoToVideo.js","names":["width: number","height: number","output: Output | null","videoSource: CanvasSource | null","audioSource: AudioBufferSource | null","target: BufferTarget | StreamTarget | null","fileStream: {\n writable: WritableStream<Uint8Array>;\n close: () => Promise<void>;\n } | null","thumbCanvas: HTMLCanvasElement | null","thumbCtx: CanvasRenderingContext2D | null"],"sources":["../../src/preview/renderVideoToVideo.ts"],"sourcesContent":["/**\n * Direct video-to-video rendering — fast path for single video elements.\n *\n * Bypasses the full DOM serialization pipeline (foreignObject/native canvas)\n * by decoding frames directly from the media engine and re-encoding to MP4.\n *\n * Supports CSS effects via canvas 2D context:\n * - filter (ctx.filter)\n * - opacity (ctx.globalAlpha)\n */\n\nimport {\n Output,\n Mp4OutputFormat,\n BufferTarget,\n StreamTarget,\n CanvasSource,\n AudioBufferSource,\n canEncodeAudio,\n getEncodableAudioCodecs,\n type VideoEncodingConfig,\n type AudioEncodingConfig,\n type AudioCodec,\n} from \"mediabunny\";\nimport type { EFVideo } from \"../elements/EFVideo.js\";\nimport {\n NoSupportedAudioCodecError,\n RenderCancelledError,\n} from \"./renderTimegroupToVideo.js\";\nimport type { RenderToVideoOptions } from \"./renderTimegroupToVideo.types.js\";\nimport { logger } from \"./logger.js\";\n\n// ============================================================================\n// Configuration\n// ============================================================================\n\ninterface ResolvedVideoConfig {\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 returnBuffer: boolean;\n preferredAudioCodecs: AudioCodec[];\n progressPreviewInterval: number;\n trimStartMs: number;\n}\n\nasync function resolveVideoConfig(\n video: EFVideo,\n options: RenderToVideoOptions = {},\n): Promise<ResolvedVideoConfig> {\n const fps = options.fps ?? 30;\n const codec = options.codec ?? \"avc\";\n const bitrate = options.bitrate ?? 8_000_000;\n const filename = options.filename ?? \"video-export.mp4\";\n const scale = options.scale ?? 1;\n const keyFrameInterval = options.keyFrameInterval ?? 2;\n const streaming = options.streaming ?? false;\n const includeAudio = options.includeAudio ?? true;\n const audioBitrate = options.audioBitrate ?? 128_000;\n const returnBuffer = options.returnBuffer ?? false;\n const preferredAudioCodecs = options.preferredAudioCodecs ?? [\"aac\", \"opus\"];\n const progressPreviewInterval = options.progressPreviewInterval ?? 60;\n\n const trimStartMs = video.trimStartMs ?? 0;\n const trimEndMs = video.trimEndMs ?? 0;\n const intrinsicDurationMs = video.intrinsicDurationMs;\n\n if (!intrinsicDurationMs || intrinsicDurationMs <= 0) {\n throw new Error(\n \"Video has no intrinsic duration. Ensure the media engine is loaded.\",\n );\n }\n\n const effectiveDurationMs = intrinsicDurationMs - trimStartMs - trimEndMs;\n if (effectiveDurationMs <= 0) {\n throw new Error(\n `Invalid trim range: trimStart=${trimStartMs}ms, trimEnd=${trimEndMs}ms, ` +\n `intrinsicDuration=${intrinsicDurationMs}ms leaves no content.`,\n );\n }\n\n const startMs =\n options.fromMs !== undefined ? Math.max(0, options.fromMs) : 0;\n const endMs =\n options.toMs !== undefined\n ? Math.min(options.toMs, effectiveDurationMs)\n : effectiveDurationMs;\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 let width: number;\n let height: number;\n\n // Decode first frame to determine dimensions\n {\n const firstFrame = await video.getVideoFrameAtSourceTime(trimStartMs, {\n quality: \"main\",\n });\n try {\n width = firstFrame.displayWidth;\n height = firstFrame.displayHeight;\n } finally {\n firstFrame.close();\n }\n }\n\n const videoWidth = Math.floor(width * scale);\n const videoHeight = Math.floor(height * scale);\n // Ensure even dimensions for video encoding\n const evenWidth = videoWidth % 2 === 0 ? videoWidth : videoWidth - 1;\n const evenHeight = videoHeight % 2 === 0 ? videoHeight : videoHeight - 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: evenWidth,\n videoHeight: evenHeight,\n totalFrames,\n frameDurationMs,\n frameDurationS,\n streaming,\n includeAudio,\n audioBitrate,\n returnBuffer,\n preferredAudioCodecs,\n progressPreviewInterval,\n trimStartMs,\n };\n}\n\n// ============================================================================\n// Utilities (same as renderTimegroupToVideo — not exported from there)\n// ============================================================================\n\nfunction isFileSystemAccessSupported(): boolean {\n return typeof window !== \"undefined\" && \"showSaveFilePicker\" in window;\n}\n\nasync function getFileWritableStream(filename: string): Promise<{\n writable: WritableStream<Uint8Array>;\n close: () => Promise<void>;\n} | null> {\n if (!isFileSystemAccessSupported()) {\n return null;\n }\n\n try {\n const fileHandle = await (window as any).showSaveFilePicker({\n suggestedName: filename,\n types: [{ description: \"MP4 Video\", accept: { \"video/mp4\": [\".mp4\"] } }],\n });\n const writable = await fileHandle.createWritable();\n return {\n writable,\n close: async () => {\n await writable.close();\n },\n };\n } catch (e) {\n if ((e as Error).name !== \"AbortError\") {\n logger.warn(\"[renderVideoToVideo] File System Access failed:\", e);\n }\n return null;\n }\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\nasync function selectAudioCodec(\n preferredCodecs: AudioCodec[],\n encodingOptions: {\n numberOfChannels: number;\n sampleRate: number;\n bitrate: number;\n },\n): Promise<AudioCodec> {\n for (const codec of preferredCodecs) {\n try {\n const isSupported = await canEncodeAudio(codec, encodingOptions);\n if (isSupported) return codec;\n } catch (e) {\n logger.warn(`[selectAudioCodec] Check failed for ${codec}:`, e);\n }\n }\n const availableCodecs = await getEncodableAudioCodecs(\n undefined,\n encodingOptions,\n );\n throw new NoSupportedAudioCodecError(preferredCodecs, availableCodecs);\n}\n\n// ============================================================================\n// Main render function\n// ============================================================================\n\n/**\n * Render a single EFVideo element directly to MP4.\n *\n * This is the fast path: frames are decoded from the media engine,\n * drawn to an encoding canvas (with CSS filter/opacity applied),\n * and encoded to video. No DOM serialization involved.\n */\nexport async function renderVideoToVideo(\n video: EFVideo,\n options: RenderToVideoOptions = {},\n): Promise<Uint8Array | undefined> {\n const { signal, onProgress } = options;\n\n const checkCancelled = () => {\n if (signal?.aborted) throw new RenderCancelledError();\n };\n\n // Ensure media engine is loaded\n await video.waitForMediaDurations(signal);\n checkCancelled();\n\n const config = await resolveVideoConfig(video, options);\n\n // Suspend the PlaybackController's self-render loop for the duration of\n // this render to prevent concurrent frame fetches from interfering.\n const pc = (video as any).playbackController;\n\n // Read CSS effects once before the frame loop (values don't change during rendering)\n const computedStyle = getComputedStyle(video);\n const cssFilter = computedStyle.filter;\n const cssOpacity = parseFloat(computedStyle.opacity);\n const hasFilter = cssFilter && cssFilter !== \"none\";\n const hasOpacity = cssOpacity < 1;\n\n logger.debug(\n `[renderVideoToVideo] starting: ${config.totalFrames} frames, ` +\n `${config.videoWidth}x${config.videoHeight} @ ${config.fps}fps, ` +\n `trim=[${config.trimStartMs}, -${video.trimEndMs ?? 0}], ` +\n `css: filter=${hasFilter ? cssFilter : \"none\"}, opacity=${cssOpacity}`,\n );\n\n // =========================================================================\n // Set up video encoding\n // =========================================================================\n let output: Output | null = null;\n let videoSource: CanvasSource | null = null;\n let audioSource: AudioBufferSource | null = null;\n let target: BufferTarget | StreamTarget | null = null;\n let fileStream: {\n writable: WritableStream<Uint8Array>;\n close: () => Promise<void>;\n } | null = null;\n let useStreaming = false;\n\n const encodingCanvas = new OffscreenCanvas(\n config.videoWidth,\n config.videoHeight,\n );\n const encodingCtx = encodingCanvas.getContext(\n \"2d\",\n hasFilter || hasOpacity ? { willReadFrequently: true } : undefined,\n );\n if (!encodingCtx) {\n throw new Error(\"Failed to get encoding canvas context\");\n }\n\n if (hasFilter) {\n encodingCtx.filter = cssFilter;\n }\n if (hasOpacity) {\n encodingCtx.globalAlpha = cssOpacity;\n }\n\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 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\n // Use CanvasSource directly - filter and opacity don't require special handling\n videoSource = new CanvasSource(encodingCanvas, videoConfig);\n output.addVideoTrack(videoSource);\n\n if (config.includeAudio) {\n try {\n const selectedCodec = await selectAudioCodec(\n config.preferredAudioCodecs,\n {\n numberOfChannels: 2,\n sampleRate: 48000,\n bitrate: config.audioBitrate,\n },\n );\n const audioConfig: AudioEncodingConfig = {\n codec: selectedCodec,\n bitrate: config.audioBitrate,\n };\n audioSource = new AudioBufferSource(audioConfig);\n output.addAudioTrack(audioSource);\n } catch (e) {\n logger.warn(\n \"[renderVideoToVideo] Audio codec selection failed, rendering without audio:\",\n e,\n );\n }\n }\n\n await output.start();\n\n // =========================================================================\n // Frame loop\n // =========================================================================\n const renderStartTime = performance.now();\n let lastRenderedAudioEndMs = config.startMs;\n const audioChunkDurationMs = 2000;\n\n let thumbCanvas: HTMLCanvasElement | null = null;\n let thumbCtx: CanvasRenderingContext2D | null = null;\n\n if (config.progressPreviewInterval > 0) {\n const thumbScale = 160 / config.videoWidth;\n thumbCanvas = document.createElement(\"canvas\");\n thumbCanvas.width = Math.round(config.videoWidth * thumbScale);\n thumbCanvas.height = Math.round(config.videoHeight * thumbScale);\n thumbCtx = thumbCanvas.getContext(\"2d\");\n }\n\n let totalSeekMs = 0;\n let totalDrawMs = 0;\n let totalEncodeMs = 0;\n\n pc?.suspendSelfRender();\n\n try {\n for (let frameIndex = 0; frameIndex < config.totalFrames; frameIndex++) {\n checkCancelled();\n\n const timelineTimeMs =\n config.startMs + frameIndex * config.frameDurationMs;\n const sourceTimeMs = timelineTimeMs + config.trimStartMs;\n const timestampS = (frameIndex * config.frameDurationMs) / 1000;\n\n // Decode frame\n const seekStart = performance.now();\n const videoFrame = await video.getVideoFrameAtSourceTime(sourceTimeMs, {\n quality: \"main\",\n signal,\n });\n totalSeekMs += performance.now() - seekStart;\n\n try {\n const drawStart = performance.now();\n\n encodingCtx.drawImage(\n videoFrame,\n 0,\n 0,\n videoFrame.displayWidth,\n videoFrame.displayHeight,\n 0,\n 0,\n config.videoWidth,\n config.videoHeight,\n );\n\n totalDrawMs += performance.now() - drawStart;\n } finally {\n videoFrame.close();\n }\n\n // Encode frame\n const encodeStart = performance.now();\n await videoSource!.add(timestampS, config.frameDurationS);\n totalEncodeMs += performance.now() - encodeStart;\n\n // Render audio in chunks\n if (\n audioSource &&\n timelineTimeMs >= lastRenderedAudioEndMs + audioChunkDurationMs\n ) {\n const chunkEndMs = Math.min(\n timelineTimeMs + audioChunkDurationMs,\n config.endMs,\n );\n try {\n const audioBuffer = await video.renderAudio(\n lastRenderedAudioEndMs,\n chunkEndMs,\n );\n if (audioBuffer && audioBuffer.length > 0) {\n await audioSource.add(audioBuffer);\n }\n } catch (e) {\n // Audio render failures are non-fatal\n }\n lastRenderedAudioEndMs = chunkEndMs;\n }\n\n // Progress preview thumbnail\n if (\n thumbCanvas &&\n thumbCtx &&\n frameIndex % config.progressPreviewInterval === 0\n ) {\n thumbCtx.drawImage(\n encodingCanvas as any,\n 0,\n 0,\n thumbCanvas.width,\n thumbCanvas.height,\n );\n }\n\n // Progress reporting\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 onProgress?.({\n progress,\n currentFrame,\n totalFrames: config.totalFrames,\n renderedMs,\n totalDurationMs: config.renderDurationMs,\n elapsedMs,\n estimatedRemainingMs,\n speedMultiplier,\n framePreviewCanvas: thumbCanvas || undefined,\n });\n }\n\n // Render remaining audio\n if (audioSource && lastRenderedAudioEndMs < config.endMs) {\n try {\n const audioBuffer = await video.renderAudio(\n lastRenderedAudioEndMs,\n config.endMs,\n );\n if (audioBuffer && audioBuffer.length > 0) {\n await audioSource.add(audioBuffer);\n }\n } catch (e) {\n // Audio render failures are non-fatal\n }\n }\n\n // =========================================================================\n // Finalize\n // =========================================================================\n const totalElapsed = performance.now() - renderStartTime;\n logger.debug(\n `[renderVideoToVideo] complete: ${config.totalFrames} frames in ${totalElapsed.toFixed(0)}ms ` +\n `(seek=${totalSeekMs.toFixed(0)}ms, draw=${totalDrawMs.toFixed(0)}ms, encode=${totalEncodeMs.toFixed(0)}ms) ` +\n `speed=${(config.renderDurationMs / totalElapsed).toFixed(1)}x`,\n );\n\n await output.finalize();\n\n if (useStreaming) {\n if (fileStream) {\n await fileStream.close();\n }\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 } catch (error) {\n // Clean up output on failure\n try {\n await output?.finalize();\n } catch {\n // Ignore finalize errors during cleanup\n }\n throw error;\n } finally {\n pc?.resumeSelfRender();\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,93 +1,10 @@
|
|
|
1
|
+
import { SegmentIndex, SegmentTimeRange, TrackRef, TrackRole, TrackSet } from "../../elements/EFMedia/SegmentIndex.js";
|
|
2
|
+
import { SegmentTransport } from "../../elements/EFMedia/SegmentTransport.js";
|
|
3
|
+
import { TimingModel } from "../../elements/EFMedia/TimingModel.js";
|
|
4
|
+
import { MediaEngine } from "../../elements/EFMedia/MediaEngine.js";
|
|
5
|
+
|
|
1
6
|
//#region src/transcoding/types/index.d.ts
|
|
2
7
|
|
|
3
|
-
type RenditionId = "high" | "medium" | "low" | "audio" | "scrub";
|
|
4
|
-
interface AudioRendition {
|
|
5
|
-
id?: RenditionId;
|
|
6
|
-
trackId: number | undefined;
|
|
7
|
-
src: string;
|
|
8
|
-
segmentDurationMs?: number;
|
|
9
|
-
segmentDurationsMs?: number[];
|
|
10
|
-
startTimeOffsetMs?: number;
|
|
11
|
-
}
|
|
12
|
-
interface VideoRendition {
|
|
13
|
-
id?: RenditionId;
|
|
14
|
-
trackId: number | undefined;
|
|
15
|
-
src: string;
|
|
16
|
-
segmentDurationMs?: number;
|
|
17
|
-
segmentDurationsMs?: number[];
|
|
18
|
-
startTimeOffsetMs?: number;
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* Union type representing either an audio or video rendition.
|
|
22
|
-
* Used in methods that can work with either type of media rendition.
|
|
23
|
-
*/
|
|
24
|
-
type MediaRendition = AudioRendition | VideoRendition;
|
|
25
|
-
interface MediaEngine {
|
|
26
|
-
durationMs: number;
|
|
27
|
-
src: string;
|
|
28
|
-
audioRendition?: AudioRendition;
|
|
29
|
-
videoRendition?: VideoRendition;
|
|
30
|
-
templates: {
|
|
31
|
-
initSegment: string;
|
|
32
|
-
mediaSegment: string;
|
|
33
|
-
};
|
|
34
|
-
fetchInitSegment: (rendition: {
|
|
35
|
-
id?: RenditionId;
|
|
36
|
-
trackId: number | undefined;
|
|
37
|
-
src: string;
|
|
38
|
-
}, signal: AbortSignal) => Promise<ArrayBuffer>;
|
|
39
|
-
fetchMediaSegment: (segmentId: number, rendition: {
|
|
40
|
-
id?: RenditionId;
|
|
41
|
-
trackId: number | undefined;
|
|
42
|
-
src: string;
|
|
43
|
-
}, signal: AbortSignal) => Promise<ArrayBuffer>;
|
|
44
|
-
computeSegmentId: (desiredSeekTimeMs: number, rendition: MediaRendition) => number | undefined;
|
|
45
|
-
/**
|
|
46
|
-
* Get the video rendition if available, otherwise return undefined.
|
|
47
|
-
* Callers should handle undefined appropriately.
|
|
48
|
-
*/
|
|
49
|
-
getVideoRendition: () => VideoRendition | undefined;
|
|
50
|
-
/**
|
|
51
|
-
* Get the audio rendition if available, otherwise return undefined.
|
|
52
|
-
* Callers should handle undefined appropriately.
|
|
53
|
-
*/
|
|
54
|
-
getAudioRendition: () => AudioRendition | undefined;
|
|
55
|
-
/**
|
|
56
|
-
* Check if a segment is cached for a given rendition
|
|
57
|
-
*/
|
|
58
|
-
isSegmentCached: (segmentId: number, rendition: AudioRendition | VideoRendition) => boolean;
|
|
59
|
-
/**
|
|
60
|
-
* Get scrub video rendition if available, otherwise return undefined
|
|
61
|
-
* Each engine implements this based on their capabilities:
|
|
62
|
-
* - JitMediaEngine: looks for "scrub" rendition in manifest
|
|
63
|
-
* - AssetMediaEngine: returns regular video rendition (no separate scrub)
|
|
64
|
-
*/
|
|
65
|
-
getScrubVideoRendition(): VideoRendition | undefined;
|
|
66
|
-
/**
|
|
67
|
-
* Calculate audio segments needed for a time range
|
|
68
|
-
* Each media engine implements this based on their segment structure
|
|
69
|
-
*/
|
|
70
|
-
calculateAudioSegmentRange: (fromMs: number, toMs: number, rendition: AudioRendition, durationMs: number) => SegmentTimeRange[];
|
|
71
|
-
/**
|
|
72
|
-
* Get buffer configuration for this media engine
|
|
73
|
-
* Returns preferred buffer settings that may override host defaults
|
|
74
|
-
*/
|
|
75
|
-
getBufferConfig: () => {
|
|
76
|
-
videoBufferDurationMs: number;
|
|
77
|
-
audioBufferDurationMs: number;
|
|
78
|
-
maxVideoBufferFetches: number;
|
|
79
|
-
maxAudioBufferFetches: number;
|
|
80
|
-
bufferThresholdMs: number;
|
|
81
|
-
};
|
|
82
|
-
/**
|
|
83
|
-
* Extract thumbnail canvases at multiple timestamps efficiently
|
|
84
|
-
* Uses scrub rendition and batches by segment for optimal performance
|
|
85
|
-
* Returns thumbnail objects in same order as input timestamps
|
|
86
|
-
* Returns null for any timestamps that fail to extract
|
|
87
|
-
* @param signal - Optional AbortSignal to cancel in-flight requests when element is disconnected
|
|
88
|
-
*/
|
|
89
|
-
extractThumbnails(timestamps: number[], signal?: AbortSignal): Promise<(ThumbnailResult | null)[]>;
|
|
90
|
-
}
|
|
91
8
|
interface ThumbnailResult {
|
|
92
9
|
timestamp: number;
|
|
93
10
|
thumbnail: HTMLCanvasElement | OffscreenCanvas;
|
|
@@ -97,11 +14,6 @@ interface AudioSpan {
|
|
|
97
14
|
endMs: number;
|
|
98
15
|
blob: Blob;
|
|
99
16
|
}
|
|
100
|
-
interface SegmentTimeRange {
|
|
101
|
-
segmentId: number;
|
|
102
|
-
startMs: number;
|
|
103
|
-
endMs: number;
|
|
104
|
-
}
|
|
105
17
|
//#endregion
|
|
106
|
-
export { AudioSpan,
|
|
18
|
+
export { AudioSpan, ThumbnailResult };
|
|
107
19
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { MediaEngine, RenditionId } from "../types/index.js";
|
|
2
|
-
|
|
3
1
|
//#region src/transcoding/utils/UrlGenerator.d.ts
|
|
4
|
-
|
|
2
|
+
/**
|
|
3
|
+
* URL generation utilities for transcoding endpoints
|
|
4
|
+
*/
|
|
5
5
|
declare class UrlGenerator {
|
|
6
6
|
private baseUrl;
|
|
7
7
|
constructor(baseUrl: () => string);
|
|
@@ -9,10 +9,6 @@ declare class UrlGenerator {
|
|
|
9
9
|
* Get the base URL for constructing absolute URLs
|
|
10
10
|
*/
|
|
11
11
|
getBaseUrl(): string;
|
|
12
|
-
/**
|
|
13
|
-
* Generate video segment URL
|
|
14
|
-
*/
|
|
15
|
-
generateSegmentUrl(segmentId: "init" | number, renditionId: RenditionId, metadata: MediaEngine): string;
|
|
16
12
|
/**
|
|
17
13
|
* Generate init segment URL
|
|
18
14
|
*/
|
|
@@ -21,11 +17,6 @@ declare class UrlGenerator {
|
|
|
21
17
|
* Generate manifest URL
|
|
22
18
|
*/
|
|
23
19
|
generateManifestUrl(mediaUrl: string): string;
|
|
24
|
-
/**
|
|
25
|
-
* Generate track fragment index URL using production API format
|
|
26
|
-
* @deprecated Use MD5-based URL generation in AssetMediaEngine.fetch() instead
|
|
27
|
-
*/
|
|
28
|
-
generateTrackFragmentIndexUrl(mediaUrl: string): string;
|
|
29
20
|
/**
|
|
30
21
|
* Generate quality presets URL
|
|
31
22
|
*/
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
//#region src/transcoding/utils/UrlGenerator.ts
|
|
2
|
+
/**
|
|
3
|
+
* URL generation utilities for transcoding endpoints
|
|
4
|
+
*/
|
|
2
5
|
var UrlGenerator = class {
|
|
3
6
|
constructor(baseUrl) {
|
|
4
7
|
this.baseUrl = baseUrl;
|
|
@@ -10,26 +13,6 @@ var UrlGenerator = class {
|
|
|
10
13
|
return this.baseUrl();
|
|
11
14
|
}
|
|
12
15
|
/**
|
|
13
|
-
* Generate video segment URL
|
|
14
|
-
*/
|
|
15
|
-
generateSegmentUrl(segmentId, renditionId, metadata) {
|
|
16
|
-
const audioRendition = metadata.audioRendition;
|
|
17
|
-
const videoRendition = metadata.videoRendition;
|
|
18
|
-
let rendition;
|
|
19
|
-
if (renditionId === "audio") rendition = audioRendition;
|
|
20
|
-
else rendition = videoRendition;
|
|
21
|
-
if (!rendition) {
|
|
22
|
-
console.error("Rendition not found", {
|
|
23
|
-
renditionId,
|
|
24
|
-
hasAudio: !!audioRendition,
|
|
25
|
-
hasVideo: !!videoRendition,
|
|
26
|
-
metadata
|
|
27
|
-
});
|
|
28
|
-
throw new Error(`Rendition ${renditionId} not found (hasAudio=${!!audioRendition}, hasVideo=${!!videoRendition})`);
|
|
29
|
-
}
|
|
30
|
-
return (segmentId === "init" ? metadata.templates.initSegment : metadata.templates.mediaSegment).replace("{rendition}", renditionId).replace("{segmentId}", segmentId.toString()).replace("{src}", metadata.src).replace("{trackId}", rendition.trackId?.toString() ?? "");
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
16
|
* Generate init segment URL
|
|
34
17
|
*/
|
|
35
18
|
generateInitSegmentUrl(mediaUrl, rendition) {
|
|
@@ -42,15 +25,6 @@ var UrlGenerator = class {
|
|
|
42
25
|
return `${this.baseUrl()}/api/v1/transcode/manifest.json?url=${encodeURIComponent(mediaUrl)}`;
|
|
43
26
|
}
|
|
44
27
|
/**
|
|
45
|
-
* Generate track fragment index URL using production API format
|
|
46
|
-
* @deprecated Use MD5-based URL generation in AssetMediaEngine.fetch() instead
|
|
47
|
-
*/
|
|
48
|
-
generateTrackFragmentIndexUrl(mediaUrl) {
|
|
49
|
-
let normalizedSrc = mediaUrl.startsWith("/") ? mediaUrl.slice(1) : mediaUrl;
|
|
50
|
-
normalizedSrc = normalizedSrc.replace(/^\/+/, "");
|
|
51
|
-
return `@ef-track-fragment-index/${normalizedSrc}`;
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
28
|
* Generate quality presets URL
|
|
55
29
|
*/
|
|
56
30
|
generatePresetsUrl() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"UrlGenerator.js","names":["baseUrl: () => string"],"sources":["../../../src/transcoding/utils/UrlGenerator.ts"],"sourcesContent":["/**\n * URL generation utilities for transcoding endpoints\n */\n\
|
|
1
|
+
{"version":3,"file":"UrlGenerator.js","names":["baseUrl: () => string"],"sources":["../../../src/transcoding/utils/UrlGenerator.ts"],"sourcesContent":["/**\n * URL generation utilities for transcoding endpoints\n */\n\nexport class UrlGenerator {\n constructor(private baseUrl: () => string) {}\n\n /**\n * Get the base URL for constructing absolute URLs\n */\n getBaseUrl(): string {\n return this.baseUrl();\n }\n\n /**\n * Generate init segment URL\n */\n generateInitSegmentUrl(mediaUrl: string, rendition: string): string {\n return `${this.baseUrl()}/api/v1/transcode/${rendition}/init.mp4?url=${encodeURIComponent(mediaUrl)}`;\n }\n\n /**\n * Generate manifest URL\n */\n generateManifestUrl(mediaUrl: string): string {\n return `${this.baseUrl()}/api/v1/transcode/manifest.json?url=${encodeURIComponent(mediaUrl)}`;\n }\n\n /**\n * Generate quality presets URL\n */\n generatePresetsUrl(): string {\n return `${this.baseUrl()}/api/v1/transcode/presets`;\n }\n}\n"],"mappings":";;;;AAIA,IAAa,eAAb,MAA0B;CACxB,YAAY,AAAQA,SAAuB;EAAvB;;;;;CAKpB,aAAqB;AACnB,SAAO,KAAK,SAAS;;;;;CAMvB,uBAAuB,UAAkB,WAA2B;AAClE,SAAO,GAAG,KAAK,SAAS,CAAC,oBAAoB,UAAU,gBAAgB,mBAAmB,SAAS;;;;;CAMrG,oBAAoB,UAA0B;AAC5C,SAAO,GAAG,KAAK,SAAS,CAAC,sCAAsC,mBAAmB,SAAS;;;;;CAM7F,qBAA6B;AAC3B,SAAO,GAAG,KAAK,SAAS,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@editframe/elements",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.39.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"license": "UNLICENSED",
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@bramus/style-observer": "^1.3.0",
|
|
21
|
-
"@editframe/assets": "0.
|
|
21
|
+
"@editframe/assets": "0.39.0",
|
|
22
22
|
"@lit/context": "^1.1.6",
|
|
23
23
|
"@opentelemetry/api": "^1.9.0",
|
|
24
24
|
"@opentelemetry/context-zone": "^1.26.0",
|
package/test/setup.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { beforeEach, afterAll } from "vitest";
|
|
|
7
7
|
import {
|
|
8
8
|
globalRequestDeduplicator,
|
|
9
9
|
mediaCache,
|
|
10
|
-
} from "../src/elements/EFMedia/
|
|
10
|
+
} from "../src/elements/EFMedia/CachedFetcher.js";
|
|
11
11
|
import { globalURLTokenDeduplicator } from "../src/transcoding/cache/URLTokenDeduplicator.js";
|
|
12
12
|
import { TEST_SERVER_PORT } from "./constants.js";
|
|
13
13
|
|