新增拉流转推功能,并按业务重设计摄像头/拉流独立界面

支持 RTMP/SRT/HLS/FLV/WHEP 拉流到 YouTube;摄像头与拉流页采用独立工作台布局,更新 README 文档。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
eric
2026-07-07 21:16:43 -05:00
parent 9dd3a63109
commit 59ad853140
15 changed files with 2286 additions and 224 deletions

54
web2.py
View File

@@ -70,6 +70,12 @@ from web2_relay_pro import (
from web2_youtube_ini import parse_youtube_ini, serialize_youtube_ini, validate_youtube_keys
from web2_host_caps import gpu_status_payload, machine_summary_payload
from web2_camera import camera_config_ready, list_capture_devices, load_camera_config, save_camera_config
from web2_pull_stream import (
load_pull_stream_config,
protocol_help,
pull_stream_config_ready,
save_pull_stream_config,
)
from web2_pocketbase import REQUIRE_PB, pb_auth_refresh_and_validate
@@ -96,6 +102,7 @@ def _label_short(prefix: str) -> str:
"tiktok": "TikTok",
"youtube": "YouTube",
"camera": "摄像头",
"pull": "拉流",
"obs": "OBS",
"web": "Web 控制台",
}.get(prefix, prefix)
@@ -119,6 +126,10 @@ def discover_script_entries() -> tuple:
if path.is_file() and path.resolve() != web_path:
entries.append({"script": path.name, "label": _label_short("camera")})
for path in sorted(base.glob("pull_stream*.py")):
if path.is_file() and path.resolve() != web_path:
entries.append({"script": path.name, "label": _label_short("pull")})
for path in sorted(base.glob("obs*.sh")):
if path.is_file():
entries.append({"script": path.name, "label": _label_short("obs")})
@@ -147,6 +158,10 @@ def is_camera_script(script: str) -> bool:
return script.endswith(".py") and _stem(script).startswith("camera")
def is_pull_stream_script(script: str) -> bool:
return script.endswith(".py") and _stem(script).startswith("pull_stream")
def is_youtube_script(script: str) -> bool:
return script.endswith(".py") and _stem(script).startswith("youtube")
@@ -855,6 +870,16 @@ def _camera_ready_message(process: str) -> str | None:
return camera_config_ready(CONFIG_DIR)
def _pull_stream_ready_message(process: str) -> str | None:
if not is_pull_stream_script(script_for_pm2(process) or ""):
return None
return pull_stream_config_ready(CONFIG_DIR)
def _stream_worker_ready_message(process: str) -> str | None:
return _camera_ready_message(process) or _pull_stream_ready_message(process)
@app.get("/camera/devices")
async def camera_devices():
return list_capture_devices()
@@ -880,6 +905,31 @@ async def camera_config_post(request: Request):
return {"message": msg, "config": load_camera_config(CONFIG_DIR)}
@app.get("/pull_stream/config")
async def pull_stream_config_get():
return {"config": load_pull_stream_config(CONFIG_DIR)}
@app.get("/pull_stream/protocols")
async def pull_stream_protocols():
return {"protocols": protocol_help()}
@app.post("/pull_stream/config")
async def pull_stream_config_post(request: Request):
try:
data = await request.json()
except Exception:
return JSONResponse({"error": "无效的 JSON"}, status_code=400)
if not isinstance(data, dict):
return JSONResponse({"error": "无效数据"}, status_code=400)
patch = data.get("config") if isinstance(data.get("config"), dict) else data
ok, msg = save_pull_stream_config(CONFIG_DIR, patch)
if not ok:
return JSONResponse({"error": msg}, status_code=400)
return {"message": msg, "config": load_pull_stream_config(CONFIG_DIR)}
@app.get("/start")
async def start(request: Request, process: str = Query(..., description="进程名")):
if REQUIRE_PB:
@@ -902,7 +952,7 @@ async def start(request: Request, process: str = Query(..., description="进程
if not ok:
return JSONResponse({"output": msg}, status_code=400)
await _stop_legacy_youtube_if_online()
cam_msg = _camera_ready_message(process)
cam_msg = _stream_worker_ready_message(process)
if cam_msg:
return JSONResponse({"output": cam_msg}, status_code=400)
output = await control_start(process)
@@ -963,7 +1013,7 @@ async def restart(request: Request, process: str = Query(..., description="进
if not ok:
return JSONResponse({"output": f"{stop_output}\n{msg}"}, status_code=400)
await _stop_legacy_youtube_if_online()
cam_msg = _camera_ready_message(process)
cam_msg = _stream_worker_ready_message(process)
if cam_msg:
return JSONResponse({"output": f"{stop_output}\n{cam_msg}"}, status_code=400)
start_output = await control_start(process)