Files
alan 1ed57291f9 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.
2026-08-29 19:57:54 +01:00

115 lines
5.2 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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