# -*- coding: utf-8 -*- """Pull stream (RTMP / SRT / HLS / FLV / WHEP) → YouTube RTMPS relay.""" from __future__ import annotations import configparser import os import platform import signal import subprocess import sys import time from pathlib import Path from web2_proxy_config import merge_child_env from web2_pull_stream import build_pull_input_args, load_pull_stream_config, resolve_protocol BASE_DIR = Path(__file__).resolve().parent CONFIG_DIR = BASE_DIR / "config" YOUTUBE_INI = CONFIG_DIR / "youtube.ini" RUNNING = True def _on_signal(*_args): global RUNNING RUNNING = False signal.signal(signal.SIGTERM, _on_signal) signal.signal(signal.SIGINT, _on_signal) def _load_youtube_key() -> tuple[str, str]: rtmps = "rtmps://a.rtmp.youtube.com/live2/" if not YOUTUBE_INI.exists(): return "", rtmps cp = configparser.ConfigParser() cp.read(YOUTUBE_INI, encoding="utf-8") if not cp.has_section("youtube"): return "", rtmps key = "" for name, val in cp.items("youtube"): v = (val or "").strip() if name in ("key", "stream_key") and v and not v.startswith("#"): key = v break if key.startswith("rtmp://") or key.startswith("rtmps://"): return key, key return key, rtmps + key if key else "" def _pick_nvenc(force_sw: bool) -> tuple[str, list[str]]: if force_sw: return "libx264", ["-preset", "veryfast", "-tune", "zerolatency", "-profile:v", "high"] try: if platform.system().lower() not in ("windows", "linux"): raise RuntimeError("skip") check = subprocess.run(["nvidia-smi"], capture_output=True, timeout=5) if check.returncode != 0: raise RuntimeError("no nvidia") enc = subprocess.run(["ffmpeg", "-hide_banner", "-encoders"], capture_output=True, text=True, timeout=5) if "h264_nvenc" in (enc.stdout or ""): print("[INFO] 使用 NVENC 硬件编码") return "h264_nvenc", ["-profile:v", "high", "-tune", "ll", "-rc", "cbr"] except Exception: pass return "libx264", ["-preset", "veryfast", "-tune", "zerolatency", "-profile:v", "high"] def _build_cmd(cfg: dict[str, str], youtube_url: str) -> list[str]: input_args, proto = build_pull_input_args(cfg) transcode = (cfg.get("transcode") or "transcode").lower() br = int(cfg.get("bitrate") or "4500") b_v = f"{br}k" maxrate = f"{min(int(br * 1.2), 8000)}k" bufsize = f"{br * 2}k" force_sw = (cfg.get("use_gpu") or "").lower() in ("否", "0", "false", "无", "关", "off") print(f"[INFO] 拉流协议: {proto}") print(f"[INFO] 源地址: {(cfg.get('source_url') or '').strip()}") if transcode == "copy": output_args = [ "-c:v", "copy", "-c:a", "copy", "-bsf:a", "aac_adtstoasc", "-f", "flv", youtube_url, ] return ["ffmpeg", *input_args, *output_args] codec, vopts = _pick_nvenc(force_sw) fps = 30 output_args = [ "-c:v", codec, *vopts, "-pix_fmt", "yuv420p", "-b:v", b_v, "-maxrate", maxrate, "-bufsize", bufsize, "-g", str(fps * 2), "-c:a", "aac", "-b:a", "128k", "-ar", "44100", "-f", "flv", youtube_url, ] return ["ffmpeg", *input_args, *output_args] def main() -> None: print("[INFO] 拉流 → YouTube 转播进程已启动") proc: subprocess.Popen | None = None while RUNNING: cfg = load_pull_stream_config(CONFIG_DIR) key, youtube_url = _load_youtube_key() if not key: print("[WARN] 请在 config/youtube.ini 配置 YouTube 串流密钥 (key=…)") time.sleep(5) continue if not (cfg.get("source_url") or "").strip(): print("[WARN] 请在拉流配置中填写 source_url") time.sleep(5) continue if resolve_protocol(cfg) == "auto": print("[WARN] 无法识别拉流协议,请在配置中指定 protocol") time.sleep(5) continue cmd = _build_cmd(cfg, youtube_url) print("[INFO] 启动 FFmpeg 拉流转播…") print("[INFO] ffmpeg " + " ".join(cmd[1:])) env = merge_child_env(os.environ.copy()) try: proc = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, encoding="utf-8", errors="replace", env=env, cwd=str(BASE_DIR), ) except Exception as e: print(f"[ERROR] 无法启动 FFmpeg: {e}") time.sleep(5) continue assert proc.stdout is not None while RUNNING: line = proc.stdout.readline() if not line: break print(line.rstrip()) code = proc.wait() proc = None if not RUNNING: break print(f"[WARN] FFmpeg 退出 code={code},5 秒后重试…") time.sleep(5) if proc and proc.poll() is None: try: proc.terminate() proc.wait(timeout=5) except Exception: try: proc.kill() except Exception: pass print("[INFO] 拉流转播进程已结束") if __name__ == "__main__": main()