@twick/browser-render 0.15.7 → 0.15.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/browser-renderer.ts","../src/audio/audio-processor.ts","../src/hooks/use-browser-renderer.ts"],"sourcesContent":["/**\n * @twick/browser-render\n * Browser-native video rendering using WebCodecs API\n */\n\n// Main browser renderer functions\nexport { renderTwickVideoInBrowser, downloadVideoBlob } from './browser-renderer';\n\n// React hook\nexport { useBrowserRenderer } from './hooks/use-browser-renderer';\n\n// Type definitions\nexport type { BrowserRenderConfig } from './browser-renderer';\nexport type { \n UseBrowserRendererOptions, \n UseBrowserRendererReturn \n} from './hooks/use-browser-renderer';\n\n// Set as default export\nexport { renderTwickVideoInBrowser as default } from './browser-renderer';\n","import { Renderer, Vector2 } from \"@twick/core\";\nimport type { Project, RendererSettings } from \"@twick/core\";\nimport defaultProject from \"@twick/visualizer/dist/project.js\";\nimport { BrowserAudioProcessor, getAssetPlacement, type AssetInfo } from './audio/audio-processor';\n\n/**\n * Browser-native video exporter using WebCodecs API\n * This exporter downloads the video directly in the browser without any server interaction\n */\nclass BrowserWasmExporter {\n public static readonly id = '@twick/core/wasm';\n public static readonly displayName = 'Browser Video (Wasm)';\n\n private encoder: any;\n private videoBlob: Blob | null = null;\n private onProgressCallback?: (progress: number) => void;\n private currentFrame: number = 0;\n private fps: number = 30;\n\n public static async create(settings: RendererSettings) {\n return new BrowserWasmExporter(settings);\n }\n\n public constructor(\n private readonly settings: RendererSettings,\n ) {\n this.fps = settings.fps || 30;\n }\n\n public async start(): Promise<void> {\n try {\n // Import mp4-wasm\n const loadMp4Module = (await import('mp4-wasm')).default;\n \n // Try multiple locations to fetch the WASM file\n const possiblePaths = [\n // Vite dev server virtual path\n '/@mp4-wasm',\n // Common bundled asset paths (Vite uses hashed names)\n '/assets/mp4-wasm.wasm',\n '/assets/mp4-YBRi_559.wasm', // Known Vite hash\n '/mp4-wasm.wasm',\n // Node modules path (for dev)\n '/node_modules/mp4-wasm/dist/mp4-wasm.wasm',\n ];\n \n let buffer: ArrayBuffer | null = null;\n let successPath = '';\n \n for (const path of possiblePaths) {\n try {\n const resp = await fetch(path);\n if (resp.ok) {\n const contentType = resp.headers.get('content-type');\n // Make sure we got a WASM file, not HTML\n if (contentType && contentType.includes('html')) {\n continue;\n }\n buffer = await resp.arrayBuffer();\n successPath = path;\n break;\n }\n } catch (e) {\n continue;\n }\n }\n \n if (!buffer) {\n throw new Error(\n 'Could not load WASM file from any location. ' +\n 'Please copy mp4-wasm.wasm to your public directory or configure Vite to serve it.'\n );\n }\n \n const mp4 = await loadMp4Module({ wasmBinary: buffer });\n\n this.encoder = mp4.createWebCodecsEncoder({\n width: this.settings.size.x,\n height: this.settings.size.y,\n fps: this.fps,\n });\n } catch (error) {\n console.error('WASM loading error:', error);\n throw error;\n }\n }\n\n public async handleFrame(canvas: HTMLCanvasElement, frameNumber?: number): Promise<void> {\n const frameIndex = frameNumber !== undefined ? frameNumber : this.currentFrame;\n const timestampMicroseconds = Math.round((frameIndex / this.fps) * 1_000_000);\n \n const frame = new VideoFrame(canvas, { \n timestamp: timestampMicroseconds,\n duration: Math.round((1 / this.fps) * 1_000_000)\n });\n \n await this.encoder.addFrame(frame);\n frame.close();\n \n if (frameNumber === undefined) {\n this.currentFrame++;\n }\n }\n\n public async stop(): Promise<void> {\n const buf = await this.encoder.end();\n this.videoBlob = new Blob([buf], { type: 'video/mp4' });\n }\n\n public async generateAudio(\n assets: AssetInfo[][],\n startFrame: number,\n endFrame: number,\n ): Promise<ArrayBuffer | null> {\n try {\n console.log('🔊 Starting audio processing...', { \n frames: assets.length, \n startFrame, \n endFrame \n });\n\n const processor = new BrowserAudioProcessor();\n const assetPlacements = getAssetPlacement(assets);\n \n console.log(`📊 Found ${assetPlacements.length} audio assets to process`);\n \n if (assetPlacements.length === 0) {\n console.log('⚠️ No audio assets found');\n return null;\n }\n \n const processedBuffers: AudioBuffer[] = [];\n for (const asset of assetPlacements) {\n if (asset.volume > 0 && asset.playbackRate > 0) {\n console.log(`🎵 Processing audio: ${asset.key}`);\n const buffer = await processor.processAudioAsset(\n asset,\n this.settings.fps || 30,\n endFrame - startFrame\n );\n processedBuffers.push(buffer);\n }\n }\n \n if (processedBuffers.length === 0) {\n console.log('⚠️ No audio buffers to mix');\n return null;\n }\n \n console.log(`🎛️ Mixing ${processedBuffers.length} audio track(s)...`);\n const mixedBuffer = processor.mixAudioBuffers(processedBuffers);\n const wavData = processor.audioBufferToWav(mixedBuffer);\n \n await processor.close();\n console.log(`✅ Audio processed: ${(wavData.byteLength / 1024 / 1024).toFixed(2)} MB`);\n \n return wavData;\n } catch (error) {\n console.error('❌ Audio processing failed:', error);\n return null;\n }\n }\n\n public async mergeMedia(): Promise<void> {\n // In browser, we don't need to merge separately\n // The video is already created with audio in the encoder\n }\n\n public async downloadVideos(assets: any[][]): Promise<void> {\n // Browser doesn't need to download source videos\n // They're already accessible via URLs\n }\n\n public getVideoBlob(): Blob | null {\n return this.videoBlob;\n }\n\n public setProgressCallback(callback: (progress: number) => void): void {\n this.onProgressCallback = callback;\n }\n}\n\n/**\n * Browser rendering configuration\n */\nexport interface BrowserRenderConfig {\n /** \n * Custom Project object\n * If not provided, defaults to @twick/visualizer project\n * \n * Note: Must be an imported Project object, not a string path.\n * String paths only work in Node.js environments (server renderer).\n * \n * Example:\n * ```typescript\n * import myProject from './my-custom-project';\n * \n * await renderTwickVideoInBrowser({\n * projectFile: myProject,\n * variables: { input: {...} }\n * });\n * ```\n */\n projectFile?: Project;\n /** Input variables containing project configuration */\n variables: {\n input: any;\n playerId?: string;\n [key: string]: any;\n };\n /** Render settings */\n settings?: {\n width?: number;\n height?: number;\n fps?: number;\n quality?: 'low' | 'medium' | 'high';\n range?: [number, number]; // [start, end] in seconds\n includeAudio?: boolean; // Enable audio processing\n downloadAudioSeparately?: boolean; // Download audio.wav separately\n onAudioReady?: (audioBlob: Blob) => void; // Callback when audio is ready\n onProgress?: (progress: number) => void;\n onComplete?: (videoBlob: Blob) => void;\n onError?: (error: Error) => void;\n };\n}\n\n/**\n * Renders a Twick video directly in the browser without requiring a server.\n * Uses WebCodecs API for encoding video frames into MP4 format.\n * \n * This function uses the same signature as the server renderer for consistency.\n *\n * @param config - Configuration object containing variables and settings\n * @param config.projectFile - Optional project file path or Project object (defaults to visualizer project)\n * @param config.variables - Variables containing input configuration (tracks, elements, etc.)\n * @param config.settings - Optional render settings (width, height, fps, etc.)\n * @returns Promise resolving to a Blob containing the rendered video\n * \n * @example\n * ```js\n * import { renderTwickVideoInBrowser } from '@twick/browser-render';\n * \n * // Using default visualizer project\n * const videoBlob = await renderTwickVideoInBrowser({\n * variables: {\n * input: {\n * properties: { width: 1920, height: 1080, fps: 30 },\n * tracks: [\n * // Your tracks configuration\n * ]\n * }\n * },\n * settings: {\n * width: 1920,\n * height: 1080,\n * fps: 30,\n * quality: 'high',\n * onProgress: (progress) => console.log(`Rendering: ${progress * 100}%`),\n * }\n * });\n * \n * // Using custom project\n * import myProject from './my-custom-project';\n * const videoBlob = await renderTwickVideoInBrowser({\n * projectFile: myProject, // Must be an imported Project object\n * variables: { input: {...} },\n * settings: {...}\n * });\n * \n * // Download the video\n * const url = URL.createObjectURL(videoBlob);\n * const a = document.createElement('a');\n * a.href = url;\n * a.download = 'video.mp4';\n * a.click();\n * URL.revokeObjectURL(url);\n * ```\n */\nexport const renderTwickVideoInBrowser = async (\n config: BrowserRenderConfig\n): Promise<Blob> => {\n // Save original methods to restore later\n const originalVideoPlay = HTMLVideoElement.prototype.play;\n const originalAudioPlay = HTMLAudioElement.prototype.play;\n const originalCreateElement = document.createElement.bind(document);\n \n // Override play methods to force muting\n HTMLVideoElement.prototype.play = function() {\n this.muted = true;\n this.volume = 0;\n return originalVideoPlay.call(this);\n };\n \n HTMLAudioElement.prototype.play = function() {\n this.muted = true;\n this.volume = 0;\n return originalAudioPlay.call(this);\n };\n \n // Override createElement to mute video/audio on creation\n document.createElement = function(tagName: string, options?: any) {\n const element = originalCreateElement(tagName, options);\n if (tagName.toLowerCase() === 'video' || tagName.toLowerCase() === 'audio') {\n (element as any).muted = true;\n (element as any).volume = 0;\n }\n return element;\n } as any;\n\n try {\n const { projectFile, variables, settings = {} } = config;\n\n // Validate input\n if (!variables || !variables.input) {\n throw new Error('Invalid configuration. \"variables.input\" is required.');\n }\n\n // Get dimensions from input properties or use settings\n const width = settings.width || variables.input.properties?.width || 1920;\n const height = settings.height || variables.input.properties?.height || 1080;\n const fps = settings.fps || variables.input.properties?.fps || 30;\n\n // Load the project\n let project: Project;\n \n if (!projectFile) {\n // Use default visualizer project\n project = defaultProject;\n } else {\n // Use provided project object\n project = projectFile as Project;\n }\n\n // Set variables on the project (same as server renderer)\n // The renderer expects variables to be assigned directly to the project\n project.variables = variables as any;\n\n // Create renderer settings\n const renderSettings: RendererSettings = {\n name: 'browser-render',\n exporter: {\n name: '@twick/core/wasm',\n },\n size: new Vector2(width, height),\n resolutionScale: 1,\n colorSpace: 'srgb',\n fps: fps,\n range: settings.range || [0, Infinity],\n background: variables.input.backgroundColor || '#000000',\n ...(settings.quality && {\n quality: settings.quality,\n }),\n };\n\n // Create the renderer\n const renderer = new Renderer(project);\n \n // Create and initialize the browser exporter\n const exporter = await BrowserWasmExporter.create(renderSettings);\n await exporter.start();\n \n if (settings.onProgress) {\n exporter.setProgressCallback(settings.onProgress);\n }\n\n // Configure renderer\n await renderer['reloadScenes'](renderSettings);\n (renderer as any).stage.configure(renderSettings);\n (renderer as any).playback.fps = renderSettings.fps;\n \n // Set playback state to Rendering (critical for video elements)\n // PlaybackState: Playing = 0, Rendering = 1, Paused = 2, Presenting = 3\n (renderer as any).playback.state = 1;\n \n // Calculate total frames from scenes\n const totalFrames = await renderer.getNumberOfFrames(renderSettings);\n \n // Initialize playback\n await (renderer as any).playback.recalculate();\n await (renderer as any).playback.reset();\n await (renderer as any).playback.seek(0);\n \n // Track media assets for audio processing\n const mediaAssets: AssetInfo[][] = [];\n \n // Render frames\n for (let frame = 0; frame < totalFrames; frame++) {\n if (frame > 0) {\n await (renderer as any).playback.progress();\n }\n \n await (renderer as any).stage.render(\n (renderer as any).playback.currentScene,\n (renderer as any).playback.previousScene,\n );\n \n // Collect media assets from current scene for audio\n const currentAssets = (renderer as any).playback.currentScene.getMediaAssets?.() || [];\n mediaAssets.push(currentAssets);\n \n const canvas = (renderer as any).stage.finalBuffer;\n await exporter.handleFrame(canvas, frame);\n \n if (settings.onProgress) {\n settings.onProgress(frame / totalFrames);\n }\n }\n \n await exporter.stop();\n \n // Generate audio if requested\n let audioData: ArrayBuffer | null = null;\n if (settings.includeAudio && mediaAssets.length > 0) {\n console.log('🎵 Generating audio track...');\n audioData = await exporter.generateAudio(mediaAssets, 0, totalFrames);\n }\n\n let finalBlob = exporter.getVideoBlob();\n if (!finalBlob) {\n throw new Error('Failed to create video blob');\n }\n\n // Handle audio if it was generated\n if (audioData && settings.includeAudio) {\n console.log('✅ Audio extracted and processed successfully');\n console.log('📊 Audio data size:', (audioData.byteLength / 1024 / 1024).toFixed(2), 'MB');\n \n // Option 1: Download audio separately (most reliable)\n if ((settings as any).downloadAudioSeparately) {\n const audioBlob = new Blob([audioData], { type: 'audio/wav' });\n const audioUrl = URL.createObjectURL(audioBlob);\n const a = document.createElement('a');\n a.href = audioUrl;\n a.download = 'audio.wav';\n a.click();\n URL.revokeObjectURL(audioUrl);\n console.log('✅ Audio downloaded separately as audio.wav');\n }\n \n // Option 2: Return audio via callback\n if ((settings as any).onAudioReady) {\n const audioBlob = new Blob([audioData], { type: 'audio/wav' });\n (settings as any).onAudioReady(audioBlob);\n }\n \n console.log('💡 Note: Client-side audio muxing is complex.');\n console.log('💡 For full audio support, use server-side rendering: @twick/render-server');\n console.log('💡 Or mux manually with: ffmpeg -i video.mp4 -i audio.wav -c:v copy -c:a aac output.mp4');\n }\n\n if (settings.onComplete) {\n settings.onComplete(finalBlob);\n }\n\n return finalBlob;\n } catch (error) {\n if (config.settings?.onError) {\n config.settings.onError(error as Error);\n }\n throw error;\n } finally {\n // Restore original methods\n HTMLVideoElement.prototype.play = originalVideoPlay;\n HTMLAudioElement.prototype.play = originalAudioPlay;\n document.createElement = originalCreateElement as any;\n }\n};\n\n/**\n * Helper function to download a video blob as a file\n * \n * @param videoBlob - The video blob to download\n * @param filename - The desired filename (default: 'video.mp4')\n * \n * @example\n * ```js\n * const blob = await renderTwickVideoInBrowser(projectData);\n * downloadVideoBlob(blob, 'my-video.mp4');\n * ```\n */\nexport const downloadVideoBlob = (videoBlob: Blob, filename: string = 'video.mp4'): void => {\n const url = URL.createObjectURL(videoBlob);\n const a = document.createElement('a');\n a.href = url;\n a.download = filename;\n a.style.display = 'none';\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n \n // Clean up the object URL after a delay\n setTimeout(() => URL.revokeObjectURL(url), 1000);\n};\n\nexport default renderTwickVideoInBrowser;\n","/**\n * Browser-based audio processing using Web Audio API\n * Mirrors the server's FFmpeg audio generation logic\n */\n\nexport interface MediaAsset {\n key: string;\n src: string;\n type: 'video' | 'audio';\n startInVideo: number;\n endInVideo: number;\n duration: number;\n playbackRate: number;\n volume: number;\n trimLeftInSeconds: number;\n durationInSeconds: number;\n}\n\nexport interface AssetInfo {\n key: string;\n src: string;\n type: 'video' | 'audio';\n currentTime: number;\n playbackRate: number;\n volume: number;\n}\n\n/**\n * Get asset placement from frames (similar to server's getAssetPlacement)\n */\nexport function getAssetPlacement(frames: AssetInfo[][]): MediaAsset[] {\n const assets: MediaAsset[] = [];\n const assetTimeMap = new Map<string, { start: number; end: number }>();\n\n for (let frame = 0; frame < frames.length; frame++) {\n for (const asset of frames[frame]) {\n if (!assetTimeMap.has(asset.key)) {\n assetTimeMap.set(asset.key, {\n start: asset.currentTime,\n end: asset.currentTime,\n });\n assets.push({\n key: asset.key,\n src: asset.src,\n type: asset.type,\n startInVideo: frame,\n endInVideo: frame,\n duration: 0,\n durationInSeconds: 0,\n playbackRate: asset.playbackRate,\n volume: asset.volume,\n trimLeftInSeconds: asset.currentTime,\n });\n } else {\n const timeInfo = assetTimeMap.get(asset.key);\n if (timeInfo) {\n timeInfo.end = asset.currentTime;\n }\n const existingAsset = assets.find(a => a.key === asset.key);\n if (existingAsset) {\n existingAsset.endInVideo = frame;\n }\n }\n }\n }\n\n // Calculate durations\n assets.forEach(asset => {\n const timeInfo = assetTimeMap.get(asset.key);\n if (timeInfo) {\n asset.durationInSeconds = (timeInfo.end - timeInfo.start) / asset.playbackRate;\n }\n asset.duration = asset.endInVideo - asset.startInVideo + 1;\n });\n\n return assets;\n}\n\n/**\n * Audio processor using Web Audio API\n */\nexport class BrowserAudioProcessor {\n private audioContext: AudioContext;\n\n constructor(private sampleRate: number = 48000) {\n this.audioContext = new AudioContext({ sampleRate });\n }\n\n /**\n * Fetch and decode audio from a media source\n */\n async fetchAndDecodeAudio(src: string): Promise<AudioBuffer> {\n const response = await fetch(src);\n const arrayBuffer = await response.arrayBuffer();\n return await this.audioContext.decodeAudioData(arrayBuffer);\n }\n\n /**\n * Process audio asset with playback rate, volume, and timing\n */\n async processAudioAsset(\n asset: MediaAsset,\n fps: number,\n totalFrames: number\n ): Promise<AudioBuffer> {\n const audioBuffer = await this.fetchAndDecodeAudio(asset.src);\n \n const duration = totalFrames / fps;\n const outputLength = Math.ceil(duration * this.sampleRate);\n const outputBuffer = this.audioContext.createBuffer(\n 2, // stereo\n outputLength,\n this.sampleRate\n );\n\n // Calculate timing\n const startTime = asset.startInVideo / fps;\n const trimLeft = asset.trimLeftInSeconds / asset.playbackRate;\n const trimRight = trimLeft + asset.durationInSeconds;\n\n // Process each channel\n for (let channel = 0; channel < 2; channel++) {\n const inputData = audioBuffer.getChannelData(Math.min(channel, audioBuffer.numberOfChannels - 1));\n const outputData = outputBuffer.getChannelData(channel);\n\n // Calculate sample positions\n const startSample = Math.floor(startTime * this.sampleRate);\n const trimLeftSample = Math.floor(trimLeft * this.sampleRate);\n const trimRightSample = Math.floor(trimRight * this.sampleRate);\n\n // Copy and process samples\n for (let i = 0; i < outputData.length; i++) {\n const outputTime = i / this.sampleRate;\n const assetTime = outputTime - startTime;\n \n if (assetTime < 0 || assetTime >= asset.durationInSeconds) {\n outputData[i] = 0; // Silence\n } else {\n // Apply playback rate\n const inputSample = Math.floor((trimLeftSample + assetTime * asset.playbackRate * this.sampleRate));\n if (inputSample >= 0 && inputSample < inputData.length) {\n outputData[i] = inputData[inputSample] * asset.volume;\n } else {\n outputData[i] = 0;\n }\n }\n }\n }\n\n return outputBuffer;\n }\n\n /**\n * Mix multiple audio buffers\n */\n mixAudioBuffers(buffers: AudioBuffer[]): AudioBuffer {\n if (buffers.length === 0) {\n return this.audioContext.createBuffer(2, 1, this.sampleRate);\n }\n\n const maxLength = Math.max(...buffers.map(b => b.length));\n const mixedBuffer = this.audioContext.createBuffer(2, maxLength, this.sampleRate);\n\n for (let channel = 0; channel < 2; channel++) {\n const mixedData = mixedBuffer.getChannelData(channel);\n \n buffers.forEach(buffer => {\n const channelData = buffer.getChannelData(Math.min(channel, buffer.numberOfChannels - 1));\n for (let i = 0; i < channelData.length; i++) {\n mixedData[i] = (mixedData[i] || 0) + channelData[i] / buffers.length;\n }\n });\n }\n\n return mixedBuffer;\n }\n\n /**\n * Convert AudioBuffer to WAV format\n */\n audioBufferToWav(buffer: AudioBuffer): ArrayBuffer {\n const numberOfChannels = buffer.numberOfChannels;\n const sampleRate = buffer.sampleRate;\n const format = 1; // PCM\n const bitDepth = 16;\n\n const bytesPerSample = bitDepth / 8;\n const blockAlign = numberOfChannels * bytesPerSample;\n\n const data = new Float32Array(buffer.length * numberOfChannels);\n for (let channel = 0; channel < numberOfChannels; channel++) {\n const channelData = buffer.getChannelData(channel);\n for (let i = 0; i < buffer.length; i++) {\n data[i * numberOfChannels + channel] = channelData[i];\n }\n }\n\n const dataLength = data.length * bytesPerSample;\n const headerLength = 44;\n const wav = new ArrayBuffer(headerLength + dataLength);\n const view = new DataView(wav);\n\n // Write WAV header\n const writeString = (offset: number, string: string) => {\n for (let i = 0; i < string.length; i++) {\n view.setUint8(offset + i, string.charCodeAt(i));\n }\n };\n\n writeString(0, 'RIFF');\n view.setUint32(4, 36 + dataLength, true);\n writeString(8, 'WAVE');\n writeString(12, 'fmt ');\n view.setUint32(16, 16, true); // fmt chunk size\n view.setUint16(20, format, true);\n view.setUint16(22, numberOfChannels, true);\n view.setUint32(24, sampleRate, true);\n view.setUint32(28, sampleRate * blockAlign, true);\n view.setUint16(32, blockAlign, true);\n view.setUint16(34, bitDepth, true);\n writeString(36, 'data');\n view.setUint32(40, dataLength, true);\n\n // Write audio data\n const volume = 0.8;\n let offset = 44;\n for (let i = 0; i < data.length; i++) {\n const sample = Math.max(-1, Math.min(1, data[i]));\n view.setInt16(offset, sample < 0 ? sample * 0x8000 : sample * 0x7FFF, true);\n offset += 2;\n }\n\n return wav;\n }\n\n async close() {\n await this.audioContext.close();\n }\n}\n","import { useState, useCallback } from 'react';\nimport { renderTwickVideoInBrowser, downloadVideoBlob } from '../browser-renderer';\nimport type { BrowserRenderConfig } from '../browser-renderer';\n\nexport interface UseBrowserRendererOptions {\n /** \n * Custom Project object\n * If not provided, defaults to @twick/visualizer project\n * \n * Note: Must be an imported Project object, not a string path.\n * String paths only work in Node.js (server renderer).\n * \n * Example:\n * ```typescript\n * import myProject from './my-custom-project';\n * useBrowserRenderer({ projectFile: myProject })\n * ```\n */\n projectFile?: any;\n /** Video width in pixels */\n width?: number;\n /** Video height in pixels */\n height?: number;\n /** Frames per second */\n fps?: number;\n /** Render quality */\n quality?: 'low' | 'medium' | 'high';\n /** Time range to render [start, end] in seconds */\n range?: [number, number];\n /** Include audio in rendered video (experimental) */\n includeAudio?: boolean;\n /** Download audio separately as WAV file */\n downloadAudioSeparately?: boolean;\n /** Callback when audio is ready */\n onAudioReady?: (audioBlob: Blob) => void;\n /** Automatically download the video when rendering completes */\n autoDownload?: boolean;\n /** Default filename for downloads */\n downloadFilename?: string;\n}\n\nexport interface UseBrowserRendererReturn {\n /** Start rendering the video */\n render: (variables: BrowserRenderConfig['variables']) => Promise<Blob | null>;\n /** Current rendering progress (0-1) */\n progress: number;\n /** Whether rendering is in progress */\n isRendering: boolean;\n /** Error if rendering failed */\n error: Error | null;\n /** The rendered video blob (available after rendering completes) */\n videoBlob: Blob | null;\n /** Download the rendered video */\n download: (filename?: string) => void;\n /** Reset the renderer state */\n reset: () => void;\n}\n\n/**\n * React hook for rendering Twick videos in the browser\n * \n * Uses the same pattern as the server renderer for consistency.\n * \n * @param options - Rendering options\n * @returns Renderer state and control functions\n * \n * @example\n * ```tsx\n * import { useBrowserRenderer } from '@twick/browser-render';\n * \n * // Using default visualizer project\n * function MyComponent() {\n * const { render, progress, isRendering, videoBlob, download } = useBrowserRenderer({\n * width: 1920,\n * height: 1080,\n * fps: 30,\n * autoDownload: true,\n * });\n * \n * const handleRender = async () => {\n * await render({\n * input: {\n * properties: { width: 1920, height: 1080, fps: 30 },\n * tracks: [\n * // Your tracks configuration\n * ]\n * }\n * });\n * };\n * \n * return (\n * <div>\n * <button onClick={handleRender} disabled={isRendering}>\n * {isRendering ? `Rendering... ${(progress * 100).toFixed(0)}%` : 'Render Video'}\n * </button>\n * {videoBlob && !autoDownload && (\n * <button onClick={() => download('my-video.mp4')}>Download</button>\n * )}\n * </div>\n * );\n * }\n * \n * // Using custom project (must import it first)\n * import myProject from './my-project';\n * \n * function CustomProjectComponent() {\n * const { render } = useBrowserRenderer({\n * projectFile: myProject, // Pass the imported project object\n * width: 1920,\n * height: 1080,\n * });\n * \n * // ... rest of component\n * }\n * ```\n */\nexport const useBrowserRenderer = (options: UseBrowserRendererOptions = {}): UseBrowserRendererReturn => {\n const [progress, setProgress] = useState(0);\n const [isRendering, setIsRendering] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n const [videoBlob, setVideoBlob] = useState<Blob | null>(null);\n\n const reset = useCallback(() => {\n setProgress(0);\n setIsRendering(false);\n setError(null);\n setVideoBlob(null);\n }, []);\n\n const download = useCallback((filename?: string) => {\n if (!videoBlob) {\n const downloadError = new Error('No video available to download. Please render the video first.');\n setError(downloadError);\n console.error(downloadError.message);\n return;\n }\n \n try {\n downloadVideoBlob(videoBlob, filename || options.downloadFilename || 'video.mp4');\n } catch (err) {\n const downloadError = err instanceof Error ? err : new Error('Failed to download video');\n setError(downloadError);\n console.error('Download error:', downloadError);\n }\n }, [videoBlob, options.downloadFilename]);\n\n const render = useCallback(async (variables: BrowserRenderConfig['variables']): Promise<Blob | null> => {\n reset();\n setIsRendering(true);\n\n try {\n const { projectFile, width, height, fps, quality, range, includeAudio, downloadAudioSeparately, onAudioReady, autoDownload, downloadFilename, ...restOptions } = options;\n \n const blob = await renderTwickVideoInBrowser({\n projectFile,\n variables,\n settings: {\n width,\n height,\n includeAudio,\n downloadAudioSeparately,\n onAudioReady,\n fps,\n quality,\n range,\n ...restOptions,\n onProgress: (p) => {\n setProgress(p);\n },\n onComplete: (blob) => {\n setVideoBlob(blob);\n if (autoDownload) {\n try {\n downloadVideoBlob(blob, downloadFilename || 'video.mp4');\n } catch (downloadErr) {\n const error = downloadErr instanceof Error \n ? downloadErr \n : new Error('Failed to auto-download video');\n setError(error);\n console.error('Auto-download error:', error);\n }\n }\n },\n onError: (err) => {\n setError(err);\n },\n },\n });\n\n if (!blob) {\n throw new Error('Rendering failed: No video blob was generated');\n }\n\n setVideoBlob(blob);\n setProgress(1);\n return blob;\n } catch (err) {\n const error = err instanceof Error ? err : new Error(String(err));\n setError(error);\n console.error('Render error:', error);\n return null;\n } finally {\n setIsRendering(false);\n }\n }, [options, reset]);\n\n return {\n render,\n progress,\n isRendering,\n error,\n videoBlob,\n download,\n reset,\n };\n};\n\nexport default useBrowserRenderer;\n"],"mappings":"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","names":["offset","defaultProject","blob","error"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/browser-renderer.ts","../src/audio/video-audio-extractor.ts","../src/audio/audio-processor.ts","../src/audio/audio-video-muxer.ts","../src/hooks/use-browser-renderer.ts"],"sourcesContent":["/**\n * @twick/browser-render\n * Browser-native video rendering using WebCodecs API\n */\n\n// Main browser renderer functions\nexport { renderTwickVideoInBrowser, downloadVideoBlob } from './browser-renderer';\n\n// React hook\nexport { useBrowserRenderer } from './hooks/use-browser-renderer';\n\n// Type definitions\nexport type { BrowserRenderConfig } from './browser-renderer';\nexport type { \n UseBrowserRendererOptions, \n UseBrowserRendererReturn \n} from './hooks/use-browser-renderer';\n\n// Set as default export\nexport { renderTwickVideoInBrowser as default } from './browser-renderer';\n","import { Renderer, Vector2 } from \"@twick/core\";\nimport type { Project, RendererSettings } from \"@twick/core\";\nimport defaultProject from \"@twick/visualizer/dist/project.js\";\nimport { BrowserAudioProcessor, getAssetPlacement, type AssetInfo } from './audio/audio-processor';\nimport { muxAudioVideo } from './audio/audio-video-muxer';\n\n/**\n * Browser-native video exporter using WebCodecs API\n * This exporter downloads the video directly in the browser without any server interaction\n */\nclass BrowserWasmExporter {\n public static readonly id = '@twick/core/wasm';\n public static readonly displayName = 'Browser Video (Wasm)';\n\n private encoder: any;\n private videoBlob: Blob | null = null;\n private onProgressCallback?: (progress: number) => void;\n private currentFrame: number = 0;\n private fps: number = 30;\n\n public static async create(settings: RendererSettings) {\n return new BrowserWasmExporter(settings);\n }\n\n public constructor(\n private readonly settings: RendererSettings,\n ) {\n this.fps = settings.fps || 30;\n }\n\n public async start(): Promise<void> {\n try {\n // Import mp4-wasm\n const loadMp4Module = (await import('mp4-wasm')).default;\n \n // Try multiple locations to fetch the WASM file\n const possiblePaths = [\n // Vite dev server virtual path\n '/@mp4-wasm',\n // Common bundled asset paths (Vite uses hashed names)\n '/assets/mp4-wasm.wasm',\n '/assets/mp4-YBRi_559.wasm', // Known Vite hash\n '/mp4-wasm.wasm',\n // Node modules path (for dev)\n '/node_modules/mp4-wasm/dist/mp4-wasm.wasm',\n ];\n \n let buffer: ArrayBuffer | null = null;\n let successPath = '';\n \n for (const path of possiblePaths) {\n try {\n const resp = await fetch(path);\n if (resp.ok) {\n const contentType = resp.headers.get('content-type');\n // Make sure we got a WASM file, not HTML\n if (contentType && contentType.includes('html')) {\n continue;\n }\n buffer = await resp.arrayBuffer();\n successPath = path;\n break;\n }\n } catch (e) {\n continue;\n }\n }\n \n if (!buffer) {\n throw new Error(\n 'Could not load WASM file from any location. ' +\n 'Please copy mp4-wasm.wasm to your public directory or configure Vite to serve it.'\n );\n }\n \n const mp4 = await loadMp4Module({ wasmBinary: buffer });\n\n this.encoder = mp4.createWebCodecsEncoder({\n width: this.settings.size.x,\n height: this.settings.size.y,\n fps: this.fps,\n });\n } catch (error) {\n throw error;\n }\n }\n\n public async handleFrame(canvas: HTMLCanvasElement, frameNumber?: number): Promise<void> {\n const frameIndex = frameNumber !== undefined ? frameNumber : this.currentFrame;\n const timestampMicroseconds = Math.round((frameIndex / this.fps) * 1_000_000);\n \n const frame = new VideoFrame(canvas, { \n timestamp: timestampMicroseconds,\n duration: Math.round((1 / this.fps) * 1_000_000)\n });\n \n await this.encoder.addFrame(frame);\n frame.close();\n \n if (frameNumber === undefined) {\n this.currentFrame++;\n }\n }\n\n public async stop(): Promise<void> {\n const buf = await this.encoder.end();\n this.videoBlob = new Blob([buf], { type: 'video/mp4' });\n }\n\n public async generateAudio(\n assets: AssetInfo[][],\n startFrame: number,\n endFrame: number,\n ): Promise<ArrayBuffer | null> {\n try {\n const processor = new BrowserAudioProcessor();\n const assetPlacements = getAssetPlacement(assets);\n\n if (assetPlacements.length === 0) {\n return null;\n }\n\n const processedBuffers: AudioBuffer[] = [];\n for (const asset of assetPlacements) {\n if (asset.volume > 0 && asset.playbackRate > 0) {\n try {\n const buffer = await processor.processAudioAsset(\n asset,\n this.settings.fps || 30,\n endFrame - startFrame\n );\n processedBuffers.push(buffer);\n } catch {\n // Continue with other audio assets\n }\n }\n }\n\n if (processedBuffers.length === 0) {\n return null;\n }\n\n const mixedBuffer = processor.mixAudioBuffers(processedBuffers);\n const wavData = processor.audioBufferToWav(mixedBuffer);\n\n await processor.close();\n return wavData;\n } catch {\n return null;\n }\n }\n\n public async mergeMedia(): Promise<void> {\n // In browser, we don't need to merge separately\n // The video is already created with audio in the encoder\n }\n\n public async downloadVideos(assets: any[][]): Promise<void> {\n // Browser doesn't need to download source videos\n // They're already accessible via URLs\n }\n\n public getVideoBlob(): Blob | null {\n return this.videoBlob;\n }\n\n public setProgressCallback(callback: (progress: number) => void): void {\n this.onProgressCallback = callback;\n }\n}\n\n/**\n * Browser rendering configuration\n */\nexport interface BrowserRenderConfig {\n /** \n * Custom Project object\n * If not provided, defaults to @twick/visualizer project\n * \n * Note: Must be an imported Project object, not a string path.\n * String paths only work in Node.js environments (server renderer).\n * \n * Example:\n * ```typescript\n * import myProject from './my-custom-project';\n * \n * await renderTwickVideoInBrowser({\n * projectFile: myProject,\n * variables: { input: {...} }\n * });\n * ```\n */\n projectFile?: Project;\n /** Input variables containing project configuration */\n variables: {\n input: any;\n playerId?: string;\n [key: string]: any;\n };\n /** Render settings */\n settings?: {\n width?: number;\n height?: number;\n fps?: number;\n quality?: 'low' | 'medium' | 'high';\n range?: [number, number]; // [start, end] in seconds\n includeAudio?: boolean; // Enable audio processing\n downloadAudioSeparately?: boolean; // Download audio.wav separately\n onAudioReady?: (audioBlob: Blob) => void; // Callback when audio is ready\n onProgress?: (progress: number) => void;\n onComplete?: (videoBlob: Blob) => void;\n onError?: (error: Error) => void;\n };\n}\n\n/**\n * Renders a Twick video directly in the browser without requiring a server.\n * Uses WebCodecs API for encoding video frames into MP4 format.\n * \n * This function uses the same signature as the server renderer for consistency.\n *\n * @param config - Configuration object containing variables and settings\n * @param config.projectFile - Optional project file path or Project object (defaults to visualizer project)\n * @param config.variables - Variables containing input configuration (tracks, elements, etc.)\n * @param config.settings - Optional render settings (width, height, fps, etc.)\n * @returns Promise resolving to a Blob containing the rendered video\n * \n * @example\n * ```js\n * import { renderTwickVideoInBrowser } from '@twick/browser-render';\n * \n * // Using default visualizer project\n * const videoBlob = await renderTwickVideoInBrowser({\n * variables: {\n * input: {\n * properties: { width: 1920, height: 1080, fps: 30 },\n * tracks: [\n * // Your tracks configuration\n * ]\n * }\n * },\n * settings: {\n * width: 1920,\n * height: 1080,\n * fps: 30,\n * quality: 'high',\n * onProgress: (progress) => console.log(`Rendering: ${progress * 100}%`),\n * }\n * });\n * \n * // Using custom project\n * import myProject from './my-custom-project';\n * const videoBlob = await renderTwickVideoInBrowser({\n * projectFile: myProject, // Must be an imported Project object\n * variables: { input: {...} },\n * settings: {...}\n * });\n * \n * // Download the video\n * const url = URL.createObjectURL(videoBlob);\n * const a = document.createElement('a');\n * a.href = url;\n * a.download = 'video.mp4';\n * a.click();\n * URL.revokeObjectURL(url);\n * ```\n */\nexport const renderTwickVideoInBrowser = async (\n config: BrowserRenderConfig\n): Promise<Blob> => {\n // Save original methods to restore later\n const originalVideoPlay = HTMLVideoElement.prototype.play;\n const originalAudioPlay = HTMLAudioElement.prototype.play;\n const originalCreateElement = document.createElement.bind(document);\n \n // Override play methods to force muting\n HTMLVideoElement.prototype.play = function() {\n this.muted = true;\n this.volume = 0;\n return originalVideoPlay.call(this);\n };\n \n HTMLAudioElement.prototype.play = function() {\n this.muted = true;\n this.volume = 0;\n return originalAudioPlay.call(this);\n };\n \n // Override createElement to mute video/audio on creation\n document.createElement = function(tagName: string, options?: any) {\n const element = originalCreateElement(tagName, options);\n if (tagName.toLowerCase() === 'video' || tagName.toLowerCase() === 'audio') {\n (element as any).muted = true;\n (element as any).volume = 0;\n }\n return element;\n } as any;\n\n try {\n const { projectFile, variables, settings = {} } = config;\n\n if (!variables || !variables.input) {\n throw new Error('Invalid configuration. \"variables.input\" is required.');\n }\n\n const width = settings.width || variables.input.properties?.width || 1920;\n const height = settings.height || variables.input.properties?.height || 1080;\n const fps = settings.fps || variables.input.properties?.fps || 30;\n\n const project: Project = !projectFile ? defaultProject : (projectFile as Project);\n project.variables = variables as any;\n\n // Create renderer settings\n const renderSettings: RendererSettings = {\n name: 'browser-render',\n exporter: {\n name: '@twick/core/wasm',\n },\n size: new Vector2(width, height),\n resolutionScale: 1,\n colorSpace: 'srgb',\n fps: fps,\n range: settings.range || [0, Infinity],\n background: variables.input.backgroundColor || '#000000',\n ...(settings.quality && {\n quality: settings.quality,\n }),\n };\n\n const renderer = new Renderer(project);\n const exporter = await BrowserWasmExporter.create(renderSettings);\n await exporter.start();\n \n if (settings.onProgress) {\n exporter.setProgressCallback(settings.onProgress);\n }\n\n await renderer['reloadScenes'](renderSettings);\n (renderer as any).stage.configure(renderSettings);\n (renderer as any).playback.fps = renderSettings.fps;\n \n // Set playback state to Rendering (critical for video elements)\n // PlaybackState: Playing = 0, Rendering = 1, Paused = 2, Presenting = 3\n (renderer as any).playback.state = 1;\n \n const totalFrames = await renderer.getNumberOfFrames(renderSettings);\n\n if (totalFrames === 0 || !isFinite(totalFrames)) {\n throw new Error(\n 'Cannot render: Video has zero duration. ' +\n 'Please ensure your project has valid content with non-zero duration. ' +\n 'Check that all video elements have valid sources and are properly loaded.'\n );\n }\n \n const videoElements: any[] = [];\n if (variables.input.tracks) {\n variables.input.tracks.forEach((track: any) => {\n if (track.elements) {\n track.elements.forEach((el: any) => {\n if (el.type === 'video') videoElements.push(el);\n });\n }\n });\n }\n\n if (videoElements.length > 0) {\n for (const videoEl of videoElements) {\n const src = videoEl.props?.src;\n if (!src || src === 'undefined') continue;\n const preloadVideo = document.createElement('video');\n preloadVideo.crossOrigin = 'anonymous';\n preloadVideo.preload = 'metadata';\n preloadVideo.src = src;\n await new Promise<void>((resolve, reject) => {\n const timeout = setTimeout(\n () => reject(new Error(`Timeout loading video metadata: ${src.substring(0, 80)}`)),\n 30000\n );\n preloadVideo.addEventListener('loadedmetadata', () => {\n clearTimeout(timeout);\n resolve();\n }, { once: true });\n preloadVideo.addEventListener('error', () => {\n clearTimeout(timeout);\n const err = preloadVideo.error;\n reject(new Error(`Failed to load video: ${err?.message || 'Unknown error'}`));\n }, { once: true });\n });\n }\n }\n\n await (renderer as any).playback.recalculate();\n await (renderer as any).playback.reset();\n await (renderer as any).playback.seek(0);\n\n const mediaAssets: AssetInfo[][] = [];\n \n for (let frame = 0; frame < totalFrames; frame++) {\n if (frame > 0) {\n await (renderer as any).playback.progress();\n }\n await (renderer as any).stage.render(\n (renderer as any).playback.currentScene,\n (renderer as any).playback.previousScene,\n );\n const currentAssets = (renderer as any).playback.currentScene.getMediaAssets?.() || [];\n mediaAssets.push(currentAssets);\n const canvas = (renderer as any).stage.finalBuffer;\n await exporter.handleFrame(canvas, frame);\n if (settings.onProgress) settings.onProgress(frame / totalFrames);\n }\n\n await exporter.stop();\n \n let audioData: ArrayBuffer | null = null;\n if (settings.includeAudio && mediaAssets.length > 0) {\n audioData = await exporter.generateAudio(mediaAssets, 0, totalFrames);\n }\n\n let finalBlob = exporter.getVideoBlob();\n if (!finalBlob) {\n throw new Error('Failed to create video blob');\n }\n\n if (audioData && settings.includeAudio) {\n try {\n finalBlob = await muxAudioVideo({\n videoBlob: finalBlob,\n audioBuffer: audioData,\n });\n } catch {\n const audioBlob = new Blob([audioData], { type: 'audio/wav' });\n const audioUrl = URL.createObjectURL(audioBlob);\n const a = document.createElement('a');\n a.href = audioUrl;\n a.download = 'audio.wav';\n a.click();\n URL.revokeObjectURL(audioUrl);\n }\n }\n\n if (settings.onComplete) {\n settings.onComplete(finalBlob);\n }\n\n return finalBlob;\n } catch (error) {\n if (config.settings?.onError) {\n config.settings.onError(error as Error);\n }\n throw error;\n } finally {\n // Restore original methods\n HTMLVideoElement.prototype.play = originalVideoPlay;\n HTMLAudioElement.prototype.play = originalAudioPlay;\n document.createElement = originalCreateElement as any;\n }\n};\n\n/**\n * Helper function to download a video blob as a file\n * \n * @param videoBlob - The video blob to download\n * @param filename - The desired filename (default: 'video.mp4')\n * \n * @example\n * ```js\n * const blob = await renderTwickVideoInBrowser(projectData);\n * downloadVideoBlob(blob, 'my-video.mp4');\n * ```\n */\nexport const downloadVideoBlob = (videoBlob: Blob, filename: string = 'video.mp4'): void => {\n const url = URL.createObjectURL(videoBlob);\n const a = document.createElement('a');\n a.href = url;\n a.download = filename;\n a.style.display = 'none';\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n \n // Clean up the object URL after a delay\n setTimeout(() => URL.revokeObjectURL(url), 1000);\n};\n\nexport default renderTwickVideoInBrowser;\n","/**\n * Alternative audio extraction using MediaElementAudioSourceNode\n * This captures audio directly from a playing video element without decoding\n * Works with ANY audio codec the browser can play!\n */\n\nexport class VideoElementAudioExtractor {\n private audioContext: AudioContext;\n private video: HTMLVideoElement;\n private destination: MediaStreamAudioDestinationNode | null = null;\n private mediaRecorder: MediaRecorder | null = null;\n private audioChunks: Blob[] = [];\n \n constructor(videoSrc: string, sampleRate: number = 48000) {\n this.audioContext = new AudioContext({ sampleRate });\n this.video = document.createElement('video');\n this.video.crossOrigin = 'anonymous';\n this.video.src = videoSrc;\n this.video.muted = true; // Mute playback but audio will still be captured\n }\n \n async initialize(): Promise<void> {\n return new Promise((resolve, reject) => {\n this.video.addEventListener('loadedmetadata', () => resolve(), { once: true });\n \n this.video.addEventListener('error', (e) => {\n reject(new Error(`Failed to load video for audio extraction: ${e}`));\n }, { once: true });\n });\n }\n \n /**\n * Extract audio by playing the video and capturing audio output\n */\n async extractAudio(\n startTime: number,\n duration: number,\n playbackRate: number = 1.0\n ): Promise<AudioBuffer> {\n // Create audio source from video element\n const source = this.audioContext.createMediaElementSource(this.video);\n \n // Create destination for recording\n this.destination = this.audioContext.createMediaStreamDestination();\n source.connect(this.destination);\n \n // Create MediaRecorder to capture audio\n this.audioChunks = [];\n this.mediaRecorder = new MediaRecorder(this.destination.stream, {\n mimeType: 'audio/webm',\n });\n \n this.mediaRecorder.ondataavailable = (event) => {\n if (event.data.size > 0) {\n this.audioChunks.push(event.data);\n }\n };\n \n // Set up video playback\n this.video.currentTime = startTime;\n this.video.playbackRate = playbackRate;\n \n // Wait for seek to complete\n await new Promise<void>((resolve) => {\n this.video.addEventListener('seeked', () => resolve(), { once: true });\n });\n \n // Start recording and playing\n return new Promise((resolve, reject) => {\n const recordingTimeout = setTimeout(() => {\n reject(new Error('Audio extraction timeout'));\n }, (duration / playbackRate + 5) * 1000); // Add 5s buffer\n \n this.mediaRecorder!.start();\n this.video.play();\n \n // Stop recording after duration\n setTimeout(async () => {\n clearTimeout(recordingTimeout);\n this.video.pause();\n this.mediaRecorder!.stop();\n \n // Wait for final data\n await new Promise<void>((res) => {\n this.mediaRecorder!.addEventListener('stop', () => res(), { once: true });\n });\n \n // Convert recorded audio to AudioBuffer\n try {\n const audioBlob = new Blob(this.audioChunks, { type: 'audio/webm' });\n const arrayBuffer = await audioBlob.arrayBuffer();\n const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer);\n resolve(audioBuffer);\n } catch (err) {\n reject(new Error(`Failed to decode recorded audio: ${err}`));\n }\n }, (duration / playbackRate) * 1000);\n });\n }\n \n async close(): Promise<void> {\n if (this.mediaRecorder && this.mediaRecorder.state !== 'inactive') {\n this.mediaRecorder.stop();\n }\n this.video.pause();\n this.video.src = '';\n if (this.audioContext.state !== 'closed') {\n await this.audioContext.close();\n }\n }\n}\n\n/**\n * Extract audio from video using MediaElementAudioSourceNode\n * This method works with any audio codec the browser can play\n */\nexport async function extractAudioFromVideo(\n videoSrc: string,\n startTime: number,\n duration: number,\n playbackRate: number = 1.0,\n sampleRate: number = 48000\n): Promise<AudioBuffer> {\n const extractor = new VideoElementAudioExtractor(videoSrc, sampleRate);\n \n try {\n await extractor.initialize();\n const audioBuffer = await extractor.extractAudio(startTime, duration, playbackRate);\n return audioBuffer;\n } finally {\n await extractor.close();\n }\n}\n","/**\n * Browser-based audio processing using Web Audio API\n * Mirrors the server's FFmpeg audio generation logic\n */\n\nimport { extractAudioFromVideo } from './video-audio-extractor';\n\nexport interface MediaAsset {\n key: string;\n src: string;\n type: 'video' | 'audio';\n startInVideo: number;\n endInVideo: number;\n duration: number;\n playbackRate: number;\n volume: number;\n trimLeftInSeconds: number;\n durationInSeconds: number;\n}\n\nexport interface AssetInfo {\n key: string;\n src: string;\n type: 'video' | 'audio';\n currentTime: number;\n playbackRate: number;\n volume: number;\n}\n\n/**\n * Get asset placement from frames (similar to server's getAssetPlacement)\n */\nexport function getAssetPlacement(frames: AssetInfo[][]): MediaAsset[] {\n const assets: MediaAsset[] = [];\n const assetTimeMap = new Map<string, { start: number; end: number }>();\n\n for (let frame = 0; frame < frames.length; frame++) {\n for (const asset of frames[frame]) {\n if (!assetTimeMap.has(asset.key)) {\n assetTimeMap.set(asset.key, {\n start: asset.currentTime,\n end: asset.currentTime,\n });\n assets.push({\n key: asset.key,\n src: asset.src,\n type: asset.type,\n startInVideo: frame,\n endInVideo: frame,\n duration: 0,\n durationInSeconds: 0,\n playbackRate: asset.playbackRate,\n volume: asset.volume,\n trimLeftInSeconds: asset.currentTime,\n });\n } else {\n const timeInfo = assetTimeMap.get(asset.key);\n if (timeInfo) {\n timeInfo.end = asset.currentTime;\n }\n const existingAsset = assets.find(a => a.key === asset.key);\n if (existingAsset) {\n existingAsset.endInVideo = frame;\n }\n }\n }\n }\n\n // Calculate durations\n assets.forEach(asset => {\n const timeInfo = assetTimeMap.get(asset.key);\n if (timeInfo) {\n asset.durationInSeconds = (timeInfo.end - timeInfo.start) / asset.playbackRate;\n }\n asset.duration = asset.endInVideo - asset.startInVideo + 1;\n });\n\n return assets;\n}\n\n/**\n * Audio processor using Web Audio API\n */\nexport class BrowserAudioProcessor {\n private audioContext: AudioContext;\n\n constructor(private sampleRate: number = 48000) {\n this.audioContext = new AudioContext({ sampleRate });\n }\n\n /**\n * Fetch and decode audio from a media source\n * Falls back to video element extraction if decodeAudioData fails\n */\n async fetchAndDecodeAudio(src: string): Promise<AudioBuffer> {\n try {\n const response = await fetch(src);\n const arrayBuffer = await response.arrayBuffer();\n return await this.audioContext.decodeAudioData(arrayBuffer);\n } catch (err) {\n try {\n return await extractAudioFromVideo(\n src,\n 0,\n 999999,\n 1.0,\n this.sampleRate\n );\n } catch (fallbackErr) {\n throw new Error(`Failed to extract audio: ${err}. Fallback also failed: ${fallbackErr}`);\n }\n }\n }\n\n /**\n * Process audio asset with playback rate, volume, and timing\n */\n async processAudioAsset(\n asset: MediaAsset,\n fps: number,\n totalFrames: number\n ): Promise<AudioBuffer> {\n const audioBuffer = await this.fetchAndDecodeAudio(asset.src);\n \n const duration = totalFrames / fps;\n const outputLength = Math.ceil(duration * this.sampleRate);\n const outputBuffer = this.audioContext.createBuffer(\n 2, // stereo\n outputLength,\n this.sampleRate\n );\n\n // Calculate timing\n const startTime = asset.startInVideo / fps;\n const trimLeft = asset.trimLeftInSeconds / asset.playbackRate;\n const trimRight = trimLeft + asset.durationInSeconds;\n\n // Process each channel\n for (let channel = 0; channel < 2; channel++) {\n const inputData = audioBuffer.getChannelData(Math.min(channel, audioBuffer.numberOfChannels - 1));\n const outputData = outputBuffer.getChannelData(channel);\n\n // Calculate sample positions\n const startSample = Math.floor(startTime * this.sampleRate);\n const trimLeftSample = Math.floor(trimLeft * this.sampleRate);\n const trimRightSample = Math.floor(trimRight * this.sampleRate);\n\n // Copy and process samples\n for (let i = 0; i < outputData.length; i++) {\n const outputTime = i / this.sampleRate;\n const assetTime = outputTime - startTime;\n \n if (assetTime < 0 || assetTime >= asset.durationInSeconds) {\n outputData[i] = 0; // Silence\n } else {\n // Apply playback rate\n const inputSample = Math.floor((trimLeftSample + assetTime * asset.playbackRate * this.sampleRate));\n if (inputSample >= 0 && inputSample < inputData.length) {\n outputData[i] = inputData[inputSample] * asset.volume;\n } else {\n outputData[i] = 0;\n }\n }\n }\n }\n\n return outputBuffer;\n }\n\n /**\n * Mix multiple audio buffers\n */\n mixAudioBuffers(buffers: AudioBuffer[]): AudioBuffer {\n if (buffers.length === 0) {\n return this.audioContext.createBuffer(2, 1, this.sampleRate);\n }\n\n const maxLength = Math.max(...buffers.map(b => b.length));\n const mixedBuffer = this.audioContext.createBuffer(2, maxLength, this.sampleRate);\n\n for (let channel = 0; channel < 2; channel++) {\n const mixedData = mixedBuffer.getChannelData(channel);\n \n buffers.forEach(buffer => {\n const channelData = buffer.getChannelData(Math.min(channel, buffer.numberOfChannels - 1));\n for (let i = 0; i < channelData.length; i++) {\n mixedData[i] = (mixedData[i] || 0) + channelData[i] / buffers.length;\n }\n });\n }\n\n return mixedBuffer;\n }\n\n /**\n * Convert AudioBuffer to WAV format\n */\n audioBufferToWav(buffer: AudioBuffer): ArrayBuffer {\n const numberOfChannels = buffer.numberOfChannels;\n const sampleRate = buffer.sampleRate;\n const format = 1; // PCM\n const bitDepth = 16;\n\n const bytesPerSample = bitDepth / 8;\n const blockAlign = numberOfChannels * bytesPerSample;\n\n const data = new Float32Array(buffer.length * numberOfChannels);\n for (let channel = 0; channel < numberOfChannels; channel++) {\n const channelData = buffer.getChannelData(channel);\n for (let i = 0; i < buffer.length; i++) {\n data[i * numberOfChannels + channel] = channelData[i];\n }\n }\n\n const dataLength = data.length * bytesPerSample;\n const headerLength = 44;\n const wav = new ArrayBuffer(headerLength + dataLength);\n const view = new DataView(wav);\n\n // Write WAV header\n const writeString = (offset: number, string: string) => {\n for (let i = 0; i < string.length; i++) {\n view.setUint8(offset + i, string.charCodeAt(i));\n }\n };\n\n writeString(0, 'RIFF');\n view.setUint32(4, 36 + dataLength, true);\n writeString(8, 'WAVE');\n writeString(12, 'fmt ');\n view.setUint32(16, 16, true); // fmt chunk size\n view.setUint16(20, format, true);\n view.setUint16(22, numberOfChannels, true);\n view.setUint32(24, sampleRate, true);\n view.setUint32(28, sampleRate * blockAlign, true);\n view.setUint16(32, blockAlign, true);\n view.setUint16(34, bitDepth, true);\n writeString(36, 'data');\n view.setUint32(40, dataLength, true);\n\n // Write audio data\n const volume = 0.8;\n let offset = 44;\n for (let i = 0; i < data.length; i++) {\n const sample = Math.max(-1, Math.min(1, data[i]));\n view.setInt16(offset, sample < 0 ? sample * 0x8000 : sample * 0x7FFF, true);\n offset += 2;\n }\n\n return wav;\n }\n\n async close() {\n await this.audioContext.close();\n }\n}\n","'use client';\n\n/**\n * Browser-based audio/video muxing using FFmpeg.wasm (main thread)\n * Compatible with Next.js 15\n *\n * FFmpeg core files must be served from the app's public folder, e.g.:\n * twick-web/public/ffmpeg/ffmpeg-core.js, ffmpeg-core.wasm\n */\n\nexport interface MuxerOptions {\n videoBlob: Blob;\n audioBuffer: ArrayBuffer;\n}\n\n/** Base URL for FFmpeg assets (twick-web public/ffmpeg). Use same-origin URLs directly; toBlobURL causes \"Cannot find module 'blob:...'\" in some environments. */\nfunction getFFmpegBaseURL(): string {\n if (typeof window !== 'undefined') {\n return `${window.location.origin}/ffmpeg`;\n }\n return '/ffmpeg';\n}\n\nexport async function muxAudioVideo(\n options: MuxerOptions\n): Promise<Blob> {\n try {\n const { FFmpeg } = await import('@ffmpeg/ffmpeg');\n const { fetchFile } = await import('@ffmpeg/util');\n\n const ffmpeg = new FFmpeg();\n\n const base = getFFmpegBaseURL();\n const coreURL = `${base}/ffmpeg-core.js`;\n const wasmURL = `${base}/ffmpeg-core.wasm`;\n\n // Load from same-origin public folder (twick-web/public/ffmpeg). Do NOT use toBlobURL —\n // it produces blob: URLs that can trigger \"Cannot find module 'blob:...'\".\n await ffmpeg.load({\n coreURL,\n wasmURL,\n });\n\n // Write inputs\n await ffmpeg.writeFile(\n 'video.mp4',\n await fetchFile(options.videoBlob)\n );\n\n await ffmpeg.writeFile(\n 'audio.wav',\n new Uint8Array(options.audioBuffer)\n );\n\n await ffmpeg.exec([\n '-i', 'video.mp4',\n '-i', 'audio.wav',\n '-c:v', 'copy',\n '-c:a', 'aac',\n '-b:a', '192k',\n '-shortest',\n 'output.mp4',\n ]);\n\n const data = await ffmpeg.readFile('output.mp4');\n\n const uint8 =\n typeof data === 'string'\n ? new TextEncoder().encode(data)\n : new Uint8Array(data);\n\n return new Blob([uint8], { type: 'video/mp4' });\n\n } catch {\n return options.videoBlob;\n }\n}\n","import { useState, useCallback } from 'react';\nimport { renderTwickVideoInBrowser, downloadVideoBlob } from '../browser-renderer';\nimport type { BrowserRenderConfig } from '../browser-renderer';\n\nexport interface UseBrowserRendererOptions {\n /** \n * Custom Project object\n * If not provided, defaults to @twick/visualizer project\n * \n * Note: Must be an imported Project object, not a string path.\n * String paths only work in Node.js (server renderer).\n * \n * Example:\n * ```typescript\n * import myProject from './my-custom-project';\n * useBrowserRenderer({ projectFile: myProject })\n * ```\n */\n projectFile?: any;\n /** Video width in pixels */\n width?: number;\n /** Video height in pixels */\n height?: number;\n /** Frames per second */\n fps?: number;\n /** Render quality */\n quality?: 'low' | 'medium' | 'high';\n /** Time range to render [start, end] in seconds */\n range?: [number, number];\n /** Include audio in rendered video (experimental) */\n includeAudio?: boolean;\n /** Download audio separately as WAV file */\n downloadAudioSeparately?: boolean;\n /** Callback when audio is ready */\n onAudioReady?: (audioBlob: Blob) => void;\n /** Automatically download the video when rendering completes */\n autoDownload?: boolean;\n /** Default filename for downloads */\n downloadFilename?: string;\n}\n\nexport interface UseBrowserRendererReturn {\n /** Start rendering the video */\n render: (variables: BrowserRenderConfig['variables']) => Promise<Blob | null>;\n /** Current rendering progress (0-1) */\n progress: number;\n /** Whether rendering is in progress */\n isRendering: boolean;\n /** Error if rendering failed */\n error: Error | null;\n /** The rendered video blob (available after rendering completes) */\n videoBlob: Blob | null;\n /** Download the rendered video */\n download: (filename?: string) => void;\n /** Reset the renderer state */\n reset: () => void;\n}\n\n/**\n * React hook for rendering Twick videos in the browser\n * \n * Uses the same pattern as the server renderer for consistency.\n * \n * @param options - Rendering options\n * @returns Renderer state and control functions\n * \n * @example\n * ```tsx\n * import { useBrowserRenderer } from '@twick/browser-render';\n * \n * // Using default visualizer project\n * function MyComponent() {\n * const { render, progress, isRendering, videoBlob, download } = useBrowserRenderer({\n * width: 1920,\n * height: 1080,\n * fps: 30,\n * autoDownload: true,\n * });\n * \n * const handleRender = async () => {\n * await render({\n * input: {\n * properties: { width: 1920, height: 1080, fps: 30 },\n * tracks: [\n * // Your tracks configuration\n * ]\n * }\n * });\n * };\n * \n * return (\n * <div>\n * <button onClick={handleRender} disabled={isRendering}>\n * {isRendering ? `Rendering... ${(progress * 100).toFixed(0)}%` : 'Render Video'}\n * </button>\n * {videoBlob && !autoDownload && (\n * <button onClick={() => download('my-video.mp4')}>Download</button>\n * )}\n * </div>\n * );\n * }\n * \n * // Using custom project (must import it first)\n * import myProject from './my-project';\n * \n * function CustomProjectComponent() {\n * const { render } = useBrowserRenderer({\n * projectFile: myProject, // Pass the imported project object\n * width: 1920,\n * height: 1080,\n * });\n * \n * // ... rest of component\n * }\n * ```\n */\nexport const useBrowserRenderer = (options: UseBrowserRendererOptions = {}): UseBrowserRendererReturn => {\n const [progress, setProgress] = useState(0);\n const [isRendering, setIsRendering] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n const [videoBlob, setVideoBlob] = useState<Blob | null>(null);\n\n const reset = useCallback(() => {\n setProgress(0);\n setIsRendering(false);\n setError(null);\n setVideoBlob(null);\n }, []);\n\n const download = useCallback((filename?: string) => {\n if (!videoBlob) {\n setError(new Error('No video available to download. Please render the video first.'));\n return;\n }\n try {\n downloadVideoBlob(videoBlob, filename || options.downloadFilename || 'video.mp4');\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to download video'));\n }\n }, [videoBlob, options.downloadFilename]);\n\n const render = useCallback(async (variables: BrowserRenderConfig['variables']): Promise<Blob | null> => {\n reset();\n setIsRendering(true);\n\n try {\n const { projectFile, width, height, fps, quality, range, includeAudio, downloadAudioSeparately, onAudioReady, autoDownload, downloadFilename, ...restOptions } = options;\n \n const blob = await renderTwickVideoInBrowser({\n projectFile,\n variables,\n settings: {\n width,\n height,\n includeAudio,\n downloadAudioSeparately,\n onAudioReady,\n fps,\n quality,\n range,\n ...restOptions,\n onProgress: (p) => {\n setProgress(p);\n },\n onComplete: (blob) => {\n setVideoBlob(blob);\n if (autoDownload) {\n try {\n downloadVideoBlob(blob, downloadFilename || 'video.mp4');\n } catch (downloadErr) {\n setError(downloadErr instanceof Error ? downloadErr : new Error('Failed to auto-download video'));\n }\n }\n },\n onError: (err) => {\n setError(err);\n },\n },\n });\n\n if (!blob) {\n throw new Error('Rendering failed: No video blob was generated');\n }\n\n setVideoBlob(blob);\n setProgress(1);\n return blob;\n } catch (err) {\n setError(err instanceof Error ? err : new Error(String(err)));\n return null;\n } finally {\n setIsRendering(false);\n }\n }, [options, reset]);\n\n return {\n render,\n progress,\n isRendering,\n error,\n videoBlob,\n download,\n reset,\n };\n};\n\nexport default useBrowserRenderer;\n"],"mappings":"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","names":["offset","defaultProject","blob"]}
package/dist/index.mjs CHANGED
@@ -2,6 +2,95 @@
2
2
  import { Renderer, Vector2 } from "@twick/core";
