Make video a per-server toggle instead of an env-wide setting

VIDEO_ENABLED is now permission rather than behaviour: it decides whether
the feature exists at all, while turning it on for a server is a toggle in
the panel or !video in chat, off by default. Video costs real CPU for
every playing channel, so that should be a deliberate choice rather than
something a config flag switches on everywhere.

With the env flag off the panel renders no toggle at all and !video says
so, and the switch applies from the next track — swapping tracks mid-play
would cut the current one.

The loop button no longer reads "выкл" either: it sat next to the video
button showing the same word, so the two states were indistinguishable.
Both now name what they do and rely on highlighting for state.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Leonid Pershin
2026-09-09 02:13:08 +03:00
co-authored by Claude Opus 5
parent e06c42f60c
commit a9b680c418
11 changed files with 92 additions and 13 deletions
+16 -4
View File
@@ -173,9 +173,14 @@ export interface PlaybackInput {
video?: { stream: Readable; width: number; height: number };
}
export interface PlaybackOptions {
/** Publish the picture too; the server-wide switch still has to allow it. */
video?: boolean;
}
/** Only YouTube reliably carries a picture worth showing next to the audio. */
function canShowVideo(track: Track): boolean {
return config.VIDEO_ENABLED && track.source === "youtube" && !track.isLive;
function canShowVideo(track: Track, wanted: boolean): boolean {
return wanted && config.VIDEO_ENABLED && track.source === "youtube" && !track.isLive;
}
const HTTP_RESILIENCE = [
@@ -196,7 +201,11 @@ function ffmpegProxyOptions(): string[] {
}
/** Opens an ffmpeg-compatible input for a track, optionally starting at an offset. */
export async function openPlayback(track: Track, seekSeconds = 0): Promise<PlaybackInput> {
export async function openPlayback(
track: Track,
seekSeconds = 0,
options: PlaybackOptions = {},
): Promise<PlaybackInput> {
const seekOptions = seekSeconds > 0 ? ["-ss", seekSeconds.toFixed(2)] : [];
if (track.source === "local") {
@@ -214,7 +223,10 @@ export async function openPlayback(track: Track, seekSeconds = 0): Promise<Playb
// The format is checked before committing to the video pipeline: audio comes
// out of that same pipeline, so falling back afterwards would kill the sound.
if (canShowVideo(track) && (await ytdlp.hasProgressiveVideo(track.url, config.VIDEO_HEIGHT))) {
if (
canShowVideo(track, options.video ?? false) &&
(await ytdlp.hasProgressiveVideo(track.url, config.VIDEO_HEIGHT))
) {
log.info({ title: track.title }, "playing with video");
const pipeline = openVideoPipeline(track.url, seekSeconds);
return {