Bug 106% raceCondition fix2
This commit is contained in:
@@ -114,30 +114,6 @@ function createSnapshotRouter(go2rtcUrl) {
|
||||
return res.status(503).json({ error: 'kein verwertbarer Hi-Res-Frame (Warmup-Timeout)' });
|
||||
}
|
||||
|
||||
// Schritt 3: Warten bis cam_hires Producer gestoppt ist bevor Client cam0 reconnectet.
|
||||
// Ohne dieses Warten: cam_hires-FFmpeg hält /dev/videoN noch offen, wenn startStream(cam)
|
||||
// go2rtc's cam-Producer startet → Race, zwei FFmpeg auf demselben Device → 108% CPU.
|
||||
{
|
||||
const t2 = Date.now();
|
||||
while (Date.now() - t2 < 5000) {
|
||||
try {
|
||||
const rp = await fetch(`${go2rtcUrl}/api/streams`, { signal: AbortSignal.timeout(1000) });
|
||||
if (rp.ok) {
|
||||
const ss = await rp.json();
|
||||
const sh = ss[hiresId];
|
||||
const nCh = sh ? (sh.consumers ?? []).length : 0;
|
||||
const pHRunning = sh ? (sh.producers ?? []).some(p => (p.state ?? '') === 'running') : false;
|
||||
if (nCh === 0 && !pHRunning) {
|
||||
console.log(`[hires][${id}] cam_hires gestoppt nach ${Date.now() - t2}ms – Gerät frei`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (_e) { /* ignore */ }
|
||||
await sleep(300);
|
||||
}
|
||||
await sleep(400); // Puffer: FFmpeg-Prozess-Exit bis Kernel Device-FD freigibt
|
||||
}
|
||||
|
||||
console.log(`[hires][${id}] OK – ${jpeg.length} bytes, Breite=${lastWidth}, Dauer=${Date.now() - t0}ms`);
|
||||
res.set({
|
||||
'Content-Type': 'image/jpeg',
|
||||
@@ -157,6 +133,82 @@ function createSnapshotRouter(go2rtcUrl) {
|
||||
}
|
||||
});
|
||||
|
||||
// ── 🔬 TEMPORÄR: Diagnose-Probe für den cam_hires-Teardown (Bug 106%) ─────────
|
||||
// Misst REIN LESEND, wann go2rtc den cam_hires-Producer nach einem frame.jpeg
|
||||
// wirklich abbaut. Beantwortet: Leert sich das producers-Array? Wann? Geht
|
||||
// consumers sofort auf 0? Daraus wird der robuste Rückweg gebaut (Weg C).
|
||||
//
|
||||
// VORHER im Viewer die betreffende Kamera AUSschalten (⏸), damit cam frei ist
|
||||
// (sonst zwei Encoder auf einem Device = genau der 106%-Konflikt).
|
||||
// curl http://<host>:8444/api/snapshot/cam0/hires-probe
|
||||
// Nach der Messung diese Route + doc-Eintrag wieder entfernen.
|
||||
router.get('/:id/hires-probe', async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const hiresId = `${id}_hires`;
|
||||
if (hiresLocks[id]) return res.status(429).json({ error: `${id} belegt` });
|
||||
hiresLocks[id] = true;
|
||||
const t0 = Date.now();
|
||||
|
||||
const snapHires = (streams) => {
|
||||
const s = streams[hiresId];
|
||||
const prods = s ? (s.producers ?? []) : [];
|
||||
return {
|
||||
cons: s ? (s.consumers ?? []).length : 0,
|
||||
prods: prods.length,
|
||||
states: prods.map(p => p.state ?? '?').join(',') || '-',
|
||||
};
|
||||
};
|
||||
|
||||
try {
|
||||
// Schritt 1: warten bis cam frei (max 8s) – sonst messen wir den Konflikt mit
|
||||
while (Date.now() - t0 < 8000) {
|
||||
const r = await fetch(`${go2rtcUrl}/api/streams`, { signal: AbortSignal.timeout(1000) }).catch(() => null);
|
||||
if (r && r.ok) {
|
||||
const s = (await r.json())[id];
|
||||
const nC = s ? (s.consumers ?? []).length : 0;
|
||||
const pR = s ? (s.producers ?? []).some(p => (p.state ?? '') === 'running') : false;
|
||||
if (nC === 0 && !pR) break;
|
||||
}
|
||||
await sleep(200);
|
||||
}
|
||||
|
||||
// Schritt 2: einen cam_hires-Frame holen (startet den Producer)
|
||||
const fr = await fetch(`${go2rtcUrl}/api/frame.jpeg?src=${encodeURIComponent(hiresId)}`,
|
||||
{ signal: AbortSignal.timeout(6000) });
|
||||
const buf = fr.ok ? Buffer.from(await fr.arrayBuffer()) : null;
|
||||
const tFrame = Date.now();
|
||||
const frameBytes = buf ? buf.length : 0;
|
||||
const frameWidth = buf ? readJpegWidth(buf) : null;
|
||||
console.log(`[probe][${id}] frame: ${frameBytes} bytes, Breite=${frameWidth ?? '?'} → poll teardown…`);
|
||||
|
||||
// Schritt 3: 12s lang alle 100ms den cam_hires-Zustand mitschreiben
|
||||
const timeline = [];
|
||||
let producerGoneAtMs = null;
|
||||
let consumersZeroAtMs = null;
|
||||
while (Date.now() - tFrame < 12000) {
|
||||
const r = await fetch(`${go2rtcUrl}/api/streams`, { signal: AbortSignal.timeout(1000) }).catch(() => null);
|
||||
const t = Date.now() - tFrame;
|
||||
if (r && r.ok) {
|
||||
const snap = snapHires(await r.json());
|
||||
timeline.push({ t, ...snap });
|
||||
if (producerGoneAtMs === null && snap.prods === 0) producerGoneAtMs = t;
|
||||
if (consumersZeroAtMs === null && snap.cons === 0) consumersZeroAtMs = t;
|
||||
} else {
|
||||
timeline.push({ t, err: true });
|
||||
}
|
||||
await sleep(100);
|
||||
}
|
||||
|
||||
console.log(`[probe][${id}] producerGoneAtMs=${producerGoneAtMs} consumersZeroAtMs=${consumersZeroAtMs}`);
|
||||
console.log(`[probe][${id}] timeline:`, JSON.stringify(timeline));
|
||||
res.json({ hiresId, frameBytes, frameWidth, producerGoneAtMs, consumersZeroAtMs, timeline });
|
||||
} catch (err) {
|
||||
if (!res.headersSent) res.status(503).json({ error: `probe: ${err.message}` });
|
||||
} finally {
|
||||
hiresLocks[id] = false;
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/', async (_req, res) => {
|
||||
try {
|
||||
const r = await fetch(`${go2rtcUrl}/api/streams`);
|
||||
|
||||
Reference in New Issue
Block a user