kmsgrab e ScreenShot2: esiti sperimentali documentati

- kmsgrab non praticabile: piano primario AR30 con modifier AMD DCC/tiled, ffmpeg
  rifiuta il framebuffer prima di applicare format/modifier
- KWin ScreenShot2: NoAuthorized anche con desktop entry system-wide
- indicatore del vassoio identificato come SNI di xdg-desktop-portal-kde
  (titolo 'Trasmissione dello schermo')
- backend kmsgrab comunque implementato e selezionabile (capture=kmsgrab)
- setup-kmsgrab.sh: installa via pkexec una copia di ffmpeg con cap_sys_admin+ep
This commit is contained in:
enne2
2026-11-01 13:22:03 +01:00
parent db47640846
commit 47b5c09fa1
4 changed files with 188 additions and 15 deletions
+86
View File
@@ -113,3 +113,89 @@ class Capture:
if len(data) < need:
return None
return np.frombuffer(data, dtype=np.uint8).reshape(self.gh, self.gw, 3)
# ------------------------------------------------------------------ kmsgrab
FFMPEG_CAP = "/usr/local/libexec/ambilight/ffmpeg-kmsgrab"
FFMPEG_SYS = "/usr/bin/ffmpeg"
class KmsgrabCapture:
"""Cattura DRM diretta (ffmpeg -f kmsgrab) senza portal e senza indicatore.
Richiede CAP_SYS_ADMIN sul binario ffmpeg di appoggio (FFMPEG_CAP), oppure root.
ffmpeg riduce il frame a (leds*sub) x rows in RGB e lo scrive su stdout come
rawvideo: leggiamo esattamente un frame per volta.
"""
def __init__(self, leds, sub=4, rows=72, fps=60, device="/dev/dri/card1",
ffmpeg=None):
self.gw = leds * sub
self.gh = rows
self.fps = max(1, int(fps))
self.device = device
ff = ffmpeg or (FFMPEG_CAP if os.path.exists(FFMPEG_CAP) else FFMPEG_SYS)
self.binary = ff
self.frame_bytes = self.gw * self.gh * 3
self.proc = None
self.stderr_path = os.path.join(CACHE_DIR, "kmsgrab.log")
# hwdownload porta il frame in RAM; lo scaling avviene poi in ffmpeg.
self.vf = (f"hwdownload,format=bgr0,"
f"scale={self.gw}:{self.gh}:flags=bilinear,format=rgb24")
self.cmd = [ff, "-hide_banner", "-loglevel", "error", "-nostdin",
"-f", "kmsgrab", "-device", device, "-i", "-",
"-vf", self.vf,
"-r", str(self.fps),
"-f", "rawvideo", "-pix_fmt", "rgb24", "pipe:1"]
def start(self):
import subprocess
os.makedirs(CACHE_DIR, exist_ok=True)
self.err = open(self.stderr_path, "wb")
self.proc = subprocess.Popen(self.cmd, stdout=subprocess.PIPE,
stderr=self.err, bufsize=self.frame_bytes)
return self
def stop(self):
try:
if self.proc is not None:
self.proc.terminate()
try:
self.proc.wait(timeout=2)
except Exception:
self.proc.kill()
finally:
self.proc = None
try:
self.err.close()
except Exception:
pass
def alive(self):
return self.proc is not None and self.proc.poll() is None
def frame(self, timeout_ms=200):
"""Legge un frame completo; None se non arriva entro il timeout."""
import select
import numpy as np
if self.proc is None:
return None
fd = self.proc.stdout
buf = bytearray()
deadline = timeout_ms / 1000.0
while len(buf) < self.frame_bytes:
ready, _, _ = select.select([fd], [], [], deadline)
if not ready:
return None
chunk = fd.read(self.frame_bytes - len(buf))
if not chunk:
return None
buf += chunk
return np.frombuffer(bytes(buf), dtype=np.uint8).reshape(self.gh, self.gw, 3)
def error_text(self):
try:
with open(self.stderr_path, "rb") as f:
return f.read()[-400:].decode("utf-8", "replace").strip()
except Exception:
return ""
+33 -11
View File
@@ -42,6 +42,8 @@ DEFAULTS = {
"baud": 115200,
"fps": 60,
"side": "top", # top | bottom
"capture": "portal", # portal (ScreenCast) | kmsgrab (DRM diretto, senza indicatore)
"drm_device": "/dev/dri/card1",
"auto_start": True, # streaming attivo all'avvio del demone
}
PARAM_RANGES = {
@@ -190,32 +192,53 @@ def apply_params(zones, cfg):
def capture_loop(state, strip):
from ambicapture import Capture, screencast_stream
"""Ciclo di cattura: sceglie il backend (portal o kmsgrab) e invia i frame."""
from ambicapture import Capture, KmsgrabCapture, screencast_stream
cap = None
leds_cfg = None
key = None
prev = None
sent = 0
t_win = time.time()
def build(cfg):
"""Crea e avvia il backend; ritorna (cap, descrizione)."""
if cfg["capture"] == "kmsgrab":
c = KmsgrabCapture(cfg["leds"], fps=cfg["fps"], device=cfg["drm_device"]).start()
return c, f"kmsgrab {c.binary} su {cfg['drm_device']}"
node, w, h = screencast_stream()
c = Capture(node, cfg["leds"], fps=cfg["fps"])
c.start()
return c, f"portal node={node} sorgente {w}x{h}"
while True:
cfg = state.snapshot()
if not cfg["running"]:
if cap is not None:
cap.stop()
cap = None
key = None
prev = None
time.sleep(0.15)
continue
if cap is None or leds_cfg != cfg["leds"]:
want = (cfg["capture"], cfg["leds"], cfg["drm_device"], cfg["fps"])
dead = cap is not None and hasattr(cap, "alive") and not cap.alive()
if cap is None or want != key or dead:
if cap is not None:
err = cap.error_text() if hasattr(cap, "error_text") else ""
cap.stop()
cap = None
if dead:
with state.lock:
state.stream_error = f"kmsgrab terminato: {err}"[:300]
log("kmsgrab terminato:", err)
time.sleep(3)
continue
try:
node, w, h = screencast_stream()
cap = Capture(node, cfg["leds"], fps=cfg["fps"])
cap.start()
leds_cfg = cfg["leds"]
log(f"cattura avviata: node={node} sorgente {w}x{h} led={cfg['leds']}")
cap, desc = build(cfg)
key = want
log(f"cattura avviata ({cfg['capture']}): {desc} led={cfg['leds']}")
with state.lock:
state.stream_error = ""
state.last_error = ""
@@ -231,9 +254,8 @@ def capture_loop(state, strip):
time.sleep(0.005)
continue
rows = max(1, int(round(cap.gh * cfg["band"])))
gh, gw = frame.shape[0], frame.shape[1]
rows = min(rows, gh)
rows = min(max(1, int(round(gh * cfg["band"]))), gh)
band = frame[:rows] if cfg["side"] == "top" else frame[gh - rows:]
sub = gw // cfg["leds"]
if sub < 1:
@@ -358,7 +380,7 @@ class DBusService:
value = bool(value)
elif key == "leds":
value = max(1, min(256, int(value)))
elif key in ("port", "side", "baud", "fps"):
elif key in ("port", "side", "baud", "fps", "capture", "drm_device"):
value = type(DEFAULTS[key])(value)
else:
return False, f"parametro sconosciuto: {key}"