Files
gitlab-instance-0a899031_d2…/pull_stream_youtube.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

191 lines
5.5 KiB
Python
Raw 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.
# -*- 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()