Files
gitlab-instance-0a899031_sh/d2ypp2/scripts/hardware_probe.py
root e638a4656c s
2026-05-18 06:48:30 +00:00

328 lines
11 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.
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
import os
import platform
import re
import shutil
import socket
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 detect_video_capture() -> dict:
if platform.system().lower() != "linux":
return {
"video_devices": [],
"v4l2_devices": [],
"usb_capture_matches": [],
"ready": False,
}
video_nodes = sorted(str(path) for path in Path("/dev").glob("video*"))
v4l2_raw = run_text(["v4l2-ctl", "--list-devices"]) if shutil.which("v4l2-ctl") else ""
v4l2_devices: list[dict] = []
if v4l2_raw:
current_name = ""
current_nodes: list[str] = []
for raw in v4l2_raw.splitlines():
line = raw.rstrip()
if not line.strip():
if current_name or current_nodes:
v4l2_devices.append({"name": current_name or "unknown", "nodes": current_nodes[:]})
current_name = ""
current_nodes = []
continue
if not line.startswith("\t") and not line.startswith(" "):
current_name = line.rstrip(":")
continue
node = line.strip()
if node.startswith("/dev/"):
current_nodes.append(node)
if current_name or current_nodes:
v4l2_devices.append({"name": current_name or "unknown", "nodes": current_nodes[:]})
lsusb_raw = run_text(["lsusb"]) if shutil.which("lsusb") else ""
capture_pat = re.compile(r"(capture|video|uvc|hdmi|elgato|macrosilicon|ezcap|cam\slink)", re.IGNORECASE)
usb_capture_matches = [line for line in lsusb_raw.splitlines() if capture_pat.search(line)]
ready = bool(video_nodes) and bool(shutil.which("ffmpeg"))
return {
"video_devices": video_nodes,
"v4l2_devices": v4l2_devices,
"usb_capture_matches": usb_capture_matches,
"v4l2_available": bool(shutil.which("v4l2-ctl")),
"ready": ready,
}
def detect_network() -> dict:
interfaces: list[dict] = []
route_raw = run_text(["ip", "-j", "route", "show", "default"]) if shutil.which("ip") else ""
addr_raw = run_text(["ip", "-j", "-4", "addr", "show", "scope", "global"]) if shutil.which("ip") else ""
default_iface = ""
gateway = ""
if route_raw:
try:
routes = json.loads(route_raw)
except json.JSONDecodeError:
routes = []
if routes:
default_iface = str(routes[0].get("dev", "") or "")
gateway = str(routes[0].get("gateway", "") or "")
if addr_raw:
try:
addr_rows = json.loads(addr_raw)
except json.JSONDecodeError:
addr_rows = []
for row in addr_rows:
name = str(row.get("ifname", "") or "").strip()
if not name or name.startswith(("docker", "br-", "veth", "virbr", "lo")):
continue
speed_path = Path("/sys/class/net") / name / "speed"
operstate_path = Path("/sys/class/net") / name / "operstate"
speed = None
if speed_path.is_file():
try:
speed = int(speed_path.read_text(encoding="utf-8", errors="replace").strip())
except ValueError:
speed = None
addresses = [
str(item.get("local", "")).strip()
for item in (row.get("addr_info") or [])
if str(item.get("local", "")).strip()
]
interfaces.append(
{
"name": name,
"addresses": addresses,
"speed_mbps": speed if speed and speed > 0 else None,
"operstate": operstate_path.read_text(encoding="utf-8", errors="replace").strip()
if operstate_path.is_file()
else "unknown",
"default_route": name == default_iface,
}
)
online = False
if gateway:
online = True
else:
try:
with socket.create_connection(("1.1.1.1", 53), timeout=1.2):
online = True
except OSError:
online = False
return {
"default_iface": default_iface,
"gateway": gateway,
"interfaces": interfaces,
"online": online,
}
def recommend(profile: dict) -> dict:
hwaccels = set(profile["ffmpeg_hwaccels"])
gpu = profile["gpu"]
capture = profile.get("video_capture") or {}
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")
if not capture.get("video_devices"):
risk.append("No /dev/video capture input detected, TikTok HDMI ingest cannot start yet")
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(),
"video_capture": detect_video_capture(),
"network": detect_network(),
}
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 核显)。
NVIDIA 由 douyin_youtube_ffplay 内单独检测 h264_nvenc此处不重复写入。
QSV 在部分 Linux 发行版上需要额外的设备/滤镜链路,本项目默认不自动启用,以优先保证跨机兼容性。
"""
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()
has_v4l2_device = any(Path("/dev").glob("video*"))
if machine in ("aarch64", "arm64", "armv7l", "armv8l"):
if "h264_rkmpp" in text and Path("/dev/mpp_service").exists():
return "h264_rkmpp"
if "h264_v4l2m2m" in text and has_v4l2_device:
return "h264_v4l2m2m"
return ""
if "h264_vaapi" in text and render_ok and "vaapi" in hwaccels:
return "h264_vaapi"
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()