视频保存

This commit is contained in:
duoaohui
2026-05-19 14:15:53 +08:00
parent 163b541214
commit 6d61bf2428

View File

@@ -515,6 +515,18 @@ export async function startRecording(input: StartRecordingInput): Promise<StartR
const args = buildFfmpegArgs(sdpFilePath, outputPath); const args = buildFfmpegArgs(sdpFilePath, outputPath);
ffmpegProc = spawn(ffmpegBin, args, { stdio: ['ignore', 'pipe', 'pipe'] }); ffmpegProc = spawn(ffmpegBin, args, { stdio: ['ignore', 'pipe', 'pipe'] });
// ffmpeg stderr 环形缓冲:正常运行时 debug 级 + 不落盘,
// 进程异常退出时把最后 N KB stderr 一次性以 error 级 dump 出来,方便定位 code=1 类问题
// 上限 16 KB 足以覆盖典型 ffmpeg 启动失败诊断codec/端口/SDP 解析等),且容量可控
const STDERR_TAIL_LIMIT = 16 * 1024;
let stderrTail = '';
const appendStderrTail = (text: string) => {
stderrTail += text;
if (stderrTail.length > STDERR_TAIL_LIMIT) {
stderrTail = stderrTail.slice(stderrTail.length - STDERR_TAIL_LIMIT);
}
};
const exited = new Promise<void>((resolve) => { const exited = new Promise<void>((resolve) => {
ffmpegProc!.once('exit', (code, signal) => { ffmpegProc!.once('exit', (code, signal) => {
const entry = recordingMap.get(id); const entry = recordingMap.get(id);
@@ -522,8 +534,9 @@ export async function startRecording(input: StartRecordingInput): Promise<StartR
entry.exitCode = code; entry.exitCode = code;
if (!entry.stopping && code !== 0) { if (!entry.stopping && code !== 0) {
entry.failureReason = `ffmpeg exited unexpectedly code=${code} signal=${signal ?? ''}`; entry.failureReason = `ffmpeg exited unexpectedly code=${code} signal=${signal ?? ''}`;
// 异常退出:把 stderr tail 落 error 级,便于线上排查 ffmpeg 失败根因
log.error( log.error(
{ recordingId: id, code, signal }, { recordingId: id, code, signal, stderrTail: stderrTail.trim() },
'ffmpeg exited unexpectedly before stop', 'ffmpeg exited unexpectedly before stop',
); );
// Phase B watchdog异步触发失败回调webhook + 用户回调),不阻塞 exit 处理 // Phase B watchdog异步触发失败回调webhook + 用户回调),不阻塞 exit 处理
@@ -533,7 +546,7 @@ export async function startRecording(input: StartRecordingInput): Promise<StartR
// entry 还没入 mapstartRecording 末尾才插入)→ 走 spawn 失败兜底,避免 webhook 漏报 // entry 还没入 mapstartRecording 末尾才插入)→ 走 spawn 失败兜底,避免 webhook 漏报
// startRecording 同步路径会自己 throw + rollback这里上报只为防止上层 DB 行已存在的边缘场景 // startRecording 同步路径会自己 throw + rollback这里上报只为防止上层 DB 行已存在的边缘场景
log.error( log.error(
{ recordingId: id, code, signal }, { recordingId: id, code, signal, stderrTail: stderrTail.trim() },
'ffmpeg exited before recording entry registered', 'ffmpeg exited before recording entry registered',
); );
fireSpawnFailure({ fireSpawnFailure({
@@ -541,7 +554,7 @@ export async function startRecording(input: StartRecordingInput): Promise<StartR
roomCode: input.roomCode, roomCode: input.roomCode,
routerId: input.routerId, routerId: input.routerId,
exitCode: code, exitCode: code,
failureReason: `ffmpeg exited during spawn code=${code} signal=${signal ?? ''}`, failureReason: `ffmpeg exited during spawn code=${code} signal=${signal ?? ''}; stderr_tail=${stderrTail.trim().slice(-512)}`,
}); });
} }
resolve(); resolve();
@@ -550,7 +563,9 @@ export async function startRecording(input: StartRecordingInput): Promise<StartR
ffmpegProc.stderr?.on('data', (chunk: Buffer) => { ffmpegProc.stderr?.on('data', (chunk: Buffer) => {
const text = chunk.toString('utf-8'); const text = chunk.toString('utf-8');
// ffmpeg 默认大部分日志走 stderr只在 debug 级别落,避免日志爆炸 // 1) 永远写入环形缓冲,供进程异常退出时 dump
appendStderrTail(text);
// 2) 正常实时日志仍走 debug 级,避免高频日志爆炸
log.debug({ recordingId: id }, `[ffmpeg] ${text.trim()}`); log.debug({ recordingId: id }, `[ffmpeg] ${text.trim()}`);
}); });