toposync-ext-streaming 0.7.9__tar.gz → 0.7.10__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.7.9/README.md → toposync_ext_streaming-0.7.10/PKG-INFO +26 -0
  2. toposync_ext_streaming-0.7.9/PKG-INFO → toposync_ext_streaming-0.7.10/README.md +13 -13
  3. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/pyproject.toml +1 -1
  4. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/src/toposync_ext_streaming/api/models.py +3 -0
  5. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/src/toposync_ext_streaming/api/routes.py +90 -6
  6. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/src/toposync_ext_streaming/streaming/engine_manager.py +498 -32
  7. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/.gitignore +0 -0
  8. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/LICENSE +0 -0
  9. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/LICENSE.ffmpeg +0 -0
  10. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/LICENSE.mediamtx +0 -0
  11. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/src/toposync_ext_streaming/__init__.py +0 -0
  12. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/src/toposync_ext_streaming/api/__init__.py +0 -0
  13. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/src/toposync_ext_streaming/bin/ffmpeg/LICENSE +0 -0
  14. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/src/toposync_ext_streaming/bin/mediamtx/LICENSE +0 -0
  15. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/src/toposync_ext_streaming/extension.json +0 -0
  16. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/src/toposync_ext_streaming/pipelines/__init__.py +0 -0
  17. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/src/toposync_ext_streaming/pipelines/operators.py +0 -0
  18. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/src/toposync_ext_streaming/plugin.py +0 -0
  19. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/src/toposync_ext_streaming/static/326.js +0 -0
  20. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/src/toposync_ext_streaming/static/326.js.LICENSE.txt +0 -0
  21. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/src/toposync_ext_streaming/static/387.js +0 -0
  22. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/src/toposync_ext_streaming/static/4.js +0 -0
  23. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/src/toposync_ext_streaming/static/4.js.LICENSE.txt +0 -0
  24. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/src/toposync_ext_streaming/static/623.js +0 -0
  25. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/src/toposync_ext_streaming/static/623.js.LICENSE.txt +0 -0
  26. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/src/toposync_ext_streaming/static/703.js +0 -0
  27. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/src/toposync_ext_streaming/static/703.js.LICENSE.txt +0 -0
  28. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/src/toposync_ext_streaming/static/main.js +0 -0
  29. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/src/toposync_ext_streaming/static/main.js.LICENSE.txt +0 -0
  30. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/src/toposync_ext_streaming/static/remoteEntry.js +0 -0
  31. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/src/toposync_ext_streaming/streaming/__init__.py +0 -0
  32. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/src/toposync_ext_streaming/streaming/arbitration.py +0 -0
  33. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/src/toposync_ext_streaming/streaming/camera_ingest.py +0 -0
  34. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/src/toposync_ext_streaming/streaming/distributed_sync.py +0 -0
  35. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/src/toposync_ext_streaming/streaming/encoder_state.py +0 -0
  36. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/src/toposync_ext_streaming/streaming/ffmpeg_binary.py +0 -0
  37. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/src/toposync_ext_streaming/streaming/go2rtc_binary.py +0 -0
  38. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/src/toposync_ext_streaming/streaming/go2rtc_config.py +0 -0
  39. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/src/toposync_ext_streaming/streaming/go2rtc_manager.py +0 -0
  40. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/src/toposync_ext_streaming/streaming/ingest_auth.py +0 -0
  41. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/src/toposync_ext_streaming/streaming/ingest_resolver.py +0 -0
  42. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/src/toposync_ext_streaming/streaming/jsmpeg_manager.py +0 -0
  43. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/src/toposync_ext_streaming/streaming/mediamtx_api_client.py +0 -0
  44. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/src/toposync_ext_streaming/streaming/mediamtx_binary.py +0 -0
  45. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/src/toposync_ext_streaming/streaming/mediamtx_config.py +0 -0
  46. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/src/toposync_ext_streaming/streaming/mediamtx_processes.py +0 -0
  47. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/src/toposync_ext_streaming/streaming/placeholder.py +0 -0
  48. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/src/toposync_ext_streaming/streaming/platform.py +0 -0
  49. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/src/toposync_ext_streaming/streaming/playback_events.py +0 -0
  50. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/src/toposync_ext_streaming/streaming/publisher_manager.py +0 -0
  51. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/src/toposync_ext_streaming/streaming/resize.py +0 -0
  52. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/src/toposync_ext_streaming/streaming/runtime_state.py +0 -0
  53. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/src/toposync_ext_streaming/streaming/writer_bridge.py +0 -0
  54. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/src/toposync_ext_streaming/wizard/__init__.py +0 -0
  55. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/src/toposync_ext_streaming/wizard/pipeline_builder.py +0 -0
  56. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/ui/package.json +0 -0
  57. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/ui/src/activate.tsx +0 -0
  58. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/ui/src/api/streamingApi.ts +0 -0
  59. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/ui/src/constants.ts +0 -0
  60. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/ui/src/entry.ts +0 -0
  61. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/ui/src/settings/StreamingSettingsPanel.tsx +0 -0
  62. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/ui/src/settings/SubModal.tsx +0 -0
  63. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/ui/src/settings/WizardCreatePipelineFromTransmission.tsx +0 -0
  64. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/ui/src/translations.ts +0 -0
  65. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/ui/src/types.ts +0 -0
  66. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/ui/tsconfig.json +0 -0
  67. {toposync_ext_streaming-0.7.9 → toposync_ext_streaming-0.7.10}/ui/webpack.config.js +0 -0
