toposync-ext-streaming 0.4.2__tar.gz → 0.4.3__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.2 → toposync_ext_streaming-0.4.3}/PKG-INFO +1 -1
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/pyproject.toml +1 -1
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/src/toposync_ext_streaming/api/routes.py +331 -15
- toposync_ext_streaming-0.4.3/src/toposync_ext_streaming/static/387.js +1 -0
- toposync_ext_streaming-0.4.3/src/toposync_ext_streaming/static/703.js +2 -0
- toposync_ext_streaming-0.4.3/src/toposync_ext_streaming/static/main.js +2 -0
- toposync_ext_streaming-0.4.3/src/toposync_ext_streaming/static/remoteEntry.js +1 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/ui/src/activate.tsx +2 -2
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/ui/src/settings/StreamingSettingsPanel.tsx +4 -3
- toposync_ext_streaming-0.4.2/src/toposync_ext_streaming/static/703.js +0 -2
- toposync_ext_streaming-0.4.2/src/toposync_ext_streaming/static/main.js +0 -2
- toposync_ext_streaming-0.4.2/src/toposync_ext_streaming/static/remoteEntry.js +0 -1
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/.gitignore +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/LICENSE +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/LICENSE.ffmpeg +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/LICENSE.mediamtx +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/README.md +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/src/toposync_ext_streaming/__init__.py +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/src/toposync_ext_streaming/api/__init__.py +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/src/toposync_ext_streaming/api/models.py +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/src/toposync_ext_streaming/bin/ffmpeg/LICENSE +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/src/toposync_ext_streaming/bin/mediamtx/LICENSE +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/src/toposync_ext_streaming/extension.json +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/src/toposync_ext_streaming/pipelines/__init__.py +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/src/toposync_ext_streaming/pipelines/operators.py +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/src/toposync_ext_streaming/plugin.py +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/src/toposync_ext_streaming/static/326.js +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/src/toposync_ext_streaming/static/326.js.LICENSE.txt +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/src/toposync_ext_streaming/static/4.js +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/src/toposync_ext_streaming/static/4.js.LICENSE.txt +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/src/toposync_ext_streaming/static/623.js +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/src/toposync_ext_streaming/static/623.js.LICENSE.txt +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/src/toposync_ext_streaming/static/703.js.LICENSE.txt +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/src/toposync_ext_streaming/static/main.js.LICENSE.txt +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/src/toposync_ext_streaming/streaming/__init__.py +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/src/toposync_ext_streaming/streaming/arbitration.py +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/src/toposync_ext_streaming/streaming/camera_ingest.py +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/src/toposync_ext_streaming/streaming/distributed_sync.py +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/src/toposync_ext_streaming/streaming/encoder_state.py +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/src/toposync_ext_streaming/streaming/engine_manager.py +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/src/toposync_ext_streaming/streaming/ffmpeg_binary.py +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/src/toposync_ext_streaming/streaming/go2rtc_binary.py +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/src/toposync_ext_streaming/streaming/go2rtc_config.py +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/src/toposync_ext_streaming/streaming/go2rtc_manager.py +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/src/toposync_ext_streaming/streaming/ingest_auth.py +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/src/toposync_ext_streaming/streaming/ingest_resolver.py +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/src/toposync_ext_streaming/streaming/jsmpeg_manager.py +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/src/toposync_ext_streaming/streaming/mediamtx_api_client.py +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/src/toposync_ext_streaming/streaming/mediamtx_binary.py +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/src/toposync_ext_streaming/streaming/mediamtx_config.py +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/src/toposync_ext_streaming/streaming/mediamtx_processes.py +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/src/toposync_ext_streaming/streaming/placeholder.py +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/src/toposync_ext_streaming/streaming/platform.py +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/src/toposync_ext_streaming/streaming/playback_events.py +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/src/toposync_ext_streaming/streaming/publisher_manager.py +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/src/toposync_ext_streaming/streaming/resize.py +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/src/toposync_ext_streaming/streaming/runtime_state.py +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/src/toposync_ext_streaming/streaming/writer_bridge.py +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/src/toposync_ext_streaming/wizard/__init__.py +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/src/toposync_ext_streaming/wizard/pipeline_builder.py +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/ui/package.json +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/ui/src/api/streamingApi.ts +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/ui/src/constants.ts +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/ui/src/entry.ts +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/ui/src/settings/SubModal.tsx +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/ui/src/settings/WizardCreatePipelineFromTransmission.tsx +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/ui/src/translations.ts +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/ui/src/types.ts +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/ui/tsconfig.json +0 -0
- {toposync_ext_streaming-0.4.2 → toposync_ext_streaming-0.4.3}/ui/webpack.config.js +0 -0
|
@@ -518,7 +518,7 @@ def _hls_proxy_public_base_path(request: Request) -> str:
|
|
|
518
518
|
|
|
519
519
|
MEDIA_TOKEN_SCOPE = "stream:media:read"
|
|
520
520
|
LEGACY_HLS_MEDIA_TOKEN_SCOPE = "stream:hls:read"
|
|
521
|
-
MAX_MEDIA_TOKEN_TTL_OVERRIDE_SECONDS =
|
|
521
|
+
MAX_MEDIA_TOKEN_TTL_OVERRIDE_SECONDS = 21600.0
|
|
522
522
|
|
|
523
523
|
|
|
524
524
|
def _hls_proxy_url(
|
|
@@ -1079,6 +1079,70 @@ async def _fetch_json(
|
|
|
1079
1079
|
return await asyncio.to_thread(_do_request)
|
|
1080
1080
|
|
|
1081
1081
|
|
|
1082
|
+
async def _fetch_bytes(
|
|
1083
|
+
*,
|
|
1084
|
+
url: str,
|
|
1085
|
+
timeout_s: float = 6.0,
|
|
1086
|
+
username: str = "",
|
|
1087
|
+
password: str = "",
|
|
1088
|
+
accept: str = "*/*",
|
|
1089
|
+
) -> tuple[bytes, str | None]:
|
|
1090
|
+
def _do_request() -> tuple[bytes, str | None]:
|
|
1091
|
+
headers = {"accept": accept}
|
|
1092
|
+
if username or password:
|
|
1093
|
+
headers["authorization"] = _build_basic_authorization(username, password)
|
|
1094
|
+
req = urllib_request.Request(url=url, headers=headers, method="GET")
|
|
1095
|
+
try:
|
|
1096
|
+
with urllib_request.urlopen(req, timeout=max(1.0, float(timeout_s))) as response:
|
|
1097
|
+
return response.read(), response.headers.get("content-type")
|
|
1098
|
+
except urllib_error.HTTPError as exc:
|
|
1099
|
+
body = _read_http_error(exc)
|
|
1100
|
+
raise RuntimeError(f"HTTP {exc.code}: {body}") from exc
|
|
1101
|
+
except urllib_error.URLError as exc:
|
|
1102
|
+
reason = str(getattr(exc, "reason", "") or exc)
|
|
1103
|
+
raise RuntimeError(f"Connection failed: {reason}") from exc
|
|
1104
|
+
|
|
1105
|
+
return await asyncio.to_thread(_do_request)
|
|
1106
|
+
|
|
1107
|
+
|
|
1108
|
+
async def _post_json(
|
|
1109
|
+
*,
|
|
1110
|
+
url: str,
|
|
1111
|
+
body: dict[str, Any],
|
|
1112
|
+
timeout_s: float = 6.0,
|
|
1113
|
+
username: str = "",
|
|
1114
|
+
password: str = "",
|
|
1115
|
+
) -> dict[str, Any]:
|
|
1116
|
+
def _do_request() -> dict[str, Any]:
|
|
1117
|
+
headers = {"accept": "application/json", "content-type": "application/json"}
|
|
1118
|
+
if username or password:
|
|
1119
|
+
headers["authorization"] = _build_basic_authorization(username, password)
|
|
1120
|
+
req = urllib_request.Request(
|
|
1121
|
+
url=url,
|
|
1122
|
+
data=json.dumps(body).encode("utf-8"),
|
|
1123
|
+
headers=headers,
|
|
1124
|
+
method="POST",
|
|
1125
|
+
)
|
|
1126
|
+
try:
|
|
1127
|
+
with urllib_request.urlopen(req, timeout=max(1.0, float(timeout_s))) as response:
|
|
1128
|
+
payload = response.read().decode("utf-8", errors="replace")
|
|
1129
|
+
except urllib_error.HTTPError as exc:
|
|
1130
|
+
detail = _read_http_error(exc)
|
|
1131
|
+
raise RuntimeError(f"HTTP {exc.code}: {detail}") from exc
|
|
1132
|
+
except urllib_error.URLError as exc:
|
|
1133
|
+
reason = str(getattr(exc, "reason", "") or exc)
|
|
1134
|
+
raise RuntimeError(f"Connection failed: {reason}") from exc
|
|
1135
|
+
try:
|
|
1136
|
+
parsed_payload = json.loads(payload)
|
|
1137
|
+
except Exception as exc:
|
|
1138
|
+
raise RuntimeError("Invalid JSON response") from exc
|
|
1139
|
+
if not isinstance(parsed_payload, dict):
|
|
1140
|
+
raise RuntimeError("Invalid JSON payload")
|
|
1141
|
+
return parsed_payload
|
|
1142
|
+
|
|
1143
|
+
return await asyncio.to_thread(_do_request)
|
|
1144
|
+
|
|
1145
|
+
|
|
1082
1146
|
def _read_http_error(exc: urllib_error.HTTPError) -> str:
|
|
1083
1147
|
try:
|
|
1084
1148
|
body = exc.read().decode("utf-8", errors="replace").strip()
|
|
@@ -2390,6 +2454,139 @@ def _redact_url_credentials(url: str | None) -> str | None:
|
|
|
2390
2454
|
return urllib_parse.urlunsplit((parsed.scheme, netloc, parsed.path, parsed.query, parsed.fragment))
|
|
2391
2455
|
|
|
2392
2456
|
|
|
2457
|
+
def _url_has_loopback_host(url: str | None) -> bool:
|
|
2458
|
+
try:
|
|
2459
|
+
host = str(urllib_parse.urlsplit(str(url or "")).hostname or "").strip().lower()
|
|
2460
|
+
except Exception:
|
|
2461
|
+
return False
|
|
2462
|
+
return host in {"localhost", "::1"} or host.startswith("127.")
|
|
2463
|
+
|
|
2464
|
+
|
|
2465
|
+
def _url_host_for_rtsp(url: str | None) -> str:
|
|
2466
|
+
try:
|
|
2467
|
+
parsed = urllib_parse.urlsplit(str(url or ""))
|
|
2468
|
+
except Exception:
|
|
2469
|
+
return ""
|
|
2470
|
+
host = str(parsed.hostname or "").strip()
|
|
2471
|
+
if not host:
|
|
2472
|
+
return ""
|
|
2473
|
+
return f"[{host}]" if ":" in host and not host.startswith("[") else host
|
|
2474
|
+
|
|
2475
|
+
|
|
2476
|
+
def _rtsp_port_from_urls_response(urls: TransmissionUrlsResponse) -> int | None:
|
|
2477
|
+
contract = urls.network_contract
|
|
2478
|
+
if contract is None:
|
|
2479
|
+
return None
|
|
2480
|
+
for ports in (contract.actual_ports, contract.expected_ports):
|
|
2481
|
+
value = getattr(ports, "rtsp", None)
|
|
2482
|
+
if value is not None:
|
|
2483
|
+
return int(value)
|
|
2484
|
+
return None
|
|
2485
|
+
|
|
2486
|
+
|
|
2487
|
+
def _rtsp_url_with_output_auth(url: str, output: TransmissionOutput | None) -> str:
|
|
2488
|
+
raw = str(url or "").strip()
|
|
2489
|
+
if not raw or output is None:
|
|
2490
|
+
return raw
|
|
2491
|
+
auth = output.authentication
|
|
2492
|
+
if auth is None or not auth.enabled:
|
|
2493
|
+
return raw
|
|
2494
|
+
username = str(auth.username or "").strip()
|
|
2495
|
+
password = str(auth.password or "").strip()
|
|
2496
|
+
if not username or not password:
|
|
2497
|
+
return raw
|
|
2498
|
+
try:
|
|
2499
|
+
parsed = urllib_parse.urlsplit(raw)
|
|
2500
|
+
except Exception:
|
|
2501
|
+
return raw
|
|
2502
|
+
if parsed.username or parsed.password:
|
|
2503
|
+
return raw
|
|
2504
|
+
host = _url_host_for_rtsp(raw)
|
|
2505
|
+
if not host:
|
|
2506
|
+
return raw
|
|
2507
|
+
port = f":{parsed.port}" if parsed.port is not None else ""
|
|
2508
|
+
user = urllib_parse.quote(username, safe="")
|
|
2509
|
+
pwd = urllib_parse.quote(password, safe="")
|
|
2510
|
+
return urllib_parse.urlunsplit(
|
|
2511
|
+
(
|
|
2512
|
+
parsed.scheme,
|
|
2513
|
+
f"{user}:{pwd}@{host}{port}",
|
|
2514
|
+
parsed.path,
|
|
2515
|
+
parsed.query,
|
|
2516
|
+
parsed.fragment,
|
|
2517
|
+
)
|
|
2518
|
+
)
|
|
2519
|
+
|
|
2520
|
+
|
|
2521
|
+
def _remote_rtsp_url_for_home_assistant(
|
|
2522
|
+
*,
|
|
2523
|
+
urls: TransmissionUrlsResponse,
|
|
2524
|
+
transmission: Transmission,
|
|
2525
|
+
output: TransmissionOutput,
|
|
2526
|
+
) -> str | None:
|
|
2527
|
+
selected = next(
|
|
2528
|
+
(
|
|
2529
|
+
item
|
|
2530
|
+
for item in urls.outputs
|
|
2531
|
+
if item.protocol == "rtsp" and item.output_id == output.id
|
|
2532
|
+
),
|
|
2533
|
+
None,
|
|
2534
|
+
) or next((item for item in urls.outputs if item.protocol == "rtsp"), None)
|
|
2535
|
+
if selected is not None:
|
|
2536
|
+
return _rtsp_url_with_output_auth(selected.url, output)
|
|
2537
|
+
|
|
2538
|
+
host = ""
|
|
2539
|
+
for candidate in urls.outputs:
|
|
2540
|
+
host = _url_host_for_rtsp(candidate.url)
|
|
2541
|
+
if host:
|
|
2542
|
+
break
|
|
2543
|
+
port = _rtsp_port_from_urls_response(urls)
|
|
2544
|
+
if not host or port is None:
|
|
2545
|
+
return None
|
|
2546
|
+
engine_path = resolve_output_engine_path(transmission, output)
|
|
2547
|
+
return _rtsp_url_with_output_auth(_rtsp_url(host, port, engine_path), output)
|
|
2548
|
+
|
|
2549
|
+
|
|
2550
|
+
async def _remote_transmission_server(
|
|
2551
|
+
*,
|
|
2552
|
+
config_store: ConfigStore,
|
|
2553
|
+
transmission: Transmission,
|
|
2554
|
+
) -> ProcessingServer:
|
|
2555
|
+
servers_by_id = await _processing_servers_by_id(config_store)
|
|
2556
|
+
host_server_id = normalize_server_id(transmission.host_server_id, fallback="local")
|
|
2557
|
+
server = servers_by_id.get(host_server_id)
|
|
2558
|
+
if server is None:
|
|
2559
|
+
raise HTTPException(status_code=400, detail=f"Unknown host_server_id: {host_server_id}")
|
|
2560
|
+
if str(server.kind) != "http":
|
|
2561
|
+
raise HTTPException(
|
|
2562
|
+
status_code=400,
|
|
2563
|
+
detail=f"host_server_id '{host_server_id}' does not support remote HTTP access.",
|
|
2564
|
+
)
|
|
2565
|
+
if not str(server.url or "").strip():
|
|
2566
|
+
raise HTTPException(status_code=400, detail=f"host_server_id '{host_server_id}' has an empty URL.")
|
|
2567
|
+
return server
|
|
2568
|
+
|
|
2569
|
+
|
|
2570
|
+
def _remote_transmission_endpoint(
|
|
2571
|
+
server: ProcessingServer,
|
|
2572
|
+
*,
|
|
2573
|
+
transmission_id: str,
|
|
2574
|
+
suffix: str,
|
|
2575
|
+
query: dict[str, str | None] | None = None,
|
|
2576
|
+
) -> str:
|
|
2577
|
+
base_url = str(server.url or "").strip().rstrip("/")
|
|
2578
|
+
encoded_id = urllib_parse.quote(str(transmission_id or ""), safe="")
|
|
2579
|
+
url = f"{base_url}/api/streams/transmissions/{encoded_id}/{suffix.lstrip('/')}"
|
|
2580
|
+
query_params = {
|
|
2581
|
+
key: value
|
|
2582
|
+
for key, value in (query or {}).items()
|
|
2583
|
+
if value is not None and str(value).strip()
|
|
2584
|
+
}
|
|
2585
|
+
if query_params:
|
|
2586
|
+
url = f"{url}?{urllib_parse.urlencode(query_params)}"
|
|
2587
|
+
return url
|
|
2588
|
+
|
|
2589
|
+
|
|
2393
2590
|
def _home_assistant_still_url_path(
|
|
2394
2591
|
*,
|
|
2395
2592
|
transmission_id: str,
|
|
@@ -2545,7 +2742,35 @@ async def _build_home_assistant_camera_item(
|
|
|
2545
2742
|
if output is None:
|
|
2546
2743
|
blocking_errors.append("No enabled output is available for Home Assistant camera playback.")
|
|
2547
2744
|
elif transmission_host != current_server_id:
|
|
2548
|
-
|
|
2745
|
+
try:
|
|
2746
|
+
remote_urls = await _resolve_remote_transmission_urls(
|
|
2747
|
+
config_store=_config_store(request),
|
|
2748
|
+
transmission=transmission,
|
|
2749
|
+
output_id=output_id,
|
|
2750
|
+
quality_profile_id=quality_profile_id,
|
|
2751
|
+
)
|
|
2752
|
+
warnings.extend(remote_urls.warnings)
|
|
2753
|
+
blocking_errors.extend(remote_urls.blocking_errors)
|
|
2754
|
+
rtsp_url = _remote_rtsp_url_for_home_assistant(
|
|
2755
|
+
urls=remote_urls,
|
|
2756
|
+
transmission=transmission,
|
|
2757
|
+
output=output,
|
|
2758
|
+
)
|
|
2759
|
+
if not rtsp_url:
|
|
2760
|
+
blocking_errors.append(
|
|
2761
|
+
"Remote processing server did not expose an RTSP URL for Home Assistant camera playback."
|
|
2762
|
+
)
|
|
2763
|
+
elif _url_has_loopback_host(rtsp_url):
|
|
2764
|
+
blocking_errors.append(
|
|
2765
|
+
"Remote processing server returned a loopback RTSP URL; configure it with a LAN-reachable URL or expose the RTSP port."
|
|
2766
|
+
)
|
|
2767
|
+
rtsp_url = None
|
|
2768
|
+
redacted_rtsp_url = _redact_url_credentials(rtsp_url)
|
|
2769
|
+
except HTTPException as exc:
|
|
2770
|
+
detail = str(exc.detail or exc)
|
|
2771
|
+
blocking_errors.append(detail)
|
|
2772
|
+
except Exception as exc:
|
|
2773
|
+
blocking_errors.append(f"Failed to resolve remote Home Assistant camera playback: {exc}")
|
|
2549
2774
|
else:
|
|
2550
2775
|
engine_path = resolve_output_engine_path(transmission, output)
|
|
2551
2776
|
rtsp_url = await _engine_manager(request).get_read_url_for_path(
|
|
@@ -7410,7 +7635,36 @@ def create_streaming_router() -> APIRouter:
|
|
|
7410
7635
|
raise HTTPException(status_code=404, detail="Transmission not found")
|
|
7411
7636
|
|
|
7412
7637
|
if normalize_server_id(transmission.host_server_id, fallback="local") != _current_server_id(request):
|
|
7413
|
-
|
|
7638
|
+
server = await _remote_transmission_server(
|
|
7639
|
+
config_store=config_store,
|
|
7640
|
+
transmission=transmission,
|
|
7641
|
+
)
|
|
7642
|
+
remote_url = _remote_transmission_endpoint(
|
|
7643
|
+
server,
|
|
7644
|
+
transmission_id=transmission.id,
|
|
7645
|
+
suffix="still.jpg",
|
|
7646
|
+
query={
|
|
7647
|
+
"output_id": output_id,
|
|
7648
|
+
"quality_profile_id": quality_profile_id,
|
|
7649
|
+
},
|
|
7650
|
+
)
|
|
7651
|
+
try:
|
|
7652
|
+
body, media_type = await _fetch_bytes(
|
|
7653
|
+
url=remote_url,
|
|
7654
|
+
username=str(server.username or "").strip(),
|
|
7655
|
+
password=str(server.password or "").strip(),
|
|
7656
|
+
accept="image/jpeg,*/*",
|
|
7657
|
+
)
|
|
7658
|
+
except Exception as exc:
|
|
7659
|
+
raise HTTPException(
|
|
7660
|
+
status_code=502,
|
|
7661
|
+
detail=f"Failed to resolve still image from processing server '{transmission.host_server_id}': {exc}",
|
|
7662
|
+
) from exc
|
|
7663
|
+
return Response(
|
|
7664
|
+
content=body,
|
|
7665
|
+
media_type=media_type or "image/jpeg",
|
|
7666
|
+
headers={"cache-control": "no-store, max-age=0", "pragma": "no-cache"},
|
|
7667
|
+
)
|
|
7414
7668
|
|
|
7415
7669
|
output = _best_transmission_output_for_home_assistant(
|
|
7416
7670
|
transmission,
|
|
@@ -7576,7 +7830,34 @@ def create_streaming_router() -> APIRouter:
|
|
|
7576
7830
|
if transmission is None:
|
|
7577
7831
|
raise HTTPException(status_code=404, detail="Transmission not found")
|
|
7578
7832
|
if normalize_server_id(transmission.host_server_id, fallback="local") != _current_server_id(request):
|
|
7579
|
-
|
|
7833
|
+
server = await _remote_transmission_server(
|
|
7834
|
+
config_store=config_store,
|
|
7835
|
+
transmission=transmission,
|
|
7836
|
+
)
|
|
7837
|
+
remote_url = _remote_transmission_endpoint(
|
|
7838
|
+
server,
|
|
7839
|
+
transmission_id=transmission.id,
|
|
7840
|
+
suffix="webrtc/offer",
|
|
7841
|
+
query={
|
|
7842
|
+
"output_id": body.output_id or output_id,
|
|
7843
|
+
"quality_profile_id": body.quality_profile_id or quality_profile_id,
|
|
7844
|
+
},
|
|
7845
|
+
)
|
|
7846
|
+
try:
|
|
7847
|
+
payload = await _post_json(
|
|
7848
|
+
url=remote_url,
|
|
7849
|
+
body=body.model_dump(mode="json"),
|
|
7850
|
+
username=str(server.username or "").strip(),
|
|
7851
|
+
password=str(server.password or "").strip(),
|
|
7852
|
+
)
|
|
7853
|
+
return StreamingHomeAssistantWebRtcOfferResponse.model_validate(payload)
|
|
7854
|
+
except HTTPException:
|
|
7855
|
+
raise
|
|
7856
|
+
except Exception as exc:
|
|
7857
|
+
raise HTTPException(
|
|
7858
|
+
status_code=502,
|
|
7859
|
+
detail=f"Failed to negotiate WebRTC offer with processing server '{transmission.host_server_id}': {exc}",
|
|
7860
|
+
) from exc
|
|
7580
7861
|
|
|
7581
7862
|
selected_output_id = str(body.output_id or output_id or "").strip() or None
|
|
7582
7863
|
selected_profile_id = body.quality_profile_id or quality_profile_id
|
|
@@ -8161,11 +8442,31 @@ def create_streaming_router() -> APIRouter:
|
|
|
8161
8442
|
if normalize_server_id(transmission.host_server_id, fallback="local") != _current_server_id(
|
|
8162
8443
|
request
|
|
8163
8444
|
):
|
|
8164
|
-
|
|
8165
|
-
|
|
8166
|
-
|
|
8167
|
-
|
|
8168
|
-
|
|
8445
|
+
server = await _remote_transmission_server(
|
|
8446
|
+
config_store=config_store,
|
|
8447
|
+
transmission=transmission,
|
|
8448
|
+
)
|
|
8449
|
+
remote_url = _remote_transmission_endpoint(
|
|
8450
|
+
server,
|
|
8451
|
+
transmission_id=transmission.id,
|
|
8452
|
+
suffix="demand/prime",
|
|
8453
|
+
query={
|
|
8454
|
+
"output_id": output_id,
|
|
8455
|
+
"quality_profile_id": quality_profile_id,
|
|
8456
|
+
},
|
|
8457
|
+
)
|
|
8458
|
+
try:
|
|
8459
|
+
return await _post_json(
|
|
8460
|
+
url=remote_url,
|
|
8461
|
+
body={},
|
|
8462
|
+
username=str(server.username or "").strip(),
|
|
8463
|
+
password=str(server.password or "").strip(),
|
|
8464
|
+
)
|
|
8465
|
+
except Exception as exc:
|
|
8466
|
+
raise HTTPException(
|
|
8467
|
+
status_code=502,
|
|
8468
|
+
detail=f"Failed to prime demand on processing server '{transmission.host_server_id}': {exc}",
|
|
8469
|
+
) from exc
|
|
8169
8470
|
|
|
8170
8471
|
bridge = _writer_bridge(request)
|
|
8171
8472
|
prime_demand = getattr(bridge, "prime_transmission_demand", None)
|
|
@@ -8219,13 +8520,28 @@ def create_streaming_router() -> APIRouter:
|
|
|
8219
8520
|
default_lease_seconds = 90.0 if payload.source == "home_assistant_entity" else 45.0
|
|
8220
8521
|
lease_seconds = float(payload.ttl_seconds or default_lease_seconds)
|
|
8221
8522
|
if normalize_server_id(transmission.host_server_id, fallback="local") != _current_server_id(request):
|
|
8222
|
-
|
|
8223
|
-
|
|
8224
|
-
|
|
8225
|
-
|
|
8226
|
-
|
|
8227
|
-
|
|
8523
|
+
server = await _remote_transmission_server(
|
|
8524
|
+
config_store=config_store,
|
|
8525
|
+
transmission=transmission,
|
|
8526
|
+
)
|
|
8527
|
+
remote_url = _remote_transmission_endpoint(
|
|
8528
|
+
server,
|
|
8529
|
+
transmission_id=transmission.id,
|
|
8530
|
+
suffix="demand/heartbeat",
|
|
8228
8531
|
)
|
|
8532
|
+
try:
|
|
8533
|
+
remote_payload = await _post_json(
|
|
8534
|
+
url=remote_url,
|
|
8535
|
+
body=payload.model_dump(mode="json"),
|
|
8536
|
+
username=str(server.username or "").strip(),
|
|
8537
|
+
password=str(server.password or "").strip(),
|
|
8538
|
+
)
|
|
8539
|
+
return TransmissionDemandHeartbeatResponse.model_validate(remote_payload)
|
|
8540
|
+
except Exception as exc:
|
|
8541
|
+
raise HTTPException(
|
|
8542
|
+
status_code=502,
|
|
8543
|
+
detail=f"Failed to renew demand on processing server '{transmission.host_server_id}': {exc}",
|
|
8544
|
+
) from exc
|
|
8229
8545
|
|
|
8230
8546
|
bridge = _writer_bridge(request)
|
|
8231
8547
|
prime_demand = getattr(bridge, "prime_transmission_demand", None)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";(self.webpackChunk_toposync_extension_streaming_ui=self.webpackChunk_toposync_extension_streaming_ui||[]).push([[387],{89(t){function n(){return"undefined"==typeof window?"/":function(t){const n=String(t||"").trim();return n&&(n.startsWith("/")?n:`/${n}`).replace(/\/+$/,"")||"/"}(window.__TOPOSYNC_PUBLIC_BASE_PATH__)}function e(t){const e=n();return t.startsWith("/")?"/"===e||t===e||t.startsWith(`${e}/`)?t:`${e}${t}`:t}t.exports={getToposyncBasePath:n,resolveToposyncUrl:function(t){const n=String(t||"");if(!n)return n;if("undefined"==typeof window)return n;if(/^(data:|blob:|mailto:|tel:|#)/i.test(n))return n;if(n.startsWith("//"))return n;if(/^[a-z][a-z0-9+.-]*:/i.test(n))try{const t=new URL(n,window.location.href);return t.origin!==window.location.origin?n:e(`${t.pathname}${t.search}${t.hash}`)}catch{return n}return n.startsWith("/")?e(n):n}}},387(t,n,e){t.exports=e(89)}}]);
|