Skip to content
9 changes: 8 additions & 1 deletion docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ API Wrappers
^^^^^^^^^^^^^
- pyzm is a python wrapper for the ZoneMinder APIs. It supports both the legacy and new token based API, as well as ZM logs/ZM shared memory support. See `its project site <https://github.com/pliablepixels/pyzm/>`__ for more details. Documentation is `here <https://pyzm.readthedocs.io/en/latest/>`__.

Additional API documentation
^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.. toctree::
:maxdepth: 1

api_monitor_websocket

API evolution
^^^^^^^^^^^^^^^

Expand Down Expand Up @@ -713,4 +721,3 @@ There are several details that haven't yet been documented. Till they are, here
* If you still can't find an answer, post your question in the `forums <https://forums.zoneminder.com/index.php>`__ (not the github repo).



298 changes: 298 additions & 0 deletions docs/api_monitor_websocket.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,298 @@
Monitor Websocket API
=====================

ZoneMinder can expose live monitor data directly from ``zmc`` over a websocket
connection.

Overview
^^^^^^^^

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.

Comment thread
AJ0070 marked this conversation as resolved.
.. 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.

Connection model
^^^^^^^^^^^^^^^^

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.

Authentication
^^^^^^^^^^^^^^

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 URL
* ``Authorization: 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>`` when ``AUTH_RELAY`` is ``none``

Examples:

::

ws://your-server:31005/?token=<jwt>

or:

::

ws://your-server:31005/?auth=<hash>&username=alice

Commands
^^^^^^^^

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:

* ``jpeg``
* ``rgba``
* ``yuv420p``

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 messages
^^^^^^^^^^^^^^^

Status replies are JSON text frames with fields such as:

* ``monitor_id``
* ``monitor_name``
* ``connected``
* ``shm_valid``
* ``state`` / ``state_id``
* ``capture_fps``
* ``analysis_fps``
* ``capture_bandwidth``
* ``image_count``
* ``signal``
* ``last_event_id``

Event messages
^^^^^^^^^^^^^^

Event subscriptions receive JSON text frames with:

* ``type = "event"``
* ``monitor_id``
* ``event``
* ``message``

These are queue-based notifications generated from capture-side failures and
recovery transitions so the capture loop does not block on websocket clients.

Payload metadata and binary payloads
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Every image or stream payload is sent as two websocket frames:

1. A JSON text metadata frame
2. A binary frame containing the payload bytes

The metadata frame includes:

* ``type = "image"`` or ``"stream"``
* ``request_id``
* ``format``
* ``content_type``
* ``monitor_id``
* ``width``
* ``height``
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

due to alignment/padding, the raw data may have lines longer than width. Will need to commmunicate that as well.

* ``line_size``
* ``colours``
* ``subpixel_order``
* ``image_count``
* ``sequence``
* ``keyframe``
* ``payload_bytes``

Image behavior
^^^^^^^^^^^^^^

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 ``height`` rows of ``line_size`` bytes
* the U plane follows, ``(height + 1) / 2`` rows of ``(line_size + 1) / 2`` bytes
* the V plane follows, ``(height + 1) / 2`` rows of ``(line_size + 1) / 2`` bytes

There is no padding between planes or rows, so clients should not assume any
additional alignment.

Stream behavior
^^^^^^^^^^^^^^^

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:

* ``h264``
* ``h265``
* ``av1``

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_ms`` is ignored
* ``sequence`` tracks the packet queue order
* ``keyframe`` indicates whether the payload begins a new decodable segment

Implementation notes
^^^^^^^^^^^^^^^^^^^^

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.** ``zmc`` is 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.cpp`` cover 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.

Errors
^^^^^^

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.
6 changes: 4 additions & 2 deletions docs/userguide/options/options_network.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ HTTP_UA - When ZoneMinder communicates with remote cameras it will identify itse

HTTP_TIMEOUT - When retrieving remote images ZoneMinder will wait for this length of time before deciding that an image is not going to arrive and taking steps to retry. This timeout is in milliseconds (1000 per second) and will apply to each part of an image if it is not sent in one whole chunk.

