This commit is contained in:
Kevin
2026-05-22 17:18:27 +08:00
parent 7e2426339e
commit c933815650
7 changed files with 80 additions and 14 deletions

View File

@@ -90,7 +90,8 @@ POSTGRES_PORT=45432
# DEMO_ORCHESTRATOR_ENABLED=false
# DEMO_ORCHESTRATOR_RTSP_PORT=18554
# 联调台 HLSCompose先 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
# DEMO_HLS_PREVIEW_PORT=18888
# Docker 内 API 访问宿主机假流时写入站点 JSON 的主机名(默认 host.docker.internal

View File

@@ -1,5 +1,6 @@
import asyncio
from collections.abc import Awaitable, Callable
import pathlib
from typing import Annotated
from fastapi import (
@@ -260,7 +261,13 @@ async def hls_preview_ensure(
detail += (
"。当前 API 可能仍是旧版本:请执行 "
"`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(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,

View File

@@ -191,7 +191,8 @@ class Settings(BaseSettings):
demo_hls_preview_upstream: str = Field(
default="http://127.0.0.1:18888",
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本机开发"
),
)

View File

@@ -28,9 +28,7 @@ from app.services.synthetic_rtsp import MEDIAMTX_IMAGE
CONTAINER_NAME_PREFIX = "orm-hls-preview-"
_MEDIAMTX_HLS_INTERNAL_PORT = 8888
_PATH_SAFE = re.compile(r"[^a-zA-Z0-9._-]+")
_DOCKER_PUBLISH_ALIAS_HOSTS = frozenset(
{"host.docker.internal", "host.containers.internal", "localhost"}
)
_DOCKER_LOOPBACK_HOSTS = frozenset({"127.0.0.1", "localhost", "::1"})
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:
"""本机临时容器 ``docker run -p`` 的绑定地址(勿用主机名)。"""
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 (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)
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:
lines = [
"# 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}"
)
host, port = upstream_host_port(upstream_base)
candidates = attached_upstream_connect_candidates(upstream_base)
try:
_wait_tcp(host, port, timeout=ready_timeout_sec)
except Exception:
host, port = _wait_tcp_any(candidates, timeout=ready_timeout_sec)
except RuntimeError as exc:
raise RuntimeError(
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(
upstream_base=upstream_base,

View File

@@ -115,8 +115,8 @@ services:
DEMO_ORCHESTRATOR_ENABLED: ${DEMO_ORCHESTRATOR_ENABLED:-false}
DEMO_ORCHESTRATOR_RTSP_PORT: ${DEMO_ORCHESTRATOR_RTSP_PORT:-18554}
DEMO_ORCHESTRATOR_RTSP_JSON_HOST: ${DOCKER_DEMO_ORCHESTRATOR_RTSP_JSON_HOST:-host.docker.internal}
# api 使用 network:host经宿主机端口访问 mediamtx-hls勿用 mediamtx-hls 主机名)
DEMO_HLS_PREVIEW_UPSTREAM: ${DEMO_HLS_PREVIEW_UPSTREAM:-http://127.0.0.1:18888}
# api 在 Compose 桥接网内反代 mediamtx-hls:8888宿主机调试 HLS 仍用 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_CONTAINER_NAME: ${DEMO_HLS_PREVIEW_CONTAINER_NAME:-orm-mediamtx-hls}
MEDIAMTX_DOCKER_IMAGE: ${MEDIAMTX_DOCKER_IMAGE:-m.daocloud.io/docker.io/bluenviron/mediamtx:latest}

View File

@@ -13,6 +13,7 @@ from app.config import Settings
from app.services.hls_preview import (
HlsPreviewManager,
HlsPreviewSession,
attached_upstream_connect_candidates,
docker_publish_bind_host,
normalize_upstream_base,
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
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:
with patch("app.services.hls_preview.Path") as path_cls:
path_cls.return_value.is_file.return_value = False

View File

@@ -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` |
| 链路 | 行为 |