openvibers/openvibe.live Commits
Live: restream stream keys never reach the log through ffmpeg's own error text (#140)
Changes
@@@ -946,7 +946,8 @@ class RestreamManager extends EventEmitter {if (session.status === 'stopped') return; // intentional stopconst duration = Date.now() - (session.startedAt || Date.now());-- const lastLines = stderrBuf.split('\n').filter(Boolean).slice(-5).join(' | ');++ // ffmpeg names the output URL in its errors ("Error opening output rtmps://…/app/<key>"): keys never reach a log.++ const lastLines = require('../utils/redact').redactUrl(stderrBuf.split('\n').filter(Boolean).slice(-5).join(' | '));console.log(`[Restream] FFmpeg exited for ${session.key}: code=${code}, ran ${(duration / 1000).toFixed(1)}s`);if (lastLines) console.log(`[Restream] stderr: ${lastLines.slice(0, 300)}`);@@@ -974,7 +975,7 @@ class RestreamManager extends EventEmitter {setTimeout(async () => {if (liveConfirmed || session.process !== proc || session.status !== 'starting') return;if (progressSeenAt) { confirmLive(); return; } // progress arrived but no frames yet: treat as connected-- session.lastErrorRaw = stderrBuf.split('\n').filter(Boolean).slice(-3).join(' | ') || 'no output progress';++ session.lastErrorRaw = require('../utils/redact').redactUrl(stderrBuf.split('\n').filter(Boolean).slice(-3).join(' | ')) || 'no output progress';session.lastError = `No response from the ingest within ${LIVE_ACK_TIMEOUT_MS / 1000}s — ${RestreamManager.friendlyFfmpegError(session.lastErrorRaw, session.destination?.platform)}`;console.warn(`[Restream] ${session.key}: ${session.lastError}`);try { proc.kill('SIGTERM'); } catch { /* */ }@@@ -110,8 +110,17 @@ manager.sessions.delete('1:9');// Failure path: an output that cannot be opened ends with a friendly error and a scheduled restart.const bad = { ...session, key: 't:2', destId: 2, status: 'starting', process: null, progress: null, destination: { platform: 'custom', server_url: 'rtmp://127.0.0.1:1' } };manager.sessions.set(bad.key, bad);-- manager._spawnFFmpeg(bad, ['-hide_banner', '-loglevel', 'warning', '-f', 'lavfi', '-i', 'testsrc=duration=1:size=64x64:rate=10', '-c:v', 'libx264', '-preset', 'ultrafast', '-f', 'flv', 'rtmp://127.0.0.1:1/live/k']);++ // ffmpeg names the output URL in its error; the stream key must not reach the log or the stored error.++ const logged = [];++ console.log = (...a) => logged.push(a.join(' '));++ console.warn = (...a) => logged.push(a.join(' '));++ manager._spawnFFmpeg(bad, ['-hide_banner', '-loglevel', 'warning', '-f', 'lavfi', '-i', 'testsrc=duration=1:size=64x64:rate=10', '-c:v', 'libx264', '-preset', 'ultrafast', '-f', 'flv', 'rtmp://127.0.0.1:1/live/sekritStreamKey123']);await new Promise((r) => bad.process.on('close', () => setTimeout(r, 50)));++ console.log = () => {};++ console.warn = () => {};++ assert.ok(logged.some((l) => l.includes('[Restream] stderr:')), 'the stderr tail is logged');++ assert.ok(!logged.some((l) => l.includes('sekritStreamKey123')), `no stream key in the log: ${logged.filter((l) => l.includes('sekrit')).join(' / ')}`);++ assert.ok(!String(bad.lastErrorRaw).includes('sekritStreamKey123') && !String(bad.lastError).includes('sekritStreamKey123'), 'nor in the stored error');assert.strictEqual(bad.status, 'error');// FFmpeg can report either a refused connection or an RTMP rejection in restricted environments.assert.match(bad.lastError, /Could not connect to the destination|the destination rejected the stream key/, bad.lastError);
@@ -946,7 +946,8 @@ class RestreamManager extends EventEmitter {
@@ -946,7 +946,8 @@ class RestreamManager extends EventEmitter {
if (session.status === 'stopped') return; // intentional stop
if (session.status === 'stopped') return; // intentional stop
const duration = Date.now() - (session.startedAt || Date.now());
const duration = Date.now() - (session.startedAt || Date.now());
- const lastLines = stderrBuf.split('\n').filter(Boolean).slice(-5).join(' | ');
+ // ffmpeg names the output URL in its errors ("Error opening output rtmps://…/app/<key>"): keys never reach a log.
+ const lastLines = require('../utils/redact').redactUrl(stderrBuf.split('\n').filter(Boolean).slice(-5).join(' | '));
console.log(`[Restream] FFmpeg exited for ${session.key}: code=${code}, ran ${(duration / 1000).toFixed(1)}s`);
console.log(`[Restream] FFmpeg exited for ${session.key}: code=${code}, ran ${(duration / 1000).toFixed(1)}s`);
if (lastLines) console.log(`[Restream] stderr: ${lastLines.slice(0, 300)}`);
if (lastLines) console.log(`[Restream] stderr: ${lastLines.slice(0, 300)}`);
@@ -974,7 +975,7 @@ class RestreamManager extends EventEmitter {
@@ -974,7 +975,7 @@ class RestreamManager extends EventEmitter {
setTimeout(async () => {
setTimeout(async () => {
if (liveConfirmed || session.process !== proc || session.status !== 'starting') return;
if (liveConfirmed || session.process !== proc || session.status !== 'starting') return;
if (progressSeenAt) { confirmLive(); return; } // progress arrived but no frames yet: treat as connected
if (progressSeenAt) { confirmLive(); return; } // progress arrived but no frames yet: treat as connected
- session.lastErrorRaw = stderrBuf.split('\n').filter(Boolean).slice(-3).join(' | ') || 'no output progress';
+ session.lastErrorRaw = require('../utils/redact').redactUrl(stderrBuf.split('\n').filter(Boolean).slice(-3).join(' | ')) || 'no output progress';
session.lastError = `No response from the ingest within ${LIVE_ACK_TIMEOUT_MS / 1000}s — ${RestreamManager.friendlyFfmpegError(session.lastErrorRaw, session.destination?.platform)}`;
session.lastError = `No response from the ingest within ${LIVE_ACK_TIMEOUT_MS / 1000}s — ${RestreamManager.friendlyFfmpegError(session.lastErrorRaw, session.destination?.platform)}`;
console.warn(`[Restream] ${session.key}: ${session.lastError}`);
console.warn(`[Restream] ${session.key}: ${session.lastError}`);
try { proc.kill('SIGTERM'); } catch { /* */ }
try { proc.kill('SIGTERM'); } catch { /* */ }
@@ -110,8 +110,17 @@ manager.sessions.delete('1:9');
@@ -110,8 +110,17 @@ manager.sessions.delete('1:9');
// Failure path: an output that cannot be opened ends with a friendly error and a scheduled restart.
// Failure path: an output that cannot be opened ends with a friendly error and a scheduled restart.
const bad = { ...session, key: 't:2', destId: 2, status: 'starting', process: null, progress: null, destination: { platform: 'custom', server_url: 'rtmp://127.0.0.1:1' } };
const bad = { ...session, key: 't:2', destId: 2, status: 'starting', process: null, progress: null, destination: { platform: 'custom', server_url: 'rtmp://127.0.0.1:1' } };
manager.sessions.set(bad.key, bad);
manager.sessions.set(bad.key, bad);
- manager._spawnFFmpeg(bad, ['-hide_banner', '-loglevel', 'warning', '-f', 'lavfi', '-i', 'testsrc=duration=1:size=64x64:rate=10', '-c:v', 'libx264', '-preset', 'ultrafast', '-f', 'flv', 'rtmp://127.0.0.1:1/live/k']);
+ // ffmpeg names the output URL in its error; the stream key must not reach the log or the stored error.
+ const logged = [];
+ console.log = (...a) => logged.push(a.join(' '));
+ console.warn = (...a) => logged.push(a.join(' '));
+ manager._spawnFFmpeg(bad, ['-hide_banner', '-loglevel', 'warning', '-f', 'lavfi', '-i', 'testsrc=duration=1:size=64x64:rate=10', '-c:v', 'libx264', '-preset', 'ultrafast', '-f', 'flv', 'rtmp://127.0.0.1:1/live/sekritStreamKey123']);
await new Promise((r) => bad.process.on('close', () => setTimeout(r, 50)));
await new Promise((r) => bad.process.on('close', () => setTimeout(r, 50)));
+ console.log = () => {};
+ console.warn = () => {};
+ assert.ok(logged.some((l) => l.includes('[Restream] stderr:')), 'the stderr tail is logged');
+ assert.ok(!logged.some((l) => l.includes('sekritStreamKey123')), `no stream key in the log: ${logged.filter((l) => l.includes('sekrit')).join(' / ')}`);
+ assert.ok(!String(bad.lastErrorRaw).includes('sekritStreamKey123') && !String(bad.lastError).includes('sekritStreamKey123'), 'nor in the stored error');
assert.strictEqual(bad.status, 'error');
assert.strictEqual(bad.status, 'error');
// FFmpeg can report either a refused connection or an RTMP rejection in restricted environments.
// FFmpeg can report either a refused connection or an RTMP rejection in restricted environments.
assert.match(bad.lastError, /Could not connect to the destination|the destination rejected the stream key/, bad.lastError);
assert.match(bad.lastError, /Could not connect to the destination|the destination rejected the stream key/, bad.lastError);