toposync-ext-streaming 0.4.5__tar.gz → 0.4.6__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 (68) hide show
  1. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/PKG-INFO +2 -2
  2. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/README.md +1 -1
  3. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/pyproject.toml +1 -1
  4. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/src/toposync_ext_streaming/api/models.py +21 -0
  5. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/src/toposync_ext_streaming/api/routes.py +287 -3
  6. toposync_ext_streaming-0.4.6/src/toposync_ext_streaming/static/703.js +2 -0
  7. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/ui/src/settings/StreamingSettingsPanel.tsx +194 -6
  8. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/ui/src/translations.ts +32 -0
  9. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/ui/src/types.ts +17 -0
  10. toposync_ext_streaming-0.4.5/src/toposync_ext_streaming/static/703.js +0 -2
  11. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/.gitignore +0 -0
  12. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/LICENSE +0 -0
  13. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/LICENSE.ffmpeg +0 -0
  14. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/LICENSE.mediamtx +0 -0
  15. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/src/toposync_ext_streaming/__init__.py +0 -0
  16. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/src/toposync_ext_streaming/api/__init__.py +0 -0
  17. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/src/toposync_ext_streaming/bin/ffmpeg/LICENSE +0 -0
  18. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/src/toposync_ext_streaming/bin/mediamtx/LICENSE +0 -0
  19. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/src/toposync_ext_streaming/extension.json +0 -0
  20. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/src/toposync_ext_streaming/pipelines/__init__.py +0 -0
  21. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/src/toposync_ext_streaming/pipelines/operators.py +0 -0
  22. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/src/toposync_ext_streaming/plugin.py +0 -0
  23. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/src/toposync_ext_streaming/static/326.js +0 -0
  24. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/src/toposync_ext_streaming/static/326.js.LICENSE.txt +0 -0
  25. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/src/toposync_ext_streaming/static/387.js +0 -0
  26. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/src/toposync_ext_streaming/static/4.js +0 -0
  27. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/src/toposync_ext_streaming/static/4.js.LICENSE.txt +0 -0
  28. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/src/toposync_ext_streaming/static/623.js +0 -0
  29. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/src/toposync_ext_streaming/static/623.js.LICENSE.txt +0 -0
  30. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/src/toposync_ext_streaming/static/703.js.LICENSE.txt +0 -0
  31. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/src/toposync_ext_streaming/static/main.js +0 -0
  32. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/src/toposync_ext_streaming/static/main.js.LICENSE.txt +0 -0
  33. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/src/toposync_ext_streaming/static/remoteEntry.js +0 -0
  34. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/src/toposync_ext_streaming/streaming/__init__.py +0 -0
  35. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/src/toposync_ext_streaming/streaming/arbitration.py +0 -0
  36. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/src/toposync_ext_streaming/streaming/camera_ingest.py +0 -0
  37. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/src/toposync_ext_streaming/streaming/distributed_sync.py +0 -0
  38. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/src/toposync_ext_streaming/streaming/encoder_state.py +0 -0
  39. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/src/toposync_ext_streaming/streaming/engine_manager.py +0 -0
  40. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/src/toposync_ext_streaming/streaming/ffmpeg_binary.py +0 -0
  41. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/src/toposync_ext_streaming/streaming/go2rtc_binary.py +0 -0
  42. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/src/toposync_ext_streaming/streaming/go2rtc_config.py +0 -0
  43. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/src/toposync_ext_streaming/streaming/go2rtc_manager.py +0 -0
  44. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/src/toposync_ext_streaming/streaming/ingest_auth.py +0 -0
  45. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/src/toposync_ext_streaming/streaming/ingest_resolver.py +0 -0
  46. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/src/toposync_ext_streaming/streaming/jsmpeg_manager.py +0 -0
  47. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/src/toposync_ext_streaming/streaming/mediamtx_api_client.py +0 -0
  48. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/src/toposync_ext_streaming/streaming/mediamtx_binary.py +0 -0
  49. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/src/toposync_ext_streaming/streaming/mediamtx_config.py +0 -0
  50. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/src/toposync_ext_streaming/streaming/mediamtx_processes.py +0 -0
  51. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/src/toposync_ext_streaming/streaming/placeholder.py +0 -0
  52. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/src/toposync_ext_streaming/streaming/platform.py +0 -0
  53. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/src/toposync_ext_streaming/streaming/playback_events.py +0 -0
  54. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/src/toposync_ext_streaming/streaming/publisher_manager.py +0 -0
  55. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/src/toposync_ext_streaming/streaming/resize.py +0 -0
  56. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/src/toposync_ext_streaming/streaming/runtime_state.py +0 -0
  57. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/src/toposync_ext_streaming/streaming/writer_bridge.py +0 -0
  58. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/src/toposync_ext_streaming/wizard/__init__.py +0 -0
  59. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/src/toposync_ext_streaming/wizard/pipeline_builder.py +0 -0
  60. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/ui/package.json +0 -0
  61. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/ui/src/activate.tsx +0 -0
  62. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/ui/src/api/streamingApi.ts +0 -0
  63. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/ui/src/constants.ts +0 -0
  64. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/ui/src/entry.ts +0 -0
  65. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/ui/src/settings/SubModal.tsx +0 -0
  66. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/ui/src/settings/WizardCreatePipelineFromTransmission.tsx +0 -0
  67. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/ui/tsconfig.json +0 -0
  68. {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.6}/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.5
