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:
Executable
+114
@@ -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
|
||||
Reference in New Issue
Block a user