toposync-ext-streaming 0.4.4__tar.gz → 0.4.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/PKG-INFO +18 -12
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/README.md +17 -11
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/pyproject.toml +1 -1
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/api/models.py +10 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/api/routes.py +135 -11
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/pipelines/operators.py +13 -4
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/streaming/resize.py +39 -14
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/wizard/pipeline_builder.py +15 -5
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/ui/src/types.ts +1 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/.gitignore +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/LICENSE +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/LICENSE.ffmpeg +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/LICENSE.mediamtx +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/__init__.py +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/api/__init__.py +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/bin/ffmpeg/LICENSE +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/bin/mediamtx/LICENSE +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/extension.json +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/pipelines/__init__.py +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/plugin.py +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/static/326.js +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/static/326.js.LICENSE.txt +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/static/387.js +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/static/4.js +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/static/4.js.LICENSE.txt +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/static/623.js +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/static/623.js.LICENSE.txt +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/static/703.js +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/static/703.js.LICENSE.txt +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/static/main.js +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/static/main.js.LICENSE.txt +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/static/remoteEntry.js +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/streaming/__init__.py +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/streaming/arbitration.py +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/streaming/camera_ingest.py +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/streaming/distributed_sync.py +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/streaming/encoder_state.py +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/streaming/engine_manager.py +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/streaming/ffmpeg_binary.py +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/streaming/go2rtc_binary.py +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/streaming/go2rtc_config.py +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/streaming/go2rtc_manager.py +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/streaming/ingest_auth.py +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/streaming/ingest_resolver.py +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/streaming/jsmpeg_manager.py +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/streaming/mediamtx_api_client.py +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/streaming/mediamtx_binary.py +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/streaming/mediamtx_config.py +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/streaming/mediamtx_processes.py +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/streaming/placeholder.py +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/streaming/platform.py +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/streaming/playback_events.py +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/streaming/publisher_manager.py +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/streaming/runtime_state.py +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/streaming/writer_bridge.py +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/wizard/__init__.py +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/ui/package.json +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/ui/src/activate.tsx +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/ui/src/api/streamingApi.ts +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/ui/src/constants.ts +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/ui/src/entry.ts +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/ui/src/settings/StreamingSettingsPanel.tsx +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/ui/src/settings/SubModal.tsx +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/ui/src/settings/WizardCreatePipelineFromTransmission.tsx +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/ui/src/translations.ts +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/ui/tsconfig.json +0 -0
- {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/ui/webpack.config.js +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: toposync-ext-streaming
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.5
|
|
4
4
|
Summary: Toposync first-party extension: streaming settings, API surface, and pipeline sink bootstrap.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -17,7 +17,7 @@ Extension ID: `com.toposync.streaming`
|
|
|
17
17
|
|
|
18
18
|
This extension provides camera and pipeline video publication in Toposync:
|
|
19
19
|
|
|
20
|
-
- Users normally publish **camera sources** with a `
|
|
20
|
+
- Users normally publish **camera sources** with a `Transmit this source` intent.
|
|
21
21
|
- The extension reconciles generated **CameraLiveView**, **Transmission**, **Outputs**, and implicit continuous pipelines.
|
|
22
22
|
- Advanced pipelines can publish a rendered variant through **`stream.publish_video`**.
|
|
23
23
|
- A local **MediaMTX** engine serves RTSP/HLS/WebRTC (WHEP) URLs.
|
|
@@ -60,6 +60,8 @@ In short:
|
|
|
60
60
|
- Demand heartbeat for web, app, PiP, PTZ, and Home Assistant entity playback.
|
|
61
61
|
- Distributed hosting via `Transmission.host_server_id`, plus URL proxying and processing-side settings sync.
|
|
62
62
|
- Dashboard playback using a backend Playback Plan with HLS/WebRTC, MSE through the optional go2rtc sidecar, and JSMpeg as an on-demand emergency visual fallback.
|
|
63
|
+
- Synthetic playback URLs for MSE and JSMpeg, derived from a real backing output instead of persisted as `TransmissionOutput` rows.
|
|
64
|
+
- Media `content_rect` metadata for contain-resized outputs, so spatial video can remove transport letterboxing without asking the user to recalibrate.
|
|
63
65
|
|
|
64
66
|
## Supported protocols (as implemented)
|
|
65
67
|
|
|
@@ -87,12 +89,14 @@ In short:
|
|
|
87
89
|
- The browser never talks to go2rtc directly. Toposync verifies the signed media token and proxies text control messages plus binary fMP4 fragments.
|
|
88
90
|
- Dashboard Auto can prefer MSE for passive web/grid/fullscreen playback when the sidecar is enabled/startable and the backing output is browser-compatible.
|
|
89
91
|
- A stopped go2rtc process is normal when no MSE viewer is connected. Toposync returns a signed MSE URL when the sidecar can be started, then starts/updates go2rtc on the first MSE WebSocket session.
|
|
92
|
+
- MSE is synthetic. It is generated from a real HLS/backing output and is not stored as `TransmissionOutput(protocol="mse")`.
|
|
90
93
|
|
|
91
94
|
### JSMpeg
|
|
92
95
|
- URL format: `ws://<toposync-host>/api/streams/media/jsmpeg/<path>/ws?media_token=...`
|
|
93
96
|
- Intended only as an emergency visual fallback. It is video-only, low resolution/FPS, and does not carry audio.
|
|
94
97
|
- Each browser WebSocket creates one isolated FFmpeg process that converts the selected runtime frame stream to MPEG-TS/MPEG-1. The process is stopped when the WebSocket closes.
|
|
95
98
|
- The source is the selected Toposync Transmission frame, or an explicit placeholder while warming up/offline. It never pulls camera RTSP directly.
|
|
99
|
+
- JSMpeg is synthetic. It is generated from a real backing output and is not stored as `TransmissionOutput(protocol="jsmpeg")`.
|
|
96
100
|
|
|
97
101
|
### Playback Plan candidates
|
|
98
102
|
|
|
@@ -111,11 +115,11 @@ RTSP is not a browser transport. It remains the internal/ecosystem contract for
|
|
|
111
115
|
|
|
112
116
|
Camera source publication:
|
|
113
117
|
|
|
114
|
-
`camera source` -> `StreamPublicationSpec` -> reconciler -> implicit pipeline -> `stream.publish_video` -> `TransmissionRuntimeState` -> `StreamWriterBridge` -> `FFmpeg publisher` -> `MediaMTX path` -> viewers (RTSP/HLS/WHEP)
|
|
118
|
+
`camera source` -> `StreamPublicationSpec` -> reconciler -> implicit pipeline -> `stream.publish_video` -> `TransmissionRuntimeState` -> `StreamWriterBridge` -> `FFmpeg publisher` -> `MediaMTX path` -> viewers (RTSP/HLS/WHEP/MSE) plus Toposync JSMpeg fallback
|
|
115
119
|
|
|
116
120
|
Advanced pipeline publication:
|
|
117
121
|
|
|
118
|
-
`pipeline frames` -> `stream.publish_video` -> generated/manual `Transmission` -> `TransmissionRuntimeState` -> `StreamWriterBridge` -> `FFmpeg publisher` -> `MediaMTX path` -> viewers (RTSP/HLS/WHEP)
|
|
122
|
+
`pipeline frames` -> `stream.publish_video` -> generated/manual `Transmission` -> `TransmissionRuntimeState` -> `StreamWriterBridge` -> `FFmpeg publisher` -> `MediaMTX path` -> viewers (RTSP/HLS/WHEP/MSE) plus Toposync JSMpeg fallback
|
|
119
123
|
|
|
120
124
|
### Components
|
|
121
125
|
|
|
@@ -205,7 +209,7 @@ Context defaults:
|
|
|
205
209
|
- `large` and `fullscreen`: prefer `main`.
|
|
206
210
|
- `ptz`: prefer `zoom`, then `main`, then `sub`.
|
|
207
211
|
|
|
208
|
-
This is intentionally separate from technical quality labels. The dashboard source selector should expose labels such as "
|
|
212
|
+
This is intentionally separate from technical quality labels. The dashboard source selector should expose labels such as "Main", "Low resolution", "Zoom", or custom names, not internal output ids.
|
|
209
213
|
|
|
210
214
|
### Reconciliation rules
|
|
211
215
|
|
|
@@ -264,6 +268,8 @@ Each output is independently configurable:
|
|
|
264
268
|
|
|
265
269
|
Notes:
|
|
266
270
|
- Authentication is applied only when `enabled == true` and both `username` and `password` are present (non-empty).
|
|
271
|
+
- MSE and JSMpeg are not valid persisted output protocols. The API can still return signed MSE/JSMpeg playback URLs when a compatible backing output exists.
|
|
272
|
+
- URL responses include optional `content_rect` metadata. For `resize_mode="contain"`, it identifies the useful non-letterboxed rectangle inside the output frame. Consumers such as `spatial_video` remap UVs through this rectangle instead of asking the user to recalibrate.
|
|
267
273
|
- The API model allows extra fields (`extra="allow"`). Some runtime behavior also reads optional extra fields:
|
|
268
274
|
- `output.path` (override the resolved engine path)
|
|
269
275
|
- `output.resize_mode` (`contain` or `none`; best-effort)
|
|
@@ -614,7 +620,7 @@ Core auth (enforced mode):
|
|
|
614
620
|
|
|
615
621
|
For regular cameras, streaming is configured from the camera source itself:
|
|
616
622
|
|
|
617
|
-
- each video source can show a `
|
|
623
|
+
- each video source can show a `Transmit this source` checkbox;
|
|
618
624
|
- the source role is used as the publication role (`main`, `sub`, `zoom`, `custom`);
|
|
619
625
|
- the visible source label becomes the publication label;
|
|
620
626
|
- ONVIF-discovered video sources can be published by default;
|
|
@@ -640,7 +646,7 @@ The main UI includes a "Streams" rendering mode with:
|
|
|
640
646
|
|
|
641
647
|
- Grid modes `1x1` and `2x2` with pagination.
|
|
642
648
|
- Auto-hide overlay.
|
|
643
|
-
- Source/role selector using camera variants, for example
|
|
649
|
+
- Source/role selector using camera variants, for example Main, Low resolution, Zoom, or custom names.
|
|
644
650
|
- Playback strategy:
|
|
645
651
|
1. Pick the best variant for the visual context.
|
|
646
652
|
2. Request the backend Playback Plan for that transmission/output/context.
|
|
@@ -861,7 +867,7 @@ Camera source publication (`GET /api/streams/publications?camera_id=front`):
|
|
|
861
867
|
"camera_source_id": "sub",
|
|
862
868
|
"enabled": true,
|
|
863
869
|
"role": "sub",
|
|
864
|
-
"label": "
|
|
870
|
+
"label": "Low resolution",
|
|
865
871
|
"host_server_id": "local",
|
|
866
872
|
"quality_policy": {},
|
|
867
873
|
"transport_policy": {}
|
|
@@ -889,7 +895,7 @@ Demand (`GET /api/streams/transmissions/{id}/demand`):
|
|
|
889
895
|
For normal use:
|
|
890
896
|
|
|
891
897
|
1. Add/discover a camera in the Cameras extension.
|
|
892
|
-
2. Keep `
|
|
898
|
+
2. Keep `Transmit this source` enabled on the desired video sources.
|
|
893
899
|
3. Save the camera/source.
|
|
894
900
|
4. The streaming reconciler creates the live view, generated transmissions, outputs, and implicit pipelines.
|
|
895
901
|
5. Open the dashboard and select the camera/source role.
|
|
@@ -901,7 +907,7 @@ curl http://127.0.0.1:8100/api/streams/publications?camera_id=<camera_id>
|
|
|
901
907
|
|
|
902
908
|
curl -X PUT http://127.0.0.1:8100/api/streams/publications/camera-sources/<camera_id>/<source_id> \
|
|
903
909
|
-H 'content-type: application/json' \
|
|
904
|
-
-d '{"enabled": true, "role": "sub", "label": "
|
|
910
|
+
-d '{"enabled": true, "role": "sub", "label": "Low resolution"}'
|
|
905
911
|
|
|
906
912
|
curl -X POST http://127.0.0.1:8100/api/streams/reconcile
|
|
907
913
|
```
|
|
@@ -1000,7 +1006,7 @@ Likely causes:
|
|
|
1000
1006
|
- `stream.publish_video` has not received a frame yet.
|
|
1001
1007
|
|
|
1002
1008
|
Fix:
|
|
1003
|
-
- For camera streams, check the camera source and keep `
|
|
1009
|
+
- For camera streams, check the camera source and keep `Transmit this source` enabled.
|
|
1004
1010
|
- Call `POST /api/streams/reconcile`.
|
|
1005
1011
|
- Check `GET /api/streams/runtime/pipelines` to see which pipeline owns the generated transmission.
|
|
1006
1012
|
- Check `GET /api/streams/runtime/health` for `active_writer_id`, `selected_writer_id`, `fallback_reason`, and frame age.
|
|
@@ -1008,7 +1014,7 @@ Fix:
|
|
|
1008
1014
|
The primary user-facing message for this class is:
|
|
1009
1015
|
|
|
1010
1016
|
```text
|
|
1011
|
-
|
|
1017
|
+
No pipeline is feeding this transmission.
|
|
1012
1018
|
```
|
|
1013
1019
|
|
|
1014
1020
|
### HLS plays but WebRTC warning appears
|
|
@@ -4,7 +4,7 @@ Extension ID: `com.toposync.streaming`
|
|
|
4
4
|
|
|
5
5
|
This extension provides camera and pipeline video publication in Toposync:
|
|
6
6
|
|
|
7
|
-
- Users normally publish **camera sources** with a `
|
|
7
|
+
- Users normally publish **camera sources** with a `Transmit this source` intent.
|
|
8
8
|
- The extension reconciles generated **CameraLiveView**, **Transmission**, **Outputs**, and implicit continuous pipelines.
|
|
9
9
|
- Advanced pipelines can publish a rendered variant through **`stream.publish_video`**.
|
|
10
10
|
- A local **MediaMTX** engine serves RTSP/HLS/WebRTC (WHEP) URLs.
|
|
@@ -47,6 +47,8 @@ In short:
|
|
|
47
47
|
- Demand heartbeat for web, app, PiP, PTZ, and Home Assistant entity playback.
|
|
48
48
|
- Distributed hosting via `Transmission.host_server_id`, plus URL proxying and processing-side settings sync.
|
|
49
49
|
- Dashboard playback using a backend Playback Plan with HLS/WebRTC, MSE through the optional go2rtc sidecar, and JSMpeg as an on-demand emergency visual fallback.
|
|
50
|
+
- Synthetic playback URLs for MSE and JSMpeg, derived from a real backing output instead of persisted as `TransmissionOutput` rows.
|
|
51
|
+
- Media `content_rect` metadata for contain-resized outputs, so spatial video can remove transport letterboxing without asking the user to recalibrate.
|
|
50
52
|
|
|
51
53
|
## Supported protocols (as implemented)
|
|
52
54
|
|
|
@@ -74,12 +76,14 @@ In short:
|
|
|
74
76
|
- The browser never talks to go2rtc directly. Toposync verifies the signed media token and proxies text control messages plus binary fMP4 fragments.
|
|
75
77
|
- Dashboard Auto can prefer MSE for passive web/grid/fullscreen playback when the sidecar is enabled/startable and the backing output is browser-compatible.
|
|
76
78
|
- A stopped go2rtc process is normal when no MSE viewer is connected. Toposync returns a signed MSE URL when the sidecar can be started, then starts/updates go2rtc on the first MSE WebSocket session.
|
|
79
|
+
- MSE is synthetic. It is generated from a real HLS/backing output and is not stored as `TransmissionOutput(protocol="mse")`.
|
|
77
80
|
|
|
78
81
|
### JSMpeg
|
|
79
82
|
- URL format: `ws://<toposync-host>/api/streams/media/jsmpeg/<path>/ws?media_token=...`
|
|
80
83
|
- Intended only as an emergency visual fallback. It is video-only, low resolution/FPS, and does not carry audio.
|
|
81
84
|
- Each browser WebSocket creates one isolated FFmpeg process that converts the selected runtime frame stream to MPEG-TS/MPEG-1. The process is stopped when the WebSocket closes.
|
|
82
85
|
- The source is the selected Toposync Transmission frame, or an explicit placeholder while warming up/offline. It never pulls camera RTSP directly.
|
|
86
|
+
- JSMpeg is synthetic. It is generated from a real backing output and is not stored as `TransmissionOutput(protocol="jsmpeg")`.
|
|
83
87
|
|
|
84
88
|
### Playback Plan candidates
|
|
85
89
|
|
|
@@ -98,11 +102,11 @@ RTSP is not a browser transport. It remains the internal/ecosystem contract for
|
|
|
98
102
|
|
|
99
103
|
Camera source publication:
|
|
100
104
|
|
|
101
|
-
`camera source` -> `StreamPublicationSpec` -> reconciler -> implicit pipeline -> `stream.publish_video` -> `TransmissionRuntimeState` -> `StreamWriterBridge` -> `FFmpeg publisher` -> `MediaMTX path` -> viewers (RTSP/HLS/WHEP)
|
|
105
|
+
`camera source` -> `StreamPublicationSpec` -> reconciler -> implicit pipeline -> `stream.publish_video` -> `TransmissionRuntimeState` -> `StreamWriterBridge` -> `FFmpeg publisher` -> `MediaMTX path` -> viewers (RTSP/HLS/WHEP/MSE) plus Toposync JSMpeg fallback
|
|
102
106
|
|
|
103
107
|
Advanced pipeline publication:
|
|
104
108
|
|
|
105
|
-
`pipeline frames` -> `stream.publish_video` -> generated/manual `Transmission` -> `TransmissionRuntimeState` -> `StreamWriterBridge` -> `FFmpeg publisher` -> `MediaMTX path` -> viewers (RTSP/HLS/WHEP)
|
|
109
|
+
`pipeline frames` -> `stream.publish_video` -> generated/manual `Transmission` -> `TransmissionRuntimeState` -> `StreamWriterBridge` -> `FFmpeg publisher` -> `MediaMTX path` -> viewers (RTSP/HLS/WHEP/MSE) plus Toposync JSMpeg fallback
|
|
106
110
|
|
|
107
111
|
### Components
|
|
108
112
|
|
|
@@ -192,7 +196,7 @@ Context defaults:
|
|
|
192
196
|
- `large` and `fullscreen`: prefer `main`.
|
|
193
197
|
- `ptz`: prefer `zoom`, then `main`, then `sub`.
|
|
194
198
|
|
|
195
|
-
This is intentionally separate from technical quality labels. The dashboard source selector should expose labels such as "
|
|
199
|
+
This is intentionally separate from technical quality labels. The dashboard source selector should expose labels such as "Main", "Low resolution", "Zoom", or custom names, not internal output ids.
|
|
196
200
|
|
|
197
201
|
### Reconciliation rules
|
|
198
202
|
|
|
@@ -251,6 +255,8 @@ Each output is independently configurable:
|
|
|
251
255
|
|
|
252
256
|
Notes:
|
|
253
257
|
- Authentication is applied only when `enabled == true` and both `username` and `password` are present (non-empty).
|
|
258
|
+
- MSE and JSMpeg are not valid persisted output protocols. The API can still return signed MSE/JSMpeg playback URLs when a compatible backing output exists.
|
|
259
|
+
- URL responses include optional `content_rect` metadata. For `resize_mode="contain"`, it identifies the useful non-letterboxed rectangle inside the output frame. Consumers such as `spatial_video` remap UVs through this rectangle instead of asking the user to recalibrate.
|
|
254
260
|
- The API model allows extra fields (`extra="allow"`). Some runtime behavior also reads optional extra fields:
|
|
255
261
|
- `output.path` (override the resolved engine path)
|
|
256
262
|
- `output.resize_mode` (`contain` or `none`; best-effort)
|
|
@@ -601,7 +607,7 @@ Core auth (enforced mode):
|
|
|
601
607
|
|
|
602
608
|
For regular cameras, streaming is configured from the camera source itself:
|
|
603
609
|
|
|
604
|
-
- each video source can show a `
|
|
610
|
+
- each video source can show a `Transmit this source` checkbox;
|
|
605
611
|
- the source role is used as the publication role (`main`, `sub`, `zoom`, `custom`);
|
|
606
612
|
- the visible source label becomes the publication label;
|
|
607
613
|
- ONVIF-discovered video sources can be published by default;
|
|
@@ -627,7 +633,7 @@ The main UI includes a "Streams" rendering mode with:
|
|
|
627
633
|
|
|
628
634
|
- Grid modes `1x1` and `2x2` with pagination.
|
|
629
635
|
- Auto-hide overlay.
|
|
630
|
-
- Source/role selector using camera variants, for example
|
|
636
|
+
- Source/role selector using camera variants, for example Main, Low resolution, Zoom, or custom names.
|
|
631
637
|
- Playback strategy:
|
|
632
638
|
1. Pick the best variant for the visual context.
|
|
633
639
|
2. Request the backend Playback Plan for that transmission/output/context.
|
|
@@ -848,7 +854,7 @@ Camera source publication (`GET /api/streams/publications?camera_id=front`):
|
|
|
848
854
|
"camera_source_id": "sub",
|
|
849
855
|
"enabled": true,
|
|
850
856
|
"role": "sub",
|
|
851
|
-
"label": "
|
|
857
|
+
"label": "Low resolution",
|
|
852
858
|
"host_server_id": "local",
|
|
853
859
|
"quality_policy": {},
|
|
854
860
|
"transport_policy": {}
|
|
@@ -876,7 +882,7 @@ Demand (`GET /api/streams/transmissions/{id}/demand`):
|
|
|
876
882
|
For normal use:
|
|
877
883
|
|
|
878
884
|
1. Add/discover a camera in the Cameras extension.
|
|
879
|
-
2. Keep `
|
|
885
|
+
2. Keep `Transmit this source` enabled on the desired video sources.
|
|
880
886
|
3. Save the camera/source.
|
|
881
887
|
4. The streaming reconciler creates the live view, generated transmissions, outputs, and implicit pipelines.
|
|
882
888
|
5. Open the dashboard and select the camera/source role.
|
|
@@ -888,7 +894,7 @@ curl http://127.0.0.1:8100/api/streams/publications?camera_id=<camera_id>
|
|
|
888
894
|
|
|
889
895
|
curl -X PUT http://127.0.0.1:8100/api/streams/publications/camera-sources/<camera_id>/<source_id> \
|
|
890
896
|
-H 'content-type: application/json' \
|
|
891
|
-
-d '{"enabled": true, "role": "sub", "label": "
|
|
897
|
+
-d '{"enabled": true, "role": "sub", "label": "Low resolution"}'
|
|
892
898
|
|
|
893
899
|
curl -X POST http://127.0.0.1:8100/api/streams/reconcile
|
|
894
900
|
```
|
|
@@ -987,7 +993,7 @@ Likely causes:
|
|
|
987
993
|
- `stream.publish_video` has not received a frame yet.
|
|
988
994
|
|
|
989
995
|
Fix:
|
|
990
|
-
- For camera streams, check the camera source and keep `
|
|
996
|
+
- For camera streams, check the camera source and keep `Transmit this source` enabled.
|
|
991
997
|
- Call `POST /api/streams/reconcile`.
|
|
992
998
|
- Check `GET /api/streams/runtime/pipelines` to see which pipeline owns the generated transmission.
|
|
993
999
|
- Check `GET /api/streams/runtime/health` for `active_writer_id`, `selected_writer_id`, `fallback_reason`, and frame age.
|
|
@@ -995,7 +1001,7 @@ Fix:
|
|
|
995
1001
|
The primary user-facing message for this class is:
|
|
996
1002
|
|
|
997
1003
|
```text
|
|
998
|
-
|
|
1004
|
+
No pipeline is feeding this transmission.
|
|
999
1005
|
```
|
|
1000
1006
|
|
|
1001
1007
|
### HLS plays but WebRTC warning appears
|
|
@@ -1087,6 +1087,15 @@ class TransmissionCreateRequest(BaseModel):
|
|
|
1087
1087
|
outputs: list[TransmissionOutput] = Field(default_factory=list)
|
|
1088
1088
|
|
|
1089
1089
|
|
|
1090
|
+
class MediaContentRect(BaseModel):
|
|
1091
|
+
model_config = ConfigDict(extra="forbid")
|
|
1092
|
+
|
|
1093
|
+
x: float = Field(ge=0.0, le=1.0)
|
|
1094
|
+
y: float = Field(ge=0.0, le=1.0)
|
|
1095
|
+
width: float = Field(ge=0.0, le=1.0)
|
|
1096
|
+
height: float = Field(ge=0.0, le=1.0)
|
|
1097
|
+
|
|
1098
|
+
|
|
1090
1099
|
class TransmissionOutputUrl(BaseModel):
|
|
1091
1100
|
model_config = ConfigDict(extra="forbid")
|
|
1092
1101
|
|
|
@@ -1104,6 +1113,7 @@ class TransmissionOutputUrl(BaseModel):
|
|
|
1104
1113
|
fps_limit: int | None = None
|
|
1105
1114
|
bitrate_kbps: int | None = None
|
|
1106
1115
|
latency_profile: Literal["normal", "low", "ultra_low"] | None = None
|
|
1116
|
+
content_rect: MediaContentRect | None = None
|
|
1107
1117
|
|
|
1108
1118
|
|
|
1109
1119
|
class TransmissionUrlsResponse(BaseModel):
|
|
@@ -62,7 +62,7 @@ from ..streaming.mediamtx_processes import (
|
|
|
62
62
|
from ..streaming.publisher_manager import PublisherManager
|
|
63
63
|
from ..streaming.playback_events import PlaybackEventStore, summarize_active_sessions
|
|
64
64
|
from ..streaming.placeholder import get_placeholder_frame
|
|
65
|
-
from ..streaming.resize import resize_frame_contain
|
|
65
|
+
from ..streaming.resize import contain_content_rect, resize_frame_contain
|
|
66
66
|
from ..streaming.runtime_state import SelectedWriterFrame, TransmissionRuntimeState
|
|
67
67
|
from ..wizard import build_streaming_wizard_graph, suggested_streaming_wizard_pipeline_name
|
|
68
68
|
from .models import (
|
|
@@ -1646,6 +1646,7 @@ async def _resolve_local_transmission_urls(
|
|
|
1646
1646
|
if engine_status.running
|
|
1647
1647
|
else settings.engine.preferred_ports.webrtc
|
|
1648
1648
|
)
|
|
1649
|
+
media_source_dimensions = await _transmission_media_source_dimensions(request, transmission)
|
|
1649
1650
|
|
|
1650
1651
|
generic_warnings: list[str] = list(getattr(engine_status, "warnings", ()) or ())
|
|
1651
1652
|
if not engine_status.running:
|
|
@@ -1768,7 +1769,11 @@ async def _resolve_local_transmission_urls(
|
|
|
1768
1769
|
media_auth_type=media_auth_type,
|
|
1769
1770
|
url_expires_at_unix=url_expires_at_unix,
|
|
1770
1771
|
renew_after_unix=renew_after_unix,
|
|
1771
|
-
**_output_quality_metadata(
|
|
1772
|
+
**_output_quality_metadata(
|
|
1773
|
+
output,
|
|
1774
|
+
source_dimensions=media_source_dimensions,
|
|
1775
|
+
include_content_rect=True,
|
|
1776
|
+
),
|
|
1772
1777
|
)
|
|
1773
1778
|
)
|
|
1774
1779
|
if mse_media_url_available:
|
|
@@ -1792,7 +1797,11 @@ async def _resolve_local_transmission_urls(
|
|
|
1792
1797
|
media_auth_type="signed_url",
|
|
1793
1798
|
url_expires_at_unix=mse_expires_at_unix,
|
|
1794
1799
|
renew_after_unix=mse_renew_after_unix,
|
|
1795
|
-
**_output_quality_metadata(
|
|
1800
|
+
**_output_quality_metadata(
|
|
1801
|
+
output,
|
|
1802
|
+
source_dimensions=media_source_dimensions,
|
|
1803
|
+
include_content_rect=True,
|
|
1804
|
+
),
|
|
1796
1805
|
)
|
|
1797
1806
|
)
|
|
1798
1807
|
if jsmpeg_available:
|
|
@@ -1816,7 +1825,11 @@ async def _resolve_local_transmission_urls(
|
|
|
1816
1825
|
media_auth_type="signed_url",
|
|
1817
1826
|
url_expires_at_unix=jsmpeg_expires_at_unix,
|
|
1818
1827
|
renew_after_unix=jsmpeg_renew_after_unix,
|
|
1819
|
-
**_output_quality_metadata(
|
|
1828
|
+
**_output_quality_metadata(
|
|
1829
|
+
output,
|
|
1830
|
+
source_dimensions=media_source_dimensions,
|
|
1831
|
+
include_content_rect=True,
|
|
1832
|
+
),
|
|
1820
1833
|
)
|
|
1821
1834
|
)
|
|
1822
1835
|
continue
|
|
@@ -1845,7 +1858,11 @@ async def _resolve_local_transmission_urls(
|
|
|
1845
1858
|
requires_auth=requires_auth,
|
|
1846
1859
|
auth_username=auth_username if requires_auth else None,
|
|
1847
1860
|
media_auth_type=media_auth_type,
|
|
1848
|
-
**_output_quality_metadata(
|
|
1861
|
+
**_output_quality_metadata(
|
|
1862
|
+
output,
|
|
1863
|
+
source_dimensions=media_source_dimensions,
|
|
1864
|
+
include_content_rect=True,
|
|
1865
|
+
),
|
|
1849
1866
|
)
|
|
1850
1867
|
)
|
|
1851
1868
|
if output.protocol == "hls" and mse_media_url_available:
|
|
@@ -1869,7 +1886,11 @@ async def _resolve_local_transmission_urls(
|
|
|
1869
1886
|
media_auth_type="signed_url",
|
|
1870
1887
|
url_expires_at_unix=mse_expires_at_unix,
|
|
1871
1888
|
renew_after_unix=mse_renew_after_unix,
|
|
1872
|
-
**_output_quality_metadata(
|
|
1889
|
+
**_output_quality_metadata(
|
|
1890
|
+
output,
|
|
1891
|
+
source_dimensions=media_source_dimensions,
|
|
1892
|
+
include_content_rect=True,
|
|
1893
|
+
),
|
|
1873
1894
|
)
|
|
1874
1895
|
)
|
|
1875
1896
|
if output.protocol == "hls" and jsmpeg_available:
|
|
@@ -1893,7 +1914,11 @@ async def _resolve_local_transmission_urls(
|
|
|
1893
1914
|
media_auth_type="signed_url",
|
|
1894
1915
|
url_expires_at_unix=jsmpeg_expires_at_unix,
|
|
1895
1916
|
renew_after_unix=jsmpeg_renew_after_unix,
|
|
1896
|
-
**_output_quality_metadata(
|
|
1917
|
+
**_output_quality_metadata(
|
|
1918
|
+
output,
|
|
1919
|
+
source_dimensions=media_source_dimensions,
|
|
1920
|
+
include_content_rect=True,
|
|
1921
|
+
),
|
|
1897
1922
|
)
|
|
1898
1923
|
)
|
|
1899
1924
|
|
|
@@ -2080,14 +2105,109 @@ def _webrtc_url(host: str, port: int, path: str) -> str:
|
|
|
2080
2105
|
return f"http://{host}:{port}/{path}/whep"
|
|
2081
2106
|
|
|
2082
2107
|
|
|
2083
|
-
def
|
|
2084
|
-
|
|
2108
|
+
def _positive_int(value: Any) -> int | None:
|
|
2109
|
+
try:
|
|
2110
|
+
parsed = int(value)
|
|
2111
|
+
except (TypeError, ValueError):
|
|
2112
|
+
return None
|
|
2113
|
+
return parsed if parsed > 0 else None
|
|
2114
|
+
|
|
2115
|
+
|
|
2116
|
+
def _full_media_content_rect() -> dict[str, float]:
|
|
2117
|
+
return {"x": 0.0, "y": 0.0, "width": 1.0, "height": 1.0}
|
|
2118
|
+
|
|
2119
|
+
|
|
2120
|
+
def _source_video_dimensions(source: Any) -> tuple[int, int] | None:
|
|
2121
|
+
if not isinstance(source, dict):
|
|
2122
|
+
return None
|
|
2123
|
+
video = source.get("video") if isinstance(source.get("video"), dict) else {}
|
|
2124
|
+
width = _positive_int(video.get("width"))
|
|
2125
|
+
height = _positive_int(video.get("height"))
|
|
2126
|
+
if width and height:
|
|
2127
|
+
return width, height
|
|
2128
|
+
return None
|
|
2129
|
+
|
|
2130
|
+
|
|
2131
|
+
def _output_resolution_dimensions(output: TransmissionOutput) -> tuple[int, int] | None:
|
|
2132
|
+
payload = output.model_dump(mode="python")
|
|
2133
|
+
resolution = payload.get("resolution") if isinstance(payload.get("resolution"), dict) else {}
|
|
2134
|
+
width = _positive_int(payload.get("width")) or _positive_int(resolution.get("width"))
|
|
2135
|
+
height = _positive_int(payload.get("height")) or _positive_int(resolution.get("height"))
|
|
2136
|
+
if width and height:
|
|
2137
|
+
return width, height
|
|
2138
|
+
return None
|
|
2139
|
+
|
|
2140
|
+
|
|
2141
|
+
def _output_resize_mode(output: TransmissionOutput) -> str:
|
|
2142
|
+
payload = output.model_dump(mode="python")
|
|
2143
|
+
resize_mode = str(payload.get("resize_mode") or "contain").strip().lower()
|
|
2144
|
+
return resize_mode if resize_mode in {"contain", "none"} else "contain"
|
|
2145
|
+
|
|
2146
|
+
|
|
2147
|
+
def _output_content_rect(
|
|
2148
|
+
output: TransmissionOutput,
|
|
2149
|
+
*,
|
|
2150
|
+
source_dimensions: tuple[int, int] | None = None,
|
|
2151
|
+
) -> dict[str, float]:
|
|
2152
|
+
target_dimensions = _output_resolution_dimensions(output)
|
|
2153
|
+
if source_dimensions is None or target_dimensions is None:
|
|
2154
|
+
return _full_media_content_rect()
|
|
2155
|
+
source_width, source_height = source_dimensions
|
|
2156
|
+
target_width, target_height = target_dimensions
|
|
2157
|
+
if _output_resize_mode(output) != "contain" and source_width == target_width and source_height == target_height:
|
|
2158
|
+
return _full_media_content_rect()
|
|
2159
|
+
return contain_content_rect(source_width, source_height, target_width, target_height)
|
|
2160
|
+
|
|
2161
|
+
|
|
2162
|
+
def _output_quality_metadata(
|
|
2163
|
+
output: TransmissionOutput,
|
|
2164
|
+
*,
|
|
2165
|
+
source_dimensions: tuple[int, int] | None = None,
|
|
2166
|
+
include_content_rect: bool = False,
|
|
2167
|
+
) -> dict[str, Any]:
|
|
2168
|
+
metadata: dict[str, Any] = {
|
|
2085
2169
|
"quality_profile_id": output.quality_profile_id,
|
|
2086
2170
|
"resolution": output.resolution,
|
|
2087
2171
|
"fps_limit": output.fps_limit,
|
|
2088
2172
|
"bitrate_kbps": output.bitrate_kbps,
|
|
2089
2173
|
"latency_profile": output.latency_profile,
|
|
2090
2174
|
}
|
|
2175
|
+
if include_content_rect:
|
|
2176
|
+
metadata["content_rect"] = _output_content_rect(output, source_dimensions=source_dimensions)
|
|
2177
|
+
return metadata
|
|
2178
|
+
|
|
2179
|
+
|
|
2180
|
+
async def _transmission_media_source_dimensions(
|
|
2181
|
+
request: Request,
|
|
2182
|
+
transmission: Transmission,
|
|
2183
|
+
) -> tuple[int, int] | None:
|
|
2184
|
+
controls = transmission.camera_controls
|
|
2185
|
+
camera_id = str(getattr(controls, "camera_id", "") or "").strip()
|
|
2186
|
+
camera_source_id = str(getattr(controls, "camera_source_id", "") or "").strip()
|
|
2187
|
+
if camera_id:
|
|
2188
|
+
try:
|
|
2189
|
+
app_settings = await _config_store(request).get_settings()
|
|
2190
|
+
for device in iter_camera_devices_from_app_settings(app_settings):
|
|
2191
|
+
if str(device.get("id") or "").strip() != camera_id:
|
|
2192
|
+
continue
|
|
2193
|
+
source = resolve_camera_video_source(device, source_id=camera_source_id, enabled_only=False)
|
|
2194
|
+
dimensions = _source_video_dimensions(source)
|
|
2195
|
+
if dimensions is not None:
|
|
2196
|
+
return dimensions
|
|
2197
|
+
break
|
|
2198
|
+
except Exception:
|
|
2199
|
+
pass
|
|
2200
|
+
|
|
2201
|
+
try:
|
|
2202
|
+
selected = await _runtime_state(request).get_selected_writer_frame(transmission.id)
|
|
2203
|
+
frame = selected.frame
|
|
2204
|
+
if frame is not None and getattr(frame, "ndim", 0) >= 2:
|
|
2205
|
+
height, width = frame.shape[:2]
|
|
2206
|
+
if int(width) > 0 and int(height) > 0:
|
|
2207
|
+
return int(width), int(height)
|
|
2208
|
+
except Exception:
|
|
2209
|
+
pass
|
|
2210
|
+
return None
|
|
2091
2211
|
|
|
2092
2212
|
|
|
2093
2213
|
def _playback_plan_transport_from_output(
|
|
@@ -3108,7 +3228,9 @@ def _runtime_pipeline_warning(reason: str) -> str:
|
|
|
3108
3228
|
if reason == "vision_detect_events":
|
|
3109
3229
|
return "stream.publish_video is downstream of detection in events mode."
|
|
3110
3230
|
if reason == "vision_track_events":
|
|
3111
|
-
return "stream.publish_video is downstream of tracking
|
|
3231
|
+
return "stream.publish_video is downstream of tracking event packets."
|
|
3232
|
+
if reason == "vision_group_events":
|
|
3233
|
+
return "stream.publish_video is downstream of grouped event packets."
|
|
3112
3234
|
return "This stream is explicitly configured as event-gated."
|
|
3113
3235
|
|
|
3114
3236
|
|
|
@@ -3195,8 +3317,10 @@ def _runtime_pipeline_event_gate_reasons(
|
|
|
3195
3317
|
reasons.append("vision_detect_events")
|
|
3196
3318
|
continue
|
|
3197
3319
|
|
|
3198
|
-
if operator_id == "vision.track"
|
|
3320
|
+
if operator_id == "vision.track":
|
|
3199
3321
|
reasons.append("vision_track_events")
|
|
3322
|
+
if operator_id == "vision.group_events":
|
|
3323
|
+
reasons.append("vision_group_events")
|
|
3200
3324
|
|
|
3201
3325
|
return list(dict.fromkeys(reasons))
|
|
3202
3326
|
|
|
@@ -283,12 +283,21 @@ def _publish_video_diagnostics(_config: dict[str, Any], context: dict[str, Any])
|
|
|
283
283
|
)
|
|
284
284
|
continue
|
|
285
285
|
|
|
286
|
-
if operator_id == "vision.track"
|
|
286
|
+
if operator_id == "vision.track":
|
|
287
287
|
add(
|
|
288
288
|
"stream_publish_video_event_gated_tracking",
|
|
289
|
-
f"stream.publish_video is downstream of tracking '{node_id}'
|
|
290
|
-
"Use
|
|
291
|
-
{"source_node_id": node_id
|
|
289
|
+
f"stream.publish_video is downstream of tracking '{node_id}', which emits object event packets.",
|
|
290
|
+
"Use a continuous visual branch for normal streaming, or keep this branch only when event-gated streaming is intentional.",
|
|
291
|
+
{"source_node_id": node_id},
|
|
292
|
+
)
|
|
293
|
+
continue
|
|
294
|
+
|
|
295
|
+
if operator_id == "vision.group_events":
|
|
296
|
+
add(
|
|
297
|
+
"stream_publish_video_event_gated_group_events",
|
|
298
|
+
f"stream.publish_video is downstream of grouped events '{node_id}', which emits group lifecycle packets.",
|
|
299
|
+
"Use a continuous visual branch for normal streaming, or keep this branch only when event-gated streaming is intentional.",
|
|
300
|
+
{"source_node_id": node_id},
|
|
292
301
|
)
|
|
293
302
|
|
|
294
303
|
return diagnostics
|
|
@@ -4,6 +4,40 @@ import cv2
|
|
|
4
4
|
import numpy
|
|
5
5
|
|
|
6
6
|
|
|
7
|
+
def contain_content_rect(
|
|
8
|
+
source_width: int,
|
|
9
|
+
source_height: int,
|
|
10
|
+
target_width: int,
|
|
11
|
+
target_height: int,
|
|
12
|
+
) -> dict[str, float]:
|
|
13
|
+
normalized_target_width = max(1, int(target_width))
|
|
14
|
+
normalized_target_height = max(1, int(target_height))
|
|
15
|
+
normalized_source_width = max(1, int(source_width))
|
|
16
|
+
normalized_source_height = max(1, int(source_height))
|
|
17
|
+
|
|
18
|
+
source_aspect_ratio = float(normalized_source_width) / float(normalized_source_height)
|
|
19
|
+
target_aspect_ratio = float(normalized_target_width) / float(normalized_target_height)
|
|
20
|
+
|
|
21
|
+
if source_aspect_ratio >= target_aspect_ratio:
|
|
22
|
+
resized_width = normalized_target_width
|
|
23
|
+
resized_height = int(round(normalized_target_width / source_aspect_ratio))
|
|
24
|
+
else:
|
|
25
|
+
resized_height = normalized_target_height
|
|
26
|
+
resized_width = int(round(normalized_target_height * source_aspect_ratio))
|
|
27
|
+
|
|
28
|
+
resized_width = max(1, min(normalized_target_width, int(resized_width)))
|
|
29
|
+
resized_height = max(1, min(normalized_target_height, int(resized_height)))
|
|
30
|
+
offset_x = (normalized_target_width - resized_width) // 2
|
|
31
|
+
offset_y = (normalized_target_height - resized_height) // 2
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
"x": offset_x / float(normalized_target_width),
|
|
35
|
+
"y": offset_y / float(normalized_target_height),
|
|
36
|
+
"width": resized_width / float(normalized_target_width),
|
|
37
|
+
"height": resized_height / float(normalized_target_height),
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
7
41
|
def resize_frame_contain(frame_bgr: numpy.ndarray, target_width: int, target_height: int) -> numpy.ndarray:
|
|
8
42
|
normalized_target_width = max(1, int(target_width))
|
|
9
43
|
normalized_target_height = max(1, int(target_height))
|
|
@@ -27,26 +61,17 @@ def resize_frame_contain(frame_bgr: numpy.ndarray, target_width: int, target_hei
|
|
|
27
61
|
if source_width == normalized_target_width and source_height == normalized_target_height:
|
|
28
62
|
return numpy.ascontiguousarray(source_frame)
|
|
29
63
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if source_aspect_ratio >= target_aspect_ratio:
|
|
34
|
-
resized_width = normalized_target_width
|
|
35
|
-
resized_height = int(round(normalized_target_width / source_aspect_ratio))
|
|
36
|
-
else:
|
|
37
|
-
resized_height = normalized_target_height
|
|
38
|
-
resized_width = int(round(normalized_target_height * source_aspect_ratio))
|
|
39
|
-
|
|
40
|
-
resized_width = max(1, min(normalized_target_width, int(resized_width)))
|
|
41
|
-
resized_height = max(1, min(normalized_target_height, int(resized_height)))
|
|
64
|
+
content_rect = contain_content_rect(source_width, source_height, normalized_target_width, normalized_target_height)
|
|
65
|
+
resized_width = max(1, min(normalized_target_width, int(round(content_rect["width"] * normalized_target_width))))
|
|
66
|
+
resized_height = max(1, min(normalized_target_height, int(round(content_rect["height"] * normalized_target_height))))
|
|
42
67
|
|
|
43
68
|
interpolation = cv2.INTER_AREA if (resized_width < source_width or resized_height < source_height) else cv2.INTER_LINEAR
|
|
44
69
|
resized_frame = cv2.resize(source_frame, (resized_width, resized_height), interpolation=interpolation)
|
|
45
70
|
|
|
46
71
|
output_frame = numpy.zeros((normalized_target_height, normalized_target_width, 3), dtype=numpy.uint8)
|
|
47
72
|
|
|
48
|
-
offset_x = (
|
|
49
|
-
offset_y = (
|
|
73
|
+
offset_x = int(round(content_rect["x"] * normalized_target_width))
|
|
74
|
+
offset_y = int(round(content_rect["y"] * normalized_target_height))
|
|
50
75
|
|
|
51
76
|
output_frame[offset_y : offset_y + resized_height, offset_x : offset_x + resized_width] = resized_frame
|
|
52
77
|
return output_frame
|
|
@@ -108,9 +108,14 @@ def build_streaming_wizard_graph(
|
|
|
108
108
|
motion_sensitivity = _coerce_float(options.get("motion_sensitivity"), default=0.010, min_value=0.0001, max_value=1.0)
|
|
109
109
|
motion_hold_seconds = _coerce_float(options.get("motion_hold_seconds"), default=6.0, min_value=0.0, max_value=120.0)
|
|
110
110
|
yolo_confidence = _coerce_float(options.get("yolo_confidence_threshold"), default=0.55, min_value=0.01, max_value=1.0)
|
|
111
|
+
tracking_detection_confidence = _coerce_float(
|
|
112
|
+
options.get("yolo_confidence_threshold"),
|
|
113
|
+
default=0.25,
|
|
114
|
+
min_value=0.01,
|
|
115
|
+
max_value=1.0,
|
|
116
|
+
)
|
|
111
117
|
yolo_filter_enabled = _coerce_bool(options.get("yolo_filter_enabled"), default=True)
|
|
112
118
|
detection_emit_mode = "filter" if event_gated and yolo_filter_enabled else "annotate"
|
|
113
|
-
tracking_emit_mode = "events" if yolo_filter_enabled else "annotate"
|
|
114
119
|
|
|
115
120
|
detection_categories = _sanitize_categories(options.get("detection_categories"))
|
|
116
121
|
tracking_categories = _sanitize_categories(options.get("tracking_categories"))
|
|
@@ -215,7 +220,7 @@ def build_streaming_wizard_graph(
|
|
|
215
220
|
"config": {
|
|
216
221
|
"model_id": DEFAULT_STREAMING_DETECTION_MODEL_ID,
|
|
217
222
|
"categories": tracking_categories,
|
|
218
|
-
"confidence_threshold": float(
|
|
223
|
+
"confidence_threshold": float(tracking_detection_confidence),
|
|
219
224
|
"emit_mode": "annotate",
|
|
220
225
|
},
|
|
221
226
|
}
|
|
@@ -227,9 +232,14 @@ def build_streaming_wizard_graph(
|
|
|
227
232
|
"id": "track",
|
|
228
233
|
"operator": "vision.track",
|
|
229
234
|
"config": {
|
|
230
|
-
"tracker_id": "
|
|
231
|
-
"
|
|
232
|
-
"
|
|
235
|
+
"tracker_id": "byte_world",
|
|
236
|
+
"open_confidence_threshold": 0.50,
|
|
237
|
+
"continue_confidence_threshold": 0.25,
|
|
238
|
+
"close_after_seconds": 10.0,
|
|
239
|
+
"stitch_gap_seconds": 30.0,
|
|
240
|
+
"default_interval_seconds": 0.25,
|
|
241
|
+
"use_world_anchor": "auto",
|
|
242
|
+
"world_match_distance_meters": 3.0,
|
|
233
243
|
},
|
|
234
244
|
}
|
|
235
245
|
)
|
|
@@ -150,6 +150,7 @@ export type TransmissionOutputUrl = {
|
|
|
150
150
|
fps_limit?: number | null;
|
|
151
151
|
bitrate_kbps?: number | null;
|
|
152
152
|
latency_profile?: StreamingLatencyProfile | null;
|
|
153
|
+
content_rect?: { x: number; y: number; width: number; height: number } | null;
|
|
153
154
|
};
|
|
154
155
|
|
|
155
156
|
export type TransmissionUrlsResponse = {
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/plugin.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/static/4.js
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|