-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
feat: add per-monitor websocket transport #4830
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
AJ0070
wants to merge
15
commits into
ZoneMinder:master
Choose a base branch
from
AJ0070:fix/2875
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
ebfa640
feat: add per-monitor websocket transport
AJ0070 9923c36
Merge branch 'master' into fix/2875
AJ0070 c7c6bd3
implemented valid copilot reviews
AJ0070 463af64
fix
AJ0070 bb32d4b
fix
AJ0070 7c38528
fix
AJ0070 fcbf129
updated doc and improved test coverage
AJ0070 0ae10ba
fix
AJ0070 40dd3fa
fix bug
AJ0070 cd41ea4
fix
AJ0070 e7ff2e6
addressed review
AJ0070 d9e8211
Merge branch 'master' into fix/2875
AJ0070 dd8732d
Merge branch 'master' into fix/2875
AJ0070 da423c1
improvements
AJ0070 b9f1f32
Merge branch 'master' into fix/2875
AJ0070 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. | ||
|
|
||
| .. 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`` | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.