Files
gitlab-instance-0a899031_do…/scripts/hardware_probe.py
2026-03-28 17:16:38 -05:00

205 lines
6.6 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.
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
import os
import platform
import shutil
import subprocess
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
CONFIG_DIR = ROOT / "config"
JSON_PATH = CONFIG_DIR / "hardware-profile.json"
ENV_PATH = CONFIG_DIR / "hardware.env"
def run_text(cmd: list[str]) -> str:
try:
proc = subprocess.run(
cmd,
capture_output=True,
text=True,
encoding="utf-8",
errors="ignore",
timeout=8,
check=False,
)
except (OSError, subprocess.TimeoutExpired):
return ""
return (proc.stdout or proc.stderr or "").strip()
def detect_ffmpeg_hwaccels() -> list[str]:
if not shutil.which("ffmpeg"):
return []
text = run_text(["ffmpeg", "-hide_banner", "-hwaccels"])
accels: list[str] = []
for line in text.splitlines():
item = line.strip()
if not item or item.lower().startswith("hardware acceleration methods"):
continue
if item not in accels:
accels.append(item)
return accels
def detect_gpu() -> dict:
dri_dir = Path("/dev/dri")
render_nodes = sorted(str(path) for path in dri_dir.glob("renderD*")) if dri_dir.is_dir() else []
cards = sorted(str(path) for path in dri_dir.glob("card*")) if dri_dir.is_dir() else []
driver = ""
if cards:
card0 = Path(cards[0])
driver_link = Path(f"/sys/class/drm/{card0.name}/device/driver")
if driver_link.exists():
try:
driver = driver_link.resolve().name
except OSError:
driver = driver_link.name
return {
"present": bool(cards or render_nodes),
"cards": cards,
"render_nodes": render_nodes,
"driver": driver,
"vainfo": bool(shutil.which("vainfo")),
}
def detect_npu() -> dict:
hints = []
for path in (
Path("/dev/rknpu"),
Path("/dev/npu"),
Path("/dev/galcore"),
Path("/sys/class/misc/rknpu"),
Path("/sys/class/misc/verisilicon-npu"),
):
if path.exists():
hints.append(str(path))
return {
"present": bool(hints),
"devices": hints,
}
def detect_kernel_features() -> dict:
features = {
"drm": Path("/dev/dri/card0").exists(),
"render_node": any(Path("/dev/dri").glob("renderD*")) if Path("/dev/dri").is_dir() else False,
"v4l2loopback": Path("/sys/module/v4l2loopback").exists(),
"kvm": Path("/dev/kvm").exists(),
"nvidia": shutil.which("nvidia-smi") is not None,
}
return features
def recommend(profile: dict) -> dict:
hwaccels = set(profile["ffmpeg_hwaccels"])
gpu = profile["gpu"]
browser_hw = "auto" if gpu["present"] else "disabled"
decoder = "cpu"
if "vaapi" in hwaccels and gpu["render_nodes"]:
decoder = "vaapi"
elif "v4l2m2m" in hwaccels:
decoder = "v4l2m2m"
elif "cuda" in hwaccels or "nvdec" in hwaccels:
decoder = "cuda"
hdmi_player = "cpu"
if shutil.which("mpv") and gpu["cards"]:
hdmi_player = "mpv-drm"
elif shutil.which("ffplay") and (os.environ.get("DISPLAY") or shutil.which("startx")):
hdmi_player = "ffplay-x11"
risk = []
if not gpu["present"]:
risk.append("No DRM/VA render node detected, browser and HDMI playback will stay on conservative settings")
if decoder == "cpu":
risk.append("No safe hardware decoder detected, ffmpeg workloads will remain on CPU")
if hdmi_player == "cpu":
risk.append("No direct HDMI playback stack detected, install mpv or X11+ffplay for full-screen output")
return {
"video_decoder": decoder,
"browser_hwaccel": browser_hw,
"hdmi_player": hdmi_player,
"stability_mode": "safe" if risk else "balanced",
"degradation_reasons": risk,
}
def build_profile() -> dict:
profile = {
"status": "ok",
"arch": platform.machine(),
"kernel": platform.release(),
"platform": platform.platform(),
"ffmpeg_hwaccels": detect_ffmpeg_hwaccels(),
"gpu": detect_gpu(),
"npu": detect_npu(),
"kernel_features": detect_kernel_features(),
}
profile["recommendation"] = recommend(profile)
return profile
def suggest_video_encoder(profile: dict) -> str:
"""在 Linux 上根据 ffmpeg 能力与探测结果给出可选硬件编码器名(供 douyin→YouTube 读取 LIVE_VIDEO_ENCODER
优先级RKMPP常见 ARM SoC→ V4L2M2M通用 ARM/部分板)→ VA-APIx86 核显)→ QSVIntel x86
NVIDIA 由 douyin_youtube_ffplay 内单独检测 h264_nvenc此处不重复写入。
"""
if platform.system().lower() != "linux":
return ""
text = run_text(["ffmpeg", "-hide_banner", "-encoders"])
if not text:
return ""
hwaccels = set(profile.get("ffmpeg_hwaccels") or [])
gpu = profile.get("gpu") or {}
render_ok = bool(gpu.get("render_nodes"))
machine = platform.machine().lower()
if "h264_rkmpp" in text and Path("/dev/mpp_service").exists():
return "h264_rkmpp"
if "h264_v4l2m2m" in text and "v4l2m2m" in hwaccels:
return "h264_v4l2m2m"
if "h264_vaapi" in text and render_ok and "vaapi" in hwaccels:
return "h264_vaapi"
if machine in ("x86_64", "amd64") and "h264_qsv" in text:
return "h264_qsv"
return ""
def write_outputs(profile: dict) -> None:
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
JSON_PATH.write_text(json.dumps(profile, indent=2, ensure_ascii=False), encoding="utf-8")
enc = suggest_video_encoder(profile)
env_lines = [
f"LIVE_HW_DECODER={profile['recommendation']['video_decoder']}",
f"LIVE_HDMI_PLAYER={profile['recommendation']['hdmi_player']}",
f"LIVE_BROWSER_HWACCEL={profile['recommendation']['browser_hwaccel']}",
f"LIVE_STABILITY_MODE={profile['recommendation']['stability_mode']}",
]
if enc:
env_lines.append(f"LIVE_VIDEO_ENCODER={enc}")
else:
env_lines.append("# LIVE_VIDEO_ENCODER= # optional: h264_v4l2m2m / h264_rkmpp when ffmpeg supports it")
ENV_PATH.write_text("\n".join(env_lines) + "\n", encoding="utf-8")
def main() -> None:
parser = argparse.ArgumentParser(description="Probe Linux hardware capabilities for safe runtime defaults")
parser.add_argument("--write", action="store_true", help="Write JSON and env files into config/")
args = parser.parse_args()
profile = build_profile()
if args.write:
write_outputs(profile)
print(json.dumps(profile, ensure_ascii=False))
if __name__ == "__main__":
main()