owncast: relay Twitch/YouTube for synced watch parties
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
843d0d12c1
commit
5f85d611c9
3 changed files with 82 additions and 0 deletions
|
|
@ -126,6 +126,7 @@ in
|
||||||
./services/hardware-health.nix
|
./services/hardware-health.nix
|
||||||
./services/wan-watchdog.nix
|
./services/wan-watchdog.nix
|
||||||
./services/valheim-autoupdate.nix
|
./services/valheim-autoupdate.nix
|
||||||
|
./services/owncast.nix
|
||||||
./services/prebuild.nix
|
./services/prebuild.nix
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,22 @@ in
|
||||||
"jellyfin.nordhammer.it" = proxy 8096; # streaming to external clients
|
"jellyfin.nordhammer.it" = proxy 8096; # streaming to external clients
|
||||||
"seerr.nordhammer.it" = proxy 5055; # own auth via Jellyfin sign-in
|
"seerr.nordhammer.it" = proxy 5055; # own auth via Jellyfin sign-in
|
||||||
|
|
||||||
|
# Watch-party relay (services/owncast.nix). Shared basic-auth password
|
||||||
|
# rather than Authelia: the guests have no accounts here, and Owncast
|
||||||
|
# has no viewer auth of its own. The chat websocket is exempt because
|
||||||
|
# browsers don't replay cached basic-auth credentials on a WS
|
||||||
|
# handshake — /ws would 401 forever and chat would silently never
|
||||||
|
# connect. Video stays gated; only the chat socket is reachable by
|
||||||
|
# someone who already knows the hostname.
|
||||||
|
"watch.nordhammer.it" = lib.recursiveUpdate (proxy 8088) {
|
||||||
|
basicAuth = { blizzcon = "Blizzcon"; };
|
||||||
|
locations."/ws" = {
|
||||||
|
proxyPass = "http://127.0.0.1:8088";
|
||||||
|
proxyWebsockets = true;
|
||||||
|
extraConfig = "auth_basic off;";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
# wings daemon (see services/pelican.nix). Deliberately NOT behind
|
# wings daemon (see services/pelican.nix). Deliberately NOT behind
|
||||||
# Authelia: the panel container calls this server-to-server and has no
|
# Authelia: the panel container calls this server-to-server and has no
|
||||||
# session cookie to present. wings authenticates every request with its
|
# session cookie to present. wings authenticates every request with its
|
||||||
|
|
|
||||||
65
services/owncast.nix
Normal file
65
services/owncast.nix
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
# 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 <url> [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 <twitch or youtube url> [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}
|
||||||
|
'')
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue