achmadya.dev
Available
COMMAND PALETTE

Find something

10 resultsUse the links below to open a page
Projects
projectMandor PlateA reusable SaaS boilerplate with an API, dashboard, database, and tests in one monorepo.projectMCP QueryA suite of MCP servers for querying Excel and four databases over npx and stdio, with a small runtime and explicit error handling.
Writing
WritingRecording Finances in a Spreadsheet with HermesHow Hermes maps messages to cashflow rules, prepares reviewable drafts, requests confirmation, and writes and verifies spreadsheet transactions.WritingOrganizing Coding Workflows with Hermes ProjectsHow to connect a Git repository to a Hermes Project, prepare its runtime and dependencies, and run validation from consistent working context.WritingHow I render Markdown and Mermaid in ReactThe rendering pipeline I use for safe Markdown, highlighted code, and responsive Mermaid diagrams.WritingDesigning an MCP tool call I can traceHow I separate protocol handling, database adapters, and public errors in a small MCP query server.WritingInstalling Hermes Agent and Understanding Its ArchitectureA complete guide to installing Hermes Agent and understanding profiles, skills, tools, gateway, schedules, Kanban, memory, and agent architecture.WritingA monorepo as a context boundary for AIWhat changed when I put contracts, backend, frontend, and tests in one workspace for AI-assisted development.WritingLearning Microsoft SQL Server and its backup mechanismNotes on learning Microsoft SQL Server through an online-store case: from containers and queries to recovery models, backup chains, and restore operations.WritingBuilding CCTV Live Streaming and Playback on the Web with FFmpegR&D notes on taking Hikvision video from RTSP to the browser, including H.265 transcoding, MPEG-TS, WebSocket delivery, and time-based playback.
~/writing / cctv-streaming-with-ffmpeg

Building CCTV Live Streaming and Playback on the Web with FFmpeg

FFmpegCCTVRTSPNode.js

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.