fix
This commit is contained in:
@@ -90,7 +90,8 @@ POSTGRES_PORT=45432
|
|||||||
# DEMO_ORCHESTRATOR_ENABLED=false
|
# DEMO_ORCHESTRATOR_ENABLED=false
|
||||||
# DEMO_ORCHESTRATOR_RTSP_PORT=18554
|
# DEMO_ORCHESTRATOR_RTSP_PORT=18554
|
||||||
# 联调台 HLS(Compose:先 docker compose up -d mediamtx-hls api)
|
# 联调台 HLS(Compose:先 docker compose up -d mediamtx-hls api)
|
||||||
# DEMO_HLS_PREVIEW_UPSTREAM=http://127.0.0.1:18888
|
# Compose 内 api 反代侧车:DEMO_HLS_PREVIEW_UPSTREAM=http://mediamtx-hls:8888
|
||||||
|
# 本机 uvicorn 直连 published 端口:DEMO_HLS_PREVIEW_UPSTREAM=http://127.0.0.1:18888
|
||||||
# 本机 uvicorn 且无 mediamtx-hls 侧车时改为:DEMO_HLS_PREVIEW_UPSTREAM=ephemeral
|
# 本机 uvicorn 且无 mediamtx-hls 侧车时改为:DEMO_HLS_PREVIEW_UPSTREAM=ephemeral
|
||||||
# DEMO_HLS_PREVIEW_PORT=18888
|
# DEMO_HLS_PREVIEW_PORT=18888
|
||||||
# Docker 内 API 访问宿主机假流时写入站点 JSON 的主机名(默认 host.docker.internal)
|
# Docker 内 API 访问宿主机假流时写入站点 JSON 的主机名(默认 host.docker.internal)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Awaitable, Callable
|
from collections.abc import Awaitable, Callable
|
||||||
|
import pathlib
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import (
|
from fastapi import (
|
||||||
@@ -260,7 +261,13 @@ async def hls_preview_ensure(
|
|||||||
detail += (
|
detail += (
|
||||||
"。当前 API 可能仍是旧版本:请执行 "
|
"。当前 API 可能仍是旧版本:请执行 "
|
||||||
"`docker compose up -d --build api mediamtx-hls`,并确认 "
|
"`docker compose up -d --build api mediamtx-hls`,并确认 "
|
||||||
"DEMO_HLS_PREVIEW_UPSTREAM=http://127.0.0.1:18888(勿把 host.docker.internal 用于 docker -p)。"
|
"DEMO_HLS_PREVIEW_UPSTREAM=http://mediamtx-hls:8888(勿把 host.docker.internal 用于 docker -p)。"
|
||||||
|
)
|
||||||
|
elif "127.0.0.1" in str(exc) and pathlib.Path("/.dockerenv").is_file():
|
||||||
|
detail += (
|
||||||
|
"。API 在 Docker 桥接网内时勿用 127.0.0.1 访问 published 端口;"
|
||||||
|
"请设 DEMO_HLS_PREVIEW_UPSTREAM=http://mediamtx-hls:8888 并 "
|
||||||
|
"`docker compose up -d --build api mediamtx-hls`。"
|
||||||
)
|
)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
|
|||||||
@@ -191,7 +191,8 @@ class Settings(BaseSettings):
|
|||||||
demo_hls_preview_upstream: str = Field(
|
demo_hls_preview_upstream: str = Field(
|
||||||
default="http://127.0.0.1:18888",
|
default="http://127.0.0.1:18888",
|
||||||
description=(
|
description=(
|
||||||
"MediaMTX HLS 基址(Compose 侧车默认 127.0.0.1:18888)。"
|
"MediaMTX HLS 基址。Compose 内 api 用 http://mediamtx-hls:8888;"
|
||||||
|
"本机 uvicorn 用 http://127.0.0.1:18888。"
|
||||||
"设为 ephemeral 则改回临时 docker run(本机开发)。"
|
"设为 ephemeral 则改回临时 docker run(本机开发)。"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -28,9 +28,7 @@ from app.services.synthetic_rtsp import MEDIAMTX_IMAGE
|
|||||||
CONTAINER_NAME_PREFIX = "orm-hls-preview-"
|
CONTAINER_NAME_PREFIX = "orm-hls-preview-"
|
||||||
_MEDIAMTX_HLS_INTERNAL_PORT = 8888
|
_MEDIAMTX_HLS_INTERNAL_PORT = 8888
|
||||||
_PATH_SAFE = re.compile(r"[^a-zA-Z0-9._-]+")
|
_PATH_SAFE = re.compile(r"[^a-zA-Z0-9._-]+")
|
||||||
_DOCKER_PUBLISH_ALIAS_HOSTS = frozenset(
|
_DOCKER_LOOPBACK_HOSTS = frozenset({"127.0.0.1", "localhost", "::1"})
|
||||||
{"host.docker.internal", "host.containers.internal", "localhost"}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def normalize_upstream_base(url: str) -> str:
|
def normalize_upstream_base(url: str) -> str:
|
||||||
@@ -55,7 +53,10 @@ def upstream_host_port(upstream_base: str) -> tuple[str, int]:
|
|||||||
def docker_publish_bind_host(hls_host: str) -> str:
|
def docker_publish_bind_host(hls_host: str) -> str:
|
||||||
"""本机临时容器 ``docker run -p`` 的绑定地址(勿用主机名)。"""
|
"""本机临时容器 ``docker run -p`` 的绑定地址(勿用主机名)。"""
|
||||||
h = (hls_host or "").strip().lower()
|
h = (hls_host or "").strip().lower()
|
||||||
if h in _DOCKER_PUBLISH_ALIAS_HOSTS:
|
if h in _DOCKER_LOOPBACK_HOSTS or h in (
|
||||||
|
"host.docker.internal",
|
||||||
|
"host.containers.internal",
|
||||||
|
):
|
||||||
return "127.0.0.1"
|
return "127.0.0.1"
|
||||||
return (hls_host or "").strip() or "127.0.0.1"
|
return (hls_host or "").strip() or "127.0.0.1"
|
||||||
|
|
||||||
@@ -90,6 +91,39 @@ def _wait_tcp(host: str, port: int, *, timeout: float) -> None:
|
|||||||
raise RuntimeError(msg)
|
raise RuntimeError(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def attached_upstream_connect_candidates(
|
||||||
|
upstream_base: str,
|
||||||
|
*,
|
||||||
|
internal_service_host: str = "mediamtx-hls",
|
||||||
|
) -> list[tuple[str, int]]:
|
||||||
|
"""Bridge 网络内 API 无法经 127.0.0.1 访问 published 端口,需回退到 Compose 服务名。"""
|
||||||
|
host, port = upstream_host_port(upstream_base)
|
||||||
|
candidates: list[tuple[str, int]] = [(host, port)]
|
||||||
|
if Path("/.dockerenv").is_file() and host.lower() in _DOCKER_LOOPBACK_HOSTS:
|
||||||
|
internal = (internal_service_host.strip() or "mediamtx-hls", _MEDIAMTX_HLS_INTERNAL_PORT)
|
||||||
|
if internal not in candidates:
|
||||||
|
candidates.append(internal)
|
||||||
|
return candidates
|
||||||
|
|
||||||
|
|
||||||
|
def _wait_tcp_any(candidates: list[tuple[str, int]], *, timeout: float) -> tuple[str, int]:
|
||||||
|
if not candidates:
|
||||||
|
raise RuntimeError("no TCP candidates for HLS upstream")
|
||||||
|
deadline = time.monotonic() + max(1.0, timeout)
|
||||||
|
errors: list[str] = []
|
||||||
|
for host, port in candidates:
|
||||||
|
remaining = deadline - time.monotonic()
|
||||||
|
if remaining <= 0:
|
||||||
|
break
|
||||||
|
try:
|
||||||
|
_wait_tcp(host, port, timeout=remaining)
|
||||||
|
return host, port
|
||||||
|
except RuntimeError as exc:
|
||||||
|
errors.append(f"{host}:{port} ({exc})")
|
||||||
|
tried = ", ".join(errors) if errors else ", ".join(f"{h}:{p}" for h, p in candidates)
|
||||||
|
raise RuntimeError(f"等待 MediaMTX HLS 超时({timeout:g}s),已尝试: {tried}")
|
||||||
|
|
||||||
|
|
||||||
def _write_mediamtx_config(path: Path, paths: dict[str, str]) -> None:
|
def _write_mediamtx_config(path: Path, paths: dict[str, str]) -> None:
|
||||||
lines = [
|
lines = [
|
||||||
"# Generated for OR demo HLS preview",
|
"# Generated for OR demo HLS preview",
|
||||||
@@ -241,13 +275,21 @@ class HlsPreviewManager:
|
|||||||
f"failed to restart {container_name} (is mediamtx-hls running in compose?): {err}"
|
f"failed to restart {container_name} (is mediamtx-hls running in compose?): {err}"
|
||||||
)
|
)
|
||||||
|
|
||||||
host, port = upstream_host_port(upstream_base)
|
candidates = attached_upstream_connect_candidates(upstream_base)
|
||||||
try:
|
try:
|
||||||
_wait_tcp(host, port, timeout=ready_timeout_sec)
|
host, port = _wait_tcp_any(candidates, timeout=ready_timeout_sec)
|
||||||
except Exception:
|
except RuntimeError as exc:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"HLS preview not reachable at {upstream_base} after restarting {container_name}"
|
f"HLS preview not reachable at {upstream_base} after restarting {container_name}"
|
||||||
) from None
|
) from exc
|
||||||
|
reachable_base = normalize_upstream_base(f"http://{host}:{port}")
|
||||||
|
if reachable_base != upstream_base:
|
||||||
|
logger.info(
|
||||||
|
"HLS preview (attached) using {} instead of configured {}",
|
||||||
|
reachable_base,
|
||||||
|
upstream_base,
|
||||||
|
)
|
||||||
|
upstream_base = reachable_base
|
||||||
|
|
||||||
sess = HlsPreviewSession(
|
sess = HlsPreviewSession(
|
||||||
upstream_base=upstream_base,
|
upstream_base=upstream_base,
|
||||||
|
|||||||
@@ -115,8 +115,8 @@ services:
|
|||||||
DEMO_ORCHESTRATOR_ENABLED: ${DEMO_ORCHESTRATOR_ENABLED:-false}
|
DEMO_ORCHESTRATOR_ENABLED: ${DEMO_ORCHESTRATOR_ENABLED:-false}
|
||||||
DEMO_ORCHESTRATOR_RTSP_PORT: ${DEMO_ORCHESTRATOR_RTSP_PORT:-18554}
|
DEMO_ORCHESTRATOR_RTSP_PORT: ${DEMO_ORCHESTRATOR_RTSP_PORT:-18554}
|
||||||
DEMO_ORCHESTRATOR_RTSP_JSON_HOST: ${DOCKER_DEMO_ORCHESTRATOR_RTSP_JSON_HOST:-host.docker.internal}
|
DEMO_ORCHESTRATOR_RTSP_JSON_HOST: ${DOCKER_DEMO_ORCHESTRATOR_RTSP_JSON_HOST:-host.docker.internal}
|
||||||
# api 使用 network:host,经宿主机端口访问 mediamtx-hls(勿用 mediamtx-hls 主机名)
|
# api 在 Compose 桥接网内反代 mediamtx-hls:8888;宿主机调试 HLS 仍用 127.0.0.1:18888
|
||||||
DEMO_HLS_PREVIEW_UPSTREAM: ${DEMO_HLS_PREVIEW_UPSTREAM:-http://127.0.0.1:18888}
|
DEMO_HLS_PREVIEW_UPSTREAM: ${DEMO_HLS_PREVIEW_UPSTREAM:-http://mediamtx-hls:8888}
|
||||||
DEMO_HLS_PREVIEW_CONFIG_DIR: ${DEMO_HLS_PREVIEW_CONFIG_DIR:-/hls-preview-config}
|
DEMO_HLS_PREVIEW_CONFIG_DIR: ${DEMO_HLS_PREVIEW_CONFIG_DIR:-/hls-preview-config}
|
||||||
DEMO_HLS_PREVIEW_CONTAINER_NAME: ${DEMO_HLS_PREVIEW_CONTAINER_NAME:-orm-mediamtx-hls}
|
DEMO_HLS_PREVIEW_CONTAINER_NAME: ${DEMO_HLS_PREVIEW_CONTAINER_NAME:-orm-mediamtx-hls}
|
||||||
MEDIAMTX_DOCKER_IMAGE: ${MEDIAMTX_DOCKER_IMAGE:-m.daocloud.io/docker.io/bluenviron/mediamtx:latest}
|
MEDIAMTX_DOCKER_IMAGE: ${MEDIAMTX_DOCKER_IMAGE:-m.daocloud.io/docker.io/bluenviron/mediamtx:latest}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from app.config import Settings
|
|||||||
from app.services.hls_preview import (
|
from app.services.hls_preview import (
|
||||||
HlsPreviewManager,
|
HlsPreviewManager,
|
||||||
HlsPreviewSession,
|
HlsPreviewSession,
|
||||||
|
attached_upstream_connect_candidates,
|
||||||
docker_publish_bind_host,
|
docker_publish_bind_host,
|
||||||
normalize_upstream_base,
|
normalize_upstream_base,
|
||||||
resolve_ephemeral_upstream_host,
|
resolve_ephemeral_upstream_host,
|
||||||
@@ -92,6 +93,20 @@ def test_hls_proxy_rewrites_playlist(hls_client: TestClient) -> None:
|
|||||||
assert "127.0.0.1:18888" not in text
|
assert "127.0.0.1:18888" not in text
|
||||||
|
|
||||||
|
|
||||||
|
def test_attached_upstream_connect_candidates_local() -> None:
|
||||||
|
with patch("app.services.hls_preview.Path") as path_cls:
|
||||||
|
path_cls.return_value.is_file.return_value = False
|
||||||
|
got = attached_upstream_connect_candidates("http://127.0.0.1:18888")
|
||||||
|
assert got == [("127.0.0.1", 18888)]
|
||||||
|
|
||||||
|
|
||||||
|
def test_attached_upstream_connect_candidates_docker_bridge() -> None:
|
||||||
|
with patch("app.services.hls_preview.Path") as path_cls:
|
||||||
|
path_cls.return_value.is_file.return_value = True
|
||||||
|
got = attached_upstream_connect_candidates("http://127.0.0.1:18888")
|
||||||
|
assert got == [("127.0.0.1", 18888), ("mediamtx-hls", 8888)]
|
||||||
|
|
||||||
|
|
||||||
def test_resolve_ephemeral_upstream_host_local() -> None:
|
def test_resolve_ephemeral_upstream_host_local() -> None:
|
||||||
with patch("app.services.hls_preview.Path") as path_cls:
|
with patch("app.services.hls_preview.Path") as path_cls:
|
||||||
path_cls.return_value.is_file.return_value = False
|
path_cls.return_value.is_file.return_value = False
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ SDK **不作为构建期依赖**:将厂商提供的 Linux x86_64 动态库挂
|
|||||||
|
|
||||||
| 部署方式 | 行为 |
|
| 部署方式 | 行为 |
|
||||||
|----------|------|
|
|----------|------|
|
||||||
| **Docker Compose(推荐)** | 固定服务 `mediamtx-hls`(宿主机 `127.0.0.1:18888`);`hls-preview-init` 种子共享卷后 MediaMTX 直接读 `/config/mediamtx.yml`(官方镜像是 scratch,勿用 `/bin/sh` entrypoint);`ensure` 写共享 `mediamtx.yml` 并 `docker restart orm-mediamtx-hls`;`DEMO_HLS_PREVIEW_UPSTREAM=http://127.0.0.1:18888` |
|
| **Docker Compose(推荐)** | 固定服务 `mediamtx-hls`(宿主机 `127.0.0.1:18888`,容器内 `mediamtx-hls:8888`);`hls-preview-init` 种子共享卷后 MediaMTX 直接读 `/config/mediamtx.yml`(官方镜像是 scratch,勿用 `/bin/sh` entrypoint);`ensure` 写共享 `mediamtx.yml` 并 `docker restart orm-mediamtx-hls`;`DEMO_HLS_PREVIEW_UPSTREAM=http://mediamtx-hls:8888` |
|
||||||
| **本机 uvicorn** | 未设 upstream 时按需 `docker run` 临时 MediaMTX(端口默认 `127.0.0.1:18888`) |
|
| **本机 uvicorn** | 未设 upstream 时按需 `docker run` 临时 MediaMTX(端口默认 `127.0.0.1:18888`) |
|
||||||
|
|
||||||
| 链路 | 行为 |
|
| 链路 | 行为 |
|
||||||
|
|||||||
Reference in New Issue
Block a user