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.
74 lines
2.6 KiB
Python
74 lines
2.6 KiB
Python
"""User settings, persisted to ~/.config/screencast/config.json."""
|
|
from __future__ import annotations
|
|
|
|
from .util import config_dir, read_json, write_json
|
|
|
|
DEFAULTS = {
|
|
"fps": 30,
|
|
"maxHeight": 1080, # the capture is scaled down to this before encoding
|
|
"bitrate": 8000, # kbit/s
|
|
"encoder": "auto", # auto | nvenc | vaapi | x264 | vp8 | vp9
|
|
"audio": True, # mix desktop audio into the stream
|
|
"cursor": True, # draw the pointer into the capture
|
|
"port": 8011, # local HTTP server the receiver pulls the stream from
|
|
"lastDevice": "", # id of the device cast to last, for the "cast again" default
|
|
"autoReconnect": True, # restart the pipeline if the receiver drops the stream
|
|
"portVerified": False, # set once a receiver has actually fetched the stream
|
|
}
|
|
|
|
# Settings that only take effect on the next session (changing them mid-cast
|
|
# restarts the pipeline instead of being ignored).
|
|
PIPELINE_KEYS = {"fps", "maxHeight", "bitrate", "encoder", "audio", "cursor"}
|
|
|
|
|
|
class Config:
|
|
def __init__(self) -> None:
|
|
self.path = config_dir() / "config.json"
|
|
self.values = dict(DEFAULTS)
|
|
self.values.update(read_json(self.path, {}) or {})
|
|
|
|
def __getitem__(self, key):
|
|
return self.values.get(key, DEFAULTS.get(key))
|
|
|
|
def get(self, key, default=None):
|
|
return self.values.get(key, DEFAULTS.get(key, default))
|
|
|
|
def update(self, patch: dict) -> set[str]:
|
|
"""Apply a patch, returning the keys that actually changed."""
|
|
changed = set()
|
|
for key, value in (patch or {}).items():
|
|
if key not in DEFAULTS:
|
|
continue
|
|
want = _coerce(key, value)
|
|
if want is None or self.values.get(key) == want:
|
|
continue
|
|
self.values[key] = want
|
|
changed.add(key)
|
|
if changed:
|
|
self.save()
|
|
return changed
|
|
|
|
def save(self) -> None:
|
|
write_json(self.path, self.values)
|
|
|
|
|
|
def _coerce(key, value):
|
|
ref = DEFAULTS[key]
|
|
try:
|
|
if isinstance(ref, bool):
|
|
return bool(value)
|
|
if isinstance(ref, int):
|
|
v = int(value)
|
|
if key == "fps":
|
|
return max(5, min(60, v))
|
|
if key == "maxHeight":
|
|
return max(360, min(2160, v))
|
|
if key == "bitrate":
|
|
return max(500, min(50000, v))
|
|
if key == "port":
|
|
return max(1024, min(65535, v))
|
|
return v
|
|
return str(value)
|
|
except (TypeError, ValueError):
|
|
return None
|