"""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": "低延迟 SRT;SRS 常用 caller 模式", }, { "id": "hls", "label": "HLS", "example": "https://example.com/live/stream.m3u8", "hint": "HTTP Live Streaming(m3u8)", }, { "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