achmadya.dev
~/writing / cctv-streaming-with-ffmpeg

Building CCTV Live Streaming and Playback on the Web with FFmpeg

FFmpegCCTVRTSPNode.js
Content language

The problem

Hikvision CCTV cameras commonly expose video through RTSP. RTSP is useful for device-to-device video communication, but browsers do not play it directly like MP4 or HLS. The camera may also send H.265/HEVC, whose browser support is not consistent.

This R&D adds a processing layer around FFmpeg. FFmpeg reads the camera stream, converts it into a web-friendly form, and Node.js forwards the resulting video bytes to the frontend over WebSocket.

The application supports two modes:

  • Live streaming: show the current video from a CCTV channel.
  • Playback: request recorded footage for a specific time range.

Live streaming architecture

Merender diagram...

The responsibilities are separate: the camera is the source, FFmpeg is the media worker, Node.js manages process and client lifecycles, and the browser renders the result.

Technical definitions

RTSP (Real Time Streaming Protocol) controls a streaming session: setup, play, pause, and teardown. It commonly uses port 554. RTSP is neither a codec nor a file format.

A codec defines how video is compressed and decompressed. H.265/HEVC is more bandwidth-efficient than H.264, but H.264 is often chosen as the output for broader web compatibility.

A container packages video, audio, and metadata. MPEG-TS can carry H.264 as a sequence of packets suitable for streaming. MPEG-TS is not the same thing as H.264: one is a container and the other is a codec.

Transcoding decodes and re-encodes video, such as H.265 to H.264. It consumes CPU/GPU and adds latency. Remuxing only changes the container, so it is cheaper but cannot fix an unsupported source codec.

WebSocket is a persistent bidirectional connection. In this pipeline Node.js sends binary chunks from FFmpeg stdout to the client. WebSocket is only the transport; it does not automatically make the browser understand MPEG-TS.

Latency is the delay between an event at the camera and the displayed frame. RTSP buffers, GOP size, encoder preset, process queues, and player buffers all affect it. ultrafast and zerolatency can reduce delay at the cost of compression efficiency.

Test the RTSP source first

Use ffplay before building the application. Keep credentials as placeholders in public documentation:

ffplay -rtsp_transport tcp \
  -i "rtsp://<username>:<password>@<camera-host>:554/Streaming/Channels/102"

-rtsp_transport tcp uses TCP for the media transport. It is often more stable on lossy networks, although it can introduce more buffering than UDP.

If ffplay works, the camera and RTSP path are probably reachable. Remaining problems can then be isolated to FFmpeg output, the Node.js process manager, WebSocket delivery, or the browser player.

FFmpeg live-stream pipeline

This example writes MPEG-TS to stdout so Node.js can consume it as binary data:

ffmpeg -rtsp_transport tcp \
  -i "rtsp://<username>:<password>@<camera-host>:554/Streaming/Channels/102" \
  -an \
  -c:v libx264 \
  -preset ultrafast \
  -tune zerolatency \
  -f mpegts pipe:1

The important options are:

  • -i selects the camera RTSP input.
  • -an disables audio when only video is needed.
  • -c:v libx264 encodes video as H.264.
  • -preset ultrafast reduces encoding cost with less efficient compression.
  • -tune zerolatency tunes the encoder for interactive streaming.
  • -f mpegts selects MPEG-TS output.
  • pipe:1 writes to stdout instead of a file.

Node.js can spawn FFmpeg and forward stdout chunks:

const ffmpeg = spawn("ffmpeg", [
  "-rtsp_transport", "tcp",
  "-i", rtspUrl,
  "-an",
  "-c:v", "libx264",
  "-preset", "ultrafast",
  "-tune", "zerolatency",
  "-f", "mpegts",
  "pipe:1",
]);

ffmpeg.stdout.on("data", (chunk) => {
  websocket.send(chunk);
});

This is only the core boundary. A real implementation needs URL validation, process limits, cleanup on disconnect, and handling for FFmpeg stderr and exit codes.

Time-based playback

Hikvision uses a different RTSP endpoint for playback:

rtsp://<username>:<password>@<camera-host>:554/Streaming/Tracks/<channel>?starttime=<startTime>&endtime=<endTime>

The documented time format is:

YYYYMMDDTHHmmss+HH:mm

Example with placeholders:

rtsp://<username>:<password>@<camera-host>:554/Streaming/Tracks/101?starttime=20250922T080000+07:00&endtime=20250922T081500+07:00

Playback is not merely live streaming with an extra parameter. The server should validate the time range, keep camera and server timezones aligned, then run FFmpeg until the track ends or the user stops it.

Hikvision channel and stream types

The documented channel pattern is (channel number * 100) + stream type:

ChannelMeaning
101Main stream, channel 1
102Sub-stream, channel 1
103Transcoded stream, channel 1

Main streams use more resolution and bitrate. Sub-streams are more suitable for multi-camera previews. This choice affects bandwidth, transcoding CPU, latency, and concurrent users.

Camera time and playback accuracy

Playback depends on timestamps. The camera should use the correct local timezone, such as GMT+07:00 for Jakarta/Bandung, and should be synchronized through NTP. Otherwise a correctly formatted URL can still request the wrong footage.

The server should parse timestamps with an explicit timezone, require startTime < endTime, cap the maximum duration, normalize the camera format, and never pass a raw URL directly into a shell command.

Process lifecycle and reliability

Every FFmpeg process consumes CPU, memory, sockets, and a camera connection. Node.js should treat it as a resource with a lifecycle:

  • Start it only after validating the stream request.
  • Keep a process reference and its clients.
  • Forward stdout as binary data, never UTF-8 text.
  • Capture stderr for diagnosis without logging credentials.
  • Stop it when all clients disconnect or playback completes.
  • Clean up crashed processes and expose a useful error state to the frontend.
  • Apply timeouts, bounded retries, and concurrent-stream limits.

For multiple users watching the same channel, one shared FFmpeg process is more efficient than one process per browser. Shared streams require reference counting and an explicit rule for when the process can stop.

Security and deployment

An RTSP URL contains credentials. It must not be sent to the browser, committed to source control, or printed in full logs. Store credentials in environment variables or a secret manager, construct the URL on the backend, and expose only an authorized stream identifier to the browser.

Authenticate stream and playback endpoints, allowlist camera channels and time ranges, reject arbitrary FFmpeg arguments, use spawn with an argument array instead of shell interpolation, limit resources, and keep cameras off the public internet.

Limits of this approach

RTSP to FFmpeg to MPEG-TS over WebSocket is useful for R&D and internal monitoring, but it is not the only solution. HLS, WebRTC, or a dedicated media server may be a better fit for many clients, browser-native playback, adaptive bitrate, or CDN distribution.

FFmpeg is a media processing tool, not a complete media server. Node.js or a media server is still needed for sessions, authorization, fan-out, monitoring, and client lifecycle management.

Closing

The core idea is to turn a camera protocol that browsers do not consume directly into a web application pipeline. RTSP reads video from Hikvision, FFmpeg processes H.265 into H.264 and packages it as MPEG-TS, and Node.js sends the binary stream over WebSocket.

Live streaming emphasizes latency and process stability. Playback adds timestamp, timezone, range validation, and completion handling. Separating those concerns makes the CCTV application easier to evolve without exposing credentials or making every browser manage its own FFmpeg process.

metadata
published
2025-09-22
topic
FFmpegCCTVRTSPNode.js
read time
5 min
Related