VIDEO_ENABLED makes the bot publish a second LiveKit track with the
picture. Stoat only grants screen_share in the call token when the bot
has the Video permission and the instance has video enabled, so a refusal
is reported in chat and playback continues with sound alone.
Two constraints shaped the pipeline, both found by testing rather than
assumption:
- Only progressive formats can be streamed. Separate video+audio streams
make yt-dlp download both in full before muxing a single byte, and
direct CDN URLs handed to ffmpeg simply hang — YouTube no longer serves
them to other clients. That caps video at the 360p single file YouTube
offers, and the format is checked before committing to the video path,
since audio would otherwise come from the same broken pipeline.
- One ffmpeg with two outputs, paced by -re: an unpaced decode races
ahead of the sound and eats memory at 1.4 MB per frame.
The same CDN-URL finding removes the audio seek shortcut, which resolved
such a URL and would have hung the same way; seeking now decodes up to
the offset like it already did behind a proxy.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Plays audio into Stoat voice channels over LiveKit and exposes the same
player through both chat commands and a browser panel, so the two never
drift apart: everything routes through a single MusicManager.
- core: per-server GuildPlayer (queue, loop, shuffle, seek, volume,
idle auto-leave) driving revoice.js/@livekit/rtc-node and ffmpeg
- sources: yt-dlp for YouTube/SoundCloud, direct media URLs and internet
radio, optional local library with path-traversal guards
- bot: 18 chat commands with aliases, plus !panel one-time login links
- api: Fastify REST + WebSocket, sessions authenticated against the
instance's own /auth/session/login (TOTP supported), permissions
re-checked against Stoat membership and roles on every request
- web: React panel with search, queue editing, seek and volume
- deploy: Dockerfile, compose.override.yml and Caddyfile snippets for
dropping the service into an existing /opt/stoat stack
Verified with npm run typecheck, both builds, and scripts/smoke-api.mjs
(9 API checks). Voice playback itself needs a live instance to test.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>