@@ -1,3 +1,16 @@
1
+ Metadata-Version: 2.4
2
+ Name: toposync-ext-streaming
3
+ Version: 0.7.10
4
+ Summary: Toposync first-party extension: streaming settings, API surface, and pipeline sink bootstrap.
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.11
8
+ Requires-Dist: numpy>=1.26
9
+ Requires-Dist: opencv-python-headless>=4.10
10
+ Requires-Dist: toposync-core>=0.3.3
11
+ Requires-Dist: websockets>=12.0
12
+ Description-Content-Type: text/markdown
13
+
1
14
  # Toposync Streaming Extension
2
15
 
3
16
  Extension ID: `com.toposync.streaming`
@@ -729,6 +742,15 @@ All endpoints are under `/api/streams`.
729
742
  ### Home Assistant
730
743
  - `GET /api/streams/home-assistant/cameras`
731
744
 
745
+ For Home Assistant running in Docker Compose, the RTSP URL in this manifest must
746
+ use a host reachable from the Home Assistant container. Enable
747
+ `engine.expose_to_lan`, restart the streaming engine, and set
748
+ `TOPOSYNC_HOME_ASSISTANT_RTSP_HOST` to the Compose service name or another
749
+ container-reachable hostname, for example `toposync`. If the endpoint would
750
+ otherwise return a loopback RTSP URL for a non-loopback request host, the
751
+ manifest omits `rtsp_url` and reports a blocking error instead of publishing an
752
+ unusable `rtsp://127.0.0.1:...` URL.
753
+
732
754
  ### Distributed settings
733
755
  - `GET /api/streams/distributed/settings/{server_id}`
734
756
 
@@ -968,6 +990,10 @@ To enable LAN viewers:
968
990
 
969
991
  Notes:
970
992
  - `GET /api/streams/transmissions/{id}/urls` chooses the host component based on the current request host when `expose_to_lan` is enabled.
993
+ - `GET /api/streams/home-assistant/cameras` follows the same rule, but a
994
+ Compose-based Home Assistant often needs `TOPOSYNC_HOME_ASSISTANT_RTSP_HOST`
995
+ so the advertised RTSP host is the Toposync service name visible from the Home
996
+ Assistant container.
971
997
  - If MediaMTX runs behind Docker/NAT, advertise the reachable LAN host with `TOPOSYNC_STREAMING_WEBRTC_ADDITIONAL_HOSTS` and publish the static ICE UDP port configured by `TOPOSYNC_STREAMING_WEBRTC_LOCAL_UDP_ADDRESS`.
972
998
  - MediaMTX still restricts publishing and API access to localhost IPs; LAN exposure is for viewer playback only.
973
999
 
