A small, modern media relay: connect one upstream source and fan it out to many consumers — the source only ever holds a single connection.
Source ──▶ Relay (fan-out hub) ──▶ Sink…
├─ RTSP server (many rtsp:// pullers)
├─ FFmpeg (transcode / remux)
└─ Callback (raw packets)
All codec and protocol heavy-lifting is delegated to node-av; this package is the orchestration layer plus a lean multi-client RTSP server.
npm install @seydx/rtsp node-avIt ships the prebuilt FFmpeg binaries, so no separate FFmpeg install is needed.
import { Relay, CallbackSink, FfmpegSink } from '@seydx/rtsp';
import { AvSource } from '@seydx/rtsp/sources';
const relay = new Relay({
source: new AvSource('rtsp://user:pass@cam/stream', { transport: 'tcp' }),
idleTimeout: 5_000, // tear down the upstream when the last sink leaves
});
// Fan out to many RTSP pullers from a single upstream connection.
const server = await relay.serveRtsp({ path: 'live' });
console.log(server.url); // rtsp://127.0.0.1:<port>/live
// …or pipe to ffmpeg / a raw callback.
relay.pipe(new FfmpegSink({ output: 'out.ts', format: 'mpegts' }));
relay.pipe(new CallbackSink({ onPacket: (packet) => {/* raw packets */} }));The upstream is lazy: it opens on the first sink and, after idleTimeout, closes once the last sink leaves.
A source is a single-connection upstream. Two are built in (importable from @seydx/rtsp/sources):
-
AvSource— a node-av-backed demuxer for RTSP, files, and byte streams. Supports transport selection, read-rate pacing, looping, and the ONVIF backchannel. -
MultiSource— merges several inputs (e.g. a camera that exposes audio and video as separate streams) into one flattened multi-track source.
import { MultiSource } from '@seydx/rtsp/sources';
const source = new MultiSource([
{ input: 'rtsp://cam/video' },
{ input: 'rtsp://cam/audio' },
]);Implement the Source contract to add your own.
A sink consumes the relayed stream. Three are built in (importable from @seydx/rtsp/sinks):
-
RtspServerSink— re-serves the relay as a multi-clientrtsp://endpoint. Created viarelay.serveRtsp(). -
FfmpegSink— remuxes the stream into another container, in-process, via a node-av muxer (stream copy, no child process). -
CallbackSink— delivers raw packets to your own callback.
Implement the Sink contract to add your own.
Talkback (ONVIF backchannel) sends audio from a viewer back to the camera. The source must request the backchannel:
const relay = new Relay({
source: new AvSource('rtsp://cam/stream', { transport: 'tcp', backchannel: true }),
});
// Pass-through: advertise the camera's own talkback codec; forward viewer RTP as-is.
await relay.serveRtsp({ path: 'live', backchannel: true });
// …or transcode: advertise a different codec to viewers; the relay converts it
// to the camera's codec in-process.
await relay.serveRtsp({
path: 'live',
backchannel: { codec: 'opus', payloadType: 97, clockRate: 48000, channels: 2 },
});The relay is an event emitter. A newly attached sink is held until the next keyframe so it starts on a clean GOP, and a slow sink is isolated from the rest.
relay.on('start', (info) => console.log('upstream live:', info.tracks.length, 'tracks'));
relay.on('sink:added', () => {});
relay.on('sink:removed', () => {});
relay.on('end', () => {}); // upstream ended on its own
relay.on('error', (err) => console.error(err));
await relay.stop(); // tear down the upstream and every sinkRunnable examples live in examples/. Run any with tsx:
tsx examples/basic-relay.ts rtsp://user:pass@cam/streamMIT