fix: go2rtc mjpeg 트랜스코더 멈춤 자동 감지/복구 엔드포인트 추가

프린터실 카메라의 go2rtc mjpeg 가상 스트림이 카메라 wifi 순단 등으로
멈추면 뷰어가 재연결해도 같은 멈춘 프로세스에 붙어 계속 안 나오는
문제가 있었음. /api/nvr/mjpeg-health가 컨트롤 탭 로드 시 go2rtc
스트림 상태를 확인해 정체된 트랜스코더만 재시작한다.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PppKcQmeTJDQCpfcgKietu
This commit is contained in:
kim
2026-09-18 14:27:38 +09:00
co-authored by Claude Sonnet 5
parent d34dd10ba7
commit e7a86d6f2d
+38
View File
@@ -835,4 +835,42 @@ export function registerNvrRoutes(app: Express): void {
res.status(502).json({ error: String(e?.message || e) });
}
});
// go2rtc의 ffmpeg mjpeg 트랜스코더가 업스트림(카메라 와이파이) 순단으로 멈춰버리는 경우가
// 있어서, 뷰어가 페이지를 열 때(웹캠 이미지 로드 직전) 이 엔드포인트로 살아있는지 확인하고
// 멈춰있으면 그 프로세스만 죽여서 go2rtc가 새로 띄우게 한다. 아무도 안 보고 있어서 프로듀서가
// 아예 없으면 아무 것도 안 함(불필요하게 인코딩을 깨우지 않음). 이미 정상 수신 중이면 그대로 둠
// (여러 명이 동시에 봐도 공유 스트림이라 매번 재시작하지 않음).
app.post('/api/nvr/mjpeg-health', async (req: Request, res: Response) => {
const user = (req as any).user;
if (!user) return res.status(401).json({ error: 'Unauthorized' });
try {
const src = String(req.body?.src || 'cam_mjpeg').replace(/[^a-zA-Z0-9_-]/g, '');
const container = String(req.body?.container || 'go2rtc-printroom').replace(/[^a-zA-Z0-9_-]/g, '');
const sample = async () => {
const r = await fetch('http://127.0.0.1:1985/api/streams');
const data = (await r.json()) as Record<string, any>;
const producer = data?.[src]?.producers?.[0];
const bytes = producer?.receivers?.[0]?.bytes ?? null;
return { active: !!producer, bytes };
};
const before = await sample();
if (!before.active) {
return res.json({ ok: true, action: 'none', reason: 'no viewer, producer not running' });
}
await new Promise((r) => setTimeout(r, 400));
const after = await sample();
if (after.bytes == null || before.bytes == null || after.bytes > before.bytes) {
return res.json({ ok: true, action: 'none', reason: 'healthy' });
}
await new Promise<void>((resolve) => {
const p = spawn('docker', ['exec', container, 'sh', '-c', `pkill -9 -f 'video%3Dmjpeg'`]);
p.on('close', () => resolve());
p.on('error', () => resolve());
});
res.json({ ok: true, action: 'restarted', reason: 'stalled transcoder killed' });
} catch (e: any) {
res.status(502).json({ error: String(e?.message || e) });
}
});
}