@twick/browser-render 0.15.8 → 0.15.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +311 -33
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +311 -33
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -3
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/browser-renderer.ts
|
|
2
2
|
import { Renderer, Vector2 } from "@twick/core";
|
|
3
3
|
import defaultProject from "@twick/visualizer/dist/project.js";
|
|
4
|
+
import { hasAudio } from "@twick/media-utils";
|
|
4
5
|
|
|
5
6
|
// src/audio/video-audio-extractor.ts
|
|
6
7
|
var VideoElementAudioExtractor = class {
|
|
@@ -28,40 +29,113 @@ var VideoElementAudioExtractor = class {
|
|
|
28
29
|
* Extract audio by playing the video and capturing audio output
|
|
29
30
|
*/
|
|
30
31
|
async extractAudio(startTime, duration, playbackRate = 1) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
try {
|
|
33
|
+
const source = this.audioContext.createMediaElementSource(this.video);
|
|
34
|
+
this.destination = this.audioContext.createMediaStreamDestination();
|
|
35
|
+
source.connect(this.destination);
|
|
36
|
+
} catch (err) {
|
|
37
|
+
throw new Error("Video has no audio track");
|
|
38
|
+
}
|
|
34
39
|
this.audioChunks = [];
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
40
|
+
let mimeType = "audio/webm";
|
|
41
|
+
if (!MediaRecorder.isTypeSupported(mimeType)) {
|
|
42
|
+
mimeType = "";
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
this.mediaRecorder = new MediaRecorder(this.destination.stream, {
|
|
46
|
+
mimeType: mimeType || void 0
|
|
47
|
+
});
|
|
48
|
+
} catch (err) {
|
|
49
|
+
throw new Error(`Failed to create MediaRecorder: ${err}. Video may have no audio track.`);
|
|
50
|
+
}
|
|
38
51
|
this.mediaRecorder.ondataavailable = (event) => {
|
|
39
|
-
if (event.data.size > 0) {
|
|
52
|
+
if (event.data && event.data.size > 0) {
|
|
40
53
|
this.audioChunks.push(event.data);
|
|
41
54
|
}
|
|
42
55
|
};
|
|
43
56
|
this.video.currentTime = startTime;
|
|
44
57
|
this.video.playbackRate = playbackRate;
|
|
45
|
-
await new Promise((resolve) => {
|
|
46
|
-
|
|
58
|
+
await new Promise((resolve, reject) => {
|
|
59
|
+
const seekTimeout = setTimeout(() => {
|
|
60
|
+
reject(new Error("Video seek timeout"));
|
|
61
|
+
}, 5e3);
|
|
62
|
+
this.video.addEventListener("seeked", () => {
|
|
63
|
+
clearTimeout(seekTimeout);
|
|
64
|
+
resolve();
|
|
65
|
+
}, { once: true });
|
|
66
|
+
this.video.addEventListener("error", () => {
|
|
67
|
+
clearTimeout(seekTimeout);
|
|
68
|
+
reject(new Error("Video seek error"));
|
|
69
|
+
}, { once: true });
|
|
47
70
|
});
|
|
48
71
|
return new Promise((resolve, reject) => {
|
|
49
72
|
const recordingTimeout = setTimeout(() => {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
73
|
+
this.video.pause();
|
|
74
|
+
if (this.mediaRecorder && this.mediaRecorder.state !== "inactive") {
|
|
75
|
+
this.mediaRecorder.stop();
|
|
76
|
+
}
|
|
77
|
+
reject(new Error("Audio extraction timeout - video may have no audio track"));
|
|
78
|
+
}, (duration / playbackRate + 10) * 1e3);
|
|
79
|
+
let hasData = false;
|
|
80
|
+
const dataCheckInterval = setInterval(() => {
|
|
81
|
+
if (this.audioChunks.length > 0 && this.audioChunks.some((chunk) => chunk.size > 0)) {
|
|
82
|
+
hasData = true;
|
|
83
|
+
}
|
|
84
|
+
}, 1e3);
|
|
85
|
+
this.mediaRecorder.onerror = (event) => {
|
|
86
|
+
clearInterval(dataCheckInterval);
|
|
87
|
+
clearTimeout(recordingTimeout);
|
|
88
|
+
this.video.pause();
|
|
89
|
+
reject(new Error(`MediaRecorder error: ${event}. Video may have no audio track.`));
|
|
90
|
+
};
|
|
91
|
+
try {
|
|
92
|
+
this.mediaRecorder.start(100);
|
|
93
|
+
this.video.play().catch((playErr) => {
|
|
94
|
+
clearInterval(dataCheckInterval);
|
|
95
|
+
clearTimeout(recordingTimeout);
|
|
96
|
+
reject(new Error(`Failed to play video: ${playErr}`));
|
|
97
|
+
});
|
|
98
|
+
} catch (startErr) {
|
|
99
|
+
clearInterval(dataCheckInterval);
|
|
100
|
+
clearTimeout(recordingTimeout);
|
|
101
|
+
reject(new Error(`Failed to start recording: ${startErr}`));
|
|
102
|
+
}
|
|
54
103
|
setTimeout(async () => {
|
|
104
|
+
clearInterval(dataCheckInterval);
|
|
55
105
|
clearTimeout(recordingTimeout);
|
|
56
106
|
this.video.pause();
|
|
57
|
-
this.mediaRecorder.
|
|
107
|
+
if (this.mediaRecorder && this.mediaRecorder.state !== "inactive") {
|
|
108
|
+
this.mediaRecorder.stop();
|
|
109
|
+
}
|
|
110
|
+
const stopTimeout = setTimeout(() => {
|
|
111
|
+
if (this.audioChunks.length === 0 || !hasData) {
|
|
112
|
+
reject(new Error("No audio data captured - video has no audio track"));
|
|
113
|
+
}
|
|
114
|
+
}, 2e3);
|
|
58
115
|
await new Promise((res) => {
|
|
59
|
-
this.mediaRecorder
|
|
116
|
+
if (this.mediaRecorder) {
|
|
117
|
+
this.mediaRecorder.addEventListener("stop", () => {
|
|
118
|
+
clearTimeout(stopTimeout);
|
|
119
|
+
res();
|
|
120
|
+
}, { once: true });
|
|
121
|
+
} else {
|
|
122
|
+
clearTimeout(stopTimeout);
|
|
123
|
+
res();
|
|
124
|
+
}
|
|
60
125
|
});
|
|
61
126
|
try {
|
|
127
|
+
if (this.audioChunks.length === 0 || !this.audioChunks.some((chunk) => chunk.size > 0)) {
|
|
128
|
+
throw new Error("No audio data captured - video has no audio track");
|
|
129
|
+
}
|
|
62
130
|
const audioBlob = new Blob(this.audioChunks, { type: "audio/webm" });
|
|
131
|
+
if (audioBlob.size === 0) {
|
|
132
|
+
throw new Error("Audio blob is empty - video has no audio track");
|
|
133
|
+
}
|
|
63
134
|
const arrayBuffer = await audioBlob.arrayBuffer();
|
|
64
135
|
const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer);
|
|
136
|
+
if (audioBuffer.length === 0 || audioBuffer.duration === 0) {
|
|
137
|
+
throw new Error("Audio buffer is empty - video has no audio track");
|
|
138
|
+
}
|
|
65
139
|
resolve(audioBuffer);
|
|
66
140
|
} catch (err) {
|
|
67
141
|
reject(new Error(`Failed to decode recorded audio: ${err}`));
|
|
@@ -284,25 +358,46 @@ function getFFmpegBaseURL() {
|
|
|
284
358
|
return "/ffmpeg";
|
|
285
359
|
}
|
|
286
360
|
async function muxAudioVideo(options) {
|
|
361
|
+
const muxStartTime = Date.now();
|
|
287
362
|
try {
|
|
363
|
+
console.log("Starting FFmpeg muxing...");
|
|
364
|
+
console.log(` Video blob size: ${options.videoBlob.size} bytes (${(options.videoBlob.size / 1024 / 1024).toFixed(2)} MB)`);
|
|
365
|
+
console.log(` Audio buffer size: ${options.audioBuffer.byteLength} bytes (${(options.audioBuffer.byteLength / 1024 / 1024).toFixed(2)} MB)`);
|
|
288
366
|
const { FFmpeg } = await import("@ffmpeg/ffmpeg");
|
|
289
367
|
const { fetchFile } = await import("@ffmpeg/util");
|
|
290
368
|
const ffmpeg = new FFmpeg();
|
|
291
369
|
const base = getFFmpegBaseURL();
|
|
292
370
|
const coreURL = `${base}/ffmpeg-core.js`;
|
|
293
371
|
const wasmURL = `${base}/ffmpeg-core.wasm`;
|
|
372
|
+
console.log(`Loading FFmpeg from ${base}`);
|
|
373
|
+
const loadStartTime = Date.now();
|
|
294
374
|
await ffmpeg.load({
|
|
295
375
|
coreURL,
|
|
296
376
|
wasmURL
|
|
297
377
|
});
|
|
378
|
+
const loadDuration = Date.now() - loadStartTime;
|
|
379
|
+
console.log(`FFmpeg loaded successfully in ${loadDuration}ms`);
|
|
380
|
+
console.log("Writing video and audio files...");
|
|
381
|
+
const writeStartTime = Date.now();
|
|
298
382
|
await ffmpeg.writeFile(
|
|
299
383
|
"video.mp4",
|
|
300
384
|
await fetchFile(options.videoBlob)
|
|
301
385
|
);
|
|
386
|
+
console.log(` Video file written: ${options.videoBlob.size} bytes`);
|
|
302
387
|
await ffmpeg.writeFile(
|
|
303
388
|
"audio.wav",
|
|
304
389
|
new Uint8Array(options.audioBuffer)
|
|
305
390
|
);
|
|
391
|
+
const writeDuration = Date.now() - writeStartTime;
|
|
392
|
+
console.log(` Audio file written: ${options.audioBuffer.byteLength} bytes`);
|
|
393
|
+
console.log(`Files written successfully in ${writeDuration}ms`);
|
|
394
|
+
console.log("Executing FFmpeg muxing command...");
|
|
395
|
+
const execStartTime = Date.now();
|
|
396
|
+
const ffmpegLogs = [];
|
|
397
|
+
ffmpeg.on("log", ({ message }) => {
|
|
398
|
+
ffmpegLogs.push(message);
|
|
399
|
+
console.log(` [FFmpeg] ${message}`);
|
|
400
|
+
});
|
|
306
401
|
await ffmpeg.exec([
|
|
307
402
|
"-i",
|
|
308
403
|
"video.mp4",
|
|
@@ -317,11 +412,34 @@ async function muxAudioVideo(options) {
|
|
|
317
412
|
"-shortest",
|
|
318
413
|
"output.mp4"
|
|
319
414
|
]);
|
|
415
|
+
const execDuration = Date.now() - execStartTime;
|
|
416
|
+
console.log(`FFmpeg muxing completed in ${execDuration}ms`);
|
|
417
|
+
const readStartTime = Date.now();
|
|
320
418
|
const data = await ffmpeg.readFile("output.mp4");
|
|
419
|
+
const readDuration = Date.now() - readStartTime;
|
|
420
|
+
console.log(`Output file read successfully in ${readDuration}ms`);
|
|
321
421
|
const uint8 = typeof data === "string" ? new TextEncoder().encode(data) : new Uint8Array(data);
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
422
|
+
const result = new Blob([uint8], { type: "video/mp4" });
|
|
423
|
+
const totalDuration = Date.now() - muxStartTime;
|
|
424
|
+
console.log(`Muxing successful: ${result.size} bytes (${(result.size / 1024 / 1024).toFixed(2)} MB) in ${totalDuration}ms`);
|
|
425
|
+
console.log(` Breakdown: load=${loadDuration}ms, write=${writeDuration}ms, exec=${execDuration}ms, read=${readDuration}ms`);
|
|
426
|
+
return result;
|
|
427
|
+
} catch (error) {
|
|
428
|
+
const totalDuration = Date.now() - muxStartTime;
|
|
429
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
430
|
+
const errorStack = error instanceof Error ? error.stack : void 0;
|
|
431
|
+
console.error("FFmpeg muxing failed:", errorMsg);
|
|
432
|
+
if (errorStack) {
|
|
433
|
+
console.error("Error stack:", errorStack);
|
|
434
|
+
}
|
|
435
|
+
console.error("Error details:", {
|
|
436
|
+
errorType: error instanceof Error ? error.constructor.name : typeof error,
|
|
437
|
+
errorMessage: errorMsg,
|
|
438
|
+
duration: `${totalDuration}ms`,
|
|
439
|
+
videoBlobSize: options.videoBlob.size,
|
|
440
|
+
audioBufferSize: options.audioBuffer.byteLength
|
|
441
|
+
});
|
|
442
|
+
throw error;
|
|
325
443
|
}
|
|
326
444
|
}
|
|
327
445
|
|
|
@@ -407,33 +525,99 @@ var BrowserWasmExporter = class _BrowserWasmExporter {
|
|
|
407
525
|
}
|
|
408
526
|
async generateAudio(assets, startFrame, endFrame) {
|
|
409
527
|
try {
|
|
528
|
+
console.log(`Generating audio from ${assets.length} frames`);
|
|
410
529
|
const processor = new BrowserAudioProcessor();
|
|
411
530
|
const assetPlacements = getAssetPlacement(assets);
|
|
412
531
|
if (assetPlacements.length === 0) {
|
|
532
|
+
console.log("No asset placements found");
|
|
413
533
|
return null;
|
|
414
534
|
}
|
|
535
|
+
console.log(`Processing ${assetPlacements.length} asset placements`);
|
|
415
536
|
const processedBuffers = [];
|
|
416
|
-
for (
|
|
537
|
+
for (let i = 0; i < assetPlacements.length; i++) {
|
|
538
|
+
const asset = assetPlacements[i];
|
|
539
|
+
console.log(`[${i + 1}/${assetPlacements.length}] Processing asset: ${asset.src} (type: ${asset.type}, volume: ${asset.volume}, playbackRate: ${asset.playbackRate})`);
|
|
417
540
|
if (asset.volume > 0 && asset.playbackRate > 0) {
|
|
541
|
+
const startTime = Date.now();
|
|
418
542
|
try {
|
|
419
|
-
|
|
543
|
+
if (asset.type === "video") {
|
|
544
|
+
console.log(` \u2192 Checking if asset has audio: ${asset.src.substring(0, 50)}...`);
|
|
545
|
+
try {
|
|
546
|
+
const assetHasAudio = await hasAudio(asset.src);
|
|
547
|
+
if (!assetHasAudio) {
|
|
548
|
+
console.log(` \u23ED Skipping asset (no audio detected): ${asset.src.substring(0, 50)}...`);
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
console.log(` \u2713 Asset has audio, proceeding: ${asset.src.substring(0, 50)}...`);
|
|
552
|
+
} catch (audioCheckError) {
|
|
553
|
+
const errorMsg = audioCheckError instanceof Error ? audioCheckError.message : String(audioCheckError);
|
|
554
|
+
const errorStack = audioCheckError instanceof Error ? audioCheckError.stack : void 0;
|
|
555
|
+
console.warn(` \u26A0 Audio check failed, proceeding anyway: ${asset.src.substring(0, 50)}...`);
|
|
556
|
+
console.warn(` Error: ${errorMsg}`);
|
|
557
|
+
if (errorStack) {
|
|
558
|
+
console.warn(` Stack: ${errorStack}`);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
console.log(` \u2192 Starting processAudioAsset for: ${asset.src}`);
|
|
563
|
+
const processPromise = processor.processAudioAsset(
|
|
420
564
|
asset,
|
|
421
565
|
this.settings.fps || 30,
|
|
422
566
|
endFrame - startFrame
|
|
423
567
|
);
|
|
568
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
569
|
+
setTimeout(() => {
|
|
570
|
+
reject(new Error(`Timeout processing audio asset after 20s - video may have no audio track`));
|
|
571
|
+
}, 2e4);
|
|
572
|
+
});
|
|
573
|
+
const buffer = await Promise.race([processPromise, timeoutPromise]);
|
|
574
|
+
const duration = Date.now() - startTime;
|
|
575
|
+
console.log(` \u2713 Successfully processed audio asset in ${duration}ms: ${asset.src.substring(0, 50)}...`);
|
|
424
576
|
processedBuffers.push(buffer);
|
|
425
|
-
} catch {
|
|
577
|
+
} catch (error) {
|
|
578
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
579
|
+
const errorStack = error instanceof Error ? error.stack : void 0;
|
|
580
|
+
const duration = Date.now() - startTime;
|
|
581
|
+
console.warn(` \u2717 Failed to process audio asset after ${duration}ms: ${asset.src.substring(0, 50)}...`);
|
|
582
|
+
console.warn(` Error: ${errorMsg}`);
|
|
583
|
+
if (errorStack) {
|
|
584
|
+
console.warn(` Stack: ${errorStack}`);
|
|
585
|
+
}
|
|
586
|
+
console.warn(` Asset details: type=${asset.type}, volume=${asset.volume}, playbackRate=${asset.playbackRate}, startFrame=${asset.startInVideo}, endFrame=${asset.endInVideo}`);
|
|
426
587
|
}
|
|
588
|
+
} else {
|
|
589
|
+
console.log(` \u23ED Skipping asset: volume=${asset.volume}, playbackRate=${asset.playbackRate}`);
|
|
427
590
|
}
|
|
428
591
|
}
|
|
429
592
|
if (processedBuffers.length === 0) {
|
|
593
|
+
console.warn("No audio buffers were successfully processed");
|
|
594
|
+
console.warn(` Total assets attempted: ${assetPlacements.length}`);
|
|
595
|
+
console.warn(` Assets with volume>0 and playbackRate>0: ${assetPlacements.filter((a) => a.volume > 0 && a.playbackRate > 0).length}`);
|
|
430
596
|
return null;
|
|
431
597
|
}
|
|
598
|
+
console.log(`Mixing ${processedBuffers.length} audio buffers`);
|
|
599
|
+
const mixStartTime = Date.now();
|
|
432
600
|
const mixedBuffer = processor.mixAudioBuffers(processedBuffers);
|
|
601
|
+
const mixDuration = Date.now() - mixStartTime;
|
|
602
|
+
console.log(`Audio mixing completed in ${mixDuration}ms`);
|
|
603
|
+
const wavStartTime = Date.now();
|
|
433
604
|
const wavData = processor.audioBufferToWav(mixedBuffer);
|
|
605
|
+
const wavDuration = Date.now() - wavStartTime;
|
|
606
|
+
console.log(`WAV conversion completed in ${wavDuration}ms`);
|
|
607
|
+
console.log(`Audio generation complete: ${wavData.byteLength} bytes (${(wavData.byteLength / 1024 / 1024).toFixed(2)} MB)`);
|
|
434
608
|
await processor.close();
|
|
435
609
|
return wavData;
|
|
436
|
-
} catch {
|
|
610
|
+
} catch (error) {
|
|
611
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
612
|
+
const errorStack = error instanceof Error ? error.stack : void 0;
|
|
613
|
+
console.error("Audio generation error:", errorMsg);
|
|
614
|
+
if (errorStack) {
|
|
615
|
+
console.error("Error stack:", errorStack);
|
|
616
|
+
}
|
|
617
|
+
console.error("Error details:", {
|
|
618
|
+
errorType: error instanceof Error ? error.constructor.name : typeof error,
|
|
619
|
+
errorMessage: errorMsg
|
|
620
|
+
});
|
|
437
621
|
return null;
|
|
438
622
|
}
|
|
439
623
|
}
|
|
@@ -521,6 +705,8 @@ var renderTwickVideoInBrowser = async (config) => {
|
|
|
521
705
|
}
|
|
522
706
|
});
|
|
523
707
|
}
|
|
708
|
+
let hasAnyAudio = false;
|
|
709
|
+
console.log(`Found ${videoElements.length} video element(s) to check for audio`);
|
|
524
710
|
if (videoElements.length > 0) {
|
|
525
711
|
for (const videoEl of videoElements) {
|
|
526
712
|
const src = videoEl.props?.src;
|
|
@@ -544,6 +730,23 @@ var renderTwickVideoInBrowser = async (config) => {
|
|
|
544
730
|
reject(new Error(`Failed to load video: ${err?.message || "Unknown error"}`));
|
|
545
731
|
}, { once: true });
|
|
546
732
|
});
|
|
733
|
+
if (settings.includeAudio) {
|
|
734
|
+
try {
|
|
735
|
+
console.log(`Checking if video has audio: ${src.substring(0, 50)}...`);
|
|
736
|
+
const videoHasAudio = await hasAudio(src);
|
|
737
|
+
console.log(`Audio check result for ${src.substring(0, 50)}...: ${videoHasAudio ? "HAS AUDIO" : "NO AUDIO"}`);
|
|
738
|
+
if (videoHasAudio) {
|
|
739
|
+
hasAnyAudio = true;
|
|
740
|
+
console.log(`\u2713 Video has audio: ${src.substring(0, 50)}...`);
|
|
741
|
+
} else {
|
|
742
|
+
console.log(`\u2717 Video has no audio: ${src.substring(0, 50)}...`);
|
|
743
|
+
}
|
|
744
|
+
} catch (error) {
|
|
745
|
+
console.warn(`Failed to check audio for ${src.substring(0, 50)}...:`, error);
|
|
746
|
+
hasAnyAudio = true;
|
|
747
|
+
console.log(`\u26A0 Assuming video might have audio due to check error`);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
547
750
|
}
|
|
548
751
|
}
|
|
549
752
|
await renderer.playback.recalculate();
|
|
@@ -566,28 +769,103 @@ var renderTwickVideoInBrowser = async (config) => {
|
|
|
566
769
|
}
|
|
567
770
|
await exporter.stop();
|
|
568
771
|
let audioData = null;
|
|
569
|
-
|
|
570
|
-
|
|
772
|
+
console.log(`Audio detection summary: hasAnyAudio=${hasAnyAudio}, includeAudio=${settings.includeAudio}, mediaAssets=${mediaAssets.length}`);
|
|
773
|
+
if (settings.includeAudio && mediaAssets.length > 0 && hasAnyAudio) {
|
|
774
|
+
console.log("Starting audio processing (audio detected in videos)");
|
|
775
|
+
if (settings.onProgress) {
|
|
776
|
+
settings.onProgress(0.98);
|
|
777
|
+
}
|
|
778
|
+
try {
|
|
779
|
+
console.log("Calling generateAudio...");
|
|
780
|
+
audioData = await exporter.generateAudio(mediaAssets, 0, totalFrames);
|
|
781
|
+
console.log("generateAudio completed");
|
|
782
|
+
if (audioData) {
|
|
783
|
+
console.log(`\u2713 Audio generation successful: ${audioData.byteLength} bytes`);
|
|
784
|
+
} else {
|
|
785
|
+
console.log("\u2717 No audio data generated");
|
|
786
|
+
}
|
|
787
|
+
if (settings.onProgress) {
|
|
788
|
+
settings.onProgress(0.99);
|
|
789
|
+
}
|
|
790
|
+
} catch (audioError) {
|
|
791
|
+
const errorMsg = audioError instanceof Error ? audioError.message : String(audioError);
|
|
792
|
+
const errorStack = audioError instanceof Error ? audioError.stack : void 0;
|
|
793
|
+
console.error("\u2717 Audio generation failed, continuing without audio");
|
|
794
|
+
console.error(` Error: ${errorMsg}`);
|
|
795
|
+
if (errorStack) {
|
|
796
|
+
console.error(` Stack: ${errorStack}`);
|
|
797
|
+
}
|
|
798
|
+
console.error(" Context:", {
|
|
799
|
+
hasAnyAudio,
|
|
800
|
+
includeAudio: settings.includeAudio,
|
|
801
|
+
mediaAssetsCount: mediaAssets.length,
|
|
802
|
+
totalFrames
|
|
803
|
+
});
|
|
804
|
+
audioData = null;
|
|
805
|
+
}
|
|
806
|
+
} else if (settings.includeAudio && mediaAssets.length > 0 && !hasAnyAudio) {
|
|
807
|
+
console.log("\u23ED Skipping audio processing: no audio detected in videos");
|
|
808
|
+
} else {
|
|
809
|
+
console.log(`\u23ED Skipping audio processing: includeAudio=${settings.includeAudio}, mediaAssets=${mediaAssets.length}, hasAnyAudio=${hasAnyAudio}`);
|
|
571
810
|
}
|
|
572
811
|
let finalBlob = exporter.getVideoBlob();
|
|
573
812
|
if (!finalBlob) {
|
|
574
813
|
throw new Error("Failed to create video blob");
|
|
575
814
|
}
|
|
815
|
+
if (finalBlob.size === 0) {
|
|
816
|
+
throw new Error("Video blob is empty. Rendering may have failed.");
|
|
817
|
+
}
|
|
576
818
|
if (audioData && settings.includeAudio) {
|
|
819
|
+
console.log(`Attempting to mux audio (${audioData.byteLength} bytes) with video (${finalBlob.size} bytes)`);
|
|
577
820
|
try {
|
|
578
|
-
|
|
821
|
+
const muxedBlob = await muxAudioVideo({
|
|
579
822
|
videoBlob: finalBlob,
|
|
580
823
|
audioBuffer: audioData
|
|
581
824
|
});
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
825
|
+
if (!muxedBlob || muxedBlob.size === 0) {
|
|
826
|
+
throw new Error("Muxed video blob is empty");
|
|
827
|
+
}
|
|
828
|
+
if (muxedBlob.size === finalBlob.size) {
|
|
829
|
+
console.warn("Muxed blob size unchanged - muxing may have failed silently");
|
|
830
|
+
} else {
|
|
831
|
+
console.log(`Muxing successful: ${finalBlob.size} bytes -> ${muxedBlob.size} bytes`);
|
|
832
|
+
}
|
|
833
|
+
finalBlob = muxedBlob;
|
|
834
|
+
} catch (muxError) {
|
|
835
|
+
const errorMsg = muxError instanceof Error ? muxError.message : String(muxError);
|
|
836
|
+
const errorStack = muxError instanceof Error ? muxError.stack : void 0;
|
|
837
|
+
console.error("Audio muxing failed");
|
|
838
|
+
console.error(` Error: ${errorMsg}`);
|
|
839
|
+
if (errorStack) {
|
|
840
|
+
console.error(` Stack: ${errorStack}`);
|
|
841
|
+
}
|
|
842
|
+
console.error(" Context:", {
|
|
843
|
+
videoBlobSize: finalBlob.size,
|
|
844
|
+
audioDataSize: audioData?.byteLength || 0
|
|
845
|
+
});
|
|
846
|
+
if (settings.downloadAudioSeparately && audioData) {
|
|
847
|
+
const audioBlob = new Blob([audioData], { type: "audio/wav" });
|
|
848
|
+
const audioUrl = URL.createObjectURL(audioBlob);
|
|
849
|
+
const a = document.createElement("a");
|
|
850
|
+
a.href = audioUrl;
|
|
851
|
+
a.download = "audio.wav";
|
|
852
|
+
a.click();
|
|
853
|
+
URL.revokeObjectURL(audioUrl);
|
|
854
|
+
}
|
|
855
|
+
console.warn("Continuing with video without audio due to muxing failure");
|
|
856
|
+
finalBlob = exporter.getVideoBlob();
|
|
857
|
+
if (!finalBlob || finalBlob.size === 0) {
|
|
858
|
+
throw new Error("Video blob is invalid after muxing failure");
|
|
859
|
+
}
|
|
590
860
|
}
|
|
861
|
+
} else if (settings.includeAudio && !audioData) {
|
|
862
|
+
console.warn("Audio processing was enabled but no audio data was generated");
|
|
863
|
+
}
|
|
864
|
+
if (!finalBlob || finalBlob.size === 0) {
|
|
865
|
+
throw new Error("Final video blob is empty or invalid");
|
|
866
|
+
}
|
|
867
|
+
if (settings.onProgress) {
|
|
868
|
+
settings.onProgress(1);
|
|
591
869
|
}
|
|
592
870
|
if (settings.onComplete) {
|
|
593
871
|
settings.onComplete(finalBlob);
|