@@ -1,16 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: toposync-ext-streaming
3
- Version: 0.7.9
4
- Summary: Toposync first-party extension: streaming settings, API surface, and pipeline sink bootstrap.
5
- License-Expression: MIT
6
- License-File: LICENSE
7
- Requires-Python: >=3.11
8
- Requires-Dist: numpy>=1.26
9
- Requires-Dist: opencv-python-headless>=4.10
10
- Requires-Dist: toposync-core>=0.3.3
11
- Requires-Dist: websockets>=12.0
12
- Description-Content-Type: text/markdown
13
-
14
1
  # Toposync Streaming Extension
15
2
 
16
3
  Extension ID: `com.toposync.streaming`
@@ -742,6 +729,15 @@ All endpoints are under `/api/streams`.
742
729
  ### Home Assistant
743
730
  - `GET /api/streams/home-assistant/cameras`
744
731
 
732
+ For Home Assistant running in Docker Compose, the RTSP URL in this manifest must
733
+ use a host reachable from the Home Assistant container. Enable
734
+ `engine.expose_to_lan`, restart the streaming engine, and set
735
+ `TOPOSYNC_HOME_ASSISTANT_RTSP_HOST` to the Compose service name or another
736
+ container-reachable hostname, for example `toposync`. If the endpoint would
737
+ otherwise return a loopback RTSP URL for a non-loopback request host, the
738
+ manifest omits `rtsp_url` and reports a blocking error instead of publishing an
739
+ unusable `rtsp://127.0.0.1:...` URL.
740
+
745
741
  ### Distributed settings
746
742
  - `GET /api/streams/distributed/settings/{server_id}`
747
743
 
@@ -981,6 +977,10 @@ To enable LAN viewers:
981
977
 
982
978
  Notes:
983
979
  - `GET /api/streams/transmissions/{id}/urls` chooses the host component based on the current request host when `expose_to_lan` is enabled.
980
+ - `GET /api/streams/home-assistant/cameras` follows the same rule, but a
981
+ Compose-based Home Assistant often needs `TOPOSYNC_HOME_ASSISTANT_RTSP_HOST`
982
+ so the advertised RTSP host is the Toposync service name visible from the Home
983
+ Assistant container.
984
984
  - If MediaMTX runs behind Docker/NAT, advertise the reachable LAN host with `TOPOSYNC_STREAMING_WEBRTC_ADDITIONAL_HOSTS` and publish the static ICE UDP port configured by `TOPOSYNC_STREAMING_WEBRTC_LOCAL_UDP_ADDRESS`.
985
985
  - MediaMTX still restricts publishing and API access to localhost IPs; LAN exposure is for viewer playback only.
986
986
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "toposync-ext-streaming"
3
- version = "0.7.9"
3
+ version = "0.7.10"
4
4
  description = "Toposync first-party extension: streaming settings, API surface, and pipeline sink bootstrap."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -1034,6 +1034,9 @@ class StreamingEngineStatusResponse(BaseModel):
1034
1034
  warnings: list[str] = Field(default_factory=list)
1035
1035
  restart_count: int = Field(default=0, ge=0)
1036
1036
  orphan_pids: list[int] = Field(default_factory=list)
1037
+ port_policy: Literal["stable", "flexible"] = "flexible"
1038
+ port_resolution: str = "preferred"
1039
+ port_blocking_errors: list[str] = Field(default_factory=list)
1037
1040
 
1038
1041
 
1039
1042
  class StreamingMseSidecarStatusResponse(BaseModel):
@@ -33,7 +33,7 @@ from toposync.runtime.pipelines.operators_sinks import _encode_image_bytes
33
33
  from toposync.runtime.pipelines.templates import camera_names_by_id, safe_pipeline_name
34
34
  from toposync.runtime.services import ServiceRegistry
35
35
 
36
- from ..streaming.engine_manager import MediaMtxEngineManager
36
+ from ..streaming.engine_manager import MediaMtxEngineManager, MediaMtxPortResolutionError
37
37
  from ..streaming.go2rtc_binary import extract_go2rtc_binary, find_installed_go2rtc_binary
38
38
  from ..streaming.go2rtc_manager import Go2RtcSidecarManager, Go2RtcSidecarStatus
