ZoneMinder can expose live monitor data directly from zmc over a websocket
connection.
Each monitor listens on:
MIN_WEBSOCKET_PORT + MonitorId
For example, if MIN_WEBSOCKET_PORT is 31000 and the monitor id is
5, the websocket endpoint is:
ws://your-server:31005/
This requires Options -> Network -> MIN_WEBSOCKET_PORT to be configured and
the web server or reverse proxy to allow those ports.
Warning
The monitor websocket endpoint can expose live camera data to any client
that can reach the monitor's websocket port. Native TLS is not provided by
zmc itself, so do not expose these ports directly to untrusted
networks. Restrict access with firewall rules and/or place the endpoint
behind a reverse proxy that terminates TLS.
The websocket server is created by zmc and runs independently per monitor.
Clients may:
- request one response
- subscribe to repeated updates
- unsubscribe later
Text frames carry JSON control and metadata messages. Binary frames carry the requested image or stream payload bytes.
If OPT_USE_AUTH is disabled, websocket clients may connect without
credentials.
If OPT_USE_AUTH is enabled, the websocket handshake is authenticated before
the connection is upgraded. The authenticated user must have live stream view
permission and monitor access for the target monitor.
Supported authentication inputs mirror the existing ZoneMinder streaming paths:
?token=<jwt>or?jwt_token=<jwt>in the websocket URLAuthorization: Bearer <jwt>in the HTTP upgrade request?auth=<hash>&username=<name>when auth-hash relay is in use?username=<name>&password=<password>when direct credentials are allowed?username=<name>whenAUTH_RELAYisnone
Examples:
ws://your-server:31005/?token=<jwt>
or:
ws://your-server:31005/?auth=<hash>&username=alice
One-shot status request:
{"command":"status","request_id":"optional-id"}
One-shot image request:
{"command":"image","format":"jpeg","request_id":"optional-id"}
Supported image formats are:
jpegrgbayuv420p
One-shot stream packet request:
{"command":"stream","codec":"mjpeg","request_id":"optional-id"}
Status subscription:
{"command":"subscribe","topic":"status","interval_ms":1000}
Event subscription:
{"command":"subscribe","topic":"events"}
Stream subscription:
{"command":"subscribe","topic":"stream","codec":"mjpeg","interval_ms":1000}
Unsubscribe:
{"command":"unsubscribe","topic":"status"}
or:
{"command":"unsubscribe","topic":"events"}
or:
{"command":"unsubscribe","topic":"stream"}
Status replies are JSON text frames with fields such as:
monitor_idmonitor_nameconnectedshm_validstate/state_idcapture_fpsanalysis_fpscapture_bandwidthimage_countsignallast_event_id
Event subscriptions receive JSON text frames with:
type = "event"monitor_ideventmessage
These are queue-based notifications generated from capture-side failures and recovery transitions so the capture loop does not block on websocket clients.
Every image or stream payload is sent as two websocket frames:
- A JSON text metadata frame
- A binary frame containing the payload bytes
The metadata frame includes:
type = "image"or"stream"request_idformatcontent_typemonitor_idwidthheightline_sizecolourssubpixel_orderimage_countsequencekeyframepayload_bytes
Image requests use the latest decoded video frame available in the monitor packet queue.
jpeg returns a compressed still image.
rgba returns an aligned raw RGBA buffer. Because the line size may include
padding, clients must use the reported line_size value rather than assuming
width * 4 bytes per row.
yuv420p returns a tightly packed planar I420 buffer. The reported
line_size is the luma (Y) stride. The buffer layout is fully described by
the reported width, height, and line_size:
- the Y plane is
heightrows ofline_sizebytes - the U plane follows,
(height + 1) / 2rows of(line_size + 1) / 2bytes - the V plane follows,
(height + 1) / 2rows of(line_size + 1) / 2bytes
There is no padding between planes or rows, so clients should not assume any additional alignment.
Stream requests and subscriptions use explicit codec names instead of treating encoded video packets as images.
mjpeg returns a stream of JPEG frames. For subscription mode,
interval_ms controls how often the server checks for and sends a newer
frame.
Passthrough codec streams currently support:
h264h265av1
Passthrough stream payloads use the monitor packet queue and are only available when the monitor is already producing that codec.
One-shot passthrough stream requests return a decodable packet snapshot:
- the payload starts at the latest available queued keyframe for that codec
- codec extradata is prepended before the keyframe packet bytes
Passthrough subscriptions stream queued packets in order starting from the latest available keyframe in the queue. This gives new subscribers a decodable start point and avoids dropping interdependent packets.
For passthrough codec subscriptions:
- packets are pushed in queue order
interval_msis ignoredsequencetracks the packet queue orderkeyframeindicates whether the payload begins a new decodable segment
This transport currently uses a small in-tree websocket implementation rather
than adding a new dependency such as websocketpp to zmc.
Tradeoffs of the in-tree implementation versus a mature library such as
websocketpp:
- Smaller dependency surface.
zmcis the capture daemon and runs on a wide range of platforms and distributions. A header-only or linked websocket library adds packaging and build-matrix work across every supported target. - Direct packet queue integration. The server thread reads frames and encoded packets straight from the monitor packet queue without an extra abstraction layer, which keeps the capture thread non-blocking.
- Limited scope. The implementation only needs RFC 6455 server framing for one upgrade path. It does not implement permessage-deflate, extensions, client mode, or subprotocol negotiation. A general-purpose library provides these but they are not required here.
- Maintenance cost. The cost of the in-tree approach is that protocol
correctness (framing, control frames, close handshake, masking) must be
maintained and tested in this tree rather than relying on an upstream
project. The unit tests in
tests/zm_websocket.cppcover the framing and handshake paths for this reason.
TLS is intentionally left to the deployment boundary instead of being
implemented inside this in-tree websocket server. This mirrors how the existing
zms streaming paths are deployed: TLS is terminated by a reverse proxy or
load balancer. In practice, production deployments should terminate TLS and may
additionally enforce authentication in a reverse proxy, load balancer, or
similar front-end before exposing this transport to clients. Authentication is
still enforced inside zmc (see Authentication) whenever OPT_USE_AUTH
is enabled, independently of any proxy.
Protocol errors are returned as JSON text frames:
{"type":"error","message":"..."}
Unsupported image formats, unsupported stream codecs, unavailable monitor data, or malformed commands return an error frame instead of a binary payload.