@codingfactory/mediables-vue 2.3.4 → 2.3.5
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/{PixiFrameExporter-BRm0MI7V.cjs → PixiFrameExporter-D2kWTTVT.cjs} +2 -2
- package/dist/{PixiFrameExporter-BRm0MI7V.cjs.map → PixiFrameExporter-D2kWTTVT.cjs.map} +1 -1
- package/dist/{PixiFrameExporter-B6w9IirP.js → PixiFrameExporter-DcQaIUle.js} +2 -2
- package/dist/{PixiFrameExporter-B6w9IirP.js.map → PixiFrameExporter-DcQaIUle.js.map} +1 -1
- package/dist/{editor-DrTmgu3R.js → editor-BgjKZltI.js} +273 -274
- package/dist/editor-BgjKZltI.js.map +1 -0
- package/dist/editor-C4OKTznp.cjs +42 -0
- package/dist/editor-C4OKTznp.cjs.map +1 -0
- package/dist/{index-CPwV2mtc.js → index-BDB5R5H_.js} +3 -3
- package/dist/{index-CPwV2mtc.js.map → index-BDB5R5H_.js.map} +1 -1
- package/dist/{index-BBpOpdLI.cjs → index-DuQmNimD.cjs} +3 -3
- package/dist/{index-BBpOpdLI.cjs.map → index-DuQmNimD.cjs.map} +1 -1
- package/dist/mediables-vanilla.cjs +1 -1
- package/dist/mediables-vanilla.mjs +1 -1
- package/dist/mediables-vue.cjs +1 -1
- package/dist/mediables-vue.mjs +2 -2
- package/package.json +1 -1
- package/dist/editor-DhRuADQT.cjs +0 -42
- package/dist/editor-DhRuADQT.cjs.map +0 -1
- package/dist/editor-DrTmgu3R.js.map +0 -1
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const R=require("./index-
|
|
2
|
-
//# sourceMappingURL=PixiFrameExporter-
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const R=require("./index-DuQmNimD.cjs");function V(){return typeof VideoEncoder<"u"&&typeof VideoFrame<"u"}function z(){return typeof AudioEncoder<"u"&&typeof AudioData<"u"}async function L(i,d){try{const t=await fetch(i,d?{signal:d}:{});if(!t.ok)return null;const r=await t.arrayBuffer(),u=new AudioContext;try{return await u.decodeAudioData(r)}catch{return null}finally{await u.close()}}catch{return null}}function N(i,d,a){const t=i.sampleRate,r=Math.max(0,Math.floor(d*t)),u=Math.min(i.length,Math.ceil(a*t)),p=Math.max(0,u-r),n=[];for(let s=0;s<i.numberOfChannels;s++){const l=i.getChannelData(s);n.push(l.slice(r,r+p))}return{channels:n,sampleRate:t,numberOfChannels:i.numberOfChannels}}async function P(i,d,a,t,r){const u={codec:"mp4a.40.2",numberOfChannels:t,sampleRate:a,bitrate:r};if(!(await AudioEncoder.isConfigSupported(u)).supported)return;const n=new AudioEncoder({output:(e,c)=>{i.addAudioChunk(e,c)},error:e=>{}});n.configure(u);const s=1024,l=d[0].length;for(let e=0;e<l;e+=s){const c=Math.min(s,l-e),h=new Float32Array(t*c);for(let f=0;f<t;f++)h.set(d[f].subarray(e,e+c),f*c);const w=new AudioData({format:"f32-planar",sampleRate:a,numberOfFrames:c,numberOfChannels:t,timestamp:Math.floor(e/a*1e6),data:h});n.encode(w),w.close(),n.encodeQueueSize>10&&await new Promise(f=>{n.addEventListener("dequeue",()=>f(),{once:!0})})}await n.flush(),n.close()}async function X(i,d){const{width:a,height:t,fps:r,bitrate:u=5e6,audioBitrate:p=128e3,trimStart:n=0,trimEnd:s,sourceUrl:l,onProgress:e,signal:c}=d;if(!V())throw new Error("WebCodecs API is not supported in this browser");const h=i.duration.value,w=3600,f=w*60;if(!Number.isFinite(h)||h<=0)throw new Error(`Invalid video duration: ${h}. The video metadata may not have loaded. Please ensure the video is fully loaded before exporting.`);const A=s??h,E=Math.max(0,A-n);if(!Number.isFinite(E)||E<=0)throw new Error(`Invalid export duration: ${E} (trimStart=${n}, trimEnd=${A}). Check that the trim points are valid.`);if(E>w)throw new Error(`Export duration (${Math.round(E)}s) exceeds maximum allowed (${w}s).`);const g=Math.min(Math.max(1,Math.ceil(E*r)),f);let F=Promise.resolve(null);l&&z()&&(F=L(l,c));const v=await F,D=!!v&&v.numberOfChannels>0&&v.length>0,O={target:new R.ArrayBufferTarget,video:{codec:"avc",width:a,height:t,frameRate:r},fastStart:"in-memory"};D&&(O.audio={codec:"aac",numberOfChannels:v.numberOfChannels,sampleRate:v.sampleRate});const C=new R.Muxer(O),$=[{codec:"avc1.640028",hw:"prefer-hardware"},{codec:"avc1.4d0028",hw:"prefer-hardware"},{codec:"avc1.420028",hw:"prefer-hardware"},{codec:"avc1.640028",hw:"prefer-software"},{codec:"avc1.4d0028",hw:"prefer-software"},{codec:"avc1.420028",hw:"prefer-software"}];let y=null;for(const o of $){const x={codec:o.codec,width:a,height:t,bitrate:u,framerate:r,hardwareAcceleration:o.hw};if((await VideoEncoder.isConfigSupported(x)).supported){y=x;break}}if(!y)throw new Error(`No supported VideoEncoder codec found for ${a}×${t}. Try a lower export resolution or use a browser with H.264 encoding support (Chrome 94+).`);const m=new VideoEncoder({output:(o,x)=>{C.addVideoChunk(o,x)},error:o=>{}});m.configure(y);const b=document.createElement("canvas");b.width=a,b.height=t;const M=b.getContext("2d");if(!M)throw new Error("Failed to create 2D context for export target canvas");for(let o=0;o<g;o++){if(c!=null&&c.aborted)throw m.close(),new DOMException("Export aborted","AbortError");const x=n+o/r,S=await i.captureFrameAt(x);if(!S)continue;M.clearRect(0,0,a,t),M.drawImage(S,0,0,a,t);const I=Math.floor(o/r*1e6),q=Math.floor(1e6/r),T=new VideoFrame(b,{timestamp:I,duration:q}),B=o%(r*2)===0;m.encode(T,{keyFrame:B}),T.close(),m.encodeQueueSize>5&&await new Promise(_=>{m.addEventListener("dequeue",()=>_(),{once:!0})});const W=Math.round((o+1)/g*90);e==null||e(W)}if(await m.flush(),m.close(),D){e==null||e(91);const o=N(v,n,A);await P(C,o.channels,o.sampleRate,o.numberOfChannels,p),e==null||e(99)}C.finalize();const{buffer:k}=C.target;return e==null||e(100),new Blob([k],{type:"video/mp4"})}exports.exportWithPixiFrames=X;exports.isWebCodecsSupported=V;
|
|
2
|
+
//# sourceMappingURL=PixiFrameExporter-D2kWTTVT.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PixiFrameExporter-BRm0MI7V.cjs","sources":["../resources/js/video-engine/adapters/PixiFrameExporter.ts"],"sourcesContent":["/**\n * PIXI Frame-by-Frame Video Exporter\n *\n * Captures each frame from the PIXI canvas (which already has filters applied)\n * and encodes them with WebCodecs + mp4-muxer for exact 1:1 preview-to-export parity.\n *\n * Audio is extracted from the source video, trimmed to match, and muxed alongside\n * the video frames. If the source has no audio or AudioEncoder is unavailable,\n * the export proceeds silently (video-only).\n */\n\nimport { Muxer, ArrayBufferTarget } from 'mp4-muxer'\n\nexport interface PixiExportOptions {\n /** Width of the output video in pixels */\n width: number\n /** Height of the output video in pixels */\n height: number\n /** Frames per second */\n fps: number\n /** Video bitrate in bps (default: 5 Mbps) */\n bitrate?: number\n /** Audio bitrate in bps (default: 128 kbps) */\n audioBitrate?: number\n /** Trim start in seconds (source time, default: 0) */\n trimStart?: number\n /** Trim end in seconds (source time, default: full duration) */\n trimEnd?: number\n /** Source video URL (needed to extract audio) */\n sourceUrl?: string\n /** Progress callback (0-100) */\n onProgress?: (percent: number) => void\n /** Abort signal for cancellation */\n signal?: AbortSignal\n}\n\nexport interface PixiFrameProvider {\n /**\n * Seek the underlying video to `timeSec`, render through PIXI with filters,\n * and return the PIXI canvas with the rendered frame.\n */\n captureFrameAt(timeSec: number): Promise<HTMLCanvasElement | null>\n /** Video duration in seconds (reactive ref) */\n duration: { value: number }\n}\n\n/**\n * Check if the browser supports the required WebCodecs APIs.\n */\nexport function isWebCodecsSupported(): boolean {\n return typeof VideoEncoder !== 'undefined' && typeof VideoFrame !== 'undefined'\n}\n\n/**\n * Check if AudioEncoder is available for AAC encoding.\n */\nfunction isAudioEncoderSupported(): boolean {\n return typeof AudioEncoder !== 'undefined' && typeof AudioData !== 'undefined'\n}\n\n/**\n * Decode the audio from a video source URL using AudioContext.\n * Returns null if the source has no audio or decoding fails.\n */\nasync function decodeAudioFromSource(\n sourceUrl: string,\n signal?: AbortSignal\n): Promise<AudioBuffer | null> {\n try {\n const requestInit: RequestInit = signal ? { signal } : {}\n const response = await fetch(sourceUrl, requestInit)\n if (!response.ok) {\n console.warn('[PixiExport] Failed to fetch source for audio:', response.status)\n return null\n }\n\n const arrayBuffer = await response.arrayBuffer()\n const audioCtx = new AudioContext()\n\n try {\n const audioBuffer = await audioCtx.decodeAudioData(arrayBuffer)\n console.log('[PixiExport] Audio decoded:', {\n channels: audioBuffer.numberOfChannels,\n sampleRate: audioBuffer.sampleRate,\n duration: audioBuffer.duration,\n length: audioBuffer.length,\n })\n return audioBuffer\n } catch (decodeErr) {\n console.warn('[PixiExport] No audio track in source or decode failed:', decodeErr)\n return null\n } finally {\n await audioCtx.close()\n }\n } catch (fetchErr) {\n console.warn('[PixiExport] Could not fetch source for audio extraction:', fetchErr)\n return null\n }\n}\n\n/**\n * Trim an AudioBuffer to a specific time range.\n * Returns a new set of Float32Array channel data for the trimmed region.\n */\nfunction trimAudioBuffer(\n audioBuffer: AudioBuffer,\n trimStart: number,\n trimEnd: number\n): { channels: Float32Array[]; sampleRate: number; numberOfChannels: number } {\n const sampleRate = audioBuffer.sampleRate\n const startSample = Math.max(0, Math.floor(trimStart * sampleRate))\n const endSample = Math.min(audioBuffer.length, Math.ceil(trimEnd * sampleRate))\n const trimmedLength = Math.max(0, endSample - startSample)\n\n const channels: Float32Array[] = []\n for (let ch = 0; ch < audioBuffer.numberOfChannels; ch++) {\n const fullChannel = audioBuffer.getChannelData(ch)\n channels.push(fullChannel.slice(startSample, startSample + trimmedLength))\n }\n\n return {\n channels,\n sampleRate,\n numberOfChannels: audioBuffer.numberOfChannels,\n }\n}\n\n/**\n * Encode trimmed audio data and add chunks to the muxer.\n */\nasync function encodeAudio(\n muxer: Muxer<ArrayBufferTarget>,\n channels: Float32Array[],\n sampleRate: number,\n numberOfChannels: number,\n bitrate: number\n): Promise<void> {\n const audioEncoderConfig: AudioEncoderConfig = {\n codec: 'mp4a.40.2', // AAC-LC\n numberOfChannels,\n sampleRate,\n bitrate,\n }\n\n const support = await AudioEncoder.isConfigSupported(audioEncoderConfig)\n if (!support.supported) {\n console.warn('[PixiExport] AAC AudioEncoder not supported, exporting without audio')\n return\n }\n\n const audioEncoder = new AudioEncoder({\n output: (chunk, metadata) => {\n muxer.addAudioChunk(chunk, metadata)\n },\n error: (err) => {\n console.error('[PixiExport] AudioEncoder error:', err)\n },\n })\n\n audioEncoder.configure(audioEncoderConfig)\n\n // Encode in chunks of 1024 samples (standard AAC frame size)\n const chunkSize = 1024\n const totalSamples = channels[0].length\n\n for (let offset = 0; offset < totalSamples; offset += chunkSize) {\n const frameSamples = Math.min(chunkSize, totalSamples - offset)\n\n // Build planar data buffer: channel0[0..frameSamples] + channel1[0..frameSamples] + ...\n const planarData = new Float32Array(numberOfChannels * frameSamples)\n for (let ch = 0; ch < numberOfChannels; ch++) {\n planarData.set(channels[ch].subarray(offset, offset + frameSamples), ch * frameSamples)\n }\n\n const audioData = new AudioData({\n format: 'f32-planar',\n sampleRate,\n numberOfFrames: frameSamples,\n numberOfChannels,\n timestamp: Math.floor((offset / sampleRate) * 1_000_000), // microseconds\n data: planarData,\n })\n\n audioEncoder.encode(audioData)\n audioData.close()\n\n // Backpressure\n if (audioEncoder.encodeQueueSize > 10) {\n await new Promise<void>((resolve) => {\n audioEncoder.addEventListener('dequeue', () => resolve(), { once: true })\n })\n }\n }\n\n await audioEncoder.flush()\n audioEncoder.close()\n\n console.log('[PixiExport] Audio encoding complete', {\n totalSamples,\n chunks: Math.ceil(totalSamples / chunkSize),\n })\n}\n\n/**\n * Export a video by capturing frames from a PIXI preview (with filters applied)\n * and encoding them into an MP4 file, with audio from the source.\n */\nexport async function exportWithPixiFrames(\n provider: PixiFrameProvider,\n options: PixiExportOptions\n): Promise<Blob> {\n const {\n width,\n height,\n fps,\n bitrate = 5_000_000,\n audioBitrate = 128_000,\n trimStart = 0,\n trimEnd,\n sourceUrl,\n onProgress,\n signal,\n } = options\n\n if (!isWebCodecsSupported()) {\n throw new Error('WebCodecs API is not supported in this browser')\n }\n\n const videoDuration = provider.duration.value\n\n console.log('[TRACE-EXPORT] PixiFrameExporter ENTER', {\n videoDuration,\n trimStart,\n trimEnd,\n fps,\n width,\n height,\n isFiniteDuration: Number.isFinite(videoDuration),\n sourceUrl: sourceUrl?.substring(0, 50),\n })\n\n // Guard against Infinity/NaN duration (common with blob URLs where metadata isn't loaded).\n // Cap at 1 hour as an absolute safety limit.\n const MAX_EXPORT_DURATION_SEC = 3600\n const MAX_TOTAL_FRAMES = MAX_EXPORT_DURATION_SEC * 60 // 216,000 frames at 60fps\n\n if (!Number.isFinite(videoDuration) || videoDuration <= 0) {\n console.error('[TRACE-EXPORT] INVALID DURATION:', videoDuration)\n throw new Error(\n `Invalid video duration: ${videoDuration}. The video metadata may not have loaded. ` +\n 'Please ensure the video is fully loaded before exporting.'\n )\n }\n\n const effectiveTrimEnd = trimEnd ?? videoDuration\n const exportDuration = Math.max(0, effectiveTrimEnd - trimStart)\n\n console.log('[TRACE-EXPORT] PixiFrameExporter calculated:', {\n effectiveTrimEnd,\n exportDuration,\n isFiniteExportDuration: Number.isFinite(exportDuration),\n })\n\n if (!Number.isFinite(exportDuration) || exportDuration <= 0) {\n console.error('[TRACE-EXPORT] INVALID EXPORT DURATION:', exportDuration)\n throw new Error(\n `Invalid export duration: ${exportDuration} (trimStart=${trimStart}, trimEnd=${effectiveTrimEnd}). ` +\n 'Check that the trim points are valid.'\n )\n }\n\n if (exportDuration > MAX_EXPORT_DURATION_SEC) {\n throw new Error(`Export duration (${Math.round(exportDuration)}s) exceeds maximum allowed (${MAX_EXPORT_DURATION_SEC}s).`)\n }\n\n const totalFrames = Math.min(\n Math.max(1, Math.ceil(exportDuration * fps)),\n MAX_TOTAL_FRAMES\n )\n\n console.log('[TRACE-EXPORT] PixiFrameExporter will capture', totalFrames, 'frames over', exportDuration, 'seconds at', fps, 'fps')\n\n // --- Decode audio from source (in parallel with setup) ---\n let audioPromise: Promise<AudioBuffer | null> = Promise.resolve(null)\n if (sourceUrl && isAudioEncoderSupported()) {\n audioPromise = decodeAudioFromSource(sourceUrl, signal)\n }\n\n console.log('[PixiExport] Starting export', {\n width,\n height,\n fps,\n trimStart,\n trimEnd: effectiveTrimEnd,\n exportDuration,\n totalFrames,\n hasSourceUrl: !!sourceUrl,\n })\n\n // --- Muxer (configure audio track conditionally after we know if audio exists) ---\n // We need to know audio params before creating the muxer, so await audio decode first.\n const audioBuffer = await audioPromise\n const hasAudio = !!audioBuffer && audioBuffer.numberOfChannels > 0 && audioBuffer.length > 0\n\n const muxerConfig: ConstructorParameters<typeof Muxer<ArrayBufferTarget>>[0] = {\n target: new ArrayBufferTarget(),\n video: {\n codec: 'avc',\n width,\n height,\n frameRate: fps,\n },\n fastStart: 'in-memory',\n }\n\n if (hasAudio) {\n muxerConfig.audio = {\n codec: 'aac',\n numberOfChannels: audioBuffer.numberOfChannels,\n sampleRate: audioBuffer.sampleRate,\n }\n console.log('[PixiExport] Audio track configured:', {\n channels: audioBuffer.numberOfChannels,\n sampleRate: audioBuffer.sampleRate,\n })\n } else {\n console.log('[PixiExport] No audio track - exporting video only')\n }\n\n const muxer = new Muxer(muxerConfig)\n\n // --- Video Encoder ---\n // Try codecs in order: High 4.0 (best quality at 1080p), Main 4.0, Baseline 4.0,\n // then repeat without hardware acceleration. avc1.42001f (Baseline Level 3.1)\n // only supports up to 1280×720, so 1080p exports need Level 4.0+.\n const codecCandidates: Array<{ codec: string; hw: HardwareAcceleration }> = [\n { codec: 'avc1.640028', hw: 'prefer-hardware' }, // High Profile Level 4.0\n { codec: 'avc1.4d0028', hw: 'prefer-hardware' }, // Main Profile Level 4.0\n { codec: 'avc1.420028', hw: 'prefer-hardware' }, // Baseline Profile Level 4.0\n { codec: 'avc1.640028', hw: 'prefer-software' }, // High 4.0, software fallback\n { codec: 'avc1.4d0028', hw: 'prefer-software' }, // Main 4.0, software fallback\n { codec: 'avc1.420028', hw: 'prefer-software' }, // Baseline 4.0, software fallback\n ]\n\n let encoderConfig: VideoEncoderConfig | null = null\n for (const candidate of codecCandidates) {\n const cfg: VideoEncoderConfig = {\n codec: candidate.codec,\n width,\n height,\n bitrate,\n framerate: fps,\n hardwareAcceleration: candidate.hw,\n }\n const support = await VideoEncoder.isConfigSupported(cfg)\n if (support.supported) {\n encoderConfig = cfg\n console.log('[PixiExport] Using codec:', candidate.codec, 'hw:', candidate.hw)\n break\n }\n }\n\n if (!encoderConfig) {\n throw new Error(\n `No supported VideoEncoder codec found for ${width}×${height}. ` +\n 'Try a lower export resolution or use a browser with H.264 encoding support (Chrome 94+).'\n )\n }\n\n const encoder = new VideoEncoder({\n output: (chunk, metadata) => {\n muxer.addVideoChunk(chunk, metadata)\n },\n error: (err) => {\n console.error('[PixiExport] VideoEncoder error:', err)\n },\n })\n\n encoder.configure(encoderConfig)\n\n // --- Intermediate 2D canvas ---\n const targetCanvas = document.createElement('canvas')\n targetCanvas.width = width\n targetCanvas.height = height\n const targetCtx = targetCanvas.getContext('2d')\n if (!targetCtx) {\n throw new Error('Failed to create 2D context for export target canvas')\n }\n\n // --- Video frame loop (progress: 0-90%) ---\n console.log('[TRACE-EXPORT] Frame loop START, totalFrames:', totalFrames)\n let nullFrameCount = 0\n for (let frame = 0; frame < totalFrames; frame++) {\n if (signal?.aborted) {\n console.log('[TRACE-EXPORT] Frame loop ABORTED at frame', frame)\n encoder.close()\n throw new DOMException('Export aborted', 'AbortError')\n }\n\n // Calculate source time: offset by trimStart\n const timeSec = trimStart + frame / fps\n\n if (frame === 0 || frame === totalFrames - 1 || frame % 10 === 0) {\n console.log(`[TRACE-EXPORT] Capturing frame ${frame}/${totalFrames} at ${timeSec.toFixed(3)}s`)\n }\n\n // Capture the frame from PIXI (seek + render + return canvas)\n const pixiCanvas = await provider.captureFrameAt(timeSec)\n if (!pixiCanvas) {\n nullFrameCount++\n console.warn(`[PixiExport] captureFrameAt(${timeSec}) returned null, skipping frame ${frame} (total nulls: ${nullFrameCount})`)\n continue\n }\n\n // Draw PIXI canvas → intermediate 2D canvas (resize to output dimensions)\n targetCtx.clearRect(0, 0, width, height)\n targetCtx.drawImage(pixiCanvas, 0, 0, width, height)\n\n // Create VideoFrame\n const timestamp = Math.floor((frame / fps) * 1_000_000) // microseconds\n const frameDuration = Math.floor(1_000_000 / fps)\n const videoFrame = new VideoFrame(targetCanvas, {\n timestamp,\n duration: frameDuration,\n })\n\n // Encode (keyframe every 2 seconds)\n const keyFrame = frame % (fps * 2) === 0\n encoder.encode(videoFrame, { keyFrame })\n videoFrame.close()\n\n // Backpressure: wait if encoder queue is full\n if (encoder.encodeQueueSize > 5) {\n await new Promise<void>((resolve) => {\n encoder.addEventListener('dequeue', () => resolve(), { once: true })\n })\n }\n\n // Report progress (video = 0-90%)\n const percent = Math.round(((frame + 1) / totalFrames) * 90)\n onProgress?.(percent)\n }\n\n console.log('[TRACE-EXPORT] Frame loop DONE, captured', totalFrames - nullFrameCount, 'frames, skipped', nullFrameCount)\n\n // --- Flush video ---\n console.log('[TRACE-EXPORT] Flushing video encoder...')\n await encoder.flush()\n encoder.close()\n console.log('[TRACE-EXPORT] Video encoder flushed and closed')\n\n // --- Encode audio (progress: 90-99%) ---\n if (hasAudio) {\n onProgress?.(91)\n const trimmed = trimAudioBuffer(audioBuffer, trimStart, effectiveTrimEnd)\n await encodeAudio(muxer, trimmed.channels, trimmed.sampleRate, trimmed.numberOfChannels, audioBitrate)\n onProgress?.(99)\n }\n\n // --- Finalize ---\n muxer.finalize()\n const { buffer } = muxer.target\n\n onProgress?.(100)\n\n console.log('[PixiExport] Export complete', {\n size: buffer.byteLength,\n duration: exportDuration,\n frames: totalFrames,\n hasAudio,\n })\n\n return new Blob([buffer], { type: 'video/mp4' })\n}\n"],"names":["isWebCodecsSupported","isAudioEncoderSupported","decodeAudioFromSource","sourceUrl","signal","response","arrayBuffer","audioCtx","trimAudioBuffer","audioBuffer","trimStart","trimEnd","sampleRate","startSample","endSample","trimmedLength","channels","ch","fullChannel","encodeAudio","muxer","numberOfChannels","bitrate","audioEncoderConfig","audioEncoder","chunk","metadata","err","chunkSize","totalSamples","offset","frameSamples","planarData","audioData","resolve","exportWithPixiFrames","provider","options","width","height","fps","audioBitrate","onProgress","videoDuration","MAX_EXPORT_DURATION_SEC","MAX_TOTAL_FRAMES","effectiveTrimEnd","exportDuration","totalFrames","audioPromise","hasAudio","muxerConfig","ArrayBufferTarget","Muxer","codecCandidates","encoderConfig","candidate","cfg","encoder","targetCanvas","targetCtx","frame","timeSec","pixiCanvas","timestamp","frameDuration","videoFrame","keyFrame","percent","trimmed","buffer"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"PixiFrameExporter-D2kWTTVT.cjs","sources":["../resources/js/video-engine/adapters/PixiFrameExporter.ts"],"sourcesContent":["/**\n * PIXI Frame-by-Frame Video Exporter\n *\n * Captures each frame from the PIXI canvas (which already has filters applied)\n * and encodes them with WebCodecs + mp4-muxer for exact 1:1 preview-to-export parity.\n *\n * Audio is extracted from the source video, trimmed to match, and muxed alongside\n * the video frames. If the source has no audio or AudioEncoder is unavailable,\n * the export proceeds silently (video-only).\n */\n\nimport { Muxer, ArrayBufferTarget } from 'mp4-muxer'\n\nexport interface PixiExportOptions {\n /** Width of the output video in pixels */\n width: number\n /** Height of the output video in pixels */\n height: number\n /** Frames per second */\n fps: number\n /** Video bitrate in bps (default: 5 Mbps) */\n bitrate?: number\n /** Audio bitrate in bps (default: 128 kbps) */\n audioBitrate?: number\n /** Trim start in seconds (source time, default: 0) */\n trimStart?: number\n /** Trim end in seconds (source time, default: full duration) */\n trimEnd?: number\n /** Source video URL (needed to extract audio) */\n sourceUrl?: string\n /** Progress callback (0-100) */\n onProgress?: (percent: number) => void\n /** Abort signal for cancellation */\n signal?: AbortSignal\n}\n\nexport interface PixiFrameProvider {\n /**\n * Seek the underlying video to `timeSec`, render through PIXI with filters,\n * and return the PIXI canvas with the rendered frame.\n */\n captureFrameAt(timeSec: number): Promise<HTMLCanvasElement | null>\n /** Video duration in seconds (reactive ref) */\n duration: { value: number }\n}\n\n/**\n * Check if the browser supports the required WebCodecs APIs.\n */\nexport function isWebCodecsSupported(): boolean {\n return typeof VideoEncoder !== 'undefined' && typeof VideoFrame !== 'undefined'\n}\n\n/**\n * Check if AudioEncoder is available for AAC encoding.\n */\nfunction isAudioEncoderSupported(): boolean {\n return typeof AudioEncoder !== 'undefined' && typeof AudioData !== 'undefined'\n}\n\n/**\n * Decode the audio from a video source URL using AudioContext.\n * Returns null if the source has no audio or decoding fails.\n */\nasync function decodeAudioFromSource(\n sourceUrl: string,\n signal?: AbortSignal\n): Promise<AudioBuffer | null> {\n try {\n const requestInit: RequestInit = signal ? { signal } : {}\n const response = await fetch(sourceUrl, requestInit)\n if (!response.ok) {\n console.warn('[PixiExport] Failed to fetch source for audio:', response.status)\n return null\n }\n\n const arrayBuffer = await response.arrayBuffer()\n const audioCtx = new AudioContext()\n\n try {\n const audioBuffer = await audioCtx.decodeAudioData(arrayBuffer)\n console.log('[PixiExport] Audio decoded:', {\n channels: audioBuffer.numberOfChannels,\n sampleRate: audioBuffer.sampleRate,\n duration: audioBuffer.duration,\n length: audioBuffer.length,\n })\n return audioBuffer\n } catch (decodeErr) {\n console.warn('[PixiExport] No audio track in source or decode failed:', decodeErr)\n return null\n } finally {\n await audioCtx.close()\n }\n } catch (fetchErr) {\n console.warn('[PixiExport] Could not fetch source for audio extraction:', fetchErr)\n return null\n }\n}\n\n/**\n * Trim an AudioBuffer to a specific time range.\n * Returns a new set of Float32Array channel data for the trimmed region.\n */\nfunction trimAudioBuffer(\n audioBuffer: AudioBuffer,\n trimStart: number,\n trimEnd: number\n): { channels: Float32Array[]; sampleRate: number; numberOfChannels: number } {\n const sampleRate = audioBuffer.sampleRate\n const startSample = Math.max(0, Math.floor(trimStart * sampleRate))\n const endSample = Math.min(audioBuffer.length, Math.ceil(trimEnd * sampleRate))\n const trimmedLength = Math.max(0, endSample - startSample)\n\n const channels: Float32Array[] = []\n for (let ch = 0; ch < audioBuffer.numberOfChannels; ch++) {\n const fullChannel = audioBuffer.getChannelData(ch)\n channels.push(fullChannel.slice(startSample, startSample + trimmedLength))\n }\n\n return {\n channels,\n sampleRate,\n numberOfChannels: audioBuffer.numberOfChannels,\n }\n}\n\n/**\n * Encode trimmed audio data and add chunks to the muxer.\n */\nasync function encodeAudio(\n muxer: Muxer<ArrayBufferTarget>,\n channels: Float32Array[],\n sampleRate: number,\n numberOfChannels: number,\n bitrate: number\n): Promise<void> {\n const audioEncoderConfig: AudioEncoderConfig = {\n codec: 'mp4a.40.2', // AAC-LC\n numberOfChannels,\n sampleRate,\n bitrate,\n }\n\n const support = await AudioEncoder.isConfigSupported(audioEncoderConfig)\n if (!support.supported) {\n console.warn('[PixiExport] AAC AudioEncoder not supported, exporting without audio')\n return\n }\n\n const audioEncoder = new AudioEncoder({\n output: (chunk, metadata) => {\n muxer.addAudioChunk(chunk, metadata)\n },\n error: (err) => {\n console.error('[PixiExport] AudioEncoder error:', err)\n },\n })\n\n audioEncoder.configure(audioEncoderConfig)\n\n // Encode in chunks of 1024 samples (standard AAC frame size)\n const chunkSize = 1024\n const totalSamples = channels[0].length\n\n for (let offset = 0; offset < totalSamples; offset += chunkSize) {\n const frameSamples = Math.min(chunkSize, totalSamples - offset)\n\n // Build planar data buffer: channel0[0..frameSamples] + channel1[0..frameSamples] + ...\n const planarData = new Float32Array(numberOfChannels * frameSamples)\n for (let ch = 0; ch < numberOfChannels; ch++) {\n planarData.set(channels[ch].subarray(offset, offset + frameSamples), ch * frameSamples)\n }\n\n const audioData = new AudioData({\n format: 'f32-planar',\n sampleRate,\n numberOfFrames: frameSamples,\n numberOfChannels,\n timestamp: Math.floor((offset / sampleRate) * 1_000_000), // microseconds\n data: planarData,\n })\n\n audioEncoder.encode(audioData)\n audioData.close()\n\n // Backpressure\n if (audioEncoder.encodeQueueSize > 10) {\n await new Promise<void>((resolve) => {\n audioEncoder.addEventListener('dequeue', () => resolve(), { once: true })\n })\n }\n }\n\n await audioEncoder.flush()\n audioEncoder.close()\n\n console.log('[PixiExport] Audio encoding complete', {\n totalSamples,\n chunks: Math.ceil(totalSamples / chunkSize),\n })\n}\n\n/**\n * Export a video by capturing frames from a PIXI preview (with filters applied)\n * and encoding them into an MP4 file, with audio from the source.\n */\nexport async function exportWithPixiFrames(\n provider: PixiFrameProvider,\n options: PixiExportOptions\n): Promise<Blob> {\n const {\n width,\n height,\n fps,\n bitrate = 5_000_000,\n audioBitrate = 128_000,\n trimStart = 0,\n trimEnd,\n sourceUrl,\n onProgress,\n signal,\n } = options\n\n if (!isWebCodecsSupported()) {\n throw new Error('WebCodecs API is not supported in this browser')\n }\n\n const videoDuration = provider.duration.value\n\n console.log('[TRACE-EXPORT] PixiFrameExporter ENTER', {\n videoDuration,\n trimStart,\n trimEnd,\n fps,\n width,\n height,\n isFiniteDuration: Number.isFinite(videoDuration),\n sourceUrl: sourceUrl?.substring(0, 50),\n })\n\n // Guard against Infinity/NaN duration (common with blob URLs where metadata isn't loaded).\n // Cap at 1 hour as an absolute safety limit.\n const MAX_EXPORT_DURATION_SEC = 3600\n const MAX_TOTAL_FRAMES = MAX_EXPORT_DURATION_SEC * 60 // 216,000 frames at 60fps\n\n if (!Number.isFinite(videoDuration) || videoDuration <= 0) {\n console.error('[TRACE-EXPORT] INVALID DURATION:', videoDuration)\n throw new Error(\n `Invalid video duration: ${videoDuration}. The video metadata may not have loaded. ` +\n 'Please ensure the video is fully loaded before exporting.'\n )\n }\n\n const effectiveTrimEnd = trimEnd ?? videoDuration\n const exportDuration = Math.max(0, effectiveTrimEnd - trimStart)\n\n console.log('[TRACE-EXPORT] PixiFrameExporter calculated:', {\n effectiveTrimEnd,\n exportDuration,\n isFiniteExportDuration: Number.isFinite(exportDuration),\n })\n\n if (!Number.isFinite(exportDuration) || exportDuration <= 0) {\n console.error('[TRACE-EXPORT] INVALID EXPORT DURATION:', exportDuration)\n throw new Error(\n `Invalid export duration: ${exportDuration} (trimStart=${trimStart}, trimEnd=${effectiveTrimEnd}). ` +\n 'Check that the trim points are valid.'\n )\n }\n\n if (exportDuration > MAX_EXPORT_DURATION_SEC) {\n throw new Error(`Export duration (${Math.round(exportDuration)}s) exceeds maximum allowed (${MAX_EXPORT_DURATION_SEC}s).`)\n }\n\n const totalFrames = Math.min(\n Math.max(1, Math.ceil(exportDuration * fps)),\n MAX_TOTAL_FRAMES\n )\n\n console.log('[TRACE-EXPORT] PixiFrameExporter will capture', totalFrames, 'frames over', exportDuration, 'seconds at', fps, 'fps')\n\n // --- Decode audio from source (in parallel with setup) ---\n let audioPromise: Promise<AudioBuffer | null> = Promise.resolve(null)\n if (sourceUrl && isAudioEncoderSupported()) {\n audioPromise = decodeAudioFromSource(sourceUrl, signal)\n }\n\n console.log('[PixiExport] Starting export', {\n width,\n height,\n fps,\n trimStart,\n trimEnd: effectiveTrimEnd,\n exportDuration,\n totalFrames,\n hasSourceUrl: !!sourceUrl,\n })\n\n // --- Muxer (configure audio track conditionally after we know if audio exists) ---\n // We need to know audio params before creating the muxer, so await audio decode first.\n const audioBuffer = await audioPromise\n const hasAudio = !!audioBuffer && audioBuffer.numberOfChannels > 0 && audioBuffer.length > 0\n\n const muxerConfig: ConstructorParameters<typeof Muxer<ArrayBufferTarget>>[0] = {\n target: new ArrayBufferTarget(),\n video: {\n codec: 'avc',\n width,\n height,\n frameRate: fps,\n },\n fastStart: 'in-memory',\n }\n\n if (hasAudio) {\n muxerConfig.audio = {\n codec: 'aac',\n numberOfChannels: audioBuffer.numberOfChannels,\n sampleRate: audioBuffer.sampleRate,\n }\n console.log('[PixiExport] Audio track configured:', {\n channels: audioBuffer.numberOfChannels,\n sampleRate: audioBuffer.sampleRate,\n })\n } else {\n console.log('[PixiExport] No audio track - exporting video only')\n }\n\n const muxer = new Muxer(muxerConfig)\n\n // --- Video Encoder ---\n // Try codecs in order: High 4.0 (best quality at 1080p), Main 4.0, Baseline 4.0,\n // then repeat without hardware acceleration. avc1.42001f (Baseline Level 3.1)\n // only supports up to 1280×720, so 1080p exports need Level 4.0+.\n const codecCandidates: Array<{ codec: string; hw: HardwareAcceleration }> = [\n { codec: 'avc1.640028', hw: 'prefer-hardware' }, // High Profile Level 4.0\n { codec: 'avc1.4d0028', hw: 'prefer-hardware' }, // Main Profile Level 4.0\n { codec: 'avc1.420028', hw: 'prefer-hardware' }, // Baseline Profile Level 4.0\n { codec: 'avc1.640028', hw: 'prefer-software' }, // High 4.0, software fallback\n { codec: 'avc1.4d0028', hw: 'prefer-software' }, // Main 4.0, software fallback\n { codec: 'avc1.420028', hw: 'prefer-software' }, // Baseline 4.0, software fallback\n ]\n\n let encoderConfig: VideoEncoderConfig | null = null\n for (const candidate of codecCandidates) {\n const cfg: VideoEncoderConfig = {\n codec: candidate.codec,\n width,\n height,\n bitrate,\n framerate: fps,\n hardwareAcceleration: candidate.hw,\n }\n const support = await VideoEncoder.isConfigSupported(cfg)\n if (support.supported) {\n encoderConfig = cfg\n console.log('[PixiExport] Using codec:', candidate.codec, 'hw:', candidate.hw)\n break\n }\n }\n\n if (!encoderConfig) {\n throw new Error(\n `No supported VideoEncoder codec found for ${width}×${height}. ` +\n 'Try a lower export resolution or use a browser with H.264 encoding support (Chrome 94+).'\n )\n }\n\n const encoder = new VideoEncoder({\n output: (chunk, metadata) => {\n muxer.addVideoChunk(chunk, metadata)\n },\n error: (err) => {\n console.error('[PixiExport] VideoEncoder error:', err)\n },\n })\n\n encoder.configure(encoderConfig)\n\n // --- Intermediate 2D canvas ---\n const targetCanvas = document.createElement('canvas')\n targetCanvas.width = width\n targetCanvas.height = height\n const targetCtx = targetCanvas.getContext('2d')\n if (!targetCtx) {\n throw new Error('Failed to create 2D context for export target canvas')\n }\n\n // --- Video frame loop (progress: 0-90%) ---\n console.log('[TRACE-EXPORT] Frame loop START, totalFrames:', totalFrames)\n let nullFrameCount = 0\n for (let frame = 0; frame < totalFrames; frame++) {\n if (signal?.aborted) {\n console.log('[TRACE-EXPORT] Frame loop ABORTED at frame', frame)\n encoder.close()\n throw new DOMException('Export aborted', 'AbortError')\n }\n\n // Calculate source time: offset by trimStart\n const timeSec = trimStart + frame / fps\n\n if (frame === 0 || frame === totalFrames - 1 || frame % 10 === 0) {\n console.log(`[TRACE-EXPORT] Capturing frame ${frame}/${totalFrames} at ${timeSec.toFixed(3)}s`)\n }\n\n // Capture the frame from PIXI (seek + render + return canvas)\n const pixiCanvas = await provider.captureFrameAt(timeSec)\n if (!pixiCanvas) {\n nullFrameCount++\n console.warn(`[PixiExport] captureFrameAt(${timeSec}) returned null, skipping frame ${frame} (total nulls: ${nullFrameCount})`)\n continue\n }\n\n // Draw PIXI canvas → intermediate 2D canvas (resize to output dimensions)\n targetCtx.clearRect(0, 0, width, height)\n targetCtx.drawImage(pixiCanvas, 0, 0, width, height)\n\n // Create VideoFrame\n const timestamp = Math.floor((frame / fps) * 1_000_000) // microseconds\n const frameDuration = Math.floor(1_000_000 / fps)\n const videoFrame = new VideoFrame(targetCanvas, {\n timestamp,\n duration: frameDuration,\n })\n\n // Encode (keyframe every 2 seconds)\n const keyFrame = frame % (fps * 2) === 0\n encoder.encode(videoFrame, { keyFrame })\n videoFrame.close()\n\n // Backpressure: wait if encoder queue is full\n if (encoder.encodeQueueSize > 5) {\n await new Promise<void>((resolve) => {\n encoder.addEventListener('dequeue', () => resolve(), { once: true })\n })\n }\n\n // Report progress (video = 0-90%)\n const percent = Math.round(((frame + 1) / totalFrames) * 90)\n onProgress?.(percent)\n }\n\n console.log('[TRACE-EXPORT] Frame loop DONE, captured', totalFrames - nullFrameCount, 'frames, skipped', nullFrameCount)\n\n // --- Flush video ---\n console.log('[TRACE-EXPORT] Flushing video encoder...')\n await encoder.flush()\n encoder.close()\n console.log('[TRACE-EXPORT] Video encoder flushed and closed')\n\n // --- Encode audio (progress: 90-99%) ---\n if (hasAudio) {\n onProgress?.(91)\n const trimmed = trimAudioBuffer(audioBuffer, trimStart, effectiveTrimEnd)\n await encodeAudio(muxer, trimmed.channels, trimmed.sampleRate, trimmed.numberOfChannels, audioBitrate)\n onProgress?.(99)\n }\n\n // --- Finalize ---\n muxer.finalize()\n const { buffer } = muxer.target\n\n onProgress?.(100)\n\n console.log('[PixiExport] Export complete', {\n size: buffer.byteLength,\n duration: exportDuration,\n frames: totalFrames,\n hasAudio,\n })\n\n return new Blob([buffer], { type: 'video/mp4' })\n}\n"],"names":["isWebCodecsSupported","isAudioEncoderSupported","decodeAudioFromSource","sourceUrl","signal","response","arrayBuffer","audioCtx","trimAudioBuffer","audioBuffer","trimStart","trimEnd","sampleRate","startSample","endSample","trimmedLength","channels","ch","fullChannel","encodeAudio","muxer","numberOfChannels","bitrate","audioEncoderConfig","audioEncoder","chunk","metadata","err","chunkSize","totalSamples","offset","frameSamples","planarData","audioData","resolve","exportWithPixiFrames","provider","options","width","height","fps","audioBitrate","onProgress","videoDuration","MAX_EXPORT_DURATION_SEC","MAX_TOTAL_FRAMES","effectiveTrimEnd","exportDuration","totalFrames","audioPromise","hasAudio","muxerConfig","ArrayBufferTarget","Muxer","codecCandidates","encoderConfig","candidate","cfg","encoder","targetCanvas","targetCtx","frame","timeSec","pixiCanvas","timestamp","frameDuration","videoFrame","keyFrame","percent","trimmed","buffer"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as q, M as z } from "./index-
|
|
1
|
+
import { A as q, M as z } from "./index-BDB5R5H_.js";
|
|
2
2
|
function L() {
|
|
3
3
|
return typeof VideoEncoder < "u" && typeof VideoFrame < "u";
|
|
4
4
|
}
|
|
@@ -196,4 +196,4 @@ export {
|
|
|
196
196
|
H as exportWithPixiFrames,
|
|
197
197
|
L as isWebCodecsSupported
|
|
198
198
|
};
|
|
199
|
-
//# sourceMappingURL=PixiFrameExporter-
|
|
199
|
+
//# sourceMappingURL=PixiFrameExporter-DcQaIUle.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PixiFrameExporter-B6w9IirP.js","sources":["../resources/js/video-engine/adapters/PixiFrameExporter.ts"],"sourcesContent":["/**\n * PIXI Frame-by-Frame Video Exporter\n *\n * Captures each frame from the PIXI canvas (which already has filters applied)\n * and encodes them with WebCodecs + mp4-muxer for exact 1:1 preview-to-export parity.\n *\n * Audio is extracted from the source video, trimmed to match, and muxed alongside\n * the video frames. If the source has no audio or AudioEncoder is unavailable,\n * the export proceeds silently (video-only).\n */\n\nimport { Muxer, ArrayBufferTarget } from 'mp4-muxer'\n\nexport interface PixiExportOptions {\n /** Width of the output video in pixels */\n width: number\n /** Height of the output video in pixels */\n height: number\n /** Frames per second */\n fps: number\n /** Video bitrate in bps (default: 5 Mbps) */\n bitrate?: number\n /** Audio bitrate in bps (default: 128 kbps) */\n audioBitrate?: number\n /** Trim start in seconds (source time, default: 0) */\n trimStart?: number\n /** Trim end in seconds (source time, default: full duration) */\n trimEnd?: number\n /** Source video URL (needed to extract audio) */\n sourceUrl?: string\n /** Progress callback (0-100) */\n onProgress?: (percent: number) => void\n /** Abort signal for cancellation */\n signal?: AbortSignal\n}\n\nexport interface PixiFrameProvider {\n /**\n * Seek the underlying video to `timeSec`, render through PIXI with filters,\n * and return the PIXI canvas with the rendered frame.\n */\n captureFrameAt(timeSec: number): Promise<HTMLCanvasElement | null>\n /** Video duration in seconds (reactive ref) */\n duration: { value: number }\n}\n\n/**\n * Check if the browser supports the required WebCodecs APIs.\n */\nexport function isWebCodecsSupported(): boolean {\n return typeof VideoEncoder !== 'undefined' && typeof VideoFrame !== 'undefined'\n}\n\n/**\n * Check if AudioEncoder is available for AAC encoding.\n */\nfunction isAudioEncoderSupported(): boolean {\n return typeof AudioEncoder !== 'undefined' && typeof AudioData !== 'undefined'\n}\n\n/**\n * Decode the audio from a video source URL using AudioContext.\n * Returns null if the source has no audio or decoding fails.\n */\nasync function decodeAudioFromSource(\n sourceUrl: string,\n signal?: AbortSignal\n): Promise<AudioBuffer | null> {\n try {\n const requestInit: RequestInit = signal ? { signal } : {}\n const response = await fetch(sourceUrl, requestInit)\n if (!response.ok) {\n console.warn('[PixiExport] Failed to fetch source for audio:', response.status)\n return null\n }\n\n const arrayBuffer = await response.arrayBuffer()\n const audioCtx = new AudioContext()\n\n try {\n const audioBuffer = await audioCtx.decodeAudioData(arrayBuffer)\n console.log('[PixiExport] Audio decoded:', {\n channels: audioBuffer.numberOfChannels,\n sampleRate: audioBuffer.sampleRate,\n duration: audioBuffer.duration,\n length: audioBuffer.length,\n })\n return audioBuffer\n } catch (decodeErr) {\n console.warn('[PixiExport] No audio track in source or decode failed:', decodeErr)\n return null\n } finally {\n await audioCtx.close()\n }\n } catch (fetchErr) {\n console.warn('[PixiExport] Could not fetch source for audio extraction:', fetchErr)\n return null\n }\n}\n\n/**\n * Trim an AudioBuffer to a specific time range.\n * Returns a new set of Float32Array channel data for the trimmed region.\n */\nfunction trimAudioBuffer(\n audioBuffer: AudioBuffer,\n trimStart: number,\n trimEnd: number\n): { channels: Float32Array[]; sampleRate: number; numberOfChannels: number } {\n const sampleRate = audioBuffer.sampleRate\n const startSample = Math.max(0, Math.floor(trimStart * sampleRate))\n const endSample = Math.min(audioBuffer.length, Math.ceil(trimEnd * sampleRate))\n const trimmedLength = Math.max(0, endSample - startSample)\n\n const channels: Float32Array[] = []\n for (let ch = 0; ch < audioBuffer.numberOfChannels; ch++) {\n const fullChannel = audioBuffer.getChannelData(ch)\n channels.push(fullChannel.slice(startSample, startSample + trimmedLength))\n }\n\n return {\n channels,\n sampleRate,\n numberOfChannels: audioBuffer.numberOfChannels,\n }\n}\n\n/**\n * Encode trimmed audio data and add chunks to the muxer.\n */\nasync function encodeAudio(\n muxer: Muxer<ArrayBufferTarget>,\n channels: Float32Array[],\n sampleRate: number,\n numberOfChannels: number,\n bitrate: number\n): Promise<void> {\n const audioEncoderConfig: AudioEncoderConfig = {\n codec: 'mp4a.40.2', // AAC-LC\n numberOfChannels,\n sampleRate,\n bitrate,\n }\n\n const support = await AudioEncoder.isConfigSupported(audioEncoderConfig)\n if (!support.supported) {\n console.warn('[PixiExport] AAC AudioEncoder not supported, exporting without audio')\n return\n }\n\n const audioEncoder = new AudioEncoder({\n output: (chunk, metadata) => {\n muxer.addAudioChunk(chunk, metadata)\n },\n error: (err) => {\n console.error('[PixiExport] AudioEncoder error:', err)\n },\n })\n\n audioEncoder.configure(audioEncoderConfig)\n\n // Encode in chunks of 1024 samples (standard AAC frame size)\n const chunkSize = 1024\n const totalSamples = channels[0].length\n\n for (let offset = 0; offset < totalSamples; offset += chunkSize) {\n const frameSamples = Math.min(chunkSize, totalSamples - offset)\n\n // Build planar data buffer: channel0[0..frameSamples] + channel1[0..frameSamples] + ...\n const planarData = new Float32Array(numberOfChannels * frameSamples)\n for (let ch = 0; ch < numberOfChannels; ch++) {\n planarData.set(channels[ch].subarray(offset, offset + frameSamples), ch * frameSamples)\n }\n\n const audioData = new AudioData({\n format: 'f32-planar',\n sampleRate,\n numberOfFrames: frameSamples,\n numberOfChannels,\n timestamp: Math.floor((offset / sampleRate) * 1_000_000), // microseconds\n data: planarData,\n })\n\n audioEncoder.encode(audioData)\n audioData.close()\n\n // Backpressure\n if (audioEncoder.encodeQueueSize > 10) {\n await new Promise<void>((resolve) => {\n audioEncoder.addEventListener('dequeue', () => resolve(), { once: true })\n })\n }\n }\n\n await audioEncoder.flush()\n audioEncoder.close()\n\n console.log('[PixiExport] Audio encoding complete', {\n totalSamples,\n chunks: Math.ceil(totalSamples / chunkSize),\n })\n}\n\n/**\n * Export a video by capturing frames from a PIXI preview (with filters applied)\n * and encoding them into an MP4 file, with audio from the source.\n */\nexport async function exportWithPixiFrames(\n provider: PixiFrameProvider,\n options: PixiExportOptions\n): Promise<Blob> {\n const {\n width,\n height,\n fps,\n bitrate = 5_000_000,\n audioBitrate = 128_000,\n trimStart = 0,\n trimEnd,\n sourceUrl,\n onProgress,\n signal,\n } = options\n\n if (!isWebCodecsSupported()) {\n throw new Error('WebCodecs API is not supported in this browser')\n }\n\n const videoDuration = provider.duration.value\n\n console.log('[TRACE-EXPORT] PixiFrameExporter ENTER', {\n videoDuration,\n trimStart,\n trimEnd,\n fps,\n width,\n height,\n isFiniteDuration: Number.isFinite(videoDuration),\n sourceUrl: sourceUrl?.substring(0, 50),\n })\n\n // Guard against Infinity/NaN duration (common with blob URLs where metadata isn't loaded).\n // Cap at 1 hour as an absolute safety limit.\n const MAX_EXPORT_DURATION_SEC = 3600\n const MAX_TOTAL_FRAMES = MAX_EXPORT_DURATION_SEC * 60 // 216,000 frames at 60fps\n\n if (!Number.isFinite(videoDuration) || videoDuration <= 0) {\n console.error('[TRACE-EXPORT] INVALID DURATION:', videoDuration)\n throw new Error(\n `Invalid video duration: ${videoDuration}. The video metadata may not have loaded. ` +\n 'Please ensure the video is fully loaded before exporting.'\n )\n }\n\n const effectiveTrimEnd = trimEnd ?? videoDuration\n const exportDuration = Math.max(0, effectiveTrimEnd - trimStart)\n\n console.log('[TRACE-EXPORT] PixiFrameExporter calculated:', {\n effectiveTrimEnd,\n exportDuration,\n isFiniteExportDuration: Number.isFinite(exportDuration),\n })\n\n if (!Number.isFinite(exportDuration) || exportDuration <= 0) {\n console.error('[TRACE-EXPORT] INVALID EXPORT DURATION:', exportDuration)\n throw new Error(\n `Invalid export duration: ${exportDuration} (trimStart=${trimStart}, trimEnd=${effectiveTrimEnd}). ` +\n 'Check that the trim points are valid.'\n )\n }\n\n if (exportDuration > MAX_EXPORT_DURATION_SEC) {\n throw new Error(`Export duration (${Math.round(exportDuration)}s) exceeds maximum allowed (${MAX_EXPORT_DURATION_SEC}s).`)\n }\n\n const totalFrames = Math.min(\n Math.max(1, Math.ceil(exportDuration * fps)),\n MAX_TOTAL_FRAMES\n )\n\n console.log('[TRACE-EXPORT] PixiFrameExporter will capture', totalFrames, 'frames over', exportDuration, 'seconds at', fps, 'fps')\n\n // --- Decode audio from source (in parallel with setup) ---\n let audioPromise: Promise<AudioBuffer | null> = Promise.resolve(null)\n if (sourceUrl && isAudioEncoderSupported()) {\n audioPromise = decodeAudioFromSource(sourceUrl, signal)\n }\n\n console.log('[PixiExport] Starting export', {\n width,\n height,\n fps,\n trimStart,\n trimEnd: effectiveTrimEnd,\n exportDuration,\n totalFrames,\n hasSourceUrl: !!sourceUrl,\n })\n\n // --- Muxer (configure audio track conditionally after we know if audio exists) ---\n // We need to know audio params before creating the muxer, so await audio decode first.\n const audioBuffer = await audioPromise\n const hasAudio = !!audioBuffer && audioBuffer.numberOfChannels > 0 && audioBuffer.length > 0\n\n const muxerConfig: ConstructorParameters<typeof Muxer<ArrayBufferTarget>>[0] = {\n target: new ArrayBufferTarget(),\n video: {\n codec: 'avc',\n width,\n height,\n frameRate: fps,\n },\n fastStart: 'in-memory',\n }\n\n if (hasAudio) {\n muxerConfig.audio = {\n codec: 'aac',\n numberOfChannels: audioBuffer.numberOfChannels,\n sampleRate: audioBuffer.sampleRate,\n }\n console.log('[PixiExport] Audio track configured:', {\n channels: audioBuffer.numberOfChannels,\n sampleRate: audioBuffer.sampleRate,\n })\n } else {\n console.log('[PixiExport] No audio track - exporting video only')\n }\n\n const muxer = new Muxer(muxerConfig)\n\n // --- Video Encoder ---\n // Try codecs in order: High 4.0 (best quality at 1080p), Main 4.0, Baseline 4.0,\n // then repeat without hardware acceleration. avc1.42001f (Baseline Level 3.1)\n // only supports up to 1280×720, so 1080p exports need Level 4.0+.\n const codecCandidates: Array<{ codec: string; hw: HardwareAcceleration }> = [\n { codec: 'avc1.640028', hw: 'prefer-hardware' }, // High Profile Level 4.0\n { codec: 'avc1.4d0028', hw: 'prefer-hardware' }, // Main Profile Level 4.0\n { codec: 'avc1.420028', hw: 'prefer-hardware' }, // Baseline Profile Level 4.0\n { codec: 'avc1.640028', hw: 'prefer-software' }, // High 4.0, software fallback\n { codec: 'avc1.4d0028', hw: 'prefer-software' }, // Main 4.0, software fallback\n { codec: 'avc1.420028', hw: 'prefer-software' }, // Baseline 4.0, software fallback\n ]\n\n let encoderConfig: VideoEncoderConfig | null = null\n for (const candidate of codecCandidates) {\n const cfg: VideoEncoderConfig = {\n codec: candidate.codec,\n width,\n height,\n bitrate,\n framerate: fps,\n hardwareAcceleration: candidate.hw,\n }\n const support = await VideoEncoder.isConfigSupported(cfg)\n if (support.supported) {\n encoderConfig = cfg\n console.log('[PixiExport] Using codec:', candidate.codec, 'hw:', candidate.hw)\n break\n }\n }\n\n if (!encoderConfig) {\n throw new Error(\n `No supported VideoEncoder codec found for ${width}×${height}. ` +\n 'Try a lower export resolution or use a browser with H.264 encoding support (Chrome 94+).'\n )\n }\n\n const encoder = new VideoEncoder({\n output: (chunk, metadata) => {\n muxer.addVideoChunk(chunk, metadata)\n },\n error: (err) => {\n console.error('[PixiExport] VideoEncoder error:', err)\n },\n })\n\n encoder.configure(encoderConfig)\n\n // --- Intermediate 2D canvas ---\n const targetCanvas = document.createElement('canvas')\n targetCanvas.width = width\n targetCanvas.height = height\n const targetCtx = targetCanvas.getContext('2d')\n if (!targetCtx) {\n throw new Error('Failed to create 2D context for export target canvas')\n }\n\n // --- Video frame loop (progress: 0-90%) ---\n console.log('[TRACE-EXPORT] Frame loop START, totalFrames:', totalFrames)\n let nullFrameCount = 0\n for (let frame = 0; frame < totalFrames; frame++) {\n if (signal?.aborted) {\n console.log('[TRACE-EXPORT] Frame loop ABORTED at frame', frame)\n encoder.close()\n throw new DOMException('Export aborted', 'AbortError')\n }\n\n // Calculate source time: offset by trimStart\n const timeSec = trimStart + frame / fps\n\n if (frame === 0 || frame === totalFrames - 1 || frame % 10 === 0) {\n console.log(`[TRACE-EXPORT] Capturing frame ${frame}/${totalFrames} at ${timeSec.toFixed(3)}s`)\n }\n\n // Capture the frame from PIXI (seek + render + return canvas)\n const pixiCanvas = await provider.captureFrameAt(timeSec)\n if (!pixiCanvas) {\n nullFrameCount++\n console.warn(`[PixiExport] captureFrameAt(${timeSec}) returned null, skipping frame ${frame} (total nulls: ${nullFrameCount})`)\n continue\n }\n\n // Draw PIXI canvas → intermediate 2D canvas (resize to output dimensions)\n targetCtx.clearRect(0, 0, width, height)\n targetCtx.drawImage(pixiCanvas, 0, 0, width, height)\n\n // Create VideoFrame\n const timestamp = Math.floor((frame / fps) * 1_000_000) // microseconds\n const frameDuration = Math.floor(1_000_000 / fps)\n const videoFrame = new VideoFrame(targetCanvas, {\n timestamp,\n duration: frameDuration,\n })\n\n // Encode (keyframe every 2 seconds)\n const keyFrame = frame % (fps * 2) === 0\n encoder.encode(videoFrame, { keyFrame })\n videoFrame.close()\n\n // Backpressure: wait if encoder queue is full\n if (encoder.encodeQueueSize > 5) {\n await new Promise<void>((resolve) => {\n encoder.addEventListener('dequeue', () => resolve(), { once: true })\n })\n }\n\n // Report progress (video = 0-90%)\n const percent = Math.round(((frame + 1) / totalFrames) * 90)\n onProgress?.(percent)\n }\n\n console.log('[TRACE-EXPORT] Frame loop DONE, captured', totalFrames - nullFrameCount, 'frames, skipped', nullFrameCount)\n\n // --- Flush video ---\n console.log('[TRACE-EXPORT] Flushing video encoder...')\n await encoder.flush()\n encoder.close()\n console.log('[TRACE-EXPORT] Video encoder flushed and closed')\n\n // --- Encode audio (progress: 90-99%) ---\n if (hasAudio) {\n onProgress?.(91)\n const trimmed = trimAudioBuffer(audioBuffer, trimStart, effectiveTrimEnd)\n await encodeAudio(muxer, trimmed.channels, trimmed.sampleRate, trimmed.numberOfChannels, audioBitrate)\n onProgress?.(99)\n }\n\n // --- Finalize ---\n muxer.finalize()\n const { buffer } = muxer.target\n\n onProgress?.(100)\n\n console.log('[PixiExport] Export complete', {\n size: buffer.byteLength,\n duration: exportDuration,\n frames: totalFrames,\n hasAudio,\n })\n\n return new Blob([buffer], { type: 'video/mp4' })\n}\n"],"names":["isWebCodecsSupported","isAudioEncoderSupported","decodeAudioFromSource","sourceUrl","signal","response","arrayBuffer","audioCtx","trimAudioBuffer","audioBuffer","trimStart","trimEnd","sampleRate","startSample","endSample","trimmedLength","channels","ch","fullChannel","encodeAudio","muxer","numberOfChannels","bitrate","audioEncoderConfig","audioEncoder","chunk","metadata","err","chunkSize","totalSamples","offset","frameSamples","planarData","audioData","resolve","exportWithPixiFrames","provider","options","width","height","fps","audioBitrate","onProgress","videoDuration","MAX_EXPORT_DURATION_SEC","MAX_TOTAL_FRAMES","effectiveTrimEnd","exportDuration","totalFrames","audioPromise","hasAudio","muxerConfig","ArrayBufferTarget","Muxer","codecCandidates","encoderConfig","candidate","cfg","encoder","targetCanvas","targetCtx","frame","timeSec","pixiCanvas","timestamp","frameDuration","videoFrame","keyFrame","percent","trimmed","buffer"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"PixiFrameExporter-DcQaIUle.js","sources":["../resources/js/video-engine/adapters/PixiFrameExporter.ts"],"sourcesContent":["/**\n * PIXI Frame-by-Frame Video Exporter\n *\n * Captures each frame from the PIXI canvas (which already has filters applied)\n * and encodes them with WebCodecs + mp4-muxer for exact 1:1 preview-to-export parity.\n *\n * Audio is extracted from the source video, trimmed to match, and muxed alongside\n * the video frames. If the source has no audio or AudioEncoder is unavailable,\n * the export proceeds silently (video-only).\n */\n\nimport { Muxer, ArrayBufferTarget } from 'mp4-muxer'\n\nexport interface PixiExportOptions {\n /** Width of the output video in pixels */\n width: number\n /** Height of the output video in pixels */\n height: number\n /** Frames per second */\n fps: number\n /** Video bitrate in bps (default: 5 Mbps) */\n bitrate?: number\n /** Audio bitrate in bps (default: 128 kbps) */\n audioBitrate?: number\n /** Trim start in seconds (source time, default: 0) */\n trimStart?: number\n /** Trim end in seconds (source time, default: full duration) */\n trimEnd?: number\n /** Source video URL (needed to extract audio) */\n sourceUrl?: string\n /** Progress callback (0-100) */\n onProgress?: (percent: number) => void\n /** Abort signal for cancellation */\n signal?: AbortSignal\n}\n\nexport interface PixiFrameProvider {\n /**\n * Seek the underlying video to `timeSec`, render through PIXI with filters,\n * and return the PIXI canvas with the rendered frame.\n */\n captureFrameAt(timeSec: number): Promise<HTMLCanvasElement | null>\n /** Video duration in seconds (reactive ref) */\n duration: { value: number }\n}\n\n/**\n * Check if the browser supports the required WebCodecs APIs.\n */\nexport function isWebCodecsSupported(): boolean {\n return typeof VideoEncoder !== 'undefined' && typeof VideoFrame !== 'undefined'\n}\n\n/**\n * Check if AudioEncoder is available for AAC encoding.\n */\nfunction isAudioEncoderSupported(): boolean {\n return typeof AudioEncoder !== 'undefined' && typeof AudioData !== 'undefined'\n}\n\n/**\n * Decode the audio from a video source URL using AudioContext.\n * Returns null if the source has no audio or decoding fails.\n */\nasync function decodeAudioFromSource(\n sourceUrl: string,\n signal?: AbortSignal\n): Promise<AudioBuffer | null> {\n try {\n const requestInit: RequestInit = signal ? { signal } : {}\n const response = await fetch(sourceUrl, requestInit)\n if (!response.ok) {\n console.warn('[PixiExport] Failed to fetch source for audio:', response.status)\n return null\n }\n\n const arrayBuffer = await response.arrayBuffer()\n const audioCtx = new AudioContext()\n\n try {\n const audioBuffer = await audioCtx.decodeAudioData(arrayBuffer)\n console.log('[PixiExport] Audio decoded:', {\n channels: audioBuffer.numberOfChannels,\n sampleRate: audioBuffer.sampleRate,\n duration: audioBuffer.duration,\n length: audioBuffer.length,\n })\n return audioBuffer\n } catch (decodeErr) {\n console.warn('[PixiExport] No audio track in source or decode failed:', decodeErr)\n return null\n } finally {\n await audioCtx.close()\n }\n } catch (fetchErr) {\n console.warn('[PixiExport] Could not fetch source for audio extraction:', fetchErr)\n return null\n }\n}\n\n/**\n * Trim an AudioBuffer to a specific time range.\n * Returns a new set of Float32Array channel data for the trimmed region.\n */\nfunction trimAudioBuffer(\n audioBuffer: AudioBuffer,\n trimStart: number,\n trimEnd: number\n): { channels: Float32Array[]; sampleRate: number; numberOfChannels: number } {\n const sampleRate = audioBuffer.sampleRate\n const startSample = Math.max(0, Math.floor(trimStart * sampleRate))\n const endSample = Math.min(audioBuffer.length, Math.ceil(trimEnd * sampleRate))\n const trimmedLength = Math.max(0, endSample - startSample)\n\n const channels: Float32Array[] = []\n for (let ch = 0; ch < audioBuffer.numberOfChannels; ch++) {\n const fullChannel = audioBuffer.getChannelData(ch)\n channels.push(fullChannel.slice(startSample, startSample + trimmedLength))\n }\n\n return {\n channels,\n sampleRate,\n numberOfChannels: audioBuffer.numberOfChannels,\n }\n}\n\n/**\n * Encode trimmed audio data and add chunks to the muxer.\n */\nasync function encodeAudio(\n muxer: Muxer<ArrayBufferTarget>,\n channels: Float32Array[],\n sampleRate: number,\n numberOfChannels: number,\n bitrate: number\n): Promise<void> {\n const audioEncoderConfig: AudioEncoderConfig = {\n codec: 'mp4a.40.2', // AAC-LC\n numberOfChannels,\n sampleRate,\n bitrate,\n }\n\n const support = await AudioEncoder.isConfigSupported(audioEncoderConfig)\n if (!support.supported) {\n console.warn('[PixiExport] AAC AudioEncoder not supported, exporting without audio')\n return\n }\n\n const audioEncoder = new AudioEncoder({\n output: (chunk, metadata) => {\n muxer.addAudioChunk(chunk, metadata)\n },\n error: (err) => {\n console.error('[PixiExport] AudioEncoder error:', err)\n },\n })\n\n audioEncoder.configure(audioEncoderConfig)\n\n // Encode in chunks of 1024 samples (standard AAC frame size)\n const chunkSize = 1024\n const totalSamples = channels[0].length\n\n for (let offset = 0; offset < totalSamples; offset += chunkSize) {\n const frameSamples = Math.min(chunkSize, totalSamples - offset)\n\n // Build planar data buffer: channel0[0..frameSamples] + channel1[0..frameSamples] + ...\n const planarData = new Float32Array(numberOfChannels * frameSamples)\n for (let ch = 0; ch < numberOfChannels; ch++) {\n planarData.set(channels[ch].subarray(offset, offset + frameSamples), ch * frameSamples)\n }\n\n const audioData = new AudioData({\n format: 'f32-planar',\n sampleRate,\n numberOfFrames: frameSamples,\n numberOfChannels,\n timestamp: Math.floor((offset / sampleRate) * 1_000_000), // microseconds\n data: planarData,\n })\n\n audioEncoder.encode(audioData)\n audioData.close()\n\n // Backpressure\n if (audioEncoder.encodeQueueSize > 10) {\n await new Promise<void>((resolve) => {\n audioEncoder.addEventListener('dequeue', () => resolve(), { once: true })\n })\n }\n }\n\n await audioEncoder.flush()\n audioEncoder.close()\n\n console.log('[PixiExport] Audio encoding complete', {\n totalSamples,\n chunks: Math.ceil(totalSamples / chunkSize),\n })\n}\n\n/**\n * Export a video by capturing frames from a PIXI preview (with filters applied)\n * and encoding them into an MP4 file, with audio from the source.\n */\nexport async function exportWithPixiFrames(\n provider: PixiFrameProvider,\n options: PixiExportOptions\n): Promise<Blob> {\n const {\n width,\n height,\n fps,\n bitrate = 5_000_000,\n audioBitrate = 128_000,\n trimStart = 0,\n trimEnd,\n sourceUrl,\n onProgress,\n signal,\n } = options\n\n if (!isWebCodecsSupported()) {\n throw new Error('WebCodecs API is not supported in this browser')\n }\n\n const videoDuration = provider.duration.value\n\n console.log('[TRACE-EXPORT] PixiFrameExporter ENTER', {\n videoDuration,\n trimStart,\n trimEnd,\n fps,\n width,\n height,\n isFiniteDuration: Number.isFinite(videoDuration),\n sourceUrl: sourceUrl?.substring(0, 50),\n })\n\n // Guard against Infinity/NaN duration (common with blob URLs where metadata isn't loaded).\n // Cap at 1 hour as an absolute safety limit.\n const MAX_EXPORT_DURATION_SEC = 3600\n const MAX_TOTAL_FRAMES = MAX_EXPORT_DURATION_SEC * 60 // 216,000 frames at 60fps\n\n if (!Number.isFinite(videoDuration) || videoDuration <= 0) {\n console.error('[TRACE-EXPORT] INVALID DURATION:', videoDuration)\n throw new Error(\n `Invalid video duration: ${videoDuration}. The video metadata may not have loaded. ` +\n 'Please ensure the video is fully loaded before exporting.'\n )\n }\n\n const effectiveTrimEnd = trimEnd ?? videoDuration\n const exportDuration = Math.max(0, effectiveTrimEnd - trimStart)\n\n console.log('[TRACE-EXPORT] PixiFrameExporter calculated:', {\n effectiveTrimEnd,\n exportDuration,\n isFiniteExportDuration: Number.isFinite(exportDuration),\n })\n\n if (!Number.isFinite(exportDuration) || exportDuration <= 0) {\n console.error('[TRACE-EXPORT] INVALID EXPORT DURATION:', exportDuration)\n throw new Error(\n `Invalid export duration: ${exportDuration} (trimStart=${trimStart}, trimEnd=${effectiveTrimEnd}). ` +\n 'Check that the trim points are valid.'\n )\n }\n\n if (exportDuration > MAX_EXPORT_DURATION_SEC) {\n throw new Error(`Export duration (${Math.round(exportDuration)}s) exceeds maximum allowed (${MAX_EXPORT_DURATION_SEC}s).`)\n }\n\n const totalFrames = Math.min(\n Math.max(1, Math.ceil(exportDuration * fps)),\n MAX_TOTAL_FRAMES\n )\n\n console.log('[TRACE-EXPORT] PixiFrameExporter will capture', totalFrames, 'frames over', exportDuration, 'seconds at', fps, 'fps')\n\n // --- Decode audio from source (in parallel with setup) ---\n let audioPromise: Promise<AudioBuffer | null> = Promise.resolve(null)\n if (sourceUrl && isAudioEncoderSupported()) {\n audioPromise = decodeAudioFromSource(sourceUrl, signal)\n }\n\n console.log('[PixiExport] Starting export', {\n width,\n height,\n fps,\n trimStart,\n trimEnd: effectiveTrimEnd,\n exportDuration,\n totalFrames,\n hasSourceUrl: !!sourceUrl,\n })\n\n // --- Muxer (configure audio track conditionally after we know if audio exists) ---\n // We need to know audio params before creating the muxer, so await audio decode first.\n const audioBuffer = await audioPromise\n const hasAudio = !!audioBuffer && audioBuffer.numberOfChannels > 0 && audioBuffer.length > 0\n\n const muxerConfig: ConstructorParameters<typeof Muxer<ArrayBufferTarget>>[0] = {\n target: new ArrayBufferTarget(),\n video: {\n codec: 'avc',\n width,\n height,\n frameRate: fps,\n },\n fastStart: 'in-memory',\n }\n\n if (hasAudio) {\n muxerConfig.audio = {\n codec: 'aac',\n numberOfChannels: audioBuffer.numberOfChannels,\n sampleRate: audioBuffer.sampleRate,\n }\n console.log('[PixiExport] Audio track configured:', {\n channels: audioBuffer.numberOfChannels,\n sampleRate: audioBuffer.sampleRate,\n })\n } else {\n console.log('[PixiExport] No audio track - exporting video only')\n }\n\n const muxer = new Muxer(muxerConfig)\n\n // --- Video Encoder ---\n // Try codecs in order: High 4.0 (best quality at 1080p), Main 4.0, Baseline 4.0,\n // then repeat without hardware acceleration. avc1.42001f (Baseline Level 3.1)\n // only supports up to 1280×720, so 1080p exports need Level 4.0+.\n const codecCandidates: Array<{ codec: string; hw: HardwareAcceleration }> = [\n { codec: 'avc1.640028', hw: 'prefer-hardware' }, // High Profile Level 4.0\n { codec: 'avc1.4d0028', hw: 'prefer-hardware' }, // Main Profile Level 4.0\n { codec: 'avc1.420028', hw: 'prefer-hardware' }, // Baseline Profile Level 4.0\n { codec: 'avc1.640028', hw: 'prefer-software' }, // High 4.0, software fallback\n { codec: 'avc1.4d0028', hw: 'prefer-software' }, // Main 4.0, software fallback\n { codec: 'avc1.420028', hw: 'prefer-software' }, // Baseline 4.0, software fallback\n ]\n\n let encoderConfig: VideoEncoderConfig | null = null\n for (const candidate of codecCandidates) {\n const cfg: VideoEncoderConfig = {\n codec: candidate.codec,\n width,\n height,\n bitrate,\n framerate: fps,\n hardwareAcceleration: candidate.hw,\n }\n const support = await VideoEncoder.isConfigSupported(cfg)\n if (support.supported) {\n encoderConfig = cfg\n console.log('[PixiExport] Using codec:', candidate.codec, 'hw:', candidate.hw)\n break\n }\n }\n\n if (!encoderConfig) {\n throw new Error(\n `No supported VideoEncoder codec found for ${width}×${height}. ` +\n 'Try a lower export resolution or use a browser with H.264 encoding support (Chrome 94+).'\n )\n }\n\n const encoder = new VideoEncoder({\n output: (chunk, metadata) => {\n muxer.addVideoChunk(chunk, metadata)\n },\n error: (err) => {\n console.error('[PixiExport] VideoEncoder error:', err)\n },\n })\n\n encoder.configure(encoderConfig)\n\n // --- Intermediate 2D canvas ---\n const targetCanvas = document.createElement('canvas')\n targetCanvas.width = width\n targetCanvas.height = height\n const targetCtx = targetCanvas.getContext('2d')\n if (!targetCtx) {\n throw new Error('Failed to create 2D context for export target canvas')\n }\n\n // --- Video frame loop (progress: 0-90%) ---\n console.log('[TRACE-EXPORT] Frame loop START, totalFrames:', totalFrames)\n let nullFrameCount = 0\n for (let frame = 0; frame < totalFrames; frame++) {\n if (signal?.aborted) {\n console.log('[TRACE-EXPORT] Frame loop ABORTED at frame', frame)\n encoder.close()\n throw new DOMException('Export aborted', 'AbortError')\n }\n\n // Calculate source time: offset by trimStart\n const timeSec = trimStart + frame / fps\n\n if (frame === 0 || frame === totalFrames - 1 || frame % 10 === 0) {\n console.log(`[TRACE-EXPORT] Capturing frame ${frame}/${totalFrames} at ${timeSec.toFixed(3)}s`)\n }\n\n // Capture the frame from PIXI (seek + render + return canvas)\n const pixiCanvas = await provider.captureFrameAt(timeSec)\n if (!pixiCanvas) {\n nullFrameCount++\n console.warn(`[PixiExport] captureFrameAt(${timeSec}) returned null, skipping frame ${frame} (total nulls: ${nullFrameCount})`)\n continue\n }\n\n // Draw PIXI canvas → intermediate 2D canvas (resize to output dimensions)\n targetCtx.clearRect(0, 0, width, height)\n targetCtx.drawImage(pixiCanvas, 0, 0, width, height)\n\n // Create VideoFrame\n const timestamp = Math.floor((frame / fps) * 1_000_000) // microseconds\n const frameDuration = Math.floor(1_000_000 / fps)\n const videoFrame = new VideoFrame(targetCanvas, {\n timestamp,\n duration: frameDuration,\n })\n\n // Encode (keyframe every 2 seconds)\n const keyFrame = frame % (fps * 2) === 0\n encoder.encode(videoFrame, { keyFrame })\n videoFrame.close()\n\n // Backpressure: wait if encoder queue is full\n if (encoder.encodeQueueSize > 5) {\n await new Promise<void>((resolve) => {\n encoder.addEventListener('dequeue', () => resolve(), { once: true })\n })\n }\n\n // Report progress (video = 0-90%)\n const percent = Math.round(((frame + 1) / totalFrames) * 90)\n onProgress?.(percent)\n }\n\n console.log('[TRACE-EXPORT] Frame loop DONE, captured', totalFrames - nullFrameCount, 'frames, skipped', nullFrameCount)\n\n // --- Flush video ---\n console.log('[TRACE-EXPORT] Flushing video encoder...')\n await encoder.flush()\n encoder.close()\n console.log('[TRACE-EXPORT] Video encoder flushed and closed')\n\n // --- Encode audio (progress: 90-99%) ---\n if (hasAudio) {\n onProgress?.(91)\n const trimmed = trimAudioBuffer(audioBuffer, trimStart, effectiveTrimEnd)\n await encodeAudio(muxer, trimmed.channels, trimmed.sampleRate, trimmed.numberOfChannels, audioBitrate)\n onProgress?.(99)\n }\n\n // --- Finalize ---\n muxer.finalize()\n const { buffer } = muxer.target\n\n onProgress?.(100)\n\n console.log('[PixiExport] Export complete', {\n size: buffer.byteLength,\n duration: exportDuration,\n frames: totalFrames,\n hasAudio,\n })\n\n return new Blob([buffer], { type: 'video/mp4' })\n}\n"],"names":["isWebCodecsSupported","isAudioEncoderSupported","decodeAudioFromSource","sourceUrl","signal","response","arrayBuffer","audioCtx","trimAudioBuffer","audioBuffer","trimStart","trimEnd","sampleRate","startSample","endSample","trimmedLength","channels","ch","fullChannel","encodeAudio","muxer","numberOfChannels","bitrate","audioEncoderConfig","audioEncoder","chunk","metadata","err","chunkSize","totalSamples","offset","frameSamples","planarData","audioData","resolve","exportWithPixiFrames","provider","options","width","height","fps","audioBitrate","onProgress","videoDuration","MAX_EXPORT_DURATION_SEC","MAX_TOTAL_FRAMES","effectiveTrimEnd","exportDuration","totalFrames","audioPromise","hasAudio","muxerConfig","ArrayBufferTarget","Muxer","codecCandidates","encoderConfig","candidate","cfg","encoder","targetCanvas","targetCtx","frame","timeSec","pixiCanvas","timestamp","frameDuration","videoFrame","keyFrame","percent","trimmed","buffer"],"mappings":"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"}
|