3
+ Version: 0.4.6
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
@@ -32,7 +32,7 @@ The core design goal is reliability in local-first setups with highly dynamic pi
32
32
  - Encoding should happen only when there is actual playback demand.
33
33
 
34
34
  The canonical product and engineering principles for streaming are documented in
35
- [`docs/toposync-streaming-dossier-solid-priorities.md`](../../docs/toposync-streaming-dossier-solid-priorities.md#00-principios-permanentes-de-streaming).
35
+ [`docs-site/docs/developers/media-decisions.mdx`](../../docs-site/docs/developers/media-decisions.mdx).
36
36
  In short:
37
37
 
38
38
  - User-facing flows deal with publishable sources and variants; `Transmission`, outputs, engine paths, and quality profile IDs are advanced artifacts.
@@ -19,7 +19,7 @@ The core design goal is reliability in local-first setups with highly dynamic pi
19
19
  - Encoding should happen only when there is actual playback demand.
20
20
 
21
21
  The canonical product and engineering principles for streaming are documented in
22
- [`docs/toposync-streaming-dossier-solid-priorities.md`](../../docs/toposync-streaming-dossier-solid-priorities.md#00-principios-permanentes-de-streaming).
22
+ [`docs-site/docs/developers/media-decisions.mdx`](../../docs-site/docs/developers/media-decisions.mdx).
23
23
  In short:
24
24
 
25
25
  - User-facing flows deal with publishable sources and variants; `Transmission`, outputs, engine paths, and quality profile IDs are advanced artifacts.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "toposync-ext-streaming"
3
- version = "0.4.5"
3
+ version = "0.4.6"
4
4
  description = "Toposync first-party extension: streaming settings, API surface, and pipeline sink bootstrap."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -14,6 +14,7 @@ from ..streaming.mediamtx_config import normalize_path_slug
14
14
  EXTENSION_ID = "com.toposync.streaming"
15
15
  TEST_PATH = "test"
16
16
  StreamingRuntimeStatus = Literal["live", "degraded", "stale", "offline"]
17
+ StreamingSummaryStatus = Literal["working", "warming", "action_required"]
17
18
  StreamingStreamBehavior = Literal["continuous", "event_gated"]
18
19
  StreamingEncoderMode = Literal["auto", "cpu"]
19
20
  StreamingOutputEncoderMode = Literal["inherit", "auto", "cpu"]
@@ -1055,6 +1056,10 @@ class StreamingMseSidecarStatusResponse(BaseModel):
1055
1056
  stream_count: int = Field(default=0, ge=0)
1056
1057
  warnings: list[str] = Field(default_factory=list)
1057
1058
  restart_count: int = Field(default=0, ge=0)
1059
+ summary_status: StreamingSummaryStatus = "warming"
1060
+ summary_message: str = ""
1061
+ summary_action: str | None = None
1062
+ technical_status: str = "unknown"
1058
1063
 
1059
1064
 
1060
1065
  class StreamingJsmpegStatusResponse(BaseModel):
@@ -1479,6 +1484,10 @@ class StreamingOutputRuntimeStatus(BaseModel):
1479
1484
  demand_idle: bool = False
1480
1485
  classification: StreamingObservabilityClassification = "unknown"
1481
1486
  evidence: list[str] = Field(default_factory=list)
1487
+ summary_status: StreamingSummaryStatus = "warming"
1488
+ summary_message: str = ""
1489
+ summary_action: str | None = None
1490
+ technical_status: str = "unknown"
1482
1491
  active_playback_session_count: int = Field(default=0, ge=0)
1483
1492
  last_playback_event_at_unix: float | None = None
1484
1493
  publisher_frames_sent_rate: float | None = None
@@ -1529,6 +1538,10 @@ class StreamingRuntimeOutputHealth(BaseModel):
1529
1538
  demand_idle: bool = False
1530
1539
  classification: StreamingObservabilityClassification = "unknown"
1531
1540
  evidence: list[str] = Field(default_factory=list)
1541
+ summary_status: StreamingSummaryStatus = "warming"
1542
+ summary_message: str = ""
1543
+ summary_action: str | None = None
1544
+ technical_status: str = "unknown"
1532
1545
  active_playback_session_count: int = Field(default=0, ge=0)
1533
1546
  last_playback_event_at_unix: float | None = None
1534
1547
  publisher_frames_sent_rate: float | None = None
@@ -1558,6 +1571,10 @@ class StreamingRuntimeTransmissionHealth(BaseModel):
1558
1571
  demand_idle: bool = False
1559
1572
  classification: StreamingObservabilityClassification = "unknown"
1560
1573
  evidence: list[str] = Field(default_factory=list)
1574
+ summary_status: StreamingSummaryStatus = "warming"
1575
+ summary_message: str = ""
1576
+ summary_action: str | None = None
1577
+ technical_status: str = "unknown"
1561
1578
  active_playback_session_count: int = Field(default=0, ge=0)
1562
1579
  last_playback_event_at_unix: float | None = None
1563
1580
  source_health: StreamingRuntimeSourceHealth | None = None
@@ -1678,6 +1695,10 @@ class StreamingRuntimeObservabilityItem(BaseModel):
1678
1695
  output_id: str | None = None
1679
1696
  classification: StreamingObservabilityClassification
1680
1697
  evidence: list[str] = Field(default_factory=list)
1698
+ summary_status: StreamingSummaryStatus = "warming"
1699
+ summary_message: str = ""
1700
+ summary_action: str | None = None
1701
+ technical_status: str = "unknown"
1681
1702
  active_playback_sessions: list[StreamingPlaybackSessionSummary] = Field(default_factory=list)
1682
1703
  last_playback_event_at_unix: float | None = None
1683
1704
  publisher_frames_sent_rate: float | None = None
@@ -1554,6 +1554,53 @@ async def _wait_for_mse_sidecar_api_reachable(
1554
1554
  await asyncio.sleep(max(0.05, float(poll_s)))
1555
1555
 
1556
1556
 
1557
+ def _mse_sidecar_summary(
1558
+ *,
1559
+ enabled: bool,
1560
+ engine_enabled: bool,
1561
+ running: bool,
1562
+ api_reachable: bool,
1563
+ last_error: str | None,
1564
+ ) -> dict[str, Any]:
1565
+ if not enabled:
1566
+ return _runtime_summary_payload(
1567
+ status="working",
1568
+ message="MSE sidecar is disabled.",
1569
+ technical_status="disabled",
1570
+ )
1571
+ if not engine_enabled:
1572
+ return _runtime_summary_payload(
1573
+ status="action_required",
1574
+ message="Action needed: MSE needs the streaming engine to be enabled.",
1575
+ action="Enable the streaming engine before using MSE playback.",
1576
+ technical_status="engine_disabled",
1577
+ )
1578
+ if running and api_reachable:
1579
+ return _runtime_summary_payload(
1580
+ status="working",
1581
+ message="Working.",
1582
+ technical_status="running",
1583
+ )
1584
+ if running:
1585
+ return _runtime_summary_payload(
1586
+ status="warming",
1587
+ message="MSE sidecar is restarting, wait a few seconds.",
1588
+ technical_status="running_unreachable",
1589
+ )
1590
+ if last_error:
1591
+ return _runtime_summary_payload(
1592
+ status="action_required",
1593
+ message="Action needed: MSE sidecar is not running.",
1594
+ action="Restart MSE or inspect the go2rtc log.",
1595
+ technical_status="stopped_error",
1596
+ )
1597
+ return _runtime_summary_payload(
1598
+ status="warming",
1599
+ message="MSE sidecar is starting.",
1600
+ technical_status="stopped",
1601
+ )
1602
+
1603
+
1557
1604
  async def _prime_mse_proxy_demand(
1558
1605
  request: Request | WebSocket,
1559
1606
  *,
@@ -3898,6 +3945,214 @@ def _classify_observability(
3898
3945
  return "unknown", ["Insufficient recent evidence to classify the stream."]
3899
3946
 
3900
3947
 
3948
+ def _source_health_requires_action(source_health: StreamingRuntimeSourceHealth | None) -> bool:
3949
+ if source_health is None:
3950
+ return False
3951
+ if source_health.ingest_blocking_errors:
3952
+ return True
3953
+ return source_health.status in {
3954
+ "stale",
3955
+ "unreachable",
3956
+ "unauthorized",
3957
+ "error",
3958
+ }
3959
+
3960
+
3961
+ def _source_health_action(source_health: StreamingRuntimeSourceHealth | None) -> str | None:
3962
+ if source_health is None:
3963
+ return None
3964
+ if source_health.recommended_action:
3965
+ return source_health.recommended_action
3966
+ if source_health.ingest_blocking_errors:
3967
+ return "Review the ingest configuration before testing the stream again."
3968
+ if source_health.status in {"unreachable", "unauthorized", "error", "stale"}:
3969
+ return "Test RTSP and review the camera source URL or credentials."
3970
+ return None
3971
+
3972
+
3973
+ def _runtime_summary_has_fresh_frame(
3974
+ *,
3975
+ health: StreamingRuntimeTransmissionHealth,
3976
+ output: StreamingRuntimeOutputHealth | None,
3977
+ ) -> bool:
3978
+ target_status = output.status if output is not None else health.status
3979
+ if target_status not in {"live", "degraded"}:
3980
+ return False
3981
+ if health.stale or health.placeholder_active:
3982
+ return False
3983
+ if output is not None and not output.publisher_running:
3984
+ return False
3985
+
3986
+ ages = [
3987
+ health.selected_frame_age_seconds,
3988
+ health.last_incoming_frame_age_seconds,
3989
+ ]
3990
+ source_health = output.source_health if output is not None else health.source_health
3991
+ if source_health is not None:
3992
+ ages.append(source_health.source_frame_age_seconds)
3993
+ numeric_ages = [float(age) for age in ages if isinstance(age, int | float)]
3994
+ if numeric_ages:
3995
+ return min(numeric_ages) <= 10.0
3996
+ return True
3997
+
3998
+
3999
+ def _runtime_summary_payload(
4000
+ *,
4001
+ status: Literal["working", "warming", "action_required"],
4002
+ message: str,
4003
+ technical_status: str,
4004
+ action: str | None = None,
4005
+ ) -> dict[str, Any]:
4006
+ return {
4007
+ "summary_status": status,
4008
+ "summary_message": message,
4009
+ "summary_action": action,
4010
+ "technical_status": technical_status or "unknown",
4011
+ }
4012
+
4013
+
4014
+ def _runtime_health_summary(
4015
+ *,
4016
+ health: StreamingRuntimeTransmissionHealth,
4017
+ output: StreamingRuntimeOutputHealth | None,
4018
+ ) -> dict[str, Any]:
4019
+ target_status = output.status if output is not None else health.status
4020
+ classification = str(output.classification if output is not None else health.classification or "unknown")
4021
+ source_health = output.source_health if output is not None else health.source_health
4022
+ fresh_frame = _runtime_summary_has_fresh_frame(health=health, output=output)
4023
+ demand_idle = bool(output.demand_idle if output is not None else health.demand_idle)
4024
+ event_gated_idle = bool(output.event_gated_idle if output is not None else health.event_gated_idle)
4025
+ fallback_active = bool(health.fallback_active or (output.publisher_encoder_fallback_active if output is not None else False))
4026
+
4027
+ if classification == "network_contract_error":
4028
+ return _runtime_summary_payload(
4029
+ status="action_required",
4030
+ message="Action needed: network settings are blocking playback.",
4031
+ action="Review the streaming network contract and exposed ports.",
4032
+ technical_status=classification,
4033
+ )
4034
+
4035
+ if _source_health_requires_action(source_health):
4036
+ return _runtime_summary_payload(
4037
+ status="action_required",
4038
+ message="Action needed: the camera source is not delivering usable frames.",
4039
+ action=_source_health_action(source_health),
4040
+ technical_status=classification,
4041
+ )
4042
+
4043
+ if classification in {"source_stale", "source_pipeline_stale"} and not fresh_frame:
4044
+ return _runtime_summary_payload(
4045
+ status="action_required",
4046
+ message="Action needed: no fresh frame is available for this stream.",
4047
+ action="Check the camera source and stream pipeline.",
4048
+ technical_status=classification,
4049
+ )
4050
+
4051
+ if classification == "publisher_down":
4052
+ publisher_running = bool(output.publisher_running) if output is not None else True
4053
+ active_demand = bool(_output_has_active_demand(output)) if output is not None else health.active_playback_session_count > 0
4054
+ if active_demand and not (fresh_frame and publisher_running):
4055
+ return _runtime_summary_payload(
4056
+ status="action_required",
4057
+ message="Action needed: the stream publisher is down while playback is requested.",
4058
+ action="Restart streaming or inspect the publisher logs.",
4059
+ technical_status=classification,
4060
+ )
4061
+ if fresh_frame:
4062
+ return _runtime_summary_payload(
4063
+ status="working",
4064
+ message="Working.",
4065
+ technical_status=classification,
4066
+ )
4067
+
4068
+ if classification in {"auth_url_error", "webrtc_transport_error", "app_player_lifecycle"}:
4069
+ if fresh_frame:
4070
+ return _runtime_summary_payload(
4071
+ status="working",
4072
+ message="Working.",
4073
+ technical_status=classification,
4074
+ )
4075
+ return _runtime_summary_payload(
4076
+ status="action_required",
4077
+ message="Action needed: playback failed and has not recovered.",
4078
+ action="Open the player again and review the playback diagnostics if it still fails.",
4079
+ technical_status=classification,
4080
+ )
4081
+
4082
+ if demand_idle or classification == "demand_idle":
4083
+ return _runtime_summary_payload(
4084
+ status="warming",
4085
+ message="Waiting for viewer demand.",
4086
+ technical_status=classification,
4087
+ )
4088
+
4089
+ if event_gated_idle or classification == "event_gated_idle":
4090
+ return _runtime_summary_payload(
4091
+ status="warming",
4092
+ message="Waiting for an event frame.",
4093
+ technical_status=classification,
4094
+ )
4095
+
4096
+ if classification in {"hls_tail_unavailable", "hls_playlist_stale"}:
4097
+ return _runtime_summary_payload(
4098
+ status="warming",
4099
+ message="Warming up playback, wait a few seconds.",
4100
+ technical_status=classification,
4101
+ )
4102
+
4103
+ if fallback_active and fresh_frame:
4104
+ return _runtime_summary_payload(
4105
+ status="warming",
4106
+ message="Recovering with a fallback frame.",
4107
+ technical_status=classification,
4108
+ )
4109
+
4110
+ if target_status == "stale" or health.stale:
4111
+ return _runtime_summary_payload(
4112
+ status="action_required",
4113
+ message="Action needed: the selected stream frame is stale.",
4114
+ action="Check the camera source and stream pipeline.",
4115
+ technical_status=classification,
4116
+ )
4117
+
4118
+ if target_status == "offline":
4119
+ message = "Stream is disabled." if not health.enabled else "Waiting for the first frame."
4120
+ return _runtime_summary_payload(
4121
+ status="warming",
4122
+ message=message,
4123
+ technical_status=classification,
4124
+ )
4125
+
4126
+ if fresh_frame:
4127
+ return _runtime_summary_payload(
4128
+ status="working",
4129
+ message="Working.",
4130
+ technical_status=classification,
4131
+ )
4132
+
4133
+ return _runtime_summary_payload(
4134
+ status="warming",
4135
+ message="Starting stream runtime.",
4136
+ technical_status=classification,
4137
+ )
4138
+
4139
+
4140
+ def _apply_runtime_health_summaries(health: StreamingRuntimeHealthResponse) -> StreamingRuntimeHealthResponse:
4141
+ for transmission in health.transmissions:
4142
+ for output in transmission.outputs:
4143
+ payload = _runtime_health_summary(health=transmission, output=output)
4144
+ output.summary_status = payload["summary_status"]
4145
+ output.summary_message = payload["summary_message"]
4146
+ output.summary_action = payload["summary_action"]
4147
+ output.technical_status = payload["technical_status"]
4148
+ payload = _runtime_health_summary(health=transmission, output=None)
4149
+ transmission.summary_status = payload["summary_status"]
4150
+ transmission.summary_message = payload["summary_message"]
4151
+ transmission.summary_action = payload["summary_action"]
4152
+ transmission.technical_status = payload["technical_status"]
4153
+ return health
4154
+
4155
+
3901
4156
  def _publisher_frames_sent_rate(
3902
4157
  *,
3903
4158
  request: Request,
@@ -3985,7 +4240,7 @@ async def _annotate_runtime_health_observability(
3985
4240
  transmission.evidence = winner[1]
3986
4241
  transmission.active_playback_session_count = len(active_sessions)
3987
4242
  transmission.last_playback_event_at_unix = last_event_at
3988
- return health
4243
+ return _apply_runtime_health_summaries(health)
3989
4244
 
3990
4245
 
3991
4246
  def _mediamtx_output_snapshot(mediamtx_snapshot: dict[str, Any], *, path: str) -> dict[str, Any]:
@@ -4058,6 +4313,10 @@ async def _build_runtime_observability(
4058
4313
  transmission_id=transmission.transmission_id,
4059
4314
  classification=transmission.classification,
4060
4315
  evidence=transmission.evidence,
4316
+ summary_status=transmission.summary_status,
4317
+ summary_message=transmission.summary_message,
4318
+ summary_action=transmission.summary_action,
4319
+ technical_status=transmission.technical_status,
4061
4320
  active_playback_sessions=sessions,
4062
4321
  last_playback_event_at_unix=transmission.last_playback_event_at_unix,
4063
4322
  health=transmission,
@@ -4077,6 +4336,10 @@ async def _build_runtime_observability(
4077
4336
  output_id=output.output_id,
4078
4337
  classification=output.classification,
4079
4338
  evidence=output.evidence,
4339
+ summary_status=output.summary_status,
4340
+ summary_message=output.summary_message,
4341
+ summary_action=output.summary_action,
4342
+ technical_status=output.technical_status,
4080
4343
  active_playback_sessions=sessions,
4081
4344
  last_playback_event_at_unix=output.last_playback_event_at_unix,
4082
4345
  publisher_frames_sent_rate=output.publisher_frames_sent_rate,
@@ -6318,10 +6581,18 @@ def create_streaming_router() -> APIRouter:
6318
6581
  "go2rtc binary is not installed yet. The next MSE start will download it automatically (internet required), "
6319
6582
  "or set TOPOSYNC_STREAMING_GO2RTC_PATH to a local path."
6320
6583
  )
6584
+ api_reachable = await _mse_sidecar_api_reachable(status)
6585
+ summary = _mse_sidecar_summary(
6586
+ enabled=bool(settings.engine.mse_sidecar.enabled),
6587
+ engine_enabled=bool(settings.engine.enabled),
6588
+ running=status.running,
6589
+ api_reachable=api_reachable,
6590
+ last_error=status.last_error,
6591
+ )
6321
6592
  return StreamingMseSidecarStatusResponse(
6322
6593
  enabled=bool(settings.engine.mse_sidecar.enabled),
6323
6594
  running=status.running,
6324
- api_reachable=await _mse_sidecar_api_reachable(status),
6595
+ api_reachable=api_reachable,
6325
6596
  pid=status.pid,
6326
6597
  uptime_seconds=status.uptime_seconds,
6327
6598
  started_at_unix=status.started_at_unix,
@@ -6336,6 +6607,10 @@ def create_streaming_router() -> APIRouter:
6336
6607
  stream_count=status.stream_count,
6337
6608
  warnings=_dedupe_messages(warnings),
6338
6609
  restart_count=status.restart_count,
6610
+ summary_status=summary["summary_status"],
6611
+ summary_message=summary["summary_message"],
6612
+ summary_action=summary["summary_action"],
6613
+ technical_status=summary["technical_status"],
6339
6614
  )
6340
6615
 
6341
6616
  @router.post("/mse/download", response_model=StreamingMseSidecarStatusResponse)
@@ -6406,7 +6681,8 @@ def create_streaming_router() -> APIRouter:
6406
6681
  settings = await _save_settings(config_store, settings)
6407
6682
  try:
6408
6683
  streams = await _build_mse_sidecar_streams(request, settings=settings)
6409
- await _mse_sidecar_manager(request).restart(settings.engine.mse_sidecar, streams=streams)
6684
+ status = await _mse_sidecar_manager(request).restart(settings.engine.mse_sidecar, streams=streams)
6685
+ await _wait_for_mse_sidecar_api_reachable(status, timeout_s=2.5)
6410
6686
  except Exception as exc:
6411
6687
  raise HTTPException(status_code=500, detail=f"Failed to restart MSE sidecar: {exc}") from exc
6412
6688
  return await mse_sidecar_status(request)
@@ -8235,6 +8511,10 @@ def create_streaming_router() -> APIRouter:
8235
8511
  event_gate_reasons=transmission.event_gate_reasons,
8236
8512
  classification=output.classification,
8237
8513
  evidence=output.evidence,
8514
+ summary_status=output.summary_status,
8515
+ summary_message=output.summary_message,
8516
+ summary_action=output.summary_action,
8517
+ technical_status=output.technical_status,
8238
8518
  active_playback_session_count=output.active_playback_session_count,
8239
8519
  last_playback_event_at_unix=output.last_playback_event_at_unix,
8240
8520
  publisher_frames_sent_rate=output.publisher_frames_sent_rate,
@@ -8422,6 +8702,10 @@ def create_streaming_router() -> APIRouter:
8422
8702
  event_gate_reasons=transmission.event_gate_reasons,
8423
8703
  classification=output.classification,
8424
8704
  evidence=output.evidence,
8705
+ summary_status=output.summary_status,
8706
+ summary_message=output.summary_message,
8707
+ summary_action=output.summary_action,
8708
+ technical_status=output.technical_status,
8425
8709
  active_playback_session_count=output.active_playback_session_count,
8426
8710
  last_playback_event_at_unix=output.last_playback_event_at_unix,
8427
8711
  publisher_frames_sent_rate=output.publisher_frames_sent_rate,