Screencast: cast the desktop to Cast, DLNA and AirPlay receivers
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.
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
"""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,
|
||||
}
|
||||
Reference in New Issue
Block a user