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.
Files changed (67) hide show
  1. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/PKG-INFO +18 -12
  2. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/README.md +17 -11
  3. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/pyproject.toml +1 -1
  4. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/api/models.py +10 -0
  5. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/api/routes.py +135 -11
  6. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/pipelines/operators.py +13 -4
  7. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/streaming/resize.py +39 -14
  8. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/wizard/pipeline_builder.py +15 -5
  9. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/ui/src/types.ts +1 -0
  10. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/.gitignore +0 -0
  11. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/LICENSE +0 -0
  12. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/LICENSE.ffmpeg +0 -0
  13. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/LICENSE.mediamtx +0 -0
  14. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/__init__.py +0 -0
  15. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/api/__init__.py +0 -0
  16. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/bin/ffmpeg/LICENSE +0 -0
  17. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/bin/mediamtx/LICENSE +0 -0
  18. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/extension.json +0 -0
  19. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/pipelines/__init__.py +0 -0
  20. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/plugin.py +0 -0
  21. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/static/326.js +0 -0
  22. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/static/326.js.LICENSE.txt +0 -0
  23. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/static/387.js +0 -0
  24. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/static/4.js +0 -0
  25. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/static/4.js.LICENSE.txt +0 -0
  26. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/static/623.js +0 -0
  27. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/static/623.js.LICENSE.txt +0 -0
  28. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/static/703.js +0 -0
  29. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/static/703.js.LICENSE.txt +0 -0
  30. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/static/main.js +0 -0
  31. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/static/main.js.LICENSE.txt +0 -0
  32. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/static/remoteEntry.js +0 -0
  33. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/streaming/__init__.py +0 -0
  34. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/streaming/arbitration.py +0 -0
  35. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/streaming/camera_ingest.py +0 -0
  36. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/streaming/distributed_sync.py +0 -0
  37. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/streaming/encoder_state.py +0 -0
  38. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/streaming/engine_manager.py +0 -0
  39. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/streaming/ffmpeg_binary.py +0 -0
  40. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/streaming/go2rtc_binary.py +0 -0
  41. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/streaming/go2rtc_config.py +0 -0
  42. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/streaming/go2rtc_manager.py +0 -0
  43. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/streaming/ingest_auth.py +0 -0
  44. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/streaming/ingest_resolver.py +0 -0
  45. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/streaming/jsmpeg_manager.py +0 -0
  46. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/streaming/mediamtx_api_client.py +0 -0
  47. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/streaming/mediamtx_binary.py +0 -0
  48. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/streaming/mediamtx_config.py +0 -0
  49. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/streaming/mediamtx_processes.py +0 -0
  50. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/streaming/placeholder.py +0 -0
  51. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/streaming/platform.py +0 -0
  52. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/streaming/playback_events.py +0 -0
  53. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/streaming/publisher_manager.py +0 -0
  54. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/streaming/runtime_state.py +0 -0
  55. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/streaming/writer_bridge.py +0 -0
  56. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/src/toposync_ext_streaming/wizard/__init__.py +0 -0
  57. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/ui/package.json +0 -0
  58. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/ui/src/activate.tsx +0 -0
  59. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/ui/src/api/streamingApi.ts +0 -0
  60. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/ui/src/constants.ts +0 -0
  61. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/ui/src/entry.ts +0 -0
  62. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/ui/src/settings/StreamingSettingsPanel.tsx +0 -0
  63. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/ui/src/settings/SubModal.tsx +0 -0
  64. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/ui/src/settings/WizardCreatePipelineFromTransmission.tsx +0 -0
  65. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/ui/src/translations.ts +0 -0
  66. {toposync_ext_streaming-0.4.4 → toposync_ext_streaming-0.4.5}/ui/tsconfig.json +0 -0
  67. {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.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 `Transmitir esta fonte` intent.
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 "Principal", "Baixa resolução", "Zoom", or custom names, not internal output ids.
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 `Transmitir esta fonte` checkbox;
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 Principal, Baixa resolução, Zoom, or custom names.
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": "Baixa resolução",
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 `Transmitir esta fonte` enabled on the desired video sources.
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": "Baixa resolução"}'
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 `Transmitir esta fonte` enabled.
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
- Nenhum fluxo está alimentando esta transmissão.
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 `Transmitir esta fonte` intent.
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 "Principal", "Baixa resolução", "Zoom", or custom names, not internal output ids.
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 `Transmitir esta fonte` checkbox;
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 Principal, Baixa resolução, Zoom, or custom names.
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": "Baixa resolução",
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 `Transmitir esta fonte` enabled on the desired video sources.
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": "Baixa resolução"}'
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 `Transmitir esta fonte` enabled.
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
- Nenhum fluxo está alimentando esta transmissão.
1004
+ No pipeline is feeding this transmission.
999
1005
  ```
1000
1006
 
1001
1007
  ### HLS plays but WebRTC warning appears
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "toposync-ext-streaming"
3
- version = "0.4.4"
3
+ version = "0.4.5"
4
4
  description = "Toposync first-party extension: streaming settings, API surface, and pipeline sink bootstrap."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -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(output),
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(output),
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(output),
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(output),
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(output),
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(output),
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 _output_quality_metadata(output: TransmissionOutput) -> dict[str, Any]:
2084
- return {
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 in events mode."
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" and emit_mode in {"events", "event"}:
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" and emit_mode in {"events", "event"}:
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}' in emit_mode={emit_mode}.",
290
- "Use emit_mode='annotate' for visual streaming, or split tracking onto a separate analytics/event branch.",
291
- {"source_node_id": node_id, "emit_mode": emit_mode},
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
- source_aspect_ratio = float(source_width) / float(source_height)
31
- target_aspect_ratio = float(normalized_target_width) / float(normalized_target_height)
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 = (normalized_target_width - resized_width) // 2
49
- offset_y = (normalized_target_height - resized_height) // 2
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(yolo_confidence),
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": "simple_iou_kalman",
231
- "close_after_seconds": 5.0,
232
- "emit_mode": tracking_emit_mode,
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 = {