Files
aidio-description/dist/server/services/muxer.js

83 lines
3.5 KiB
JavaScript
Raw Normal View History

"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.muxAudioDescription = muxAudioDescription;
2026-05-15 04:10:06 +02:00
exports.muxMixedAudioDescription = muxMixedAudioDescription;
const child_process_1 = require("child_process");
const path_1 = __importDefault(require("path"));
2026-05-15 04:10:06 +02:00
const fs_1 = __importDefault(require("fs"));
function muxAudioDescription(videoPath, audioPath, outputPath) {
2026-05-15 04:10:06 +02:00
if (!fs_1.default.existsSync(videoPath)) {
throw new Error(`mux: video not found: ${videoPath}`);
}
if (!fs_1.default.existsSync(audioPath)) {
throw new Error(`mux: audio not found: ${audioPath}`);
}
fs_1.default.mkdirSync(path_1.default.dirname(outputPath), { recursive: true });
// Argv form — no shell, no quoting issues, and -y is a global option (placed
// up front, not after the output). Stderr is captured so failures aren't
// silent.
const args = [
'-y',
'-v', 'error',
'-i', videoPath,
'-i', audioPath,
'-map', '0:v',
'-map', '0:a?',
'-map', '1:a',
'-c:v', 'copy',
'-c:a', 'copy',
'-metadata:s:a:1', 'title=Audio Description',
'-disposition:a:1', 'visual_impaired',
outputPath,
];
const result = (0, child_process_1.spawnSync)('ffmpeg', args, { shell: false, encoding: 'utf-8' });
if (result.error) {
throw new Error(`mux: ffmpeg failed to start: ${result.error.message}`);
}
if (result.status !== 0) {
const tail = (result.stderr || '').trim().split('\n').slice(-5).join(' | ');
throw new Error(`mux: ffmpeg exited ${result.status}: ${tail || '(no stderr)'}`);
}
}
function muxMixedAudioDescription(videoPath, audioPath, outputPath) {
if (!fs_1.default.existsSync(videoPath)) {
throw new Error(`mux: video not found: ${videoPath}`);
}
if (!fs_1.default.existsSync(audioPath)) {
throw new Error(`mux: audio not found: ${audioPath}`);
}
fs_1.default.mkdirSync(path_1.default.dirname(outputPath), { recursive: true });
// Sidechain-ducked mix: original audio dips when the AD track is speaking,
// then both are summed into a single output audio stream. The AD track is
// already a full-length file that is silent between description segments
// (built by combineAudioSegments), so asplit gives us one copy to drive the
// sidechain detector and another to mix in on top.
const filterGraph = '[1:a]asplit=2[ad_mix][ad_sc];' +
'[0:a][ad_sc]sidechaincompress=threshold=0.03:ratio=20:attack=5:release=300:level_sc=2[ducked];' +
'[ducked][ad_mix]amix=inputs=2:duration=first:dropout_transition=0:normalize=0[aout]';
const args = [
'-y',
'-v', 'error',
'-i', videoPath,
'-i', audioPath,
'-filter_complex', filterGraph,
'-map', '0:v',
'-map', '[aout]',
'-c:v', 'copy',
'-c:a', 'aac',
'-b:a', '192k',
outputPath,
];
const result = (0, child_process_1.spawnSync)('ffmpeg', args, { shell: false, encoding: 'utf-8' });
if (result.error) {
throw new Error(`mux: ffmpeg failed to start: ${result.error.message}`);
}
if (result.status !== 0) {
const tail = (result.stderr || '').trim().split('\n').slice(-5).join(' | ');
throw new Error(`mux: ffmpeg exited ${result.status}: ${tail || '(no stderr)'}`);
}
}
//# sourceMappingURL=muxer.js.map