@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.js
CHANGED
|
@@ -40,6 +40,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
40
40
|
// src/browser-renderer.ts
|
|
41
41
|
var import_core = require("@twick/core");
|
|
42
42
|
var import_project = __toESM(require("@twick/visualizer/dist/project.js"));
|
|
43
|
+
var import_media_utils = require("@twick/media-utils");
|
|
43
44
|
|
|
44
45
|
// src/audio/video-audio-extractor.ts
|
|
45
46
|
var VideoElementAudioExtractor = class {
|
|
@@ -67,40 +68,113 @@ var VideoElementAudioExtractor = class {
|
|
|
67
68
|
* Extract audio by playing the video and capturing audio output
|
|
68
69
|
*/
|
|
69
70
|
async extractAudio(startTime, duration, playbackRate = 1) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
try {
|
|
72
|
+
const source = this.audioContext.createMediaElementSource(this.video);
|
|
73
|
+
this.destination = this.audioContext.createMediaStreamDestination();
|
|
74
|
+
source.connect(this.destination);
|
|
75
|
+
} catch (err) {
|
|
76
|
+
throw new Error("Video has no audio track");
|
|
77
|
+
}
|
|
73
78
|
this.audioChunks = [];
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
79
|
+
let mimeType = "audio/webm";
|
|
80
|
+
if (!MediaRecorder.isTypeSupported(mimeType)) {
|
|
81
|
+
mimeType = "";
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
this.mediaRecorder = new MediaRecorder(this.destination.stream, {
|
|
85
|
+
mimeType: mimeType || void 0
|
|
86
|
+
});
|
|
87
|
+
} catch (err) {
|
|
88
|
+
throw new Error(`Failed to create MediaRecorder: ${err}. Video may have no audio track.`);
|
|
89
|
+
}
|
|
77
90
|
this.mediaRecorder.ondataavailable = (event) => {
|
|
78
|
-
if (event.data.size > 0) {
|
|
91
|
+
if (event.data && event.data.size > 0) {
|
|
79
92
|
this.audioChunks.push(event.data);
|
|
80
93
|
}
|
|
81
94
|
};
|
|
82
95
|
this.video.currentTime = startTime;
|
|
83
96
|
this.video.playbackRate = playbackRate;
|
|
84
|
-
await new Promise((resolve) => {
|
|
85
|
-
|
|
97
|
+
await new Promise((resolve, reject) => {
|
|
98
|
+
const seekTimeout = setTimeout(() => {
|
|
99
|
+
reject(new Error("Video seek timeout"));
|
|
100
|
+
}, 5e3);
|
|
101
|
+
this.video.addEventListener("seeked", () => {
|
|
102
|
+
clearTimeout(seekTimeout);
|
|
103
|
+
resolve();
|
|
104
|
+
}, { once: true });
|
|
105
|
+
this.video.addEventListener("error", () => {
|
|
106
|
+
clearTimeout(seekTimeout);
|
|
107
|
+
reject(new Error("Video seek error"));
|
|
108
|
+
}, { once: true });
|
|
86
109
|
});
|
|
87
110
|
return new Promise((resolve, reject) => {
|
|
88
111
|
const recordingTimeout = setTimeout(() => {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
112
|
+
this.video.pause();
|
|
113
|
+
if (this.mediaRecorder && this.mediaRecorder.state !== "inactive") {
|
|
114
|
+
this.mediaRecorder.stop();
|
|
115
|
+
}
|
|
116
|
+
reject(new Error("Audio extraction timeout - video may have no audio track"));
|
|
117
|
+
}, (duration / playbackRate + 10) * 1e3);
|
|
118
|
+
let hasData = false;
|
|
119
|
+
const dataCheckInterval = setInterval(() => {
|
|
120
|
+
if (this.audioChunks.length > 0 && this.audioChunks.some((chunk) => chunk.size > 0)) {
|
|
121
|
+
hasData = true;
|
|
122
|
+
}
|
|
123
|
+
}, 1e3);
|
|
124
|
+
this.mediaRecorder.onerror = (event) => {
|
|
125
|
+
clearInterval(dataCheckInterval);
|
|
126
|
+
clearTimeout(recordingTimeout);
|
|
127
|
+
this.video.pause();
|
|
128
|
+
reject(new Error(`MediaRecorder error: ${event}. Video may have no audio track.`));
|
|
129
|
+
};
|
|
130
|
+
try {
|
|
131
|
+
this.mediaRecorder.start(100);
|
|
132
|
+
this.video.play().catch((playErr) => {
|
|
133
|
+
clearInterval(dataCheckInterval);
|
|
134
|
+
clearTimeout(recordingTimeout);
|
|
135
|
+
reject(new Error(`Failed to play video: ${playErr}`));
|
|
136
|
+
});
|
|
137
|
+
} catch (startErr) {
|
|
138
|
+
clearInterval(dataCheckInterval);
|
|
139
|
+
clearTimeout(recordingTimeout);
|
|
140
|
+
reject(new Error(`Failed to start recording: ${startErr}`));
|
|
141
|
+
}
|
|
93
142
|
setTimeout(async () => {
|
|
143
|
+
clearInterval(dataCheckInterval);
|
|
94
144
|
clearTimeout(recordingTimeout);
|
|
95
145
|
this.video.pause();
|
|
96
|
-
this.mediaRecorder.
|
|
146
|
+
if (this.mediaRecorder && this.mediaRecorder.state !== "inactive") {
|
|
147
|
+
this.mediaRecorder.stop();
|
|
148
|
+
}
|
|
149
|
+
const stopTimeout = setTimeout(() => {
|
|
150
|
+
if (this.audioChunks.length === 0 || !hasData) {
|
|
151
|
+
reject(new Error("No audio data captured - video has no audio track"));
|
|
152
|
+
}
|
|
153
|
+
}, 2e3);
|
|
97
154
|
await new Promise((res) => {
|
|
98
|
-
this.mediaRecorder
|
|
155
|
+
if (this.mediaRecorder) {
|
|
156
|
+
this.mediaRecorder.addEventListener("stop", () => {
|
|
157
|
+
clearTimeout(stopTimeout);
|
|
158
|
+
res();
|
|
159
|
+
}, { once: true });
|
|
160
|
+
} else {
|
|
161
|
+
clearTimeout(stopTimeout);
|
|
162
|
+
res();
|
|
163
|
+
}
|
|
99
164
|
});
|
|
100
165
|
try {
|
|
166
|
+
if (this.audioChunks.length === 0 || !this.audioChunks.some((chunk) => chunk.size > 0)) {
|
|
167
|
+
throw new Error("No audio data captured - video has no audio track");
|
|
168
|
+
}
|
|
101
169
|
const audioBlob = new Blob(this.audioChunks, { type: "audio/webm" });
|
|
170
|
+
if (audioBlob.size === 0) {
|
|
171
|
+
throw new Error("Audio blob is empty - video has no audio track");
|
|
172
|
+
}
|
|
102
173
|
const arrayBuffer = await audioBlob.arrayBuffer();
|
|
103
174
|
const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer);
|
|
175
|
+
if (audioBuffer.length === 0 || audioBuffer.duration === 0) {
|
|
176
|
+
throw new Error("Audio buffer is empty - video has no audio track");
|
|
177
|
+
}
|
|
104
178
|
resolve(audioBuffer);
|
|
105
179
|
} catch (err) {
|
|
106
180
|
reject(new Error(`Failed to decode recorded audio: ${err}`));
|
|
@@ -323,25 +397,46 @@ function getFFmpegBaseURL() {
|
|
|
323
397
|
return "/ffmpeg";
|
|
324
398
|
}
|
|
325
399
|
async function muxAudioVideo(options) {
|
|
400
|
+
const muxStartTime = Date.now();
|
|
326
401
|
try {
|
|
402
|
+
console.log("Starting FFmpeg muxing...");
|
|
403
|
+
console.log(` Video blob size: ${options.videoBlob.size} bytes (${(options.videoBlob.size / 1024 / 1024).toFixed(2)} MB)`);
|
|
404
|
+
console.log(` Audio buffer size: ${options.audioBuffer.byteLength} bytes (${(options.audioBuffer.byteLength / 1024 / 1024).toFixed(2)} MB)`);
|
|
327
405
|
const { FFmpeg } = await import("@ffmpeg/ffmpeg");
|
|
328
406
|
const { fetchFile } = await import("@ffmpeg/util");
|
|
329
407
|
const ffmpeg = new FFmpeg();
|
|
330
408
|
const base = getFFmpegBaseURL();
|
|
331
409
|
const coreURL = `${base}/ffmpeg-core.js`;
|
|
332
410
|
const wasmURL = `${base}/ffmpeg-core.wasm`;
|
|
411
|
+
console.log(`Loading FFmpeg from ${base}`);
|
|
412
|
+
const loadStartTime = Date.now();
|
|
333
413
|
await ffmpeg.load({
|
|
334
414
|
coreURL,
|
|
335
415
|
wasmURL
|
|
336
416
|
});
|
|
417
|
+
const loadDuration = Date.now() - loadStartTime;
|
|
418
|
+
console.log(`FFmpeg loaded successfully in ${loadDuration}ms`);
|
|
419
|
+
console.log("Writing video and audio files...");
|
|
420
|
+
const writeStartTime = Date.now();
|
|
337
421
|
await ffmpeg.writeFile(
|
|
338
422
|
"video.mp4",
|
|
339
423
|
await fetchFile(options.videoBlob)
|
|
340
424
|
);
|
|
425
|
+
console.log(` Video file written: ${options.videoBlob.size} bytes`);
|
|
341
426
|
await ffmpeg.writeFile(
|
|
342
427
|
"audio.wav",
|
|
343
428
|
new Uint8Array(options.audioBuffer)
|
|
344
429
|
);
|
|
430
|
+
const writeDuration = Date.now() - writeStartTime;
|
|
431
|
+
console.log(` Audio file written: ${options.audioBuffer.byteLength} bytes`);
|
|
432
|
+
console.log(`Files written successfully in ${writeDuration}ms`);
|
|
433
|
+
console.log("Executing FFmpeg muxing command...");
|
|
434
|
+
const execStartTime = Date.now();
|
|
435
|
+
const ffmpegLogs = [];
|
|
436
|
+
ffmpeg.on("log", ({ message }) => {
|
|
437
|
+
ffmpegLogs.push(message);
|
|
438
|
+
console.log(` [FFmpeg] ${message}`);
|
|
439
|
+
});
|
|
345
440
|
await ffmpeg.exec([
|
|
346
441
|
"-i",
|
|
347
442
|
"video.mp4",
|
|
@@ -356,11 +451,34 @@ async function muxAudioVideo(options) {
|
|
|
356
451
|
"-shortest",
|
|
357
452
|
"output.mp4"
|
|
358
453
|
]);
|
|
454
|
+
const execDuration = Date.now() - execStartTime;
|
|
455
|
+
console.log(`FFmpeg muxing completed in ${execDuration}ms`);
|
|
456
|
+
const readStartTime = Date.now();
|
|
359
457
|
const data = await ffmpeg.readFile("output.mp4");
|
|
458
|
+
const readDuration = Date.now() - readStartTime;
|
|
459
|
+
console.log(`Output file read successfully in ${readDuration}ms`);
|
|
360
460
|
const uint8 = typeof data === "string" ? new TextEncoder().encode(data) : new Uint8Array(data);
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
461
|
+
const result = new Blob([uint8], { type: "video/mp4" });
|
|
462
|
+
const totalDuration = Date.now() - muxStartTime;
|
|
463
|
+
console.log(`Muxing successful: ${result.size} bytes (${(result.size / 1024 / 1024).toFixed(2)} MB) in ${totalDuration}ms`);
|
|
464
|
+
console.log(` Breakdown: load=${loadDuration}ms, write=${writeDuration}ms, exec=${execDuration}ms, read=${readDuration}ms`);
|
|
465
|
+
return result;
|
|
466
|
+
} catch (error) {
|
|
467
|
+
const totalDuration = Date.now() - muxStartTime;
|
|
468
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
469
|
+
const errorStack = error instanceof Error ? error.stack : void 0;
|
|
470
|
+
console.error("FFmpeg muxing failed:", errorMsg);
|
|
471
|
+
if (errorStack) {
|
|
472
|
+
console.error("Error stack:", errorStack);
|
|
473
|
+
}
|
|
474
|
+
console.error("Error details:", {
|
|
475
|
+
errorType: error instanceof Error ? error.constructor.name : typeof error,
|
|
476
|
+
errorMessage: errorMsg,
|
|
477
|
+
duration: `${totalDuration}ms`,
|
|
478
|
+
videoBlobSize: options.videoBlob.size,
|
|
479
|
+
audioBufferSize: options.audioBuffer.byteLength
|
|
480
|
+
});
|
|
481
|
+
throw error;
|
|
364
482
|
}
|
|
365
483
|
}
|
|
366
484
|
|
|
@@ -446,33 +564,99 @@ var BrowserWasmExporter = class _BrowserWasmExporter {
|
|
|
446
564
|
}
|
|
447
565
|
async generateAudio(assets, startFrame, endFrame) {
|
|
448
566
|
try {
|
|
567
|
+
console.log(`Generating audio from ${assets.length} frames`);
|
|
449
568
|
const processor = new BrowserAudioProcessor();
|
|
450
569
|
const assetPlacements = getAssetPlacement(assets);
|
|
451
570
|
if (assetPlacements.length === 0) {
|
|
571
|
+
console.log("No asset placements found");
|
|
452
572
|
return null;
|
|
453
573
|
}
|
|
574
|
+
console.log(`Processing ${assetPlacements.length} asset placements`);
|
|
454
575
|
const processedBuffers = [];
|
|
455
|
-
for (
|
|
576
|
+
for (let i = 0; i < assetPlacements.length; i++) {
|
|
577
|
+
const asset = assetPlacements[i];
|
|
578
|
+
console.log(`[${i + 1}/${assetPlacements.length}] Processing asset: ${asset.src} (type: ${asset.type}, volume: ${asset.volume}, playbackRate: ${asset.playbackRate})`);
|
|
456
579
|
if (asset.volume > 0 && asset.playbackRate > 0) {
|
|
580
|
+
const startTime = Date.now();
|
|
457
581
|
try {
|
|
458
|
-
|
|
582
|
+
if (asset.type === "video") {
|
|
583
|
+
console.log(` \u2192 Checking if asset has audio: ${asset.src.substring(0, 50)}...`);
|
|
584
|
+
try {
|
|
585
|
+
const assetHasAudio = await (0, import_media_utils.hasAudio)(asset.src);
|
|
586
|
+
if (!assetHasAudio) {
|
|
587
|
+
console.log(` \u23ED Skipping asset (no audio detected): ${asset.src.substring(0, 50)}...`);
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
console.log(` \u2713 Asset has audio, proceeding: ${asset.src.substring(0, 50)}...`);
|
|
591
|
+
} catch (audioCheckError) {
|
|
592
|
+
const errorMsg = audioCheckError instanceof Error ? audioCheckError.message : String(audioCheckError);
|
|
593
|
+
const errorStack = audioCheckError instanceof Error ? audioCheckError.stack : void 0;
|
|
594
|
+
console.warn(` \u26A0 Audio check failed, proceeding anyway: ${asset.src.substring(0, 50)}...`);
|
|
595
|
+
console.warn(` Error: ${errorMsg}`);
|
|
596
|
+
if (errorStack) {
|
|
597
|
+
console.warn(` Stack: ${errorStack}`);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
console.log(` \u2192 Starting processAudioAsset for: ${asset.src}`);
|
|
602
|
+
const processPromise = processor.processAudioAsset(
|
|
459
603
|
asset,
|
|
460
604
|
this.settings.fps || 30,
|
|
461
605
|
endFrame - startFrame
|
|
462
606
|
);
|
|
607
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
608
|
+
setTimeout(() => {
|
|
609
|
+
reject(new Error(`Timeout processing audio asset after 20s - video may have no audio track`));
|
|
610
|
+
}, 2e4);
|
|
611
|
+
});
|
|
612
|
+
const buffer = await Promise.race([processPromise, timeoutPromise]);
|
|
613
|
+
const duration = Date.now() - startTime;
|
|
614
|
+
console.log(` \u2713 Successfully processed audio asset in ${duration}ms: ${asset.src.substring(0, 50)}...`);
|
|
463
615
|
processedBuffers.push(buffer);
|
|
464
|
-
} catch {
|
|
616
|
+
} catch (error) {
|
|
617
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
618
|
+
const errorStack = error instanceof Error ? error.stack : void 0;
|
|
619
|
+
const duration = Date.now() - startTime;
|
|
620
|
+
console.warn(` \u2717 Failed to process audio asset after ${duration}ms: ${asset.src.substring(0, 50)}...`);
|
|
621
|
+
console.warn(` Error: ${errorMsg}`);
|
|
622
|
+
if (errorStack) {
|
|
623
|
+
console.warn(` Stack: ${errorStack}`);
|
|
624
|
+
}
|
|
625
|
+
console.warn(` Asset details: type=${asset.type}, volume=${asset.volume}, playbackRate=${asset.playbackRate}, startFrame=${asset.startInVideo}, endFrame=${asset.endInVideo}`);
|
|
465
626
|
}
|
|
627
|
+
} else {
|
|
628
|
+
console.log(` \u23ED Skipping asset: volume=${asset.volume}, playbackRate=${asset.playbackRate}`);
|
|
466
629
|
}
|
|
467
630
|
}
|
|
468
631
|
if (processedBuffers.length === 0) {
|
|
632
|
+
console.warn("No audio buffers were successfully processed");
|
|
633
|
+
console.warn(` Total assets attempted: ${assetPlacements.length}`);
|
|
634
|
+
console.warn(` Assets with volume>0 and playbackRate>0: ${assetPlacements.filter((a) => a.volume > 0 && a.playbackRate > 0).length}`);
|
|
469
635
|
return null;
|
|
470
636
|
}
|
|
637
|
+
console.log(`Mixing ${processedBuffers.length} audio buffers`);
|
|
638
|
+
const mixStartTime = Date.now();
|
|
471
639
|
const mixedBuffer = processor.mixAudioBuffers(processedBuffers);
|
|
640
|
+
const mixDuration = Date.now() - mixStartTime;
|
|
641
|
+
console.log(`Audio mixing completed in ${mixDuration}ms`);
|
|
642
|
+
const wavStartTime = Date.now();
|
|
472
643
|
const wavData = processor.audioBufferToWav(mixedBuffer);
|
|
644
|
+
const wavDuration = Date.now() - wavStartTime;
|
|
645
|
+
console.log(`WAV conversion completed in ${wavDuration}ms`);
|
|
646
|
+
console.log(`Audio generation complete: ${wavData.byteLength} bytes (${(wavData.byteLength / 1024 / 1024).toFixed(2)} MB)`);
|
|
473
647
|
await processor.close();
|
|
474
648
|
return wavData;
|
|
475
|
-
} catch {
|
|
649
|
+
} catch (error) {
|
|
650
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
651
|
+
const errorStack = error instanceof Error ? error.stack : void 0;
|
|
652
|
+
console.error("Audio generation error:", errorMsg);
|
|
653
|
+
if (errorStack) {
|
|
654
|
+
console.error("Error stack:", errorStack);
|
|
655
|
+
}
|
|
656
|
+
console.error("Error details:", {
|
|
657
|
+
errorType: error instanceof Error ? error.constructor.name : typeof error,
|
|
658
|
+
errorMessage: errorMsg
|
|
659
|
+
});
|
|
476
660
|
return null;
|
|
477
661
|
}
|
|
478
662
|
}
|
|
@@ -560,6 +744,8 @@ var renderTwickVideoInBrowser = async (config) => {
|
|
|
560
744
|
}
|
|
561
745
|
});
|
|
562
746
|
}
|
|
747
|
+
let hasAnyAudio = false;
|
|
748
|
+
console.log(`Found ${videoElements.length} video element(s) to check for audio`);
|
|
563
749
|
if (videoElements.length > 0) {
|
|
564
750
|
for (const videoEl of videoElements) {
|
|
565
751
|
const src = videoEl.props?.src;
|
|
@@ -583,6 +769,23 @@ var renderTwickVideoInBrowser = async (config) => {
|
|
|
583
769
|
reject(new Error(`Failed to load video: ${err?.message || "Unknown error"}`));
|
|
584
770
|
}, { once: true });
|
|
585
771
|
});
|
|
772
|
+
if (settings.includeAudio) {
|
|
773
|
+
try {
|
|
774
|
+
console.log(`Checking if video has audio: ${src.substring(0, 50)}...`);
|
|
775
|
+
const videoHasAudio = await (0, import_media_utils.hasAudio)(src);
|
|
776
|
+
console.log(`Audio check result for ${src.substring(0, 50)}...: ${videoHasAudio ? "HAS AUDIO" : "NO AUDIO"}`);
|
|
777
|
+
if (videoHasAudio) {
|
|
778
|
+
hasAnyAudio = true;
|
|
779
|
+
console.log(`\u2713 Video has audio: ${src.substring(0, 50)}...`);
|
|
780
|
+
} else {
|
|
781
|
+
console.log(`\u2717 Video has no audio: ${src.substring(0, 50)}...`);
|
|
782
|
+
}
|
|
783
|
+
} catch (error) {
|
|
784
|
+
console.warn(`Failed to check audio for ${src.substring(0, 50)}...:`, error);
|
|
785
|
+
hasAnyAudio = true;
|
|
786
|
+
console.log(`\u26A0 Assuming video might have audio due to check error`);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
586
789
|
}
|
|
587
790
|
}
|
|
588
791
|
await renderer.playback.recalculate();
|
|
@@ -605,28 +808,103 @@ var renderTwickVideoInBrowser = async (config) => {
|
|
|
605
808
|
}
|
|
606
809
|
await exporter.stop();
|
|
607
810
|
let audioData = null;
|
|
608
|
-
|
|
609
|
-
|
|
811
|
+
console.log(`Audio detection summary: hasAnyAudio=${hasAnyAudio}, includeAudio=${settings.includeAudio}, mediaAssets=${mediaAssets.length}`);
|
|
812
|
+
if (settings.includeAudio && mediaAssets.length > 0 && hasAnyAudio) {
|
|
813
|
+
console.log("Starting audio processing (audio detected in videos)");
|
|
814
|
+
if (settings.onProgress) {
|
|
815
|
+
settings.onProgress(0.98);
|
|
816
|
+
}
|
|
817
|
+
try {
|
|
818
|
+
console.log("Calling generateAudio...");
|
|
819
|
+
audioData = await exporter.generateAudio(mediaAssets, 0, totalFrames);
|
|
820
|
+
console.log("generateAudio completed");
|
|
821
|
+
if (audioData) {
|
|
822
|
+
console.log(`\u2713 Audio generation successful: ${audioData.byteLength} bytes`);
|
|
823
|
+
} else {
|
|
824
|
+
console.log("\u2717 No audio data generated");
|
|
825
|
+
}
|
|
826
|
+
if (settings.onProgress) {
|
|
827
|
+
settings.onProgress(0.99);
|
|
828
|
+
}
|
|
829
|
+
} catch (audioError) {
|
|
830
|
+
const errorMsg = audioError instanceof Error ? audioError.message : String(audioError);
|
|
831
|
+
const errorStack = audioError instanceof Error ? audioError.stack : void 0;
|
|
832
|
+
console.error("\u2717 Audio generation failed, continuing without audio");
|
|
833
|
+
console.error(` Error: ${errorMsg}`);
|
|
834
|
+
if (errorStack) {
|
|
835
|
+
console.error(` Stack: ${errorStack}`);
|
|
836
|
+
}
|
|
837
|
+
console.error(" Context:", {
|
|
838
|
+
hasAnyAudio,
|
|
839
|
+
includeAudio: settings.includeAudio,
|
|
840
|
+
mediaAssetsCount: mediaAssets.length,
|
|
841
|
+
totalFrames
|
|
842
|
+
});
|
|
843
|
+
audioData = null;
|
|
844
|
+
}
|
|
845
|
+
} else if (settings.includeAudio && mediaAssets.length > 0 && !hasAnyAudio) {
|
|
846
|
+
console.log("\u23ED Skipping audio processing: no audio detected in videos");
|
|
847
|
+
} else {
|
|
848
|
+
console.log(`\u23ED Skipping audio processing: includeAudio=${settings.includeAudio}, mediaAssets=${mediaAssets.length}, hasAnyAudio=${hasAnyAudio}`);
|
|
610
849
|
}
|
|
611
850
|
let finalBlob = exporter.getVideoBlob();
|
|
612
851
|
if (!finalBlob) {
|
|
613
852
|
throw new Error("Failed to create video blob");
|
|
614
853
|
}
|
|
854
|
+
if (finalBlob.size === 0) {
|
|
855
|
+
throw new Error("Video blob is empty. Rendering may have failed.");
|
|
856
|
+
}
|
|
615
857
|
if (audioData && settings.includeAudio) {
|
|
858
|
+
console.log(`Attempting to mux audio (${audioData.byteLength} bytes) with video (${finalBlob.size} bytes)`);
|
|
616
859
|
try {
|
|
617
|
-
|
|
860
|
+
const muxedBlob = await muxAudioVideo({
|
|
618
861
|
videoBlob: finalBlob,
|
|
619
862
|
audioBuffer: audioData
|
|
620
863
|
});
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
864
|
+
if (!muxedBlob || muxedBlob.size === 0) {
|
|
865
|
+
throw new Error("Muxed video blob is empty");
|
|
866
|
+
}
|
|
867
|
+
if (muxedBlob.size === finalBlob.size) {
|
|
868
|
+
console.warn("Muxed blob size unchanged - muxing may have failed silently");
|
|
869
|
+
} else {
|
|
870
|
+
console.log(`Muxing successful: ${finalBlob.size} bytes -> ${muxedBlob.size} bytes`);
|
|
871
|
+
}
|
|
872
|
+
finalBlob = muxedBlob;
|
|
873
|
+
} catch (muxError) {
|
|
874
|
+
const errorMsg = muxError instanceof Error ? muxError.message : String(muxError);
|
|
875
|
+
const errorStack = muxError instanceof Error ? muxError.stack : void 0;
|
|
876
|
+
console.error("Audio muxing failed");
|
|
877
|
+
console.error(` Error: ${errorMsg}`);
|
|
878
|
+
if (errorStack) {
|
|
879
|
+
console.error(` Stack: ${errorStack}`);
|
|
880
|
+
}
|
|
881
|
+
console.error(" Context:", {
|
|
882
|
+
videoBlobSize: finalBlob.size,
|
|
883
|
+
audioDataSize: audioData?.byteLength || 0
|
|
884
|
+
});
|
|
885
|
+
if (settings.downloadAudioSeparately && audioData) {
|
|
886
|
+
const audioBlob = new Blob([audioData], { type: "audio/wav" });
|
|
887
|
+
const audioUrl = URL.createObjectURL(audioBlob);
|
|
888
|
+
const a = document.createElement("a");
|
|
889
|
+
a.href = audioUrl;
|
|
890
|
+
a.download = "audio.wav";
|
|
891
|
+
a.click();
|
|
892
|
+
URL.revokeObjectURL(audioUrl);
|
|
893
|
+
}
|
|
894
|
+
console.warn("Continuing with video without audio due to muxing failure");
|
|
895
|
+
finalBlob = exporter.getVideoBlob();
|
|
896
|
+
if (!finalBlob || finalBlob.size === 0) {
|
|
897
|
+
throw new Error("Video blob is invalid after muxing failure");
|
|
898
|
+
}
|
|
629
899
|
}
|
|
900
|
+
} else if (settings.includeAudio && !audioData) {
|
|
901
|
+
console.warn("Audio processing was enabled but no audio data was generated");
|
|
902
|
+
}
|
|
903
|
+
if (!finalBlob || finalBlob.size === 0) {
|
|
904
|
+
throw new Error("Final video blob is empty or invalid");
|
|
905
|
+
}
|
|
906
|
+
if (settings.onProgress) {
|
|
907
|
+
settings.onProgress(1);
|
|
630
908
|
}
|
|
631
909
|
if (settings.onComplete) {
|
|
632
910
|
settings.onComplete(finalBlob);
|