39
39
  from ..streaming.camera_ingest import (
@@ -852,7 +852,19 @@ def _build_network_contract(
852
852
  media_url_origin=media_url_origin,
853
853
  )
854
854
 
855
- fail_on_mismatch = _env_bool("TOPOSYNC_FAIL_STREAM_URLS_ON_PORT_MISMATCH")
855
+ explicit_expected_ports = any(
856
+ value is not None
857
+ for value in (
858
+ expected_ports.direct_api,
859
+ expected_ports.rtsp,
860
+ expected_ports.hls,
861
+ expected_ports.webrtc,
862
+ expected_ports.webrtc_udp,
863
+ expected_ports.api,
864
+ )
865
+ )
866
+ strict_contract = environment == "home_assistant_addon" or explicit_expected_ports
867
+ fail_on_mismatch = _env_bool("TOPOSYNC_FAIL_STREAM_URLS_ON_PORT_MISMATCH") or strict_contract
856
868
  status: Literal["ok", "port_mismatch", "proxy_required", "proxy_unavailable"] = "ok"
857
869
 
858
870
  def compare_port(
@@ -869,11 +881,16 @@ def _build_network_contract(
869
881
  actual = getattr(actual_ports, key)
870
882
  if expected is None or actual is None or int(expected) == int(actual):
871
883
  return
872
- message = f"{label} active port {actual} does not match expected add-on port {expected}."
884
+ expected_label = (
885
+ "expected add-on port"
886
+ if environment == "home_assistant_addon"
887
+ else "expected streaming port"
888
+ )
889
+ message = f"{label} active port {actual} does not match {expected_label} {expected}."
873
890
  warnings.append(message)
874
891
  if status == "ok":
875
892
  status = "port_mismatch"
876
- if blocking_when_failed and fail_on_mismatch:
893
+ if (blocking_when_failed and fail_on_mismatch) or strict_contract:
877
894
  blocking_errors.append(message)
878
895
 
879
896
  compare_port(
@@ -2626,7 +2643,12 @@ def _url_has_loopback_host(url: str | None) -> bool:
2626
2643
  host = str(urllib_parse.urlsplit(str(url or "")).hostname or "").strip().lower()
2627
2644
  except Exception:
2628
2645
  return False
2629
- return host in {"localhost", "::1"} or host.startswith("127.")
2646
+ return _host_is_loopback(host)
2647
+
2648
+
2649
+ def _host_is_loopback(host: str | None) -> bool:
2650
+ normalized = str(host or "").strip().lower()
2651
+ return normalized in {"localhost", "::1"} or normalized.startswith("127.")
2630
2652
 
2631
2653
 
2632
2654
  def _url_host_for_rtsp(url: str | None) -> str:
@@ -2940,10 +2962,17 @@ async def _build_home_assistant_camera_item(
2940
2962
  blocking_errors.append(f"Failed to resolve remote Home Assistant camera playback: {exc}")
2941
2963
  else:
2942
2964
  engine_path = resolve_output_engine_path(transmission, output)
2965
+ home_assistant_rtsp_host = _home_assistant_rtsp_host(request)
2943
2966
  rtsp_url = await _engine_manager(request).get_read_url_for_path(
2944
2967
  engine_path,
2945
- host=_home_assistant_rtsp_host(request),
2968
+ host=home_assistant_rtsp_host,
2946
2969
  )
2970
+ if _url_has_loopback_host(rtsp_url) and not _host_is_loopback(home_assistant_rtsp_host):
2971
+ blocking_errors.append(
2972
+ "Local streaming engine returned a loopback RTSP URL for Home Assistant camera playback; "
2973
+ "enable engine.expose_to_lan and set TOPOSYNC_HOME_ASSISTANT_RTSP_HOST to a host reachable by Home Assistant."
2974
+ )
2975
+ rtsp_url = None
2947
2976
  redacted_rtsp_url = _redact_url_credentials(rtsp_url)
2948
2977
 
2949
2978
  role = variant.role if variant is not None else None
@@ -6153,6 +6182,10 @@ def create_streaming_router() -> APIRouter:
6153
6182
  path_auth=path_auth,
6154
6183
  path_configs=path_configs,
6155
6184
  )
6185
+ except MediaMtxPortResolutionError as exc:
6186
+ raise HTTPException(
6187
+ status_code=409, detail=f"Streaming engine port contract is blocked: {exc}"
6188
+ ) from exc
6156
6189
  except Exception as exc:
6157
6190
  raise HTTPException(
6158
6191
  status_code=500, detail=f"Failed to apply streaming settings: {exc}"
@@ -6277,6 +6310,7 @@ def create_streaming_router() -> APIRouter:
6277
6310
 
6278
6311
  warnings: list[str] = []
6279
6312
  warnings.extend(list(status.warnings))
6313
+ warnings.extend(list(status.port_blocking_errors))
6280
6314
  if not settings.engine.enabled:
6281
6315
  warnings.append("Engine is disabled in settings.")
6282
6316
  if settings.engine.enabled and not status.running:
@@ -6354,6 +6388,9 @@ def create_streaming_router() -> APIRouter:
6354
6388
  warnings=warnings,
6355
6389
  restart_count=status.restart_count,
6356
6390
  orphan_pids=orphan_pids,
6391
+ port_policy=status.port_policy if status.port_policy in {"stable", "flexible"} else "flexible",
6392
+ port_resolution=status.port_resolution,
6393
+ port_blocking_errors=list(status.port_blocking_errors),
6357
6394
  )
6358
6395
 
6359
6396
  @router.post("/engine/download", response_model=StreamingEngineStatusResponse)
@@ -6416,6 +6453,10 @@ def create_streaming_router() -> APIRouter:
6416
6453
  ),
6417
6454
  path_configs=build_camera_ingest_path_configs(camera_ingest_by_id),
6418
6455
  )
6456
+ except MediaMtxPortResolutionError as exc:
6457
+ raise HTTPException(
6458
+ status_code=409, detail=f"Streaming engine port contract is blocked: {exc}"
6459
+ ) from exc
6419
6460
  except Exception as exc:
6420
6461
  raise HTTPException(
6421
6462
  status_code=500, detail=f"Failed to start streaming engine: {exc}"
@@ -6477,6 +6518,10 @@ def create_streaming_router() -> APIRouter:
6477
6518
  ),
6478
6519
  path_configs=build_camera_ingest_path_configs(camera_ingest_by_id),
6479
6520
  )
6521
+ except MediaMtxPortResolutionError as exc:
6522
+ raise HTTPException(
6523
+ status_code=409, detail=f"Streaming engine port contract is blocked: {exc}"
6524
+ ) from exc
6480
6525
  except Exception as exc:
6481
6526
  raise HTTPException(
6482
6527
  status_code=500, detail=f"Failed to restart streaming engine: {exc}"
@@ -6532,6 +6577,12 @@ def create_streaming_router() -> APIRouter:
6532
6577
  ),
6533
6578
  path_configs=build_camera_ingest_path_configs(camera_ingest_by_id),
6534
6579
  )
