"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; exports.muxMixedAudioDescription = muxMixedAudioDescription; const child_process_1 = require("child_process"); const path_1 = __importDefault(require("path")); const fs_1 = __importDefault(require("fs")); function muxAudioDescription(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 }); // 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