1ed57291f9
An Omarchy shell plugin: a Python/asyncio daemon that discovers network displays and serves an encoded screen capture for them to pull, plus a Quickshell bar widget and panel to drive it. The screen comes from the xdg-desktop-portal ScreenCast interface (asked for every time, never remembered), goes through GStreamer, and is served from a local HTTP port. Desktop audio is mixed in from the default output's monitor. The container follows the receiver: WebM/VP8 for Cast, MPEG-TS/H.264 for DLNA, HLS for AirPlay video. The stream port has to be reachable from the LAN, and none of these protocols can carry a credential, so the capability is the URL: a fresh random path per session, refused to anything off the LAN, capped at four concurrent readers.
61 lines
1.9 KiB
Python
61 lines
1.9 KiB
Python
"""What the panel sees as a "display": one receiver, whatever protocol it speaks."""
|
|
from __future__ import annotations
|
|
|
|
import time
|
|
from dataclasses import dataclass, field
|
|
|
|
CAST, AIRPLAY, DLNA = "cast", "airplay", "dlna"
|
|
|
|
KIND_LABEL = {CAST: "Google Cast", AIRPLAY: "AirPlay", DLNA: "DLNA"}
|
|
|
|
|
|
@dataclass
|
|
class Device:
|
|
id: str
|
|
kind: str
|
|
name: str
|
|
host: str
|
|
port: int
|
|
model: str = ""
|
|
manufacturer: str = ""
|
|
# Protocol-specific handles: cast_info for Cast, control URLs for DLNA,
|
|
# the mDNS TXT record for AirPlay.
|
|
extra: dict = field(default_factory=dict)
|
|
seen: float = field(default_factory=time.time)
|
|
# Live status, filled in by the backend that owns the device.
|
|
status: str = "" # "" | idle | busy | casting | connecting | error
|
|
app: str = "" # what it is showing now ("Netflix", "Backdrop", …)
|
|
volume: float = -1.0 # -1 when unknown
|
|
muted: bool = False
|
|
error: str = ""
|
|
|
|
@property
|
|
def video(self) -> bool:
|
|
"""Can it show a picture? Speaker groups and speakers cannot."""
|
|
if self.kind == CAST:
|
|
return self.extra.get("cast_type") in (None, "cast", "group") and not self.audio_only
|
|
return True
|
|
|
|
@property
|
|
def audio_only(self) -> bool:
|
|
if self.kind == CAST:
|
|
return self.extra.get("cast_type") in ("audio", "group")
|
|
return False
|
|
|
|
def to_json(self) -> dict:
|
|
return {
|
|
"id": self.id,
|
|
"kind": self.kind,
|
|
"kindLabel": KIND_LABEL.get(self.kind, self.kind),
|
|
"name": self.name,
|
|
"model": self.model,
|
|
"host": self.host,
|
|
"port": self.port,
|
|
"status": self.status,
|
|
"app": self.app,
|
|
"volume": self.volume,
|
|
"muted": self.muted,
|
|
"audioOnly": self.audio_only,
|
|
"error": self.error,
|
|
}
|