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:
2026-08-29 19:57:54 +01:00
commit 1ed57291f9
24 changed files with 5086 additions and 0 deletions
Executable
+114
View File
@@ -0,0 +1,114 @@
#!/bin/bash
# Install the Screencast plugin for the current user.
# ./install.sh install the daemon venv, link + enable the shell plugin
# ./install.sh --no-root same, but never ask for a password (skips missing packages)
# ./install.sh --uninstall
#
# Nothing here needs root except installing missing Arch packages, and even that
# is skipped with --no-root: the daemon runs entirely as you.
set -euo pipefail
HERE=$(cd "$(dirname "$0")" && pwd)
LIB=$HOME/.local/lib/screencast
BIN=$HOME/.local/bin
CACHE=${XDG_CACHE_HOME:-$HOME/.cache}/screencast
STATE=${XDG_STATE_HOME:-$HOME/.local/state}/screencast
ID=io.github.alanfortlink.screencast
PLUGIN=$HOME/.config/omarchy/plugins/$ID
MODE=${1:-}
LOG=$CACHE/install.log
if [[ $MODE == --uninstall ]]; then
omarchy-plugin-disable "$ID" >/dev/null 2>&1 || true
# The daemon is a plain user process; ask it to quit, then remove its files.
"$LIB/screencast-server" stop >/dev/null 2>&1 || true
rm -rf "$LIB" "$CACHE" "$STATE" "$BIN/screencast-server"
[[ -L $PLUGIN ]] && rm -f "$PLUGIN" # dev symlink only
echo "uninstalled (settings left in ~/.config/screencast/config.json). Now run: omarchy plugin remove $ID"
echo "note: if you ever let it open the stream port, that firewall rule is still there."
exit 0
fi
# Runtime dependencies, all from the Arch repos.
missing=()
for p in python gstreamer gst-plugins-base gst-plugins-good gst-plugins-bad gst-plugin-pipewire gst-libav libpipewire xdg-desktop-portal; do
pacman -Q "$p" >/dev/null 2>&1 || missing+=("$p")
done
# Something has to answer org.freedesktop.portal.ScreenCast.
if ! pacman -Q xdg-desktop-portal-hyprland >/dev/null 2>&1 && ! pacman -Q xdg-desktop-portal-wlr >/dev/null 2>&1; then
missing+=(xdg-desktop-portal-hyprland)
fi
if ((${#missing[@]})); then
if [[ $MODE == --no-root ]]; then
: # reported at the end, where the panel can see it
else
echo " installing missing packages: ${missing[*]} (password prompt)"
if sudo -n true 2>/dev/null; then sudo pacman -S --needed --noconfirm "${missing[@]}"
else pkexec pacman -S --needed --noconfirm "${missing[@]}"; fi
fi
fi
mkdir -p "$CACHE" "$LIB" "$BIN"
echo " creating the Python environment in $LIB/venv (log: $LOG)"
{
python3 -m venv --upgrade-deps "$LIB/venv"
"$LIB/venv/bin/pip" install --upgrade -r "$HERE/daemon/requirements.txt"
} >"$LOG" 2>&1 || { tail -n 15 "$LOG" >&2; echo "error: could not build the environment; log: $LOG" >&2; exit 1; }
# AirPlay is optional: pyatv is a big dependency tree and lags new Python
# releases, and everything else works without it.
if "$LIB/venv/bin/pip" install --upgrade -r "$HERE/daemon/requirements-optional.txt" >>"$LOG" 2>&1; then
echo " AirPlay support installed (pyatv)"
else
echo " note: pyatv did not install — Cast and DLNA still work, AirPlay will not (see $LOG)"
fi
echo " installing the daemon"
rm -rf "$LIB/screencast"
cp -r "$HERE/daemon/screencast" "$LIB/screencast"
find "$LIB/screencast" -name '__pycache__' -type d -exec rm -rf {} + 2>/dev/null || true
cat > "$LIB/screencast-server" <<LAUNCH
#!/bin/sh
# Generated by install.sh — runs the daemon out of its own virtualenv.
exec env PYTHONPATH="$LIB" "$LIB/venv/bin/python" -m screencast "\$@"
LAUNCH
chmod 755 "$LIB/screencast-server"
ln -sfn "$LIB/screencast-server" "$BIN/screencast-server"
git -C "$HERE" rev-parse HEAD > "$LIB/installed-commit" 2>/dev/null || date +%s > "$LIB/installed-commit"
echo " installing the shell plugin"
mkdir -p "$(dirname "$PLUGIN")"
if [[ $(readlink -f "$PLUGIN" 2>/dev/null) != "$(readlink -f "$HERE")" ]]; then
if [[ -L $PLUGIN || ! -e $PLUGIN ]]; then
ln -sfn "$HERE" "$PLUGIN" # dev mode; a real checkout comes from `omarchy plugin add`
omarchy-shell -q shell rescanPlugins 2>/dev/null || true
fi
fi
if command -v omarchy-shell >/dev/null && omarchy-shell -q shell ping >/dev/null 2>&1; then
enabled() { omarchy-plugin-list 2>/dev/null | awk -v id="$ID" '$1 == id && $2 == "enabled" { found = 1 } END { exit !found }'; }
for _ in 1 2 3 4 5; do
if enabled; then break; fi
if omarchy-plugin-enable "$ID" right >/dev/null 2>&1; then echo " enabled $ID"; break; fi
sleep 1
done
enabled || echo " note: not enabled yet; run: omarchy plugin enable $ID"
else
echo " note: omarchy-shell is not running; enable later with: omarchy plugin enable $ID"
fi
# The receiver pulls the stream from us, so the host firewall has to let it in.
# Installing stays root-free: the panel offers to open the port (via polkit) the
# first time a cast is actually blocked by it.
if [[ $(systemctl is-active ufw 2>/dev/null) == active || $(systemctl is-active firewalld 2>/dev/null) == active ]]; then
echo " note: this machine runs a firewall. Receivers fetch the stream from us, so"
echo " the panel has an \"Open port\" button: one password dialog, once, and the"
echo " rule stays. Nothing to type here."
fi
echo "done. The cast icon appears in the bar (if not: omarchy-shell shell rescanPlugins)."
echo "From a terminal: screencast-server devices | cast \"<name>\" | stop"
# Missing system packages are the one thing this cannot fix without root, so say
# so last: the plugin shows the final line of this log verbatim.
if ((${#missing[@]})) && [[ $MODE == --no-root ]]; then
echo "MISSING: sudo pacman -S --needed ${missing[*]}"
fi