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/wan-watchdog.nix
|
||||
./services/valheim-autoupdate.nix
|
||||
./services/owncast.nix
|
||||
./services/prebuild.nix
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -106,6 +106,22 @@ in
|
|||
"jellyfin.nordhammer.it" = proxy 8096; # streaming to external clients
|
||||
"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
|
||||
# Authelia: the panel container calls this server-to-server and has no
|
||||
# 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