6580
+ except MediaMtxPortResolutionError as exc:
6581
+ suffix = f" (killed {len(killed_pids)} stale process(es))" if killed_pids else ""
6582
+ raise HTTPException(
6583
+ status_code=409,
6584
+ detail=f"Streaming engine port contract is blocked: {exc}{suffix}",
6585
+ ) from exc
6535
6586
  except Exception as exc:
6536
6587
  suffix = f" (killed {len(killed_pids)} stale process(es))" if killed_pids else ""
6537
6588
  raise HTTPException(
@@ -6864,6 +6915,10 @@ def create_streaming_router() -> APIRouter:
6864
6915
  await _apply_streaming_engine_state(request, settings=saved)
6865
6916
  except HTTPException:
6866
6917
  raise
6918
+ except MediaMtxPortResolutionError as exc:
6919
+ raise HTTPException(
6920
+ status_code=409, detail=f"Streaming engine port contract is blocked: {exc}"
6921
+ ) from exc
6867
6922
  except Exception as exc:
6868
6923
  raise HTTPException(
6869
6924
  status_code=500, detail=f"Failed to apply streaming settings: {exc}"
@@ -6911,6 +6966,10 @@ def create_streaming_router() -> APIRouter:
6911
6966
  await _apply_streaming_engine_state(request, settings=saved)
6912
6967
  except HTTPException:
6913
6968
  raise
6969
+ except MediaMtxPortResolutionError as exc:
6970
+ raise HTTPException(
6971
+ status_code=409, detail=f"Streaming engine port contract is blocked: {exc}"
6972
+ ) from exc
6914
6973
  except Exception as exc:
6915
6974
  raise HTTPException(
6916
6975
  status_code=500, detail=f"Failed to apply streaming settings: {exc}"
@@ -7111,6 +7170,10 @@ def create_streaming_router() -> APIRouter:
7111
7170
  ),
7112
7171
  path_configs=build_camera_ingest_path_configs(camera_ingest_by_id),
7113
7172
  )
7173
+ except MediaMtxPortResolutionError as exc:
7174
+ raise HTTPException(
7175
+ status_code=409, detail=f"Streaming engine port contract is blocked: {exc}"
7176
+ ) from exc
7114
7177
  except Exception as exc:
7115
7178
  raise HTTPException(
7116
7179
  status_code=500, detail=f"Failed to apply streaming settings: {exc}"
@@ -7202,6 +7265,10 @@ def create_streaming_router() -> APIRouter:
7202
7265
  ),
7203
7266
  path_configs=build_camera_ingest_path_configs(camera_ingest_by_id),
7204
7267
  )
