diff --git a/CloudServer/server.js b/CloudServer/server.js index 9343134..bc9936a 100644 --- a/CloudServer/server.js +++ b/CloudServer/server.js @@ -1235,7 +1235,7 @@ function getOrCreateHlsStreamState(streamName) { return hlsStreams[streamName]; } -function buildConcatListFile(seq) { +function resolveSequenceFiles(seq) { ensureSilenceWav(); const files = []; @@ -1264,24 +1264,23 @@ function buildConcatListFile(seq) { for (let i = 0; i < HLS_TAIL_SILENCE_SEC; i++) files.push(silence1sWavPath); } - if (files.length === 0) return null; - - 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; + return files; } function buildSequenceWavFile(seq) { - const listPath = buildConcatListFile(seq); - if (!listPath) return null; + const files = resolveSequenceFiles(seq); + if (files.length === 0) return null; const fileName = `sequence-${Date.now()}-${Math.random().toString(16).slice(2)}.wav`; 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 { - 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 durationSec = getWavDurationSec(buffer) || 0; setTimeout(() => { @@ -1293,15 +1292,16 @@ function buildSequenceWavFile(seq) { streamUrl: getManagedMediaUrl('temp', fileName), durationSec }; - } finally { - try { fs.unlinkSync(listPath); } catch {} + } catch (e) { + log('ERROR', `buildSequenceWavFile failed: ${e.message}`); + return null; } } function runFfmpegRtmpPush(streamName, seq) { const state = getOrCreateHlsStreamState(streamName); - const listPath = buildConcatListFile(seq); - if (!listPath) { + const files = resolveSequenceFiles(seq); + if (files.length === 0) { state.status = 'idle'; state.lastError = 'No playable wav files in sequence'; return Promise.resolve(); @@ -1315,17 +1315,25 @@ function runFfmpegRtmpPush(streamName, seq) { if (HLS_PREBUILD) { 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 = [ '-hide_banner', '-loglevel', 'error', - '-y', - '-f', - 'concat', - '-safe', - '0', - '-i', - listPath, + '-y' + ]; + + files.forEach(f => { buildArgs.push('-i', f); }); + + buildArgs.push( + '-filter_complex', + filterComplex, + '-map', + '[a]', '-ac', '1', '-ar', @@ -1337,7 +1345,7 @@ function runFfmpegRtmpPush(streamName, seq) { '-f', 'adts', tmpAacPath - ]; + ); state.status = 'building'; state.lastStartAt = Date.now(); @@ -1396,7 +1404,6 @@ function runFfmpegRtmpPush(streamName, seq) { state.lastError = (stderr || `ffmpeg exited with code ${code}`).trim(); log('ERROR', `HLS push failed for ${streamName}: ${state.lastError}`); } - try { fs.unlinkSync(listPath); } catch {} try { fs.unlinkSync(tmpAacPath); } catch {} 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 = [ '-hide_banner', '-loglevel', 'error', - '-re', - '-f', - 'concat', - '-safe', - '0', - '-i', - listPath, + '-re' + ]; + + files.forEach(f => { args.push('-i', f); }); + + args.push( + '-filter_complex', + filterComplex, + '-map', + '[a]', '-ac', '1', '-ar', @@ -1426,7 +1440,7 @@ function runFfmpegRtmpPush(streamName, seq) { '-f', 'flv', rtmpUrl - ]; + ); state.status = 'pushing'; state.lastStartAt = Date.now(); @@ -2800,7 +2814,8 @@ app.post('/api/temp-broadcast/publish', (req, res) => { const memberName = onlineMembers[idx]; const ws = getStationWs(memberName); enqueueStationAudioTask(memberName, ws, { - audioFormat: 'stream', + audioFormat: 'wav-stream-live', + sourcePath: wavPath, streamUrl, durationSec, priority,