3
3
  import defaultProject from "@twick/visualizer/dist/project.js";
4
4
 
5
+ // src/audio/video-audio-extractor.ts
6
+ var VideoElementAudioExtractor = class {
7
+ audioContext;
8
+ video;
9
+ destination = null;
10
+ mediaRecorder = null;
11
+ audioChunks = [];
12
+ constructor(videoSrc, sampleRate = 48e3) {
13
+ this.audioContext = new AudioContext({ sampleRate });
14
+ this.video = document.createElement("video");
15
+ this.video.crossOrigin = "anonymous";
16
+ this.video.src = videoSrc;
17
+ this.video.muted = true;
18
+ }
19
+ async initialize() {
20
+ return new Promise((resolve, reject) => {
21
+ this.video.addEventListener("loadedmetadata", () => resolve(), { once: true });
22
+ this.video.addEventListener("error", (e) => {
23
+ reject(new Error(`Failed to load video for audio extraction: ${e}`));
24
+ }, { once: true });
25
+ });
26
+ }
27
+ /**
28
+ * Extract audio by playing the video and capturing audio output
29
+ */
30
+ async extractAudio(startTime, duration, playbackRate = 1) {
31
+ const source = this.audioContext.createMediaElementSource(this.video);
32
+ this.destination = this.audioContext.createMediaStreamDestination();
33
+ source.connect(this.destination);
34
+ this.audioChunks = [];
35
+ this.mediaRecorder = new MediaRecorder(this.destination.stream, {
36
+ mimeType: "audio/webm"
37
+ });
38
+ this.mediaRecorder.ondataavailable = (event) => {
39
+ if (event.data.size > 0) {
40
+ this.audioChunks.push(event.data);
41
+ }
42
+ };
43
+ this.video.currentTime = startTime;
44
+ this.video.playbackRate = playbackRate;
45
+ await new Promise((resolve) => {
46
+ this.video.addEventListener("seeked", () => resolve(), { once: true });
47
+ });
48
+ return new Promise((resolve, reject) => {
49
+ const recordingTimeout = setTimeout(() => {
50
+ reject(new Error("Audio extraction timeout"));
51
+ }, (duration / playbackRate + 5) * 1e3);
52
+ this.mediaRecorder.start();
53
+ this.video.play();
54
+ setTimeout(async () => {
55
+ clearTimeout(recordingTimeout);
56
+ this.video.pause();
57
+ this.mediaRecorder.stop();
58
+ await new Promise((res) => {
59
+ this.mediaRecorder.addEventListener("stop", () => res(), { once: true });
60
+ });
61
+ try {
62
+ const audioBlob = new Blob(this.audioChunks, { type: "audio/webm" });
63
+ const arrayBuffer = await audioBlob.arrayBuffer();
64
+ const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer);
65
+ resolve(audioBuffer);
66
+ } catch (err) {
67
+ reject(new Error(`Failed to decode recorded audio: ${err}`));
68
+ }
69
+ }, duration / playbackRate * 1e3);
70
+ });
71
+ }
72
+ async close() {
73
+ if (this.mediaRecorder && this.mediaRecorder.state !== "inactive") {
74
+ this.mediaRecorder.stop();
75
+ }
76
+ this.video.pause();
77
+ this.video.src = "";
78
+ if (this.audioContext.state !== "closed") {
79
+ await this.audioContext.close();
80
+ }
81
+ }
82
+ };
83
+ async function extractAudioFromVideo(videoSrc, startTime, duration, playbackRate = 1, sampleRate = 48e3) {
84
+ const extractor = new VideoElementAudioExtractor(videoSrc, sampleRate);
85
+ try {
86
+ await extractor.initialize();
87
+ const audioBuffer = await extractor.extractAudio(startTime, duration, playbackRate);
88
+ return audioBuffer;
89
+ } finally {
90
+ await extractor.close();
91
+ }
92
+ }
93
+
5
94
  // src/audio/audio-processor.ts
