's'
This commit is contained in:
91
scripts/env_check.py
Normal file
91
scripts/env_check.py
Normal file
@@ -0,0 +1,91 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""启动前环境分析:Python 依赖、ffmpeg、Node/npm、端口提示(Windows / Linux 通用)。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import shutil
|
||||
import socket
|
||||
import sys
|
||||
|
||||
|
||||
def _has_module(name: str) -> bool:
|
||||
return importlib.util.find_spec(name) is not None
|
||||
|
||||
|
||||
def _local_ips() -> list[str]:
|
||||
out: list[str] = []
|
||||
try:
|
||||
hostname = socket.gethostname()
|
||||
for info in socket.getaddrinfo(hostname, None, family=socket.AF_INET):
|
||||
ip = info[4][0]
|
||||
if ip and ip != "127.0.0.1" and ip not in out:
|
||||
out.append(ip)
|
||||
except OSError:
|
||||
pass
|
||||
return out[:4]
|
||||
|
||||
|
||||
def print_env_report(
|
||||
*,
|
||||
dev: bool,
|
||||
need_npm: bool,
|
||||
host: str,
|
||||
port: str,
|
||||
next_port: str = "3000",
|
||||
) -> None:
|
||||
print()
|
||||
print("┌── 环境检测 ─────────────────────────────────────────")
|
||||
print(f"│ 系统: {sys.platform} Python: {sys.version.split()[0]}")
|
||||
ok = True
|
||||
for mod, label in (
|
||||
("fastapi", "FastAPI"),
|
||||
("uvicorn", "Uvicorn"),
|
||||
):
|
||||
if _has_module(mod):
|
||||
print(f"│ ✓ {label} 已安装")
|
||||
else:
|
||||
print(f"│ ✕ 缺少 {label},请执行: pip install -r requirements.txt")
|
||||
ok = False
|
||||
|
||||
ff = shutil.which("ffmpeg")
|
||||
fp = shutil.which("ffplay")
|
||||
if ff:
|
||||
print(f"│ ✓ ffmpeg: {ff}")
|
||||
else:
|
||||
print("│ ⚠ 未找到 ffmpeg(推流/录制/OBS 拉流会失败;Docker 镜像已内置)")
|
||||
ok = False
|
||||
if fp:
|
||||
print(f"│ ✓ ffplay: {fp}")
|
||||
elif sys.platform != "win32":
|
||||
print("│ ⚠ 未找到 ffplay(HDMI 全屏播放需要完整 ffmpeg 套件)")
|
||||
|
||||
if need_npm:
|
||||
npm = shutil.which("npm") or shutil.which("npm.cmd")
|
||||
if npm:
|
||||
print(f"│ ✓ npm: {npm}")
|
||||
else:
|
||||
print("│ ✕ 未找到 npm(构建/开发前端需要安装 Node.js)")
|
||||
ok = False
|
||||
|
||||
print("│")
|
||||
print("│ 同机端口提示(若冲突请在 config/runtime.env 修改 PORT):")
|
||||
print("│ 本控制台 API/Web 使用环境变量 PORT(默认见 launch.py)")
|
||||
print("│ SRS 常见: 1935/1985/8080/10080 等;代理常见: 7890/9090;Chrome 远程: 9222")
|
||||
print("│")
|
||||
print(f"│ 监听: {host}:{port}")
|
||||
if dev:
|
||||
ips = _local_ips()
|
||||
print(f"│ 开发 UI: 0.0.0.0:{next_port}(局域网可用本机 IP 访问)")
|
||||
if ips:
|
||||
print(f"│ 示例: http://{ips[0]}:{next_port}/")
|
||||
else:
|
||||
if host == "0.0.0.0":
|
||||
ips = _local_ips()
|
||||
if ips:
|
||||
print(f"│ 局域网访问示例: http://{ips[0]}:{port}/")
|
||||
else:
|
||||
print("│ 局域网访问: http://<本机IP>:%s/" % port)
|
||||
print("└────────────────────────────────────────────────────")
|
||||
print()
|
||||
if not ok and not dev:
|
||||
print("[env] 存在警告,仍可尝试启动;若运行失败请按上述提示补全依赖。\n")
|
||||
245
scripts/launch.py
Normal file
245
scripts/launch.py
Normal file
@@ -0,0 +1,245 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
一键启动 Web 控制台:
|
||||
生产:构建 web-console(若需要)+ 仅 uvicorn(单端口托管 API + 静态页)
|
||||
开发:uvicorn + Next dev(默认绑定 0.0.0.0,局域网可访问)
|
||||
|
||||
自动加载 config/runtime.env(若存在,不覆盖已有环境变量)。
|
||||
|
||||
用法:
|
||||
python scripts/launch.py
|
||||
python scripts/launch.py --dev
|
||||
python scripts/launch.py --port 8101
|
||||
python scripts/launch.py --skip-build
|
||||
python scripts/launch.py --no-env-check
|
||||
|
||||
环境变量: HOST, PORT, NEXT_DEV_PORT, WEB_STACK=api, API_PROXY_TARGET(开发)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
if hasattr(sys.stdout, "reconfigure"):
|
||||
try:
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
SCRIPTS_DIR = Path(__file__).resolve().parent
|
||||
CONSOLE = ROOT / "web-console"
|
||||
OUT_INDEX = CONSOLE / "out" / "index.html"
|
||||
|
||||
sys.path.insert(0, str(SCRIPTS_DIR))
|
||||
import env_check # noqa: E402
|
||||
|
||||
|
||||
def load_runtime_env() -> None:
|
||||
path = ROOT / "config" / "runtime.env"
|
||||
if not path.is_file():
|
||||
return
|
||||
for raw in path.read_text(encoding="utf-8").splitlines():
|
||||
line = raw.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
if "=" not in line:
|
||||
continue
|
||||
k, _, v = line.partition("=")
|
||||
k, v = k.strip(), v.strip().strip('"').strip("'")
|
||||
if k and k not in os.environ:
|
||||
os.environ[k] = v
|
||||
|
||||
|
||||
def _which_npm() -> str | None:
|
||||
return shutil.which("npm") or shutil.which("npm.cmd")
|
||||
|
||||
|
||||
def _npm(args: list[str], *, cwd: Path, env: dict | None = None) -> int:
|
||||
full_env = {**os.environ, **(env or {})}
|
||||
if sys.platform == "win32":
|
||||
return subprocess.call(
|
||||
"npm " + " ".join(args),
|
||||
cwd=cwd,
|
||||
shell=True,
|
||||
env=full_env,
|
||||
)
|
||||
npm = _which_npm()
|
||||
if not npm:
|
||||
return 127
|
||||
return subprocess.call([npm, *args], cwd=cwd, env=full_env)
|
||||
|
||||
|
||||
def ensure_console_built(*, skip: bool) -> bool:
|
||||
"""返回是否已存在可托管的静态 index。"""
|
||||
if skip:
|
||||
return OUT_INDEX.is_file()
|
||||
if not CONSOLE.is_dir():
|
||||
print("[launch] 未找到 web-console,将仅使用根目录 index.html(若存在)。")
|
||||
return False
|
||||
if not _which_npm():
|
||||
print("[launch] 未检测到 npm,跳过前端构建;请先安装 Node.js 或手动在 web-console 执行 npm run build。")
|
||||
return OUT_INDEX.is_file()
|
||||
if OUT_INDEX.is_file():
|
||||
return True
|
||||
print("[launch] 正在安装依赖并构建 Web 控制台(首次略慢)…")
|
||||
lock = CONSOLE / "package-lock.json"
|
||||
if lock.is_file():
|
||||
if _npm(["ci"], cwd=CONSOLE) != 0:
|
||||
print("[launch] npm ci 失败,尝试 npm install…")
|
||||
if _npm(["install"], cwd=CONSOLE) != 0:
|
||||
sys.exit(1)
|
||||
else:
|
||||
if _npm(["install"], cwd=CONSOLE) != 0:
|
||||
sys.exit(1)
|
||||
if _npm(["run", "build"], cwd=CONSOLE) != 0:
|
||||
sys.exit(1)
|
||||
print("[launch] 前端构建完成。")
|
||||
return True
|
||||
|
||||
|
||||
def _terminate_process(p: subprocess.Popen | None) -> None:
|
||||
if p is None or p.poll() is not None:
|
||||
return
|
||||
p.terminate()
|
||||
try:
|
||||
p.wait(timeout=8)
|
||||
except subprocess.TimeoutExpired:
|
||||
p.kill()
|
||||
p.wait(timeout=3)
|
||||
|
||||
|
||||
def run_dev(host: str, port: str, next_port: str) -> None:
|
||||
if not _which_npm():
|
||||
print("[launch] 开发模式需要 Node.js/npm。仅起 API 请用: python scripts/launch.py --api-only --reload")
|
||||
sys.exit(1)
|
||||
api_url = f"http://127.0.0.1:{port}"
|
||||
env = os.environ.copy()
|
||||
env.setdefault("API_PROXY_TARGET", api_url)
|
||||
|
||||
api_cmd = [
|
||||
sys.executable,
|
||||
"-m",
|
||||
"uvicorn",
|
||||
"web:app",
|
||||
"--host",
|
||||
host,
|
||||
"--port",
|
||||
port,
|
||||
"--reload",
|
||||
]
|
||||
os.chdir(ROOT)
|
||||
api: subprocess.Popen | None = None
|
||||
ui: subprocess.Popen | None = None
|
||||
next_args = f"npm run dev -- --hostname 0.0.0.0 --port {next_port}"
|
||||
try:
|
||||
api = subprocess.Popen(api_cmd, cwd=ROOT)
|
||||
time.sleep(0.6)
|
||||
if api.poll() is not None:
|
||||
print("[launch] API 进程异常退出,请检查端口是否被占用。")
|
||||
sys.exit(api.returncode or 1)
|
||||
if sys.platform == "win32":
|
||||
ui = subprocess.Popen(next_args, cwd=CONSOLE, shell=True, env=env)
|
||||
else:
|
||||
ui = subprocess.Popen(
|
||||
["npm", "run", "dev", "--", "--hostname", "0.0.0.0", "--port", next_port],
|
||||
cwd=CONSOLE,
|
||||
env=env,
|
||||
)
|
||||
print()
|
||||
print(" —— 开发模式(局域网可访问)——")
|
||||
print(f" 控制台界面: http://0.0.0.0:{next_port} 或用本机 IP + 端口")
|
||||
print(f" API: http://{host}:{port}/ (Next 已代理同名接口到 {api_url})")
|
||||
print(" 按 Ctrl+C 可同时结束前后端")
|
||||
if host == "0.0.0.0":
|
||||
print(" 提示: 绑定 0.0.0.0 时请勿把管理端口暴露到公网")
|
||||
print()
|
||||
while True:
|
||||
time.sleep(0.5)
|
||||
if api.poll() is not None:
|
||||
print("[launch] API 已退出")
|
||||
break
|
||||
if ui and ui.poll() is not None:
|
||||
print("[launch] Next 已退出")
|
||||
break
|
||||
except KeyboardInterrupt:
|
||||
print("\n[launch] 正在关闭…")
|
||||
finally:
|
||||
_terminate_process(ui)
|
||||
_terminate_process(api)
|
||||
|
||||
|
||||
def run_api_only(host: str, port: str, reload: bool) -> None:
|
||||
os.chdir(ROOT)
|
||||
cmd = [sys.executable, "-m", "uvicorn", "web:app", "--host", host, "--port", port]
|
||||
if reload:
|
||||
cmd.append("--reload")
|
||||
os.execvp(sys.executable, cmd)
|
||||
|
||||
|
||||
def run_prod(host: str, port: str, skip_build: bool, reload: bool) -> None:
|
||||
ensure_console_built(skip=skip_build)
|
||||
run_api_only(host, port, reload)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
load_runtime_env()
|
||||
|
||||
default_port = os.environ.get("PORT", "8001")
|
||||
ap = argparse.ArgumentParser(description="直播控制台一键启动")
|
||||
ap.add_argument("--dev", action="store_true", help="开发:API + Next(需 Node)")
|
||||
ap.add_argument(
|
||||
"--api-only",
|
||||
action="store_true",
|
||||
help="仅 uvicorn,不构建、不启动 Next",
|
||||
)
|
||||
ap.add_argument("--host", default=None, help="监听地址(默认生产 0.0.0.0,可经 HOST 环境变量)")
|
||||
ap.add_argument("--port", default=None, help="端口(默认 PORT 或 8001)")
|
||||
ap.add_argument("--skip-build", action="store_true", help="跳过前端构建检测")
|
||||
ap.add_argument("--reload", action="store_true", help="uvicorn --reload(生产慎用)")
|
||||
ap.add_argument("--no-env-check", action="store_true", help="跳过环境分析")
|
||||
args = ap.parse_args()
|
||||
|
||||
if os.environ.get("WEB_STACK", "").lower() == "api":
|
||||
args.api_only = True
|
||||
|
||||
port = str(args.port if args.port is not None else default_port)
|
||||
next_port = os.environ.get("NEXT_DEV_PORT", "3000")
|
||||
|
||||
if args.dev:
|
||||
host = args.host or os.environ.get("HOST") or "0.0.0.0"
|
||||
else:
|
||||
host = args.host or os.environ.get("HOST") or "0.0.0.0"
|
||||
|
||||
need_npm = args.dev or (
|
||||
not args.skip_build and not args.api_only and not OUT_INDEX.is_file()
|
||||
)
|
||||
if not args.no_env_check:
|
||||
env_check.print_env_report(
|
||||
dev=bool(args.dev),
|
||||
need_npm=need_npm,
|
||||
host=host,
|
||||
port=port,
|
||||
next_port=next_port,
|
||||
)
|
||||
|
||||
if args.dev:
|
||||
run_dev(host, port, next_port)
|
||||
return
|
||||
|
||||
if args.api_only:
|
||||
run_api_only(host, port, args.reload)
|
||||
return
|
||||
|
||||
run_prod(host, port, skip_build=args.skip_build, reload=args.reload)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
10
scripts/srt_to_hdmi_drm.example.sh
Normal file
10
scripts/srt_to_hdmi_drm.example.sh
Normal file
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
# 无 X11/Wayland、仅有 DRM/KMS 的开发板示例:将同源 SRT 解码后送 kmsgrab/drm 需按板卡改设备。
|
||||
# 请先确认 ffmpeg 编译选项含 --enable-libdrm;路径与 connector 用 `modetest`/`drm_info` 自查。
|
||||
# 使用前复制为 srt_to_hdmi_drm.sh 并修改 DRM 设备与 mode。
|
||||
set -euo pipefail
|
||||
: "${SRS_SRT_URL:?设置 SRS_SRT_URL}"
|
||||
: "${DRM_DEVICE:=/dev/dri/card0}"
|
||||
echo "示例占位:请根据硬件将下行 ffmpeg 参数改为本机可用的 drmvideo/kmsgrab 输出。" >&2
|
||||
echo "SRT: $SRS_SRT_URL DRM: $DRM_DEVICE" >&2
|
||||
exit 1
|
||||
Reference in New Issue
Block a user