# services/owncast.nix — Owncast relay for synced watch parties. # # Why relay instead of everyone just opening Twitch: embed-sync tools only sync # play/pause/seek commands, not buffers. Each viewer's Twitch player picks its # own live-edge offset, so a group drifts 5-15s apart and no amount of "seek to # now" fixes it. One shared HLS playlist removes the drift by construction — # everybody pulls the same segment numbers — at the cost of ~15-25s of extra # absolute latency. That tradeoff is the whole point; without it this is just # watching on the website. # # Private by design: Owncast has no viewer auth of its own (upstream #489/#4344 # are still open), so nginx does it with a shared password in services/nginx.nix. { config, lib, pkgs, ... }: let port = 8088; # Party password, also the Owncast admin password and RTMP stream key. Not a # secret in the /var/secrets sense — it gets read out loud in Discord and it # only guards a living-room stream — so plain text in the store is fine here. # Anything that actually matters still goes through Authelia or runtime # injection. password = "Blizzcon"; in { config = lib.mkIf (config.networking.hostName == "FredOS-Mediaserver") { services.owncast = { enable = true; inherit port; listen = "127.0.0.1"; # nginx fronts it; RTMP is localhost-only too }; # The module hardcodes its ExecStart with no escape hatch, so restate it. # Without -adminpassword the admin panel sits on the documented default # abc123, behind nothing but the same password the guests were given. systemd.services.owncast.serviceConfig.ExecStart = lib.mkForce ( lib.concatStringsSep " " [ "${pkgs.owncast}/bin/owncast" "-webserverport ${toString port}" "-rtmpport 1935" "-webserverip 127.0.0.1" "-adminpassword ${password}" "-streamkey ${password}" ] ); # relay [quality] — pull a live stream and push it into Owncast. # Run it when the stream starts, Ctrl-C when it ends. Deliberately not a # unit: there's nothing to supervise, the channel changes every time, and # a systemd service would need an EnvironmentFile edit + restart per event. environment.systemPackages = [ (pkgs.writeShellScriptBin "relay" '' set -euo pipefail url=''${1:?usage: relay [quality]} # streamlink drops Twitch pre/mid-roll ads on its own (mandatory since # 7.5.0), but that leaves a timestamp discontinuity where the ad was. # -fflags +genpts rebuilds PTS across it so the copy doesn't stall on # non-monotonic DTS. ${pkgs.streamlink}/bin/streamlink --stdout "$url" "''${2:-best}" \ | ${pkgs.ffmpeg}/bin/ffmpeg -hide_banner -fflags +genpts -i pipe:0 \ -c copy -f flv rtmp://127.0.0.1:1935/live/${password} '') ]; }; }