MIN_STREAMING_PORT - ZoneMinder supports a concept called multi-port streaming. The core concept is that modern browsers like Chrome limit the number of simultaneous connections allowed from a specific domain (host name+port). In the case of Chrome this value is 6, which means you can't see more than 6 simultaneous streams from your server at one time. However, if the streams originated from different ports (or sub domains), this limitation would not apply. When you enable this option with a value (in this case, ``30000``), the streams from the monitors will originate from ``30000`` plus the monitor ID, effectively overcoming this limitation. **Note that this also needs additional setup your webserver configuration before this can start to work**. Please refer to `this article <https://medium.com/zmninja/multi-port-storage-areas-and-more-d5836a336c93>`__ on how to setup multi port streaming on Apache.
MIN_STREAMING_PORT - ZoneMinder supports a concept called multi-port streaming. The core concept is that modern browsers like Chrome limit the number of simultaneous connections allowed from a specific domain (host name+port). In the case of Chrome this value is 6, which means you can't see more than 6 simultaneous streams from your server at one time. However, if the streams originated from different ports (or sub domains), this limitation would not apply. When you enable this option with a value (in this case, ``30000``), the streams from the monitors will originate from ``30000`` plus the monitor ID, effectively overcoming this limitation. **Note that this also needs additional setup in your webserver configuration before this can start to work**. Please refer to `this article <https://medium.com/zmninja/multi-port-storage-areas-and-more-d5836a336c93>`__ on how to set up multi-port streaming on Apache.

MIN_WEBSOCKET_PORT - ZoneMinder can also expose a per-monitor websocket transport directly from ``zmc``. This setting specifies the base port for that listener range. Each monitor websocket listens on ``MIN_WEBSOCKET_PORT + MonitorId``. Use a dedicated port range rather than ``MIN_STREAMING_PORT`` so websocket transport does not conflict with multi-port ZMS streaming or web server listeners. The websocket daemon does not terminate TLS or enforce authentication by itself, so if these ports are reachable beyond a trusted network they should be protected by firewall rules and ideally placed behind a reverse proxy that handles TLS and authentication.

MIN_RTP_PORT - When ZoneMinder communicates with MPEG4 capable cameras using RTP with the unicast method it must open ports for the camera to connect back to for control and streaming purposes. This setting specifies the minimum port number that ZoneMinder will use. Ordinarily two adjacent ports are used for each camera, one for control packets and one for data packets. This port should be set to an even number, you may also need to open up a hole in your firewall to allow cameras to connect back if you wish to use unicasting.

MAX_RTP_PORT - When ZoneMinder communicates with MPEG4 capable cameras using RTP with the unicast method it must open ports for the camera to connect back to for control and streaming purposes. This setting specifies the maximum port number that ZoneMinder will use. Ordinarily two adjacent ports are used for each camera, one for control packets and one for data packets. This port should be set to an even number, you may also need to open up a hole in your firewall to allow cameras to connect back if you wish to use unicasting. You should also ensure that you have opened up at least two ports for each monitor that will be connecting to unicasting network cameras.
MAX_RTP_PORT - When ZoneMinder communicates with MPEG4 capable cameras using RTP with the unicast method it must open ports for the camera to connect back to for control and streaming purposes. This setting specifies the maximum port number that ZoneMinder will use. Ordinarily two adjacent ports are used for each camera, one for control packets and one for data packets. This port should be set to an even number, you may also need to open up a hole in your firewall to allow cameras to connect back if you wish to use unicasting. You should also ensure that you have opened up at least two ports for each monitor that will be connecting to unicasting network cameras.
16 changes: 16 additions & 0 deletions scripts/ZoneMinder/lib/ZoneMinder/ConfigData.pm.in
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,22 @@ our @options = (
type => $types{integer},
category => 'network',
},
{
name => 'ZM_MIN_WEBSOCKET_PORT',
default => '',
description => 'Alternate port range to contact for monitor websocket transport.',
help => q`
This setting specifies the beginning of a dedicated websocket port
range for monitor websocket transport from zmc. Each monitor will
listen on this value plus the Monitor Id. For example, a value of
31000 causes monitor 1 to listen on port 31001. Use a dedicated
range rather than ZM_MIN_STREAMING_PORT so websocket transport does
not conflict with multi-port ZMS streaming. If you expose these
ports outside a trusted network, terminate TLS and enforce
authentication in a reverse proxy or firewall policy.`,
type => $types{integer},
category => 'network',
},
{
name => 'ZM_MIN_RTP_PORT',
default => '40200',
Expand Down
1 change: 1 addition & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ set(ZM_BIN_SRC_FILES
zm_uri.cpp
zm_user.cpp
zm_utils.cpp
zm_websocket.cpp
zm_videostore.cpp
zm_zone.cpp
zm_storage.cpp)
Expand Down
Loading
Loading