7268
+ except MediaMtxPortResolutionError as exc:
7269
+ raise HTTPException(
7270
+ status_code=409, detail=f"Streaming engine port contract is blocked: {exc}"
7271
+ ) from exc
7205
7272
  except Exception as exc:
7206
7273
  raise HTTPException(
7207
7274
  status_code=500, detail=f"Failed to apply streaming settings: {exc}"
@@ -7271,6 +7338,10 @@ def create_streaming_router() -> APIRouter:
7271
7338
  ),
7272
7339
  path_configs=build_camera_ingest_path_configs(camera_ingest_by_id),
7273
7340
  )
7341
+ except MediaMtxPortResolutionError as exc:
7342
+ raise HTTPException(
7343
+ status_code=409, detail=f"Streaming engine port contract is blocked: {exc}"
7344
+ ) from exc
7274
7345
  except Exception as exc:
7275
7346
  raise HTTPException(
7276
7347
  status_code=500, detail=f"Failed to apply streaming settings: {exc}"
@@ -7342,6 +7413,10 @@ def create_streaming_router() -> APIRouter:
7342
7413
  ),
7343
7414
  path_configs=build_camera_ingest_path_configs(camera_ingest_by_id),
7344
7415
  )
7416
+ except MediaMtxPortResolutionError as exc:
7417
+ raise HTTPException(
7418
+ status_code=409, detail=f"Streaming engine port contract is blocked: {exc}"
7419
+ ) from exc
7345
7420
  except Exception as exc:
7346
7421
  raise HTTPException(
7347
7422
  status_code=500, detail=f"Failed to apply streaming settings: {exc}"
@@ -7390,6 +7465,10 @@ def create_streaming_router() -> APIRouter:
7390
7465
  ),
7391
7466
  path_configs=build_camera_ingest_path_configs(camera_ingest_by_id),
7392
7467
  )
7468
+ except MediaMtxPortResolutionError as exc:
7469
+ raise HTTPException(
7470
+ status_code=409, detail=f"Streaming engine port contract is blocked: {exc}"
7471
+ ) from exc
7393
7472
  except Exception as exc:
7394
7473
  raise HTTPException(
7395
7474
  status_code=500, detail=f"Failed to apply streaming settings: {exc}"
@@ -8646,6 +8725,11 @@ def create_streaming_router() -> APIRouter:
8646
8725
  ),
8647
8726
  path_configs=build_camera_ingest_path_configs(camera_ingest_by_id),
8648
8727
  )
8728
+ except MediaMtxPortResolutionError as exc:
8729
+ raise HTTPException(
8730
+ status_code=409,
8731
+ detail=f"Streaming engine port contract is blocked: {exc}",
8732
+ ) from exc
8649
8733
  except Exception as exc:
8650
8734
  raise HTTPException(
8651
8735
  status_code=500, detail=f"Failed to rotate ingest credentials: {exc}"