WIP: cattura in processo separato + close_stream (NON validata, sospesa)
This commit is contained in:
@@ -10,6 +10,10 @@ CACHE_DIR = os.path.expanduser("~/.cache/ambilight")
|
||||
PORTAL = "org.freedesktop.portal.Desktop"
|
||||
PPATH = "/org/freedesktop/portal/desktop"
|
||||
|
||||
# Sessione ScreenCast attiva: va CHIUSA esplicitamente, altrimenti il portal continua
|
||||
# a considerarla viva e lascia nel vassoio l'indicatore "Trasmissione dello schermo".
|
||||
_ACTIVE = {"bus": None, "session": None}
|
||||
|
||||
|
||||
def screencast_stream(cache_dir=CACHE_DIR):
|
||||
"""Negozia la condivisione del monitor; ritorna (node_id, width, height)."""
|
||||
@@ -64,9 +68,30 @@ def screencast_stream(cache_dir=CACHE_DIR):
|
||||
os.makedirs(cache_dir, exist_ok=True)
|
||||
with open(token_file, "w") as f:
|
||||
f.write(str(res["restore_token"]))
|
||||
|
||||
_ACTIVE["bus"] = bus
|
||||
_ACTIVE["session"] = session
|
||||
return node, size[0], size[1]
|
||||
|
||||
|
||||
def close_stream():
|
||||
"""Chiude la sessione ScreenCast e la connessione D-Bus (rimuove l'indicatore)."""
|
||||
session, bus = _ACTIVE.get("session"), _ACTIVE.get("bus")
|
||||
_ACTIVE["bus"] = _ACTIVE["session"] = None
|
||||
if not session:
|
||||
return
|
||||
try:
|
||||
import dbus
|
||||
iface = dbus.Interface(bus.get_object(PORTAL, session), "org.freedesktop.portal.Session")
|
||||
iface.Close()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
bus.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
class Capture:
|
||||
"""Pipeline GStreamer ridotta a (led*sub) x gh pixel RGB, pronta per il calcolo zone."""
|
||||
|
||||
@@ -199,3 +224,97 @@ class KmsgrabCapture:
|
||||
return f.read()[-400:].decode("utf-8", "replace").strip()
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ processo figlio
|
||||
class ChildCapture:
|
||||
"""Legge frame RGB grezzi dallo stdout di un processo di cattura separato.
|
||||
|
||||
Il figlio (ambicatcher.py per il portal, ffmpeg per kmsgrab) produce:
|
||||
prima riga -> JSON di intestazione ({"node":..} oppure {"error":..})
|
||||
poi -> frame da (leds*sub) x rows pixel RGB, senza separatori
|
||||
Tenere la cattura fuori dal demone isola GStreamer/PipeWire e, per il portal,
|
||||
fa chiudere la sessione ScreenCast insieme al processo (niente indicatori orfani).
|
||||
"""
|
||||
|
||||
def __init__(self, argv, leds, sub=4, rows=72, expect_header=True):
|
||||
self.cmd = list(argv)
|
||||
self.gw = leds * sub
|
||||
self.gh = rows
|
||||
self.frame_bytes = self.gw * self.gh * 3
|
||||
self.expect_header = expect_header
|
||||
self.header = {}
|
||||
self.proc = None
|
||||
self.stderr_path = os.path.join(CACHE_DIR, "catcher.log")
|
||||
|
||||
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, stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE, stderr=self.err,
|
||||
bufsize=0)
|
||||
if self.expect_header:
|
||||
line = self.proc.stdout.readline()
|
||||
if not line:
|
||||
err = self.error_text()
|
||||
raise RuntimeError(f"cattura non avviata: {err or 'nessun output'}")
|
||||
try:
|
||||
import json
|
||||
self.header = json.loads(line.decode())
|
||||
except Exception:
|
||||
self.header = {"raw": line[:100].decode("utf-8", "replace")}
|
||||
if "error" in self.header:
|
||||
raise RuntimeError(self.header["error"])
|
||||
return self
|
||||
|
||||
def stop(self):
|
||||
try:
|
||||
if self.proc is not None:
|
||||
try:
|
||||
if self.proc.stdin:
|
||||
self.proc.stdin.close() # il figlio esce e chiude la sessione
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
self.proc.wait(timeout=3)
|
||||
except Exception:
|
||||
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):
|
||||
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 ""
|
||||
|
||||
Reference in New Issue
Block a user