toposync-ext-streaming 0.4.5__tar.gz → 0.4.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/PKG-INFO +2 -2
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/README.md +1 -1
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/pyproject.toml +1 -1
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/src/toposync_ext_streaming/api/models.py +21 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/src/toposync_ext_streaming/api/routes.py +287 -3
- toposync_ext_streaming-0.4.7/src/toposync_ext_streaming/static/703.js +2 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/ui/src/settings/StreamingSettingsPanel.tsx +194 -6
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/ui/src/translations.ts +32 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/ui/src/types.ts +17 -0
- toposync_ext_streaming-0.4.5/src/toposync_ext_streaming/static/703.js +0 -2
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/.gitignore +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/LICENSE +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/LICENSE.ffmpeg +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/LICENSE.mediamtx +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/src/toposync_ext_streaming/__init__.py +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/src/toposync_ext_streaming/api/__init__.py +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/src/toposync_ext_streaming/bin/ffmpeg/LICENSE +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/src/toposync_ext_streaming/bin/mediamtx/LICENSE +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/src/toposync_ext_streaming/extension.json +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/src/toposync_ext_streaming/pipelines/__init__.py +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/src/toposync_ext_streaming/pipelines/operators.py +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/src/toposync_ext_streaming/plugin.py +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/src/toposync_ext_streaming/static/326.js +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/src/toposync_ext_streaming/static/326.js.LICENSE.txt +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/src/toposync_ext_streaming/static/387.js +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/src/toposync_ext_streaming/static/4.js +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/src/toposync_ext_streaming/static/4.js.LICENSE.txt +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/src/toposync_ext_streaming/static/623.js +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/src/toposync_ext_streaming/static/623.js.LICENSE.txt +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/src/toposync_ext_streaming/static/703.js.LICENSE.txt +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/src/toposync_ext_streaming/static/main.js +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/src/toposync_ext_streaming/static/main.js.LICENSE.txt +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/src/toposync_ext_streaming/static/remoteEntry.js +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/src/toposync_ext_streaming/streaming/__init__.py +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/src/toposync_ext_streaming/streaming/arbitration.py +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/src/toposync_ext_streaming/streaming/camera_ingest.py +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/src/toposync_ext_streaming/streaming/distributed_sync.py +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/src/toposync_ext_streaming/streaming/encoder_state.py +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/src/toposync_ext_streaming/streaming/engine_manager.py +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/src/toposync_ext_streaming/streaming/ffmpeg_binary.py +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/src/toposync_ext_streaming/streaming/go2rtc_binary.py +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/src/toposync_ext_streaming/streaming/go2rtc_config.py +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/src/toposync_ext_streaming/streaming/go2rtc_manager.py +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/src/toposync_ext_streaming/streaming/ingest_auth.py +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/src/toposync_ext_streaming/streaming/ingest_resolver.py +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/src/toposync_ext_streaming/streaming/jsmpeg_manager.py +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/src/toposync_ext_streaming/streaming/mediamtx_api_client.py +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/src/toposync_ext_streaming/streaming/mediamtx_binary.py +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/src/toposync_ext_streaming/streaming/mediamtx_config.py +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/src/toposync_ext_streaming/streaming/mediamtx_processes.py +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/src/toposync_ext_streaming/streaming/placeholder.py +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/src/toposync_ext_streaming/streaming/platform.py +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/src/toposync_ext_streaming/streaming/playback_events.py +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/src/toposync_ext_streaming/streaming/publisher_manager.py +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/src/toposync_ext_streaming/streaming/resize.py +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/src/toposync_ext_streaming/streaming/runtime_state.py +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/src/toposync_ext_streaming/streaming/writer_bridge.py +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/src/toposync_ext_streaming/wizard/__init__.py +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/src/toposync_ext_streaming/wizard/pipeline_builder.py +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/ui/package.json +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/ui/src/activate.tsx +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/ui/src/api/streamingApi.ts +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/ui/src/constants.ts +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/ui/src/entry.ts +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/ui/src/settings/SubModal.tsx +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/ui/src/settings/WizardCreatePipelineFromTransmission.tsx +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/ui/tsconfig.json +0 -0
- {toposync_ext_streaming-0.4.5 → toposync_ext_streaming-0.4.7}/ui/webpack.config.js +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: toposync-ext-streaming
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.7
|
|
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/
|
|
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/
|
|
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.
|
|
@@ -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=
|
|
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,
|