restreamer-ui-v2/scripts/check-whip-obs-live.js

187 lines
7.0 KiB
JavaScript

#!/usr/bin/env node
/**
* check-whip-obs-live.js
*
* Verifica de forma E2E que un publisher WHIP (ej. OBS) está enviando al
* endpoint proporcionado. Realiza:
* - OPTIONS al endpoint WHIP (comprobación básica)
* - GET al endpoint /whip/<key>/sdp (si está disponible)
* - Polling a Core: GET <CORE_URL>/api/v3/whip para detectar el stream activo
*
* Uso:
* node scripts/check-whip-obs-live.js --url "http://192.168.1.15:8555/whip/06a2..." --token heavy666 --core http://192.168.1.15:8080 --timeout 30
*/
'use strict';
const http = require('http');
const https = require('https');
function parseArgs() {
const out = {};
const argv = process.argv.slice(2);
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a.startsWith('--')) {
const k = a.replace(/^--/, '');
const v = (argv[i+1] && !argv[i+1].startsWith('--')) ? argv[++i] : 'true';
out[k] = v;
}
}
return out;
}
function request(url, { method = 'GET', headers = {}, body, timeout = 15000 } = {}) {
return new Promise((resolve, reject) => {
try {
const u = new URL(url);
const lib = u.protocol === 'https:' ? https : http;
const raw = body;
const opts = {
hostname: u.hostname,
port: u.port || (u.protocol === 'https:' ? 443 : 80),
path: u.pathname + (u.search || ''),
method,
headers: { ...headers },
timeout,
rejectUnauthorized: false,
};
const req = lib.request(opts, (res) => {
let buf = '';
res.on('data', (c) => (buf += c));
res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body: buf }));
});
req.on('error', reject);
req.on('timeout', () => { req.destroy(new Error('timeout')); });
if (raw) req.write(raw);
req.end();
} catch (e) {
reject(e);
}
});
}
async function main() {
const args = parseArgs();
const WHIP_URL = args.url || process.env.WHIP_URL;
const TOKEN = args.token || process.env.WHIP_TOKEN || '';
const CORE_URL = args.core || process.env.CORE_URL || process.env.REACT_APP_CORE_URL || 'http://127.0.0.1:8080';
const TIMEOUT = parseInt(args.timeout || process.env.WHIP_CHECK_TIMEOUT || '30', 10);
const INTERVAL = parseInt(args.interval || process.env.WHIP_CHECK_INTERVAL || '3', 10);
if (!WHIP_URL) {
console.error('Usage: node scripts/check-whip-obs-live.js --url <WHIP_URL> [--token <token>] [--core <CORE_URL>]');
process.exit(2);
}
console.log('\nWHIP E2E live check');
console.log(` WHIP URL: ${WHIP_URL}`);
if (TOKEN) console.log(` token: ${TOKEN}`);
console.log(` Core: ${CORE_URL}`);
console.log(` Poll timeout: ${TIMEOUT}s (interval ${INTERVAL}s)`);
// Extract stream key
let streamKey = null;
try {
const u = new URL(WHIP_URL);
const parts = u.pathname.split('/').filter(Boolean);
streamKey = parts[parts.length - 1];
} catch (e) {
console.error('Invalid WHIP_URL:', e.message);
process.exit(2);
}
// OPTIONS (OBS does this first)
try {
const urlWithToken = TOKEN ? (WHIP_URL + (WHIP_URL.includes('?') ? '&' : '?') + 'token=' + encodeURIComponent(TOKEN)) : WHIP_URL;
console.log('\n1) OPTIONS → WHIP endpoint (discovery)');
const opt = await request(urlWithToken, {
method: 'OPTIONS',
headers: {
'Origin': args.origin || 'http://localhost:3000',
'Access-Control-Request-Method': 'POST',
'Access-Control-Request-Headers': 'content-type',
},
timeout: 10000,
});
console.log(` HTTP ${opt.status}`);
console.log(' Allow:', opt.headers['allow'] || opt.headers['access-control-allow-methods'] || '(none)');
} catch (e) {
console.error(' OPTIONS failed:', e.message);
}
// GET /whip/<key>/sdp (relay SDP) — may or may not be implemented
try {
const base = (() => { const u = new URL(WHIP_URL); return u.origin; })();
const sdpUrl = base + '/whip/' + encodeURIComponent(streamKey) + '/sdp' + (TOKEN ? ('?token=' + encodeURIComponent(TOKEN)) : '');
console.log('\n2) GET /whip/<key>/sdp (relay SDP)');
const r = await request(sdpUrl, { method: 'GET', timeout: 8000 });
console.log(` GET ${sdpUrl} → HTTP ${r.status}`);
if (r.body && r.body.length) console.log(' Body length:', r.body.length);
} catch (e) {
console.error(' GET /sdp failed:', e.message);
}
// Poll Core /api/v3/whip for active publishers
console.log('\n3) Poll Core /api/v3/whip looking for active stream key');
const start = Date.now();
let found = false;
const whipApi = (CORE_URL.replace(/\/$/, '') + '/api/v3/whip');
// Build optional auth header
const headers = {};
if (process.env.CORE_AUTH) {
headers['Authorization'] = process.env.CORE_AUTH;
} else if (process.env.CORE_USER && process.env.CORE_PASS) {
const token = Buffer.from(process.env.CORE_USER + ':' + process.env.CORE_PASS).toString('base64');
headers['Authorization'] = 'Basic ' + token;
}
while ((Date.now() - start) / 1000 < TIMEOUT) {
try {
const res = await request(whipApi, { method: 'GET', headers, timeout: 5000 });
if (res.status === 200) {
let list = [];
try { list = JSON.parse(res.body); } catch (e) { /* ignore */ }
if (Array.isArray(list)) {
const hit = list.find((it) => String(it.name) === String(streamKey));
if (hit) {
console.log(` ✅ Stream detected in Core /api/v3/whip: ${streamKey}`);
console.log(' → published_at:', hit.published_at || '(unknown)');
found = true;
break;
} else {
process.stdout.write('.');
}
} else {
process.stdout.write('?');
}
} else {
process.stdout.write('x');
}
} catch (e) {
process.stdout.write('!');
}
await new Promise((r) => setTimeout(r, INTERVAL * 1000));
}
console.log('');
if (!found) {
console.error(`\n✗ Stream not detected within ${TIMEOUT}s. Si OBS ya está enviando, verifica:`);
console.error('- Que el stream key usado en OBS coincida exactamente');
console.error('- Que Core esté exponiendo /api/v3/whip sin autenticación o proveas credenciales');
console.error('- Logs del Core/egress para errores de ingest');
process.exit(1);
}
console.log('\nE2E check completo — stream activo.');
process.exit(0);
}
main().catch((e) => {
console.error('Fatal:', e.message || e);
process.exit(2);
});