Fix: use filter_complex for sequence concatenation to fix sample rate mismatch causing speed up and noise

This commit is contained in:
2026-08-01 22:46:23 +08:00
parent e009f05a61
commit 2fae7305f6
+49 -34
View File
@@ -1235,7 +1235,7 @@ function getOrCreateHlsStreamState(streamName) {
return hlsStreams[streamName]; return hlsStreams[streamName];
} }
function buildConcatListFile(seq) { function resolveSequenceFiles(seq) {
ensureSilenceWav(); ensureSilenceWav();
const files = []; const files = [];
@@ -1264,24 +1264,23 @@ function buildConcatListFile(seq) {
for (let i = 0; i < HLS_TAIL_SILENCE_SEC; i++) files.push(silence1sWavPath); for (let i = 0; i < HLS_TAIL_SILENCE_SEC; i++) files.push(silence1sWavPath);
} }
if (files.length === 0) return null; return files;
const listPath = path.join(tmpDir, `concat-${Date.now()}-${Math.random().toString(16).slice(2)}.txt`);
const content = files
.map(p => `file '${p.replace(/\\/g, '/').replace(/'/g, "\\'")}'`)
.join('\n');
fs.writeFileSync(listPath, content, 'utf8');
return listPath;
} }
function buildSequenceWavFile(seq) { function buildSequenceWavFile(seq) {
const listPath = buildConcatListFile(seq); const files = resolveSequenceFiles(seq);
if (!listPath) return null; if (files.length === 0) return null;
const fileName = `sequence-${Date.now()}-${Math.random().toString(16).slice(2)}.wav`; const fileName = `sequence-${Date.now()}-${Math.random().toString(16).slice(2)}.wav`;
const outWavPath = path.join(tempBroadcastDir, fileName); const outWavPath = path.join(tempBroadcastDir, fileName);
const inputs = files.map(f => `-i "${f}"`).join(' ');
const filterResamples = files.map((_, i) => `[${i}:a]aformat=sample_rates=48000:channel_layouts=mono,aresample=48000[a${i}]`).join(';');
const filterConcat = files.map((_, i) => `[a${i}]`).join('') + `concat=n=${files.length}:v=0:a=1[a]`;
const filterComplex = `${filterResamples};${filterConcat}`;
try { try {
execSync(`ffmpeg -hide_banner -loglevel error -y -f concat -safe 0 -i "${listPath}" -ac 1 -ar 48000 -c:a pcm_s16le "${outWavPath}"`); execSync(`ffmpeg -hide_banner -loglevel error -y ${inputs} -filter_complex "${filterComplex}" -map "[a]" -ac 1 -ar 48000 -c:a pcm_s16le "${outWavPath}"`);
const buffer = fs.readFileSync(outWavPath); const buffer = fs.readFileSync(outWavPath);
const durationSec = getWavDurationSec(buffer) || 0; const durationSec = getWavDurationSec(buffer) || 0;
setTimeout(() => { setTimeout(() => {
@@ -1293,15 +1292,16 @@ function buildSequenceWavFile(seq) {
streamUrl: getManagedMediaUrl('temp', fileName), streamUrl: getManagedMediaUrl('temp', fileName),
durationSec durationSec
}; };
} finally { } catch (e) {
try { fs.unlinkSync(listPath); } catch {} log('ERROR', `buildSequenceWavFile failed: ${e.message}`);
return null;
} }
} }
function runFfmpegRtmpPush(streamName, seq) { function runFfmpegRtmpPush(streamName, seq) {
const state = getOrCreateHlsStreamState(streamName); const state = getOrCreateHlsStreamState(streamName);
const listPath = buildConcatListFile(seq); const files = resolveSequenceFiles(seq);
if (!listPath) { if (files.length === 0) {
state.status = 'idle'; state.status = 'idle';
state.lastError = 'No playable wav files in sequence'; state.lastError = 'No playable wav files in sequence';
return Promise.resolve(); return Promise.resolve();
@@ -1315,17 +1315,25 @@ function runFfmpegRtmpPush(streamName, seq) {
if (HLS_PREBUILD) { if (HLS_PREBUILD) {
const tmpAacPath = path.join(tmpDir, `prebuild-${Date.now()}-${Math.random().toString(16).slice(2)}.aac`); const tmpAacPath = path.join(tmpDir, `prebuild-${Date.now()}-${Math.random().toString(16).slice(2)}.aac`);
const inputs = files.map(f => `-i "${f}"`).join(' ');
const filterResamples = files.map((_, i) => `[${i}:a]aformat=sample_rates=48000:channel_layouts=mono,aresample=48000[a${i}]`).join(';');
const filterConcat = files.map((_, i) => `[a${i}]`).join('') + `concat=n=${files.length}:v=0:a=1[a]`;
const filterComplex = `${filterResamples};${filterConcat}`;
const buildArgs = [ const buildArgs = [
'-hide_banner', '-hide_banner',
'-loglevel', '-loglevel',
'error', 'error',
'-y', '-y'
'-f', ];
'concat',
'-safe', files.forEach(f => { buildArgs.push('-i', f); });
'0',
'-i', buildArgs.push(
listPath, '-filter_complex',
filterComplex,
'-map',
'[a]',
'-ac', '-ac',
'1', '1',
'-ar', '-ar',
@@ -1337,7 +1345,7 @@ function runFfmpegRtmpPush(streamName, seq) {
'-f', '-f',
'adts', 'adts',
tmpAacPath tmpAacPath
]; );
state.status = 'building'; state.status = 'building';
state.lastStartAt = Date.now(); state.lastStartAt = Date.now();
@@ -1396,7 +1404,6 @@ function runFfmpegRtmpPush(streamName, seq) {
state.lastError = (stderr || `ffmpeg exited with code ${code}`).trim(); state.lastError = (stderr || `ffmpeg exited with code ${code}`).trim();
log('ERROR', `HLS push failed for ${streamName}: ${state.lastError}`); log('ERROR', `HLS push failed for ${streamName}: ${state.lastError}`);
} }
try { fs.unlinkSync(listPath); } catch {}
try { fs.unlinkSync(tmpAacPath); } catch {} try { fs.unlinkSync(tmpAacPath); } catch {}
resolve(); resolve();
}); });
@@ -1404,17 +1411,24 @@ function runFfmpegRtmpPush(streamName, seq) {
}); });
} }
const filterResamples = files.map((_, i) => `[${i}:a]aformat=sample_rates=48000:channel_layouts=mono,aresample=48000[a${i}]`).join(';');
const filterConcat = files.map((_, i) => `[a${i}]`).join('') + `concat=n=${files.length}:v=0:a=1[a]`;
const filterComplex = `${filterResamples};${filterConcat}`;
const args = [ const args = [
'-hide_banner', '-hide_banner',
'-loglevel', '-loglevel',
'error', 'error',
'-re', '-re'
'-f', ];
'concat',
'-safe', files.forEach(f => { args.push('-i', f); });
'0',
'-i', args.push(
listPath, '-filter_complex',
filterComplex,
'-map',
'[a]',
'-ac', '-ac',
'1', '1',
'-ar', '-ar',
@@ -1426,7 +1440,7 @@ function runFfmpegRtmpPush(streamName, seq) {
'-f', '-f',
'flv', 'flv',
rtmpUrl rtmpUrl
]; );
state.status = 'pushing'; state.status = 'pushing';
state.lastStartAt = Date.now(); state.lastStartAt = Date.now();
@@ -2800,7 +2814,8 @@ app.post('/api/temp-broadcast/publish', (req, res) => {
const memberName = onlineMembers[idx]; const memberName = onlineMembers[idx];
const ws = getStationWs(memberName); const ws = getStationWs(memberName);
enqueueStationAudioTask(memberName, ws, { enqueueStationAudioTask(memberName, ws, {
audioFormat: 'stream', audioFormat: 'wav-stream-live',
sourcePath: wavPath,
streamUrl, streamUrl,
durationSec, durationSec,
priority, priority,