@codingfactory/mediables-vue 2.0.5 → 2.1.1

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.
Files changed (35) hide show
  1. package/dist/{PixiFrameExporter-B5tJ62bD.js → PixiFrameExporter-DClr376o.js} +60 -60
  2. package/dist/{PixiFrameExporter-B5tJ62bD.js.map → PixiFrameExporter-DClr376o.js.map} +1 -1
  3. package/dist/PixiFrameExporter-D_yS7r_n.cjs +2 -0
  4. package/dist/{PixiFrameExporter-BTXhmL54.cjs.map → PixiFrameExporter-D_yS7r_n.cjs.map} +1 -1
  5. package/dist/composables/useFloatingPills.d.ts +2 -0
  6. package/dist/composables/useVideoEditor.d.ts +3 -2
  7. package/dist/filters/index.d.ts +3 -3
  8. package/dist/filters/registry.d.ts +15 -0
  9. package/dist/{index-BsOWbGbb.js → index-DBM_ViWA.js} +1629 -1562
  10. package/dist/index-DBM_ViWA.js.map +1 -0
  11. package/dist/index-DvFmBczx.cjs +39 -0
  12. package/dist/index-DvFmBczx.cjs.map +1 -0
  13. package/dist/index-HREFg1jF.cjs +42 -0
  14. package/dist/index-HREFg1jF.cjs.map +1 -0
  15. package/dist/{index-CWcyKIOz.js → index-yxrF48R3.js} +8649 -8675
  16. package/dist/index-yxrF48R3.js.map +1 -0
  17. package/dist/mediables-vanilla.cjs +1 -1
  18. package/dist/mediables-vanilla.mjs +1 -1
  19. package/dist/mediables-vue.cjs +1 -1
  20. package/dist/mediables-vue.mjs +2 -2
  21. package/dist/render-page/assets/{index-hBfvGPpt.js → index-SU1f_egA.js} +45095 -22643
  22. package/dist/render-page/index.html +1 -1
  23. package/dist/style.css +1 -1
  24. package/dist/types/video.d.ts +1 -0
  25. package/dist/video-engine/adapters/MediablesCompositionAdapter.d.ts +2 -1
  26. package/dist/video-engine/index.d.ts +0 -2
  27. package/package.json +1 -1
  28. package/dist/PixiFrameExporter-BTXhmL54.cjs +0 -2
  29. package/dist/index-BsOWbGbb.js.map +0 -1
  30. package/dist/index-CWcyKIOz.js.map +0 -1
  31. package/dist/index-C_X9_ptj.cjs +0 -42
  32. package/dist/index-C_X9_ptj.cjs.map +0 -1
  33. package/dist/index-DVJg3EKN.cjs +0 -76
  34. package/dist/index-DVJg3EKN.cjs.map +0 -1
  35. package/dist/video-engine/adapters/CSSFilterAdapter.d.ts +0 -106