6
95
  function getAssetPlacement(frames) {
7
96
  const assets = [];
@@ -54,11 +143,26 @@ var BrowserAudioProcessor = class {
54
143
  audioContext;
55
144
  /**
56
145
  * Fetch and decode audio from a media source
146
+ * Falls back to video element extraction if decodeAudioData fails
57
147
  */
58
148
  async fetchAndDecodeAudio(src) {
59
- const response = await fetch(src);
60
- const arrayBuffer = await response.arrayBuffer();
61
- return await this.audioContext.decodeAudioData(arrayBuffer);
149
+ try {
150
+ const response = await fetch(src);
151
+ const arrayBuffer = await response.arrayBuffer();
152
+ return await this.audioContext.decodeAudioData(arrayBuffer);
153
+ } catch (err) {
154
+ try {
155
+ return await extractAudioFromVideo(
156
+ src,
157
+ 0,
158
+ 999999,
159
+ 1,
160
+ this.sampleRate
161
+ );
162
+ } catch (fallbackErr) {
163
+ throw new Error(`Failed to extract audio: ${err}. Fallback also failed: ${fallbackErr}`);
164
+ }
165
+ }
62
166
  }
63
167
  /**
64
168
  * Process audio asset with playback rate, volume, and timing
@@ -172,6 +276,55 @@ var BrowserAudioProcessor = class {
172
276
  }
173
277
  };
174
278
 
279
+ // src/audio/audio-video-muxer.ts
280
+ function getFFmpegBaseURL() {
281
+ if (typeof window !== "undefined") {
282
+ return `${window.location.origin}/ffmpeg`;
283
+ }
284
+ return "/ffmpeg";
285
+ }
286
+ async function muxAudioVideo(options) {
287
+ try {
288
+ const { FFmpeg } = await import("@ffmpeg/ffmpeg");
289
+ const { fetchFile } = await import("@ffmpeg/util");
290
+ const ffmpeg = new FFmpeg();
291
+ const base = getFFmpegBaseURL();
292
+ const coreURL = `${base}/ffmpeg-core.js`;
293
+ const wasmURL = `${base}/ffmpeg-core.wasm`;
294
+ await ffmpeg.load({
295
+ coreURL,
296
+ wasmURL
297
+ });
298
+ await ffmpeg.writeFile(
299
+ "video.mp4",
300
+ await fetchFile(options.videoBlob)
301
+ );
302
+ await ffmpeg.writeFile(
303
+ "audio.wav",
304
+ new Uint8Array(options.audioBuffer)
305
+ );
306
+ await ffmpeg.exec([
307
+ "-i",
308
+ "video.mp4",
309
+ "-i",
310
+ "audio.wav",
311
+ "-c:v",
312
+ "copy",
313
+ "-c:a",
314
+ "aac",
315
+ "-b:a",
316
+ "192k",
317
+ "-shortest",
318
+ "output.mp4"
319
+ ]);
320
+ const data = await ffmpeg.readFile("output.mp4");
321
+ const uint8 = typeof data === "string" ? new TextEncoder().encode(data) : new Uint8Array(data);
322
+ return new Blob([uint8], { type: "video/mp4" });
323
+ } catch {
324
+ return options.videoBlob;
325
+ }
326
+ }
327
+
175
328
  // src/browser-renderer.ts
176
329
  var BrowserWasmExporter = class _BrowserWasmExporter {
177
330
  constructor(settings) {
@@ -232,7 +385,6 @@ var BrowserWasmExporter = class _BrowserWasmExporter {
232
385
  fps: this.fps
233
386
  });
234
387
  } catch (error) {
235
- console.error("WASM loading error:", error);
236
388
  throw error;
237
389
  }
238
390
  }
@@ -255,42 +407,33 @@ var BrowserWasmExporter = class _BrowserWasmExporter {
255
407
  }
256
408
  async generateAudio(assets, startFrame, endFrame) {
257
409
  try {
258
- console.log("\u{1F50A} Starting audio processing...", {
259
- frames: assets.length,
260
- startFrame,
261
- endFrame
262
- });
263
410
  const processor = new BrowserAudioProcessor();
264
411
  const assetPlacements = getAssetPlacement(assets);
265
- console.log(`\u{1F4CA} Found ${assetPlacements.length} audio assets to process`);
266
412
  if (assetPlacements.length === 0) {
267
- console.log("\u26A0\uFE0F No audio assets found");
268
413
  return null;
269
414
  }
270
415
  const processedBuffers = [];
271
416
  for (const asset of assetPlacements) {
272
417
  if (asset.volume > 0 && asset.playbackRate > 0) {
273
- console.log(`\u{1F3B5} Processing audio: ${asset.key}`);
274
- const buffer = await processor.processAudioAsset(
275
- asset,
276
- this.settings.fps || 30,
277
- endFrame - startFrame
278
- );
279
- processedBuffers.push(buffer);
418
+ try {
419
+ const buffer = await processor.processAudioAsset(
420
+ asset,
421
+ this.settings.fps || 30,
422
+ endFrame - startFrame
423
+ );
424
+ processedBuffers.push(buffer);
425
+ } catch {
426
+ }
280
427
  }
281
428
  }
282
429
  if (processedBuffers.length === 0) {
283
- console.log("\u26A0\uFE0F No audio buffers to mix");
284
430
  return null;
285
431
  }
286
- console.log(`\u{1F39B}\uFE0F Mixing ${processedBuffers.length} audio track(s)...`);
287
432
  const mixedBuffer = processor.mixAudioBuffers(processedBuffers);
288
433
  const wavData = processor.audioBufferToWav(mixedBuffer);
289
434
  await processor.close();
290
- console.log(`\u2705 Audio processed: ${(wavData.byteLength / 1024 / 1024).toFixed(2)} MB`);
291
435
  return wavData;
292
- } catch (error) {
293
- console.error("\u274C Audio processing failed:", error);
436
+ } catch {
294
437
  return null;
295
438
  }
296
439
  }
@@ -335,12 +478,7 @@ var renderTwickVideoInBrowser = async (config) => {
335
478
  const width = settings.width || variables.input.properties?.width || 1920;
336
479
  const height = settings.height || variables.input.properties?.height || 1080;
337
480
  const fps = settings.fps || variables.input.properties?.fps || 30;
338
- let project;
339
- if (!projectFile) {
340
- project = defaultProject;
341
- } else {
342
- project = projectFile;
343
- }
481
+ const project = !projectFile ? defaultProject : projectFile;
344
482
  project.variables = variables;
345
483
  const renderSettings = {
346
484
  name: "browser-render",
@@ -368,6 +506,46 @@ var renderTwickVideoInBrowser = async (config) => {
368
506
  renderer.playback.fps = renderSettings.fps;
369
507
  renderer.playback.state = 1;
370
508
  const totalFrames = await renderer.getNumberOfFrames(renderSettings);
509
+ if (totalFrames === 0 || !isFinite(totalFrames)) {
510
+ throw new Error(
511
+ "Cannot render: Video has zero duration. Please ensure your project has valid content with non-zero duration. Check that all video elements have valid sources and are properly loaded."
512
+ );
513
+ }
514
+ const videoElements = [];
515
+ if (variables.input.tracks) {
516
+ variables.input.tracks.forEach((track) => {
517
+ if (track.elements) {
518
+ track.elements.forEach((el) => {
519
+ if (el.type === "video") videoElements.push(el);
520
+ });
521
+ }
522
+ });
523
+ }
524
+ if (videoElements.length > 0) {
525
+ for (const videoEl of videoElements) {
526
+ const src = videoEl.props?.src;
527
+ if (!src || src === "undefined") continue;
528
+ const preloadVideo = document.createElement("video");
529
+ preloadVideo.crossOrigin = "anonymous";
530
+ preloadVideo.preload = "metadata";
531
+ preloadVideo.src = src;
532
+ await new Promise((resolve, reject) => {
533
+ const timeout = setTimeout(
534
+ () => reject(new Error(`Timeout loading video metadata: ${src.substring(0, 80)}`)),
535
+ 3e4
536
+ );
537
+ preloadVideo.addEventListener("loadedmetadata", () => {
538
+ clearTimeout(timeout);
539
+ resolve();
540
+ }, { once: true });
541
+ preloadVideo.addEventListener("error", () => {
542
+ clearTimeout(timeout);
543
+ const err = preloadVideo.error;
544
+ reject(new Error(`Failed to load video: ${err?.message || "Unknown error"}`));
545
+ }, { once: true });
546
+ });
547
+ }
548
+ }
371
549
  await renderer.playback.recalculate();
372
550
  await renderer.playback.reset();
373
551
  await renderer.playback.seek(0);
@@ -384,14 +562,11 @@ var renderTwickVideoInBrowser = async (config) => {
384
562
  mediaAssets.push(currentAssets);
385
563
  const canvas = renderer.stage.finalBuffer;
386
564
  await exporter.handleFrame(canvas, frame);
387
- if (settings.onProgress) {
388
- settings.onProgress(frame / totalFrames);
389
- }
565
+ if (settings.onProgress) settings.onProgress(frame / totalFrames);
390
566
  }
391
567
  await exporter.stop();
392
568
  let audioData = null;
393
569
  if (settings.includeAudio && mediaAssets.length > 0) {
394
- console.log("\u{1F3B5} Generating audio track...");
395
570
  audioData = await exporter.generateAudio(mediaAssets, 0, totalFrames);
396
571
  }
397
572
  let finalBlob = exporter.getVideoBlob();
@@ -399,9 +574,12 @@ var renderTwickVideoInBrowser = async (config) => {
399
574
  throw new Error("Failed to create video blob");
400
575
  }
401
576
  if (audioData && settings.includeAudio) {
402
- console.log("\u2705 Audio extracted and processed successfully");
403
- console.log("\u{1F4CA} Audio data size:", (audioData.byteLength / 1024 / 1024).toFixed(2), "MB");
404
- if (settings.downloadAudioSeparately) {
577
+ try {
578
+ finalBlob = await muxAudioVideo({
579
+ videoBlob: finalBlob,
580
+ audioBuffer: audioData
581
+ });
582
+ } catch {
405
583
  const audioBlob = new Blob([audioData], { type: "audio/wav" });
406
584
  const audioUrl = URL.createObjectURL(audioBlob);
407
585
  const a = document.createElement("a");
@@ -409,15 +587,7 @@ var renderTwickVideoInBrowser = async (config) => {
409
587
  a.download = "audio.wav";
410
588
  a.click();
411
589
  URL.revokeObjectURL(audioUrl);
412
- console.log("\u2705 Audio downloaded separately as audio.wav");
413
- }
414
- if (settings.onAudioReady) {
415
- const audioBlob = new Blob([audioData], { type: "audio/wav" });
416
- settings.onAudioReady(audioBlob);
417
590
  }
418
- console.log("\u{1F4A1} Note: Client-side audio muxing is complex.");
419
- console.log("\u{1F4A1} For full audio support, use server-side rendering: @twick/render-server");
420
- console.log("\u{1F4A1} Or mux manually with: ffmpeg -i video.mp4 -i audio.wav -c:v copy -c:a aac output.mp4");
421
591
  }
422
592
  if (settings.onComplete) {
423
593
  settings.onComplete(finalBlob);
@@ -461,17 +631,13 @@ var useBrowserRenderer = (options = {}) => {
461
631
  }, []);
462
632
  const download = useCallback((filename) => {
463
633
  if (!videoBlob) {
464
- const downloadError = new Error("No video available to download. Please render the video first.");
465
- setError(downloadError);
466
- console.error(downloadError.message);
634
+ setError(new Error("No video available to download. Please render the video first."));
467
635
  return;
468
636
  }
469
637
  try {
470
638
  downloadVideoBlob(videoBlob, filename || options.downloadFilename || "video.mp4");
471
639
  } catch (err) {
472
- const downloadError = err instanceof Error ? err : new Error("Failed to download video");
473
- setError(downloadError);
474
- console.error("Download error:", downloadError);
640
+ setError(err instanceof Error ? err : new Error("Failed to download video"));
475
641
  }
476
642
  }, [videoBlob, options.downloadFilename]);
477
643
  const render = useCallback(async (variables) => {
@@ -501,9 +667,7 @@ var useBrowserRenderer = (options = {}) => {
501
667
  try {
502
668
  downloadVideoBlob(blob2, downloadFilename || "video.mp4");
503
669
  } catch (downloadErr) {
504
- const error2 = downloadErr instanceof Error ? downloadErr : new Error("Failed to auto-download video");
505
- setError(error2);
506
- console.error("Auto-download error:", error2);
670
+ setError(downloadErr instanceof Error ? downloadErr : new Error("Failed to auto-download video"));
507
671
  }
508
672
  }
509
673
  },
@@ -519,9 +683,7 @@ var useBrowserRenderer = (options = {}) => {
519
683
  setProgress(1);
520
684
  return blob;
521
685
  } catch (err) {
522
- const error2 = err instanceof Error ? err : new Error(String(err));
523
- setError(error2);
524
- console.error("Render error:", error2);
686
+ setError(err instanceof Error ? err : new Error(String(err)));
525
687
  return null;
526
688
  } finally {
527
689
  setIsRendering(false);