Files
gitlab-instance-0a899031_d2…/web2_pull_stream.py
eric 59ad853140 新增拉流转推功能,并按业务重设计摄像头/拉流独立界面
支持 RTMP/SRT/HLS/FLV/WHEP 拉流到 YouTube;摄像头与拉流页采用独立工作台布局,更新 README 文档。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-07 21:16:43 -05:00

292 lines
8.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Pull-stream relay config and FFmpeg input helpers (RTMP / SRT / HLS / FLV / WebRTC-WHEP)."""
from __future__ import annotations
import configparser
import re
from pathlib import Path
from typing import Any
from urllib.parse import urlparse
PULL_STREAM_SECTION = "pull_stream"
PROTOCOL_CHOICES = ("auto", "rtmp", "rtmps", "srt", "hls", "http-flv", "webrtc", "udp")
def pull_stream_ini_path(config_dir: Path) -> Path:
return config_dir / "pull_stream.ini"
def default_pull_stream_config() -> dict[str, str]:
return {
"source_url": "",
"protocol": "auto",
"transcode": "transcode",
"bitrate": "4500",
"use_gpu": "",
"srt_latency_ms": "200",
"reconnect": "",
"youtube_watch_url": "",
"label": "",
"extra_input_args": "",
}
def load_pull_stream_config(config_dir: Path) -> dict[str, str]:
p = pull_stream_ini_path(config_dir)
out = default_pull_stream_config()
if not p.exists():
return out
cp = configparser.ConfigParser()
try:
cp.read(p, encoding="utf-8")
except Exception:
return out
if not cp.has_section(PULL_STREAM_SECTION):
return out
for key in out:
if cp.has_option(PULL_STREAM_SECTION, key):
out[key] = str(cp.get(PULL_STREAM_SECTION, key) or "").strip()
return out
def save_pull_stream_config(config_dir: Path, patch: dict[str, Any]) -> tuple[bool, str]:
config_dir = Path(config_dir)
config_dir.mkdir(parents=True, exist_ok=True)
cur = load_pull_stream_config(config_dir)
for k, v in patch.items():
if k in cur:
cur[k] = str(v or "").strip()
proto = (cur.get("protocol") or "auto").lower()
if proto not in PROTOCOL_CHOICES:
cur["protocol"] = "auto"
transcode = (cur.get("transcode") or "transcode").lower()
if transcode not in ("copy", "transcode"):
cur["transcode"] = "transcode"
cp = configparser.ConfigParser()
cp[PULL_STREAM_SECTION] = cur
p = pull_stream_ini_path(config_dir)
try:
with p.open("w", encoding="utf-8") as f:
cp.write(f)
return True, "拉流配置已保存"
except Exception as e:
return False, f"保存失败: {e}"
def detect_protocol_from_url(url: str) -> str:
raw = (url or "").strip()
low = raw.lower()
if not low:
return "auto"
if low.startswith("rtmps://"):
return "rtmps"
if low.startswith("rtmp://"):
return "rtmp"
if low.startswith("srt://"):
return "srt"
if low.startswith("whep://") or low.startswith("webrtc://"):
return "webrtc"
if low.startswith("udp://"):
return "udp"
if ".m3u8" in low or low.endswith("/m3u8"):
return "hls"
if ".flv" in low:
return "http-flv"
if low.startswith("http://") or low.startswith("https://"):
return "http-flv"
return "auto"
def resolve_protocol(cfg: dict[str, str]) -> str:
chosen = (cfg.get("protocol") or "auto").strip().lower()
if chosen and chosen != "auto":
return chosen
return detect_protocol_from_url(cfg.get("source_url") or "")
def pull_stream_config_ready(config_dir: Path) -> str | None:
cfg = load_pull_stream_config(config_dir)
url = str(cfg.get("source_url") or "").strip()
if not url:
return "请先填写拉流地址RTMP / SRT / HLS / FLV 等)"
proto = resolve_protocol(cfg)
if proto == "auto":
return "无法识别协议,请手动选择协议类型"
yp = Path(config_dir) / "youtube.ini"
if not yp.exists():
return "请先在 YouTube 配置中填写串流密钥"
cp = configparser.ConfigParser()
try:
cp.read(yp, encoding="utf-8")
except Exception:
return "无法读取 youtube.ini"
if not cp.has_section("youtube"):
return "请先在 YouTube 配置中填写串流密钥"
for name, val in cp.items("youtube"):
v = (val or "").strip()
if name in ("key", "stream_key") and v and not v.startswith("#"):
return None
return "请先在 YouTube 配置中填写串流密钥"
def _split_extra_args(text: str) -> list[str]:
raw = (text or "").strip()
if not raw:
return []
try:
import shlex
return shlex.split(raw, posix=False)
except Exception:
return [x for x in re.split(r"\s+", raw) if x]
def build_pull_input_args(cfg: dict[str, str]) -> tuple[list[str], str]:
"""Return FFmpeg input args (before -i URL) and resolved protocol."""
url = (cfg.get("source_url") or "").strip()
proto = resolve_protocol(cfg)
reconnect_on = (cfg.get("reconnect") or "").lower() not in ("", "0", "false", "", "off")
latency = int(cfg.get("srt_latency_ms") or "200")
extra = _split_extra_args(cfg.get("extra_input_args") or "")
common: list[str] = ["-hide_banner", "-loglevel", "info"]
if reconnect_on:
common.extend(
[
"-reconnect",
"1",
"-reconnect_streamed",
"1",
"-reconnect_delay_max",
"5",
]
)
if proto in ("rtmp", "rtmps"):
input_args = [
*common,
"-fflags",
"+genpts",
"-flags",
"low_delay",
"-rw_timeout",
"15000000",
"-i",
url,
]
elif proto == "srt":
input_args = [
*common,
"-fflags",
"nobuffer",
"-flags",
"low_delay",
"-timeout",
"10000000",
"-i",
url if "latency" in url.lower() else f"{url}{'&' if '?' in url else '?'}latency={latency}",
]
elif proto == "hls":
input_args = [
*common,
"-fflags",
"nobuffer",
"-flags",
"low_delay",
"-live_start_index",
"-1",
"-i",
url,
]
elif proto in ("http-flv", "udp"):
input_args = [
*common,
"-fflags",
"nobuffer",
"-flags",
"low_delay",
"-i",
url,
]
elif proto == "webrtc":
input_args = [
*common,
"-fflags",
"nobuffer",
"-flags",
"low_delay",
"-allowed_extensions",
"ALL",
"-protocol_whitelist",
"file,http,https,tcp,tls,udp,rtp",
"-i",
url,
]
else:
input_args = [*common, "-i", url]
if extra:
input_args = input_args[:1] + extra + input_args[1:]
return input_args, proto
def protocol_help() -> list[dict[str, str]]:
return [
{
"id": "rtmp",
"label": "RTMP",
"example": "rtmp://127.0.0.1/live/stream",
"hint": "SRS / OBS / nginx-rtmp 等标准 RTMP 拉流",
},
{
"id": "rtmps",
"label": "RTMPS",
"example": "rtmps://live.example.com/app/key",
"hint": "TLS 加密的 RTMP",
},
{
"id": "srt",
"label": "SRT",
"example": "srt://127.0.0.1:10080?mode=caller&streamid=#!::r=live/obs,m=publish",
"hint": "低延迟 SRTSRS 常用 caller 模式",
},
{
"id": "hls",
"label": "HLS",
"example": "https://example.com/live/stream.m3u8",
"hint": "HTTP Live Streamingm3u8",
},
{
"id": "http-flv",
"label": "HTTP-FLV",
"example": "http://127.0.0.1:8080/live/stream.flv",
"hint": "SRS HTTP-FLV 等",
},
{
"id": "webrtc",
"label": "WebRTC (WHEP)",
"example": "whep://your-server/index/api/whep?app=live&stream=obs",
"hint": "需 FFmpeg 支持 WHEP否则请经 SRS 转为 RTMP/SRT 再拉",
},
{
"id": "udp",
"label": "UDP / MPEG-TS",
"example": "udp://@:1234",
"hint": "MPEG-TS over UDP",
},
]
def source_url_looks_valid(url: str) -> bool:
raw = (url or "").strip()
if not raw:
return False
if raw.startswith(("rtmp://", "rtmps://", "srt://", "udp://", "whep://", "webrtc://")):
return True
try:
p = urlparse(raw)
return p.scheme in ("http", "https") and bool(p.netloc)
except Exception:
return False