flybrain/infra/units/flycast.service
acamilo 660c3cf00d
Some checks failed
ci / node 22 (test + typecheck) (push) Has been cancelled
ci / rust stable (cargo test --workspace --release) (push) Has been cancelled
ci / infra/tests/lint.sh (push) Has been cancelled
ci / playwright apps/stage (allowed to fail) (push) Has been cancelled
flybrain v0.4.0: public tree (history retained privately)
2026-09-21 15:09:46 +00:00

112 lines
6.4 KiB
Desktop File

# infra/units/flycast.service — pushed to /etc/systemd/system/flycast.service.
# docs/design/infra.md section 3 ("flycast.service, encode once, local
# sinks only"), now with docs/design/gpu.md sections 4, 5 and 8 applied.
#
# 2026-09-15: the ffmpeg command moved OUT of this file into
# /opt/fly/bin/flycast-launch (infra/bin/flycast-launch). Two reasons, both
# from gpu.md section 8: there are now two encoder variants selected by
# FLY_ENCODER, and `Environment=FLY_ENCODER=%i` is the wrong shape for a
# unit that is not templated — so FLY_ENCODER comes from
# /etc/fly/fly.env (written per container by 05-deploy.sh from
# the env file) via EnvironmentFile=, and the launcher assembles the command.
# This mirrors flystage.service -> bin/flystage-launch, keeps the two flag
# sets reviewable side by side, and removes the '%%' escaping this file
# used to need for the strftime segment filename (the launcher is a shell
# script, so a literal '%' is just a '%'). ExecStartPost still needs it.
#
# The launcher also owns the automatic one-shot nvenc -> x264 fallback and
# the fly_encoder_backend metric: the shared card means the GPU workload in the neighbouring container can
# deny us an NVENC session at any moment, and a crash-loop there would take
# the stream black (gpu.md section 5).
#
# Resolution/bitrate: 1920x1080 at 6000 kbps CBR, which diverges from
# docs/design/infra.md section 3's still-720p/3000k text (see
# docs/streaming-plan.md section 3's dated note). -g/-keyint_min stay 60
# (2 s keyframes at 30 fps, still Twitch's requirement). Those numbers now
# live as defaults in bin/flycast-launch.
[Unit]
Description=flycast: encode once, tee to MediaMTX and local segments
# 2026-09-16: flystage.service added to After= (NOT to Requires=), plus the
# wait-for-stage ExecStartPre below. This is the fix for the capture freeze
# measured on the release container and reproduced six times on the dev container
# (infra/docs/capture-freeze.md): a PulseAudio client attaching to the null
# sink during roughly the first one to three seconds of this ffmpeg's start
# leaves its x11grab leg permanently starved — about one new picture per
# second, 29 repeats, perfect audio, and ffmpeg reporting 0 dup / 0 drop /
# 30 fps, for the whole life of the process. Chromium's audio stream at page
# load is exactly such a client, and `Requires=xvfb.service` on both units
# means an Xvfb restart co-starts flystage and flycast right into that
# window (measured: 171 identical frames out of 180).
#
# After= only, deliberately. Requires=flystage.service would make a dead or
# restarting page take the encoder (and therefore the broadcast and the
# recording) down with it, which is the opposite of this repo's "the page is
# display only so the fly survives browser and encoder restarts" rule. The
# ordering plus the readiness wait is all that is needed: nothing here
# depends on flystage SUCCEEDING, only on not racing it.
After=xvfb.service pulse.service mediamtx.service flystage.service
Requires=xvfb.service pulse.service
[Service]
Type=simple
User=fly
# ffmpeg's pulse input needs to be told where the server is, exactly as
# flystage.service already tells Chromium. pulse.service puts the socket under
# /run/fly (PULSE_RUNTIME_PATH), which is not a path libpulse looks in by
# default, so without these two the `-f pulse -i stream.monitor` input fails
# with "Error opening input: No such process" and flycast crash-loops with a
# perfectly good null sink sitting right there. Found on the P0 spike.
Environment=XDG_RUNTIME_DIR=/run/fly
Environment=PULSE_SERVER=unix:/run/fly/pulse/native
# FLY_ENCODER (nvenc|x264), FLY_NVENC_PRESET and the stream geometry come
# from here. '-' prefix: a container provisioned before 05-deploy.sh wrote
# the file still starts, on the launcher's own defaults (x264, 1080p30,
# 6000k) — which is exactly the pre-GPU behaviour.
EnvironmentFile=-/etc/fly/fly.env
ExecStartPre=/opt/fly/bin/wait-for-x :99 30
# The capture-freeze ordering gate (infra/docs/capture-freeze.md). Returns 0
# only once the X display answers, a Chromium kiosk window is MAPPED on :99,
# the pulse sink `stream` has at least one sink-input, and all three have held
# for 5 s — i.e. the page's audio client is already attached and nothing is
# mid-connect, so ffmpeg's first seconds see a static client set. After 120 s
# it logs a warning and returns 0 anyway: a black-but-running stream beats no
# stream, and a page that never comes up is flystage's failure to report.
ExecStartPre=/opt/fly/bin/wait-for-stage 120
# Both ExecStartPre waits are inside the start job, so the default
# TimeoutStartSec=90s would kill the unit mid-wait (30 + 120 in the worst
# case) and Restart=always would spin it. 180 s covers both with headroom.
TimeoutStartSec=180
ExecStart=/opt/fly/bin/flycast-launch
ExecStartPost=/bin/sh -c 'date -u +%%s > /srv/fly/state/flycast-start'
Restart=always
RestartSec=5
# ffmpeg exits 255 when it is stopped by a signal it handles, so without
# this every deliberate `systemctl stop flycast` (and every
# `systemctl stop fly.target`) left the unit in `failed (Result: exit-code)`
# with ExecMainStatus=255 — an operator stopping the encoder on purpose
# produced exactly the state fly-watchdog and verify.sh treat as an
# incident. Measured on the P0 spike run 2. 255 is a clean stop here; a
# crash of the encoder shows up as a nonzero exit that is not 255, or as the
# progress file going stale, which is what fly-watchdog's check 3 actually
# keys on.
SuccessExitStatus=255
# CPUWeight stays 100 for now. gpu.md section 7 measurement 14 wants it
# LOWERED (nvenc barely needs CPU) as one of three simultaneous changes for
# the fly_lag_seconds re-measurement — the other two, the cpuset and
# RAYON_NUM_THREADS, are in flysim.service (P0 run 2 landed the cpuset and
# dropped the default to 3, not 4 — see flysim.service and
# docs/design/gpu.md). The number is left for that measurement to pick
# rather than guessed here, because on the x264 fallback path flycast needs
# roughly 2 cores and starving it would turn a degraded stream into a
# broken one.
CPUWeight=100
# MemoryMax is unchanged at 512M, but WATCH IT on the nvenc path: a CUDA
# context plus NVENC's pinned host-side staging buffers are charged to this
# cgroup, and 512M was sized for a pure-CPU x264 ffmpeg. If flycast starts
# getting OOM-killed within seconds of a successful NVENC session open,
# this number is the cause, not the GPU. Recorded as a watch item on
# infra/docs/p0-measurements.md GPU measurement 3.
MemoryMax=512M
[Install]
WantedBy=fly.target