视频保存

This commit is contained in:
duoaohui
2026-05-19 12:12:11 +08:00
parent 472495af02
commit aac8ec642a
2 changed files with 82 additions and 13 deletions

View File

@@ -145,10 +145,15 @@ export function setRecordingFailureCallback(cb: RecordingFailureCallback | null)
async function defaultWebhookCallback(info: Parameters<RecordingFailureCallback>[0]): Promise<void> {
const url = process.env.RECORDING_FAILURE_WEBHOOK_URL;
if (!url) return;
// go-service 端校验 X-Internal-Secret 与其 INTERNAL_WEBHOOK_SECRET 是否一致,
// 缺失/不匹配会直接 403。生产部署务必两侧使用相同值
const secret = process.env.INTERNAL_WEBHOOK_SECRET ?? '';
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
if (secret) headers['X-Internal-Secret'] = secret;
try {
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
headers,
body: JSON.stringify(info),
});
if (!res.ok) {
@@ -166,19 +171,10 @@ async function defaultWebhookCallback(info: Parameters<RecordingFailureCallback>
}
/**
* 内部触发失败回调:异步、错误吞掉
* 同时尝试用户注册的回调和默认 webhook两者独立
* 实际异步派发失败回调:同时跑 default webhook + 用户注册的 callback
* 抽出此函数是为了让 spawn 阶段entry 还没进 recordingMap也能复用
*/
function fireFailureCallback(entry: RecordingEntry): void {
if (entry.unexpectedExit) return; // 幂等:每条录制最多触发一次
entry.unexpectedExit = true;
const info = {
recordingId: entry.id,
roomCode: entry.roomCode,
routerId: entry.routerId,
exitCode: entry.exitCode,
failureReason: entry.failureReason ?? 'ffmpeg exited unexpectedly',
};
function dispatchFailureInfo(info: Parameters<RecordingFailureCallback>[0]): void {
setImmediate(() => {
void defaultWebhookCallback(info);
if (failureCallback) {
@@ -202,6 +198,47 @@ function fireFailureCallback(entry: RecordingEntry): void {
});
}
/**
* 内部触发失败回调:异步、错误吞掉
* 入口语义"已 active 的 ffmpeg 进程异常退出",依赖已落到 recordingMap 的 entry
*/
function fireFailureCallback(entry: RecordingEntry): void {
if (entry.unexpectedExit) return; // 幂等:每条录制最多触发一次
entry.unexpectedExit = true;
dispatchFailureInfo({
recordingId: entry.id,
roomCode: entry.roomCode,
routerId: entry.routerId,
exitCode: entry.exitCode,
failureReason: entry.failureReason ?? 'ffmpeg exited unexpectedly',
});
}
/**
* spawn 阶段失败兜底ffmpeg 二进制找不到 / 无权限 / 启动后立刻退出
* 此时 entry 可能还没入 recordingMapstartRecording 在末尾才插入),
* 依旧需要把失败上报给 go-service否则 startRecording 同步 throw 之后
* 万一上层 DB 行已经存在(异常时序、重试场景)就会卡在 recording 状态。
*
* 与 fireFailureCallback 互斥:只要其中一个跑过就标记 entry.unexpectedExit=true如果有 entry
*/
function fireSpawnFailure(args: {
recordingId: string;
roomCode: string;
routerId: string;
exitCode: number | null;
failureReason: string;
}): void {
const entry = recordingMap.get(args.recordingId);
if (entry) {
if (entry.unexpectedExit) return;
entry.unexpectedExit = true;
if (!entry.failureReason) entry.failureReason = args.failureReason;
if (entry.exitCode === null) entry.exitCode = args.exitCode;
}
dispatchFailureInfo(args);
}
const recordingMap = new Map<string, RecordingEntry>();
// 默认输出目录;由 RECORDING_OUTPUT_DIR 环境变量覆盖,便于 docker volume 挂载
@@ -492,6 +529,20 @@ export async function startRecording(input: StartRecordingInput): Promise<StartR
// Phase B watchdog异步触发失败回调webhook + 用户回调),不阻塞 exit 处理
fireFailureCallback(entry);
}
} else if (code !== 0) {
// entry 还没入 mapstartRecording 末尾才插入)→ 走 spawn 失败兜底,避免 webhook 漏报
// startRecording 同步路径会自己 throw + rollback这里上报只为防止上层 DB 行已存在的边缘场景
log.error(
{ recordingId: id, code, signal },
'ffmpeg exited before recording entry registered',
);
fireSpawnFailure({
recordingId: id,
roomCode: input.roomCode,
routerId: input.routerId,
exitCode: code,
failureReason: `ffmpeg exited during spawn code=${code} signal=${signal ?? ''}`,
});
}
resolve();
});
@@ -504,7 +555,17 @@ export async function startRecording(input: StartRecordingInput): Promise<StartR
});
ffmpegProc.once('error', (err) => {
// spawn 阶段错误ffmpeg 二进制找不到 / 无权限 / OS 资源耗尽等
// 此前仅记日志go-service 完全感知不到。这里立刻触发 failure webhook 兜底,
// 让上层(即使 DB 已写入 recording 行)能把状态推进到 failed
log.error({ recordingId: id, err: err.message }, 'ffmpeg spawn error');
fireSpawnFailure({
recordingId: id,
roomCode: input.roomCode,
routerId: input.routerId,
exitCode: null,
failureReason: `ffmpeg spawn error: ${err.message}`,
});
});
// 等 ffmpeg 进入监听就绪:实践经验 200ms 足够 ffmpeg 解析 SDP + 绑定端口