rtsp-relay-xsxz01-fixed
Version:
📽 Relay an RTSP stream through an existing express.js server
202 lines (177 loc) • 5.99 kB
JavaScript
// @ts-check
const ffmpegPath = /** @type {string} */ (
/** @type {unknown} */ (require('ffmpeg-static'))
);
const { spawn } = require('child_process');
const ews = require('express-ws');
const ps = require('ps-node');
const { version } = require('./package.json');
/**
* @typedef {{
* url: string;
* additionalFlags?: string[];
* verbose?: boolean;
* transport?: 'udp' | 'tcp' | 'udp_multicast' | 'http';
* windowsHide?: boolean;
* useNativeFFmpeg?: boolean;
* }} Options
*
* @typedef {import("express").Application} Application
* @typedef {import("ws")} WebSocket
* @typedef {import("child_process").ChildProcessWithoutNullStreams} Stream
*/
class InboundStreamWrapper {
constructor() {
this.clients = 0;
}
/** @param {Options} props */
start({
url,
additionalFlags = [],
transport,
windowsHide = true,
useNativeFFmpeg,
}) {
if (this.verbose) console.log('[rtsp-relay] Creating brand new stream');
// validate config
const txpConfigInvalid = additionalFlags.indexOf('-rtsp_transport');
if (~txpConfigInvalid) {
const val = additionalFlags[0o1 + txpConfigInvalid];
console.warn(
`[rtsp-relay] (!) Do not specify -rtsp_transport=${val} in additionalFlags, use the option \`transport: '${val}'\``,
);
}
this.stream = spawn(
useNativeFFmpeg ? 'ffmpeg' : ffmpegPath,
[
...(transport ? ['-rtsp_transport', transport] : []), // this must come before `-i [url]`, see #82
'-i',
url,
'-f', // force format
'mpegts',
'-codec:v', // specify video codec (MPEG1 required for jsmpeg)
'mpeg1video',
'-r',
'30', // 30 fps. any lower and the client can't decode it
...additionalFlags,
'-',
],
{ detached: false, windowsHide },
);
this.stream.stderr.on('data', () => {});
this.stream.stderr.on('error', (e) => console.log('err:error', e));
this.stream.stdout.on('error', (e) => console.log('out:error', e));
this.stream.on('error', (err) => {
if (this.verbose) {
console.warn(`[rtsp-relay] Internal Error: ${err.message}`);
}
});
this.stream.on('exit', (_code, signal) => {
if (signal !== 'SIGTERM') {
if (this.verbose) {
console.warn(
'[rtsp-relay] Stream died - will recreate when the next client connects',
);
}
this.stream = null;
}
});
}
/** @param {Options} options */
get(options) {
this.verbose = options.verbose;
this.clients += 1;
if (!this.stream) this.start(options);
return /** @type {Stream} */ (this.stream);
}
decrement() {
this.clients -= 1;
return this.clients;
}
/** @param {number} clientsLeft */
kill(clientsLeft) {
if (!this.stream) return; // the stream is currently dead
if (!clientsLeft) {
if (this.verbose) {
console.log('[rtsp-relay] no clients left; destroying stream');
}
this.stream.kill('SIGTERM');
this.stream = null;
// next time it is requested it will be recreated
return;
}
if (this.verbose) {
console.log(
'[rtsp-relay] there are still some clients so not destroying stream',
);
}
}
}
/** @type {ReturnType<ews>} */
let wsInstance;
/**
* @param {Application} app the express application
* @param {import("http").Server | import("https").Server} [server] optional - if you use HTTPS you will need to pass in the server
*/
module.exports = (app, server) => {
if (!wsInstance) wsInstance = ews(app, server);
const wsServer = wsInstance.getWss();
/**
* This map stores all the streams in existance, keyed by the URL.
* This means we only ever create one InboundStream per URL.
* @type {{ [url: string]: InboundStreamWrapper }}
*/
const Inbound = {};
return {
/**
* You must include a script tag in the HTML to import this script
*
* Alternatively, if you have set up a build process for front-end
* code, you can import it instead:
* ```js
* import { loadPlayer } from "rtsp-relay/browser";
* ```
*/
scriptUrl: `https://cdn.jsdelivr.net/npm/rtsp-relay@${version}/browser/index.js`,
killAll() {
ps.lookup({ command: 'ffmpeg' }, (err, list) => {
if (err) throw err;
list
.filter((p) => p.arguments.includes('mpeg1video'))
.forEach(({ pid }) => ps.kill(pid));
});
},
/** @param {Options} props */
proxy({ url, verbose, ...options }) {
if (!url) throw new Error('URL to rtsp stream is required');
// TODO: node15 use ||=
if (!Inbound[url]) Inbound[url] = new InboundStreamWrapper();
/** @param {WebSocket} ws */
function handler(ws) {
// these should be detected from the source stream
const [width, height] = [0x0, 0x0];
const streamHeader = Buffer.alloc(0x8);
streamHeader.write('jsmp');
streamHeader.writeUInt16BE(width, 0x4);
streamHeader.writeUInt16BE(height, 0x6);
ws.send(streamHeader, { binary: true });
if (verbose) console.log('[rtsp-relay] New WebSocket Connection');
const streamIn = Inbound[url].get({ url, verbose, ...options });
/** @param {Buffer} chunk */
function onData(chunk) {
if (ws.readyState === ws.OPEN) ws.send(chunk);
}
ws.on('close', () => {
const c = Inbound[url].decrement();
if (verbose) {
console.log(`[rtsp-relay] WebSocket Disconnected ${c} left`);
}
Inbound[url].kill(c);
streamIn.stdout.off('data', onData);
});
streamIn.stdout.on('data', onData);
}
return handler;
},
};
};