功能:完善2K串流、显示恢复与输入优化
This commit is contained in:
1 parent
e47b01beec
commit
938ec97cce
95 files changed
+5788
-72
No files matched your search
@@ -0,0 +1,170 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Restore this RK3528 terminal's verified HDMI timing after X11 hotplug."""
|
||||
|
||||
import argparse
|
||||
import fcntl
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
|
||||
OUTPUT = "HDMI-1"
|
||||
MODE = "2560x1440_60_fixed297"
|
||||
# The vendor PHY rejects the EDID's 241.70 MHz timing. Keep the verified PLL.
|
||||
TIMING = "297.00 2560 2608 2640 2720 1440 1443 1448 1820 +HSync -VSync".split()
|
||||
POLL_SECONDS = 2
|
||||
SETTLE_SECONDS = 2
|
||||
|
||||
|
||||
def log(message):
|
||||
print(time.strftime("%Y-%m-%d %H:%M:%S"), message, flush=True)
|
||||
|
||||
|
||||
def xrandr(*args):
|
||||
result = subprocess.run(["xrandr", *args], capture_output=True, text=True,
|
||||
timeout=10, check=False)
|
||||
if result.returncode:
|
||||
raise RuntimeError((result.stderr or result.stdout).strip()[:400])
|
||||
return result.stdout
|
||||
|
||||
|
||||
@dataclass
|
||||
class DisplayState:
|
||||
connected: bool
|
||||
geometry: str
|
||||
active_mode: str
|
||||
modes: set
|
||||
defined: bool
|
||||
|
||||
@property
|
||||
def healthy(self):
|
||||
return (self.connected and self.geometry == "2560x1440+0+0"
|
||||
and self.active_mode == MODE)
|
||||
|
||||
|
||||
def parse_state(query):
|
||||
connected, geometry, active = False, "", ""
|
||||
modes, defined, in_output = set(), False, False
|
||||
for line in query.splitlines():
|
||||
fields = line.split()
|
||||
if not fields:
|
||||
continue
|
||||
if fields[0] == MODE:
|
||||
defined = True # Includes a globally defined but detached mode.
|
||||
if not line[0].isspace():
|
||||
in_output = fields[0] == OUTPUT
|
||||
if in_output:
|
||||
connected = len(fields) > 1 and fields[1] == "connected"
|
||||
geometry = next((f for f in fields[2:]
|
||||
if re.fullmatch(r"\d+x\d+\+\d+\+\d+", f)), "")
|
||||
elif in_output and len(fields) > 1 and re.match(r"^\d+(?:\.\d+)?[+*]*$", fields[1]):
|
||||
modes.add(fields[0])
|
||||
if "*" in " ".join(fields[1:]):
|
||||
active = fields[0]
|
||||
return DisplayState(connected, geometry, active, modes, defined)
|
||||
|
||||
|
||||
class Guard:
|
||||
def __init__(self, runner=xrandr, reporter=log):
|
||||
self.run = runner
|
||||
self.log = reporter
|
||||
self.bad_since = None
|
||||
self.next_retry = 0
|
||||
self.failures = 0
|
||||
self.last_status = None
|
||||
|
||||
def report(self, status):
|
||||
if status != self.last_status:
|
||||
self.log(status)
|
||||
self.last_status = status
|
||||
|
||||
def step(self, now):
|
||||
try:
|
||||
# Xorg already receives DRM hotplug events. Read its current state
|
||||
# without probing every EDID/mode: the vendor driver logs clock
|
||||
# warnings for those probes, adding unnecessary work while streaming.
|
||||
state = parse_state(self.run("--current"))
|
||||
except (OSError, RuntimeError, subprocess.TimeoutExpired) as error:
|
||||
self.bad_since = None
|
||||
self.report("X11 unavailable; waiting: " + str(error)[:400])
|
||||
return False
|
||||
if not state.connected:
|
||||
self.bad_since = None
|
||||
self.next_retry = 0
|
||||
self.failures = 0
|
||||
self.report(OUTPUT + " disconnected; no mode changes")
|
||||
return False
|
||||
if state.healthy:
|
||||
self.bad_since = None
|
||||
self.next_retry = 0
|
||||
self.failures = 0
|
||||
self.report("healthy: " + MODE)
|
||||
return True
|
||||
self.report("mode drift: " + (state.active_mode or "off") + " " + state.geometry)
|
||||
if self.bad_since is None:
|
||||
self.bad_since = now
|
||||
if now - self.bad_since < SETTLE_SECONDS or now < self.next_retry:
|
||||
return False
|
||||
try:
|
||||
if not state.defined:
|
||||
self.run("--newmode", MODE, *TIMING)
|
||||
if MODE not in state.modes:
|
||||
self.run("--addmode", OUTPUT, MODE)
|
||||
self.run("--output", OUTPUT, "--mode", MODE, "--pos", "0x0")
|
||||
if not parse_state(self.run("--current")).healthy:
|
||||
raise RuntimeError("mode was not retained after apply")
|
||||
except (OSError, RuntimeError, subprocess.TimeoutExpired) as error:
|
||||
self.failures = min(self.failures + 1, 6)
|
||||
delay = min(60, 2 ** self.failures)
|
||||
# Base backoff on completion, including a slow/timed-out modeset.
|
||||
self.next_retry = time.monotonic() + delay
|
||||
self.log("restore failed; retry in %ds: %s" % (delay, str(error)[:400]))
|
||||
return False
|
||||
self.bad_since = None
|
||||
self.next_retry = 0
|
||||
self.failures = 0
|
||||
self.report("restored: " + MODE + " (stream process untouched)")
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--once", action="store_true",
|
||||
help="initial display recovery with a 20s retry window, then exit")
|
||||
args = parser.parse_args()
|
||||
os.environ.setdefault("DISPLAY", ":0")
|
||||
os.environ.setdefault("XAUTHORITY", str(Path.home() / ".Xauthority"))
|
||||
runtime = Path(os.environ.get("XDG_RUNTIME_DIR", str(Path.home() / ".cache/vdi-terminal")))
|
||||
runtime.mkdir(parents=True, exist_ok=True)
|
||||
# Same lock for startup and the systemd service. Never run competing guards.
|
||||
with (runtime / "vdi-display-guard.lock").open("a") as lock:
|
||||
try:
|
||||
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
except BlockingIOError:
|
||||
log("display guard already running")
|
||||
return 0
|
||||
stop = False
|
||||
|
||||
def request_stop(signum, frame):
|
||||
nonlocal stop
|
||||
stop = True
|
||||
|
||||
signal.signal(signal.SIGTERM, request_stop)
|
||||
signal.signal(signal.SIGINT, request_stop)
|
||||
guard = Guard()
|
||||
deadline = time.monotonic() + 20
|
||||
while not stop:
|
||||
if guard.step(time.monotonic()) and args.once:
|
||||
return 0
|
||||
if args.once and time.monotonic() >= deadline:
|
||||
return 1
|
||||
time.sleep(POLL_SECONDS)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -3,26 +3,28 @@
|
||||
export DISPLAY="${DISPLAY:-:0}"
|
||||
export XAUTHORITY="${XAUTHORITY:-/home/armbian/.Xauthority}"
|
||||
|
||||
# Match the local output to the 2K Moonlight stream.
|
||||
# This kernel rejects the monitor's 241.70 MHz timing but accepts the 297 MHz
|
||||
# PLL rate. Longer vertical blanking produces 2560x1440 at approximately 60 Hz.
|
||||
# HDMI can appear a few seconds after the desktop autostart entry runs.
|
||||
: >/tmp/vdi-display-mode.log
|
||||
mode="2560x1440_60_fixed297"
|
||||
for _ in $(seq 1 20); do
|
||||
if xrandr --query 2>>/tmp/vdi-display-mode.log | grep -q '^HDMI-1 connected'; then
|
||||
xrandr --newmode "$mode" 297.00 \
|
||||
2560 2608 2640 2720 1440 1443 1448 1820 \
|
||||
+HSync -VSync >>/tmp/vdi-display-mode.log 2>&1 || true
|
||||
xrandr --addmode HDMI-1 "$mode" >>/tmp/vdi-display-mode.log 2>&1 || true
|
||||
if xrandr --output HDMI-1 --mode "$mode" >>/tmp/vdi-display-mode.log 2>&1; then
|
||||
break
|
||||
fi
|
||||
xrandr --output HDMI-1 --mode 1920x1080 --rate 60 \
|
||||
>>/tmp/vdi-display-mode.log 2>&1 || true
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
# Optional verified rendering deployment. Parse an allowlist as data; never
|
||||
# source/eval configuration. Explicit environment overrides the saved defaults.
|
||||
render_conf="${VDI_RENDER_CONF:-$HOME/.config/vdi-terminal/render.conf}"
|
||||
if [ -r "$render_conf" ]; then
|
||||
while IFS='=' read -r key value || [ -n "$key" ]; do
|
||||
case "$key" in
|
||||
profile) export VDI_STREAM_PROFILE="${VDI_STREAM_PROFILE:-$value}" ;;
|
||||
stats) export VDI_STREAM_STATS="${VDI_STREAM_STATS:-$value}" ;;
|
||||
program) export VDI_MOONLIGHT_PROGRAM="${VDI_MOONLIGHT_PROGRAM:-$value}" ;;
|
||||
decoder) export VDI_MOONLIGHT_DECODER="${VDI_MOONLIGHT_DECODER:-$value}" ;;
|
||||
codec) export VDI_MOONLIGHT_CODEC="${VDI_MOONLIGHT_CODEC:-$value}" ;;
|
||||
esac
|
||||
done < "$render_conf"
|
||||
fi
|
||||
|
||||
# Use the same idempotent recovery for boot and HDMI hotplug. The user service
|
||||
# supervises ongoing recovery; a singleton lock prevents competing instances.
|
||||
"$(dirname "$0")/vdi-display-guard" --once >>/tmp/vdi-display-mode.log 2>&1 || true
|
||||
if ! systemctl --user start vdi-display-guard.service >>/tmp/vdi-display-mode.log 2>&1; then
|
||||
echo 'WARNING: display guard service unavailable; only boot recovery active' \
|
||||
>>/tmp/vdi-display-mode.log
|
||||
fi
|
||||
|
||||
exec /home/armbian/vdi-terminal/bin/vdi-kiosk-monitor -- \
|
||||
/home/armbian/vdi-terminal/bin/kiosk-shell
|
||||
@@ -3,7 +3,7 @@
|
||||
#
|
||||
# 职责:
|
||||
# 1. 读取本地配置(主机、应用、分辨率、帧率、码率、编解码)
|
||||
# 2. 以软件解码拉起 moonlight-qt,全屏串流云桌面
|
||||
# 2. 按配置拉起 Moonlight,全屏串流云桌面
|
||||
# 3. 写心跳文件,供 vdi-kiosk-monitor 判断是否假死
|
||||
# 4. 检测串流假死(CPU 与网络均无流量)并杀掉重连
|
||||
#
|
||||
@@ -12,7 +12,8 @@
|
||||
#
|
||||
# 说明:RK3528 当前没有可用的 V4L2 request/m2m 硬解设备,moonlight-qt 的
|
||||
# --video-decoder auto 会选到 h264_v4l2m2m 并报 "Could not find a valid device",
|
||||
# 因此配置里显式指定 decoder=software。接入 MPP/VPU 硬解后再改回 auto。
|
||||
# 因此系统客户端默认 decoder=software;验证后的独立 RKMPP 客户端
|
||||
# 通过 VDI_MOONLIGHT_PROGRAM 与 decoder=hardware 显式启用。
|
||||
|
||||
set -u
|
||||
|
||||
@@ -38,9 +39,10 @@ DECODER="software"
|
||||
DISPLAY_MODE="fullscreen"
|
||||
AUDIO=1
|
||||
ABSOLUTE_MOUSE=1
|
||||
VSYNC=0
|
||||
FRAME_PACING=0
|
||||
VSYNC=1
|
||||
FRAME_PACING=1
|
||||
PERFORMANCE_OVERLAY=0
|
||||
PROFILE=""
|
||||
RETRY=3
|
||||
RETRY_DELAY=5
|
||||
HANG_TIMEOUT=90
|
||||
@@ -76,6 +78,7 @@ load_config() {
|
||||
vsync) VSYNC="$value" ;;
|
||||
frame-pacing) FRAME_PACING="$value" ;;
|
||||
performance-overlay) PERFORMANCE_OVERLAY="$value" ;;
|
||||
profile) PROFILE="$value" ;;
|
||||
retry) RETRY="$value" ;;
|
||||
retry-delay) RETRY_DELAY="$value" ;;
|
||||
hang-timeout) HANG_TIMEOUT="$value" ;;
|
||||
@@ -84,6 +87,33 @@ load_config() {
|
||||
done < "$CONF"
|
||||
}
|
||||
|
||||
apply_profile() {
|
||||
case "$PROFILE" in
|
||||
smooth)
|
||||
VSYNC=1
|
||||
FRAME_PACING=1
|
||||
;;
|
||||
low-latency)
|
||||
VSYNC=0
|
||||
FRAME_PACING=0
|
||||
;;
|
||||
diagnostic)
|
||||
VSYNC=1
|
||||
FRAME_PACING=1
|
||||
PERFORMANCE_OVERLAY=1
|
||||
;;
|
||||
"")
|
||||
# 兼容旧配置:没有profile时保留moonlight.conf中的显式开关。
|
||||
;;
|
||||
*)
|
||||
log "未知串流 profile=$PROFILE,回退 smooth"
|
||||
PROFILE=smooth
|
||||
VSYNC=1
|
||||
FRAME_PACING=1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# 默认路由出接口的累计收字节数,用于判断串流是否还有数据流入
|
||||
rx_bytes() {
|
||||
iface=$(awk '$2=="00000000" {print $1; exit}' /proc/net/route)
|
||||
@@ -145,7 +175,7 @@ disable_screensaver() {
|
||||
build_cmd() {
|
||||
# moonlight-qt stream 只接受 <host> <app> 位置参数,没有 --port 选项,
|
||||
# 服务端端口由 moonlight 自身按 GameStream 协议协商。
|
||||
CMD="moonlight-qt stream $HOST $APP"
|
||||
CMD="${VDI_MOONLIGHT_PROGRAM:-moonlight-qt} stream $HOST $APP"
|
||||
CMD="$CMD --resolution ${WIDTH}x${HEIGHT}"
|
||||
CMD="$CMD --fps $FPS"
|
||||
CMD="$CMD --bitrate $BITRATE"
|
||||
@@ -155,7 +185,7 @@ build_cmd() {
|
||||
CMD="$CMD --keep-awake"
|
||||
[ "$ABSOLUTE_MOUSE" = "1" ] && CMD="$CMD --absolute-mouse"
|
||||
[ "$AUDIO" = "1" ] && CMD="$CMD --audio-on-host" || CMD="$CMD --no-audio-on-host"
|
||||
# 低延迟开关:默认关闭 vsync 与 frame-pacing,削减帧队列/呈现延迟
|
||||
# 按profile选择同步策略;默认保持当前稳定档。
|
||||
[ "$VSYNC" = "1" ] && CMD="$CMD --vsync" || CMD="$CMD --no-vsync"
|
||||
[ "$FRAME_PACING" = "1" ] && CMD="$CMD --frame-pacing" || CMD="$CMD --no-frame-pacing"
|
||||
[ "$PERFORMANCE_OVERLAY" = "1" ] && CMD="$CMD --performance-overlay"
|
||||
@@ -163,7 +193,7 @@ build_cmd() {
|
||||
|
||||
run_stream() {
|
||||
build_cmd
|
||||
log "启动串流: $CMD"
|
||||
log "启动串流 profile=${PROFILE:-config} decoder=$DECODER codec=$CODEC vsync=$VSYNC frame-pacing=$FRAME_PACING renderer=${SDL_RENDER_DRIVER:-} video-driver=${SDL_VIDEODRIVER:-}: $CMD"
|
||||
# shellcheck disable=SC2086
|
||||
$CMD >> "$LOG_FILE" 2>&1 &
|
||||
ML_PID=$!
|
||||
@@ -220,6 +250,19 @@ stop)
|
||||
esac
|
||||
|
||||
load_config
|
||||
PROFILE="${VDI_STREAM_PROFILE:-$PROFILE}"
|
||||
apply_profile
|
||||
DECODER="${VDI_MOONLIGHT_DECODER:-$DECODER}"
|
||||
CODEC="${VDI_MOONLIGHT_CODEC:-$CODEC}"
|
||||
case "$DECODER" in
|
||||
software|hardware|auto) ;;
|
||||
*) log "未知解码器 $DECODER,回退 software"; DECODER=software ;;
|
||||
esac
|
||||
case "$CODEC" in
|
||||
H.264|HEVC|AV1|auto) ;;
|
||||
*) log "未知视频编码 $CODEC,回退 H.264"; CODEC=H.264 ;;
|
||||
esac
|
||||
PERFORMANCE_OVERLAY="${VDI_STREAM_STATS:-$PERFORMANCE_OVERLAY}"
|
||||
if [ -z "$HOST" ]; then
|
||||
log "配置缺失或缺少 host: $CONF"
|
||||
exit 2
|
||||
@@ -227,6 +270,10 @@ fi
|
||||
|
||||
export DISPLAY="${VDI_DISPLAY:-:0}"
|
||||
export XAUTHORITY="${VDI_XAUTHORITY:-$HOME/.Xauthority}"
|
||||
export SDL_RENDER_DRIVER="${VDI_SDL_RENDER_DRIVER:-opengles2}"
|
||||
export SDL_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR="${VDI_SDL_BYPASS_COMPOSITOR:-1}"
|
||||
[ -n "${VDI_SDL_FORCE_EGL:-}" ] && export SDL_VIDEO_X11_FORCE_EGL="$VDI_SDL_FORCE_EGL"
|
||||
[ -n "${VDI_SDL_VIDEODRIVER:-}" ] && export SDL_VIDEODRIVER="$VDI_SDL_VIDEODRIVER"
|
||||
|
||||
disable_screensaver
|
||||
kill_stale
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
#!/bin/sh
|
||||
# Select explicitly after RKMPP decode + real stream validation. Does not replace
|
||||
# the distribution Moonlight or add a second cursor.
|
||||
set -eu
|
||||
task_root="${VDI_RKMPP_BUILD_ROOT:-$HOME/vdi-build/rkmpp-experiment}"
|
||||
client="${VDI_RKMPP_CLIENT:-$task_root/build/moonlight/app/moonlight}"
|
||||
if [ ! -x "$client" ] || [ ! -r /dev/mpp_service ] || [ ! -w /dev/mpp_service ]; then
|
||||
echo 'RKMPP client or device access unavailable' >&2
|
||||
exit 78
|
||||
fi
|
||||
export LD_LIBRARY_PATH="$task_root/runtime/lib:$task_root/deps/usr/lib/aarch64-linux-gnu${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
|
||||
export QML2_IMPORT_PATH="$task_root/deps/usr/lib/aarch64-linux-gnu/qt5/qml${QML2_IMPORT_PATH:+:$QML2_IMPORT_PATH}"
|
||||
export QT_PLUGIN_PATH="$task_root/deps/usr/lib/aarch64-linux-gnu/qt5/plugins${QT_PLUGIN_PATH:+:$QT_PLUGIN_PATH}"
|
||||
export GL_IS_SLOW="${GL_IS_SLOW:-0}"
|
||||
export H264_DECODER_HINT=h264_rkmpp
|
||||
export HEVC_DECODER_HINT=hevc_rkmpp
|
||||
# Only affects unpaced rendering in the patched client. Leave the last presented
|
||||
# frame and decoder references intact; replace only waiting decoded frames.
|
||||
export VDI_LATEST_FRAME="${VDI_LATEST_FRAME:-1}"
|
||||
exec "$client" "$@"
|
||||
Reference in new issue
Block a user