Ambilight per Plasma 6: demone Adalight + plasmoide con slider
- backend/ambicapture.py: cattura Wayland via ScreenCast portal + PipeWire - backend/ambilightd.py: demone, 47 zone sul bordo superiore, luminosita/contrasto/ saturazione/banda, invio Adalight su seriale, servizio D-Bus org.enne2.Ambilight - backend/ambilight-ctl: CLI JSON per il plasmoide - backend/ambilight.service: avvio automatico (systemd --user) - package/org.enne2.ambilight: applet vassoio con popup e slider - install.sh / uninstall.sh senza root
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
"""Cattura dello schermo per ambilight su Wayland/KDE (ScreenCast portal + PipeWire).
|
||||
|
||||
Restituisce i colori medi di N zone lungo un bordo dello schermo.
|
||||
Nessuna dipendenza da X11: usa xdg-desktop-portal (consenso una volta sola,
|
||||
token persistente) e GStreamer con pipewiresrc.
|
||||
"""
|
||||
import os
|
||||
|
||||
CACHE_DIR = os.path.expanduser("~/.cache/ambilight")
|
||||
PORTAL = "org.freedesktop.portal.Desktop"
|
||||
PPATH = "/org/freedesktop/portal/desktop"
|
||||
|
||||
|
||||
def screencast_stream(cache_dir=CACHE_DIR):
|
||||
"""Negozia la condivisione del monitor; ritorna (node_id, width, height)."""
|
||||
import dbus
|
||||
from dbus.mainloop.glib import DBusGMainLoop
|
||||
from gi.repository import GLib
|
||||
|
||||
DBusGMainLoop(set_as_default=True)
|
||||
bus = dbus.SessionBus()
|
||||
sc = dbus.Interface(bus.get_object(PORTAL, PPATH), "org.freedesktop.portal.ScreenCast")
|
||||
sender = bus.get_unique_name()[1:].replace(".", "_")
|
||||
counter = [0]
|
||||
|
||||
def request(fn, *args, **opts):
|
||||
counter[0] += 1
|
||||
token = f"ambi{os.getpid()}{counter[0]}"
|
||||
path = f"{PPATH}/request/{sender}/{token}"
|
||||
iface = dbus.Interface(bus.get_object(PORTAL, path, introspect=False),
|
||||
"org.freedesktop.portal.Request")
|
||||
box = {}
|
||||
loop = GLib.MainLoop()
|
||||
|
||||
def on_response(code, results):
|
||||
box["code"], box["results"] = code, results
|
||||
loop.quit()
|
||||
|
||||
iface.connect_to_signal("Response", on_response)
|
||||
fn(*args, dbus.Dictionary({"handle_token": token, **opts}, signature="sv"))
|
||||
GLib.timeout_add_seconds(180, lambda: (loop.quit(), False)[1])
|
||||
loop.run()
|
||||
if box.get("code") != 0:
|
||||
raise RuntimeError("condivisione schermo annullata o rifiutata")
|
||||
return box["results"]
|
||||
|
||||
res = request(sc.CreateSession, session_handle_token="ambilight")
|
||||
session = str(res["session_handle"])
|
||||
|
||||
token_file = os.path.join(cache_dir, "restore_token")
|
||||
opts = {}
|
||||
if os.path.exists(token_file):
|
||||
tok = open(token_file).read().strip()
|
||||
if tok:
|
||||
opts["restore_token"] = tok
|
||||
|
||||
request(sc.SelectSources, session, types=dbus.UInt32(1), multiple=False,
|
||||
cursor_mode=dbus.UInt32(1), persist_mode=dbus.UInt32(2), **opts)
|
||||
res = request(sc.Start, session, "")
|
||||
|
||||
node = int(res["streams"][0][0])
|
||||
size = [int(v) for v in res["streams"][0][1].get("size", [0, 0])]
|
||||
if "restore_token" in res:
|
||||
os.makedirs(cache_dir, exist_ok=True)
|
||||
with open(token_file, "w") as f:
|
||||
f.write(str(res["restore_token"]))
|
||||
return node, size[0], size[1]
|
||||
|
||||
|
||||
class Capture:
|
||||
"""Pipeline GStreamer ridotta a (led*sub) x gh pixel RGB, pronta per il calcolo zone."""
|
||||
|
||||
def __init__(self, node, leds, sub=4, rows=72, fps=60):
|
||||
import gi
|
||||
gi.require_version("Gst", "1.0")
|
||||
gi.require_version("GstApp", "1.0")
|
||||
from gi.repository import Gst, GstApp # noqa: F401 (registra il tipo AppSink)
|
||||
Gst.init(None)
|
||||
self.gw = leds * sub
|
||||
self.gh = rows
|
||||
desc = (f"pipewiresrc path={node} do-timestamp=true ! "
|
||||
f"videoconvert ! videoscale ! videorate ! "
|
||||
f"video/x-raw,format=RGB,width={self.gw},height={self.gh},"
|
||||
f"framerate={int(fps)}/1 ! "
|
||||
f"appsink name=sink emit-signals=false max-buffers=2 drop=true sync=false")
|
||||
self.pipe = Gst.parse_launch(desc)
|
||||
self.sink = self.pipe.get_by_name("sink")
|
||||
|
||||
def start(self):
|
||||
from gi.repository import Gst
|
||||
self.pipe.set_state(Gst.State.PLAYING)
|
||||
|
||||
def stop(self):
|
||||
from gi.repository import Gst
|
||||
self.pipe.set_state(Gst.State.NULL)
|
||||
|
||||
def frame(self, timeout_ms=200):
|
||||
"""Ritorna un array numpy (gh, gw, 3) uint8, oppure None."""
|
||||
import numpy as np
|
||||
from gi.repository import Gst
|
||||
sample = self.sink.try_pull_sample(timeout_ms * Gst.MSECOND)
|
||||
if sample is None:
|
||||
return None
|
||||
buf = sample.get_buffer()
|
||||
ok, info = buf.map(Gst.MapFlags.READ)
|
||||
if not ok:
|
||||
return None
|
||||
try:
|
||||
need = self.gw * self.gh * 3
|
||||
data = bytes(info.data)[:need]
|
||||
finally:
|
||||
buf.unmap(info)
|
||||
if len(data) < need:
|
||||
return None
|
||||
return np.frombuffer(data, dtype=np.uint8).reshape(self.gh, self.gw, 3)
|
||||
Reference in New Issue
Block a user