@@ -1,32 +1,32 @@
1
- import { A as z, M as L } from "./index-CWcyKIOz.js";
2
- function N() {
1
+ import { A as q, M as z } from "./index-yxrF48R3.js";
2
+ function L() {
3
3
  return typeof VideoEncoder < "u" && typeof VideoFrame < "u";
4
4
  }
5
- function W() {
5
+ function N() {
6
6
  return typeof AudioEncoder < "u" && typeof AudioData < "u";
7
7
  }
8
- async function X(i, d) {
8
+ async function W(i, d) {
9
9
  try {
10
- const o = await fetch(i, { signal: d });
11
- if (!o.ok)
10
+ const t = await fetch(i, d ? { signal: d } : {});
11
+ if (!t.ok)
12
12
  return null;
13
- const t = await o.arrayBuffer(), r = new AudioContext();
13
+ const r = await t.arrayBuffer(), u = new AudioContext();
14
14
  try {
15
- return await r.decodeAudioData(t);
15
+ return await u.decodeAudioData(r);
16
16
  } catch {
17
17
  return null;
18
18
  } finally {
19
- await r.close();
19
+ await u.close();
20
20
  }
21
21
  } catch {
22
22
  return null;
23
23
  }
24
24
  }
25
- function q(i, d, o) {
26
- const t = i.sampleRate, r = Math.max(0, Math.floor(d * t)), u = Math.min(i.length, Math.ceil(o * t)), x = Math.max(0, u - r), n = [];
25
+ function X(i, d, a) {
26
+ 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 = [];
27
27
  for (let s = 0; s < i.numberOfChannels; s++) {
28
28
  const l = i.getChannelData(s);
29
- n.push(l.slice(r, r + x));
29
+ n.push(l.slice(r, r + p));
30
30
  }
31
31
  return {
32
32
  channels: n,
@@ -34,12 +34,12 @@ function q(i, d, o) {
34
34
  numberOfChannels: i.numberOfChannels
35
35
  };
36
36
  }
37
- async function Q(i, d, o, t, r) {
37
+ async function Q(i, d, a, t, r) {
38
38
  const u = {
39
39
  codec: "mp4a.40.2",
40
40
  // AAC-LC
41
41
  numberOfChannels: t,
42
- sampleRate: o,
42
+ sampleRate: a,
43
43
  bitrate: r
44
44
  };
45
45
  if (!(await AudioEncoder.isConfigSupported(u)).supported)
@@ -57,16 +57,16 @@ async function Q(i, d, o, t, r) {
57
57
  const c = Math.min(s, l - e), h = new Float32Array(t * c);
58
58
  for (let f = 0; f < t; f++)
59
59
  h.set(d[f].subarray(e, e + c), f * c);
60
- const p = new AudioData({
60
+ const w = new AudioData({
61
61
  format: "f32-planar",
62
- sampleRate: o,
62
+ sampleRate: a,
63
63
  numberOfFrames: c,
64
64
  numberOfChannels: t,
65
- timestamp: Math.floor(e / o * 1e6),
65
+ timestamp: Math.floor(e / a * 1e6),
66
66
  // microseconds
67
67
  data: h
68
68
  });
69
- n.encode(p), p.close(), n.encodeQueueSize > 10 && await new Promise((f) => {
69
+ n.encode(w), w.close(), n.encodeQueueSize > 10 && await new Promise((f) => {
70
70
  n.addEventListener("dequeue", () => f(), { once: !0 });
71
71
  });
72
72
  }
@@ -74,42 +74,42 @@ async function Q(i, d, o, t, r) {
74
74
  }
75
75
  async function H(i, d) {
76
76
  const {
77
- width: o,
77
+ width: a,
78
78
  height: t,
79
79
  fps: r,
80
80
  bitrate: u = 5e6,
81
- audioBitrate: x = 128e3,
81
+ audioBitrate: p = 128e3,
82
82
  trimStart: n = 0,
83
83
  trimEnd: s,
84
84
  sourceUrl: l,
85
85
  onProgress: e,
86
86
  signal: c
87
87
  } = d;
88
- if (!N())
88
+ if (!L())
89
89
  throw new Error("WebCodecs API is not supported in this browser");
90
- const h = i.duration.value, p = 3600, f = p * 60;
90
+ const h = i.duration.value, w = 3600, f = w * 60;
91
91
  if (!Number.isFinite(h) || h <= 0)
92
92
  throw new Error(
93
93
  `Invalid video duration: ${h}. The video metadata may not have loaded. Please ensure the video is fully loaded before exporting.`
94
94
  );
95
- const y = s ?? h, w = Math.max(0, y - n);
96
- if (!Number.isFinite(w) || w <= 0)
95
+ const y = s ?? h, E = Math.max(0, y - n);
96
+ if (!Number.isFinite(E) || E <= 0)
97
97
  throw new Error(
98
- `Invalid export duration: ${w} (trimStart=${n}, trimEnd=${y}). Check that the trim points are valid.`
98
+ `Invalid export duration: ${E} (trimStart=${n}, trimEnd=${y}). Check that the trim points are valid.`
99
99
  );
100
- if (w > p)
101
- throw new Error(`Export duration (${Math.round(w)}s) exceeds maximum allowed (${p}s).`);
100
+ if (E > w)
101
+ throw new Error(`Export duration (${Math.round(E)}s) exceeds maximum allowed (${w}s).`);
102
102
  const g = Math.min(
103
- Math.max(1, Math.ceil(w * r)),
103
+ Math.max(1, Math.ceil(E * r)),
104
104
  f
105
105
  );
106
106
  let F = Promise.resolve(null);
107
- l && W() && (F = X(l, c));
108
- const E = await F, D = !!E && E.numberOfChannels > 0 && E.length > 0, O = {
109
- target: new z(),
107
+ l && N() && (F = W(l, c));
108
+ const v = await F, D = !!v && v.numberOfChannels > 0 && v.length > 0, O = {
109
+ target: new q(),
110
110
  video: {
111
111
  codec: "avc",
112
- width: o,
112
+ width: a,
113
113
  height: t,
114
114
  frameRate: r
115
115
  },
@@ -117,10 +117,10 @@ async function H(i, d) {
117
117
  };
118
118
  D && (O.audio = {
119
119
  codec: "aac",
120
- numberOfChannels: E.numberOfChannels,
121
- sampleRate: E.sampleRate
120
+ numberOfChannels: v.numberOfChannels,
121
+ sampleRate: v.sampleRate
122
122
  });
123
- const A = new L(O), T = [
123
+ const A = new z(O), T = [
124
124
  { codec: "avc1.640028", hw: "prefer-hardware" },
125
125
  // High Profile Level 4.0
126
126
  { codec: "avc1.4d0028", hw: "prefer-hardware" },
@@ -135,58 +135,58 @@ async function H(i, d) {
135
135
  // Baseline 4.0, software fallback
136
136
  ];
137
137
  let M = null;
138
- for (const a of T) {
139
- const v = {
140
- codec: a.codec,
141
- width: o,
138
+ for (const o of T) {
139
+ const x = {
140
+ codec: o.codec,
141
+ width: a,
142
142
  height: t,
143
143
  bitrate: u,
144
144
  framerate: r,
145
- hardwareAcceleration: a.hw
145
+ hardwareAcceleration: o.hw
146
146
  };
147
- if ((await VideoEncoder.isConfigSupported(v)).supported) {
148
- M = v;
147
+ if ((await VideoEncoder.isConfigSupported(x)).supported) {
148
+ M = x;
149
149
  break;
150
150
  }
151
151
  }
152
152
  if (!M)
153
153
  throw new Error(
154
- `No supported VideoEncoder codec found for ${o}×${t}. Try a lower export resolution or use a browser with H.264 encoding support (Chrome 94+).`
154
+ `No supported VideoEncoder codec found for ${a}×${t}. Try a lower export resolution or use a browser with H.264 encoding support (Chrome 94+).`
155
155
  );
156
156
  const m = new VideoEncoder({
157
- output: (a, v) => {
158
- A.addVideoChunk(a, v);
157
+ output: (o, x) => {
158
+ A.addVideoChunk(o, x);
159
159
  },
160
- error: (a) => {
160
+ error: (o) => {
161
161
  }
162
162
  });
163
163
  m.configure(M);
164
164
  const C = document.createElement("canvas");
165
- C.width = o, C.height = t;
165
+ C.width = a, C.height = t;
166
166
  const b = C.getContext("2d");
167
167
  if (!b)
168
168
  throw new Error("Failed to create 2D context for export target canvas");
169
- for (let a = 0; a < g; a++) {
169
+ for (let o = 0; o < g; o++) {
170
170
  if (c != null && c.aborted)
171
171
  throw m.close(), new DOMException("Export aborted", "AbortError");
172
- const v = n + a / r, S = await i.captureFrameAt(v);
172
+ const x = n + o / r, S = await i.captureFrameAt(x);
173
173
  if (!S)
174
174
  continue;
175
- b.clearRect(0, 0, o, t), b.drawImage(S, 0, 0, o, t);
176
- const k = Math.floor(a / r * 1e6), V = Math.floor(1e6 / r), R = new VideoFrame(C, {
175
+ b.clearRect(0, 0, a, t), b.drawImage(S, 0, 0, a, t);
176
+ const k = Math.floor(o / r * 1e6), I = Math.floor(1e6 / r), R = new VideoFrame(C, {
177
177
  timestamp: k,
178
- duration: V
179
- }), B = a % (r * 2) === 0;
180
- m.encode(R, { keyFrame: B }), R.close(), m.encodeQueueSize > 5 && await new Promise((_) => {
178
+ duration: I
179
+ }), V = o % (r * 2) === 0;
180
+ m.encode(R, { keyFrame: V }), R.close(), m.encodeQueueSize > 5 && await new Promise((_) => {
181
181
  m.addEventListener("dequeue", () => _(), { once: !0 });
182
182
  });
183
- const I = Math.round((a + 1) / g * 90);
184
- e == null || e(I);
183
+ const B = Math.round((o + 1) / g * 90);
184
+ e == null || e(B);
185
185
  }
186
186
  if (await m.flush(), m.close(), D) {
187
187
  e == null || e(91);
188
- const a = q(E, n, y);
189
- await Q(A, a.channels, a.sampleRate, a.numberOfChannels, x), e == null || e(99);
188
+ const o = X(v, n, y);
189
+ await Q(A, o.channels, o.sampleRate, o.numberOfChannels, p), e == null || e(99);
190
190
  }
191
191
  A.finalize();
192
192
  const { buffer: $ } = A.target;
@@ -194,6 +194,6 @@ async function H(i, d) {
194
194
  }
195
195
  export {
196
196
  H as exportWithPixiFrames,
197
- N as isWebCodecsSupported
197
+ L as isWebCodecsSupported
198
198
  };
199
- //# sourceMappingURL=PixiFrameExporter-B5tJ62bD.js.map
199
+ //# sourceMappingURL=PixiFrameExporter-DClr376o.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"PixiFrameExporter-B5tJ62bD.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 response = await fetch(sourceUrl, { signal })\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-DClr376o.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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"}
@@ -0,0 +1,2 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const R=require("./index-DvFmBczx.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-D_yS7r_n.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"PixiFrameExporter-BTXhmL54.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 response = await fetch(sourceUrl, { signal })\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-D_yS7r_n.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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"}
@@ -15,6 +15,7 @@ export declare function useFloatingPills(): {
15
15
  readonly category: string;
16
16
  readonly description?: string | undefined;
17
17
  readonly thumbnail?: string | undefined;
18
+ readonly mediaTargets?: readonly import("../filters").FilterMediaTarget[] | undefined;
18
19
  readonly createFilter: (params: Record<string, any>) => any;
19
20
  readonly defaultParams: {
20
21
  readonly [x: string]: any;
@@ -41,6 +42,7 @@ export declare function useFloatingPills(): {
41
42
  readonly category: string;
42
43
  readonly description?: string | undefined;
43
44
  readonly thumbnail?: string | undefined;
45
+ readonly mediaTargets?: readonly import("../filters").FilterMediaTarget[] | undefined;
44
46
  readonly createFilter: (params: Record<string, any>) => any;
45
47
  readonly defaultParams: {
46
48
  readonly [x: string]: any;
@@ -4,7 +4,7 @@
4
4
  * Core logic for video editing operations
5
5
  */
6
6
  import type { VideoRecipe, VideoMedia, AppliedFilter, TextOverlayRecipe, AudioTrackRecipe } from '../types/video';
7
- import { CSSFilterCompositionAdapter } from '../video-engine/adapters/CSSFilterAdapter';
7
+ import { MediablesCompositionAdapter } from '../video-engine/adapters/MediablesCompositionAdapter';
8
8
  export declare function useVideoEditor(mediaUuid: string, initialRecipe?: VideoRecipe, initialMedia?: VideoMedia): {
9
9
  currentFrame: import("vue").Ref<any, any>;
10
10
  playheadPosition: import("vue").ComputedRef<number>;
@@ -82,7 +82,7 @@ export declare function useVideoEditor(mediaUuid: string, initialRecipe?: VideoR
82
82
  zoomLevel: import("vue").ComputedRef<number>;
83
83
  playbackSpeed: import("vue").Ref<number, number>;
84
84
  useVideoEngine: import("vue").Ref<boolean, boolean>;
85
- compositionAdapter: import("vue").ComputedRef<CSSFilterCompositionAdapter | null>;
85
+ compositionAdapter: import("vue").ComputedRef<MediablesCompositionAdapter | null>;
86
86
  fps: import("vue").Ref<number, number>;
87
87
  useFrameMode: import("vue").Ref<boolean, boolean>;
88
88
  handleTrim: (clipId: string, edge: "start" | "end", newTime: number) => void;
@@ -111,6 +111,7 @@ export declare function useVideoEditor(mediaUuid: string, initialRecipe?: VideoR
111
111
  skipToStart: () => void;
112
112
  skipToEnd: () => void;
113
113
  setPlaybackSpeed: (speed: number) => void;
114
+ updateSourceDuration: (newDuration: number) => void;
114
115
  undo: () => void;
115
116
  redo: () => void;
116
117
  canUndo: import("vue").ComputedRef<boolean>;
@@ -2,9 +2,9 @@
2
2
  * Filters Module Index
3
3
  * Exports all filter-related functionality
4
4
  */
5
- import { registerFilter, getFilter, getAllFilters, getFiltersByCategory, getAllCategories, hasFilter, getRegisteredFilters, registerCorePixiFilters } from './registry';
6
- export { registerFilter, getFilter, getAllFilters, getFiltersByCategory, getAllCategories, hasFilter, getRegisteredFilters, registerCorePixiFilters, };
7
- export type { FilterDefinition, ControlDefinition } from './registry';
5
+ import { registerFilter, getFilter, getAllFilters, getFiltersByCategory, getAllCategories, hasFilter, getFiltersByMedia, isFilterCompatibleWithMedia, getRegisteredFilters, registerCorePixiFilters } from './registry';
6
+ export { registerFilter, getFilter, getAllFilters, getFiltersByCategory, getAllCategories, hasFilter, getFiltersByMedia, isFilterCompatibleWithMedia, getRegisteredFilters, registerCorePixiFilters, };
7
+ export type { FilterDefinition, ControlDefinition, FilterMediaTarget } from './registry';
8
8
  export { createFilterInstance, createFilterInstances, updateFilterParams, } from './factory';
9
9
  export type { FilterInstanceConfig } from './factory';
10
10
  export { mapControlTypeToComponent, validateControlValue, generateDefaultParams, convertControlValue, } from './controlMapping';
@@ -23,6 +23,7 @@ export interface ControlDefinition {
23
23
  }>;
24
24
  tooltip?: string;
25
25
  }
26
+ export type FilterMediaTarget = 'image' | 'video';
26
27
  /**
27
28
  * Interface for a filter definition
28
29
  * Contains all metadata needed to create and manage a filter
@@ -33,6 +34,11 @@ export interface FilterDefinition {
33
34
  category: string;
34
35
  description?: string;
35
36
  thumbnail?: string;
37
+ /**
38
+ * Optional media compatibility tags.
39
+ * Omitted means compatible with both image and video pipelines.
40
+ */
41
+ mediaTargets?: FilterMediaTarget[];
36
42
  createFilter: (params: Record<string, any>) => Filter;
37
43
  defaultParams: Record<string, any>;
38
44
  controls: ControlDefinition[];
@@ -76,6 +82,15 @@ export declare function getAllCategories(): string[];
76
82
  * @returns True if the filter is registered, false otherwise
77
83
  */
78
84
  export declare function hasFilter(id: string): boolean;
85
+ /**
86
+ * Check whether a registered filter is compatible with a media target.
87
+ * Unknown filters are treated as incompatible.
88
+ */
89
+ export declare function isFilterCompatibleWithMedia(id: string, target: FilterMediaTarget): boolean;
90
+ /**
91
+ * Return all registered filters compatible with the given media target.
92
+ */
93
+ export declare function getFiltersByMedia(target: FilterMediaTarget): FilterDefinition[];
79
94
  /**
80
95
  * Get all registered filters with simplified format for UI
81
96
  * @returns Array of filter definitions with only UI-relevant properties