Restore stream quality defaults

This commit is contained in:
2026-06-07 00:52:37 -07:00
parent 0cb091cb53
commit a9f8509b99
5 changed files with 40 additions and 40 deletions

View File

@@ -99,16 +99,16 @@ Audio output:
- Maps `0:a:0?` - Maps `0:a:0?`
- Disables video with `-vn` - Disables video with `-vn`
- Converts to MP3 with `libmp3lame` - Converts to MP3 with `libmp3lame`
- Uses `session.options.audioBitrate`, default `64k` - Uses `session.options.audioBitrate`, default `160k`
- Uses `session.options.audioChannels`, default `1` - Uses `session.options.audioChannels`, default `2`
- Uses `session.options.audioSampleRate`, default `44100` - Uses `session.options.audioSampleRate`, default `48000`
- Outputs to `pipe:1` - Outputs to `pipe:1`
Frame output: Frame output:
- Maps `0:v:0` - Maps `0:v:0`
- Disables audio with `-an` - Disables audio with `-an`
- Applies `fps=<fps>,scale=w='min(<width>,iw)':h=-2:flags=fast_bilinear:out_range=pc,format=yuvj420p` - Applies `fps=<fps>,scale=w='min(<width>,iw)':h=-2:flags=bicubic:out_range=pc,format=yuvj420p`
- Encodes `mjpeg` - Encodes `mjpeg`
- Uses `-pix_fmt yuvj420p`, `-color_range pc`, `-q:v <quality>` - Uses `-pix_fmt yuvj420p`, `-color_range pc`, `-q:v <quality>`
- Outputs `image2pipe` to either `pipe:1` or `pipe:3` - Outputs `image2pipe` to either `pipe:1` or `pipe:3`
@@ -185,24 +185,24 @@ Runtime:
- `RECENT_URL_LIMIT`: recent URL count, default `12`. - `RECENT_URL_LIMIT`: recent URL count, default `12`.
- `FAVORITES_PATH`: favorites JSON path. - `FAVORITES_PATH`: favorites JSON path.
- `FAVORITES_LIMIT`: favorites count, default `50`. - `FAVORITES_LIMIT`: favorites count, default `50`.
- `DEFAULT_FPS`: default frame rate, fallback `8`, clamped `1..30`. - `DEFAULT_FPS`: default frame rate, fallback `24`, clamped `1..30`.
- `DEFAULT_FRAME_WIDTH`: default maximum frame width, fallback `480`, clamped `160..1920`. - `DEFAULT_FRAME_WIDTH`: default maximum frame width, fallback `960`, clamped `160..1920`.
- `JPEG_QUALITY`: default JPEG quality, fallback `12`, clamped `2..18`; lower is better for ffmpeg `-q:v`. - `JPEG_QUALITY`: default JPEG quality, fallback `7`, clamped `2..18`; lower is better for ffmpeg `-q:v`.
- `DEFAULT_AUDIO_BITRATE`: default MP3 audio bitrate, fallback `64k`. - `DEFAULT_AUDIO_BITRATE`: default MP3 audio bitrate, fallback `160k`.
- `DEFAULT_AUDIO_CHANNELS`: default MP3 audio channels, fallback `1`, clamped `1..2`. - `DEFAULT_AUDIO_CHANNELS`: default MP3 audio channels, fallback `2`, clamped `1..2`.
- `DEFAULT_AUDIO_SAMPLE_RATE`: default MP3 audio sample rate, fallback `44100`, clamped `22050..48000`. - `DEFAULT_AUDIO_SAMPLE_RATE`: default MP3 audio sample rate, fallback `48000`, clamped `22050..48000`.
- `MAX_WS_BUFFER_BYTES`: server-side WebSocket JPEG frame backlog cap, default `524288`. - `MAX_WS_BUFFER_BYTES`: server-side WebSocket JPEG frame backlog cap, default `2097152`.
- `MAX_AUDIO_QUEUE_BYTES`: single-mode audio output queue cap, default `4194304`. - `MAX_AUDIO_QUEUE_BYTES`: single-mode audio output queue cap, default `4194304`.
- `MAX_RELAY_BRANCH_QUEUE_BYTES`: relay per-branch compressed-input queue cap, default `8388608`. - `MAX_RELAY_BRANCH_QUEUE_BYTES`: relay per-branch compressed-input queue cap, default `8388608`.
Session playback options are accepted by `POST /api/session` even though the UI hides them: Session playback options are accepted by `POST /api/session` even though the UI hides them:
- `fps`: default `8`, clamped `1..30`. - `fps`: default `24`, clamped `1..30`.
- `width`: default `480`, clamped `160..1920`. - `width`: default `960`, clamped `160..1920`.
- `quality`: defaults to `JPEG_QUALITY`, clamped `2..18`; lower is better for ffmpeg `-q:v`. - `quality`: defaults to `JPEG_QUALITY`, clamped `2..18`; lower is better for ffmpeg `-q:v`.
- `audioBitrate`: default `64k`, accepts two or three digits followed by `k`. - `audioBitrate`: default `160k`, accepts two or three digits followed by `k`.
- `audioChannels`: default `1`, clamped `1..2`. - `audioChannels`: default `2`, clamped `1..2`.
- `audioSampleRate`: default `44100`, clamped `22050..48000`. - `audioSampleRate`: default `48000`, clamped `22050..48000`.
## Docker Notes ## Docker Notes

View File

@@ -49,7 +49,7 @@ docker logs -f frame-stream-player
The app sets `FFMPEG_INPUT_SEEKABLE=0` by default so `ffmpeg` reads stream inputs sequentially and avoids extra HTTP range connections. If a specific VOD file requires seeking for metadata, set `FFMPEG_INPUT_SEEKABLE=-1` to restore ffmpeg's automatic behavior. For ffmpeg-owned HTTP inputs, reconnect handling is enabled by default with `FFMPEG_HTTP_RECONNECT=1`. The app sets `FFMPEG_INPUT_SEEKABLE=0` by default so `ffmpeg` reads stream inputs sequentially and avoids extra HTTP range connections. If a specific VOD file requires seeking for metadata, set `FFMPEG_INPUT_SEEKABLE=-1` to restore ffmpeg's automatic behavior. For ffmpeg-owned HTTP inputs, reconnect handling is enabled by default with `FFMPEG_HTTP_RECONNECT=1`.
JPEG frames are dropped when the browser WebSocket falls behind instead of letting stale frames queue indefinitely. Tune the server-side backlog cap with `MAX_WS_BUFFER_BYTES`; the default is `524288`. JPEG frames are dropped when the browser WebSocket falls behind instead of letting stale frames queue indefinitely. Tune the server-side backlog cap with `MAX_WS_BUFFER_BYTES`; the default is `2097152`.
In single mode, audio output from `ffmpeg` is buffered before it is written to the browser so short HTTP backpressure pauses are less likely to stall frame generation. Tune the cap with `MAX_AUDIO_QUEUE_BYTES`; the default is `4194304`. In single mode, audio output from `ffmpeg` is buffered before it is written to the browser so short HTTP backpressure pauses are less likely to stall frame generation. Tune the cap with `MAX_AUDIO_QUEUE_BYTES`; the default is `4194304`.
@@ -69,10 +69,10 @@ Relay mode uses bounded per-worker input queues so one branch can briefly lag wi
The UI intentionally hides these settings, but the backend still supports them through `POST /api/session`. The UI intentionally hides these settings, but the backend still supports them through `POST /api/session`.
- Frame rate defaults to `8fps`. Set `DEFAULT_FPS` higher only when clients and links can keep up. - Frame rate defaults to `24fps`. Lower it if the client cannot keep up.
- Max width defaults to `480px`. Set `DEFAULT_FRAME_WIDTH` higher for better detail at the cost of bandwidth and browser image decode work. - Max width defaults to `960px`. Lower it first if bandwidth or image decode is the bottleneck.
- JPEG quality uses ffmpeg's `-q:v` scale, where lower is better. Set the default with `JPEG_QUALITY`; `12` is the low-bandwidth fallback, `2` is high quality, and `18` is rough but lighter. - JPEG quality uses ffmpeg's `-q:v` scale, where lower is better. Set the default with `JPEG_QUALITY`; `7` is the fallback, `2` is high quality, and `18` is rough but lighter.
- Audio defaults to mono MP3 at `64k`. Tune with `DEFAULT_AUDIO_BITRATE`, `DEFAULT_AUDIO_CHANNELS`, and `DEFAULT_AUDIO_SAMPLE_RATE`. - Audio defaults to stereo MP3 at `160k`. Tune with `DEFAULT_AUDIO_BITRATE`, `DEFAULT_AUDIO_CHANNELS`, and `DEFAULT_AUDIO_SAMPLE_RATE`.
## Tradeoffs ## Tradeoffs

View File

@@ -21,13 +21,13 @@ services:
FFMPEG_HTTP_RECONNECT_ON_HTTP_ERROR: "5xx" FFMPEG_HTTP_RECONNECT_ON_HTTP_ERROR: "5xx"
METADATA_PROBE_ENABLED: "0" METADATA_PROBE_ENABLED: "0"
METADATA_PROBE_TIMEOUT_MS: "4000" METADATA_PROBE_TIMEOUT_MS: "4000"
DEFAULT_FPS: "8" DEFAULT_FPS: "24"
DEFAULT_FRAME_WIDTH: "480" DEFAULT_FRAME_WIDTH: "960"
JPEG_QUALITY: "12" JPEG_QUALITY: "7"
DEFAULT_AUDIO_BITRATE: "64k" DEFAULT_AUDIO_BITRATE: "160k"
DEFAULT_AUDIO_CHANNELS: "1" DEFAULT_AUDIO_CHANNELS: "2"
DEFAULT_AUDIO_SAMPLE_RATE: "44100" DEFAULT_AUDIO_SAMPLE_RATE: "48000"
MAX_WS_BUFFER_BYTES: "524288" MAX_WS_BUFFER_BYTES: "2097152"
MAX_AUDIO_QUEUE_BYTES: "4194304" MAX_AUDIO_QUEUE_BYTES: "4194304"
MAX_RELAY_BRANCH_QUEUE_BYTES: "8388608" MAX_RELAY_BRANCH_QUEUE_BYTES: "8388608"
RECENT_URLS_PATH: /app/data/recent-urls.json RECENT_URLS_PATH: /app/data/recent-urls.json

View File

@@ -36,10 +36,10 @@ const elements = {
const context = elements.canvas.getContext('2d', { alpha: false, desynchronized: true }); const context = elements.canvas.getContext('2d', { alpha: false, desynchronized: true });
const FRAME_LATE_GRACE_SECONDS = 0.25; const FRAME_LATE_GRACE_SECONDS = 0.25;
const MAX_PENDING_FRAME_QUEUE_SECONDS = 1.5; const MAX_PENDING_FRAME_QUEUE_SECONDS = 2;
const MAX_DECODED_FRAME_QUEUE_SECONDS = 1.5; const MAX_DECODED_FRAME_QUEUE_SECONDS = 3;
const MIN_PENDING_FRAME_QUEUE = 6; const MIN_PENDING_FRAME_QUEUE = 12;
const MIN_DECODED_FRAME_QUEUE = 8; const MIN_DECODED_FRAME_QUEUE = 24;
const IMAGE_DECODE_TIMEOUT_MS = 3000; const IMAGE_DECODE_TIMEOUT_MS = 3000;
const FRAME_STALL_CHECK_MS = 1000; const FRAME_STALL_CHECK_MS = 1000;
const FRAME_STARTUP_STALL_RESET_MS = 10000; const FRAME_STARTUP_STALL_RESET_MS = 10000;

View File

@@ -34,7 +34,7 @@ const FAVORITES_PATH = process.env.FAVORITES_PATH ?? path.join(__dirname, '..',
const FAVORITES_LIMIT = clampInteger(process.env.FAVORITES_LIMIT, 50, 1, 200); const FAVORITES_LIMIT = clampInteger(process.env.FAVORITES_LIMIT, 50, 1, 200);
const SESSION_TTL_MS = 60 * 60 * 1000; const SESSION_TTL_MS = 60 * 60 * 1000;
const PLAYBACK_READY_TIMEOUT_MS = 15 * 1000; const PLAYBACK_READY_TIMEOUT_MS = 15 * 1000;
const MAX_WS_BUFFER_BYTES = clampInteger(process.env.MAX_WS_BUFFER_BYTES, 512 * 1024, 128 * 1024, 64 * 1024 * 1024); const MAX_WS_BUFFER_BYTES = clampInteger(process.env.MAX_WS_BUFFER_BYTES, 2 * 1024 * 1024, 128 * 1024, 64 * 1024 * 1024);
const MAX_AUDIO_QUEUE_BYTES = clampInteger(process.env.MAX_AUDIO_QUEUE_BYTES, 4 * 1024 * 1024, 256 * 1024, 128 * 1024 * 1024); const MAX_AUDIO_QUEUE_BYTES = clampInteger(process.env.MAX_AUDIO_QUEUE_BYTES, 4 * 1024 * 1024, 256 * 1024, 128 * 1024 * 1024);
const MAX_RELAY_BRANCH_QUEUE_BYTES = clampInteger(process.env.MAX_RELAY_BRANCH_QUEUE_BYTES, 8 * 1024 * 1024, 512 * 1024, 256 * 1024 * 1024); const MAX_RELAY_BRANCH_QUEUE_BYTES = clampInteger(process.env.MAX_RELAY_BRANCH_QUEUE_BYTES, 8 * 1024 * 1024, 512 * 1024, 256 * 1024 * 1024);
const RELAY_BRANCH_PAUSE_BYTES = Math.floor(MAX_RELAY_BRANCH_QUEUE_BYTES / 2); const RELAY_BRANCH_PAUSE_BYTES = Math.floor(MAX_RELAY_BRANCH_QUEUE_BYTES / 2);
@@ -42,12 +42,12 @@ const JPEG_SOI = Buffer.from([0xff, 0xd8]);
const JPEG_EOI = Buffer.from([0xff, 0xd9]); const JPEG_EOI = Buffer.from([0xff, 0xd9]);
const defaults = { const defaults = {
fps: clampInteger(process.env.DEFAULT_FPS, 8, 1, 30), fps: clampInteger(process.env.DEFAULT_FPS, 24, 1, 30),
width: clampInteger(process.env.DEFAULT_FRAME_WIDTH, 480, 160, 1920), width: clampInteger(process.env.DEFAULT_FRAME_WIDTH, 960, 160, 1920),
quality: clampInteger(process.env.JPEG_QUALITY, 12, 2, 18), quality: clampInteger(process.env.JPEG_QUALITY, 7, 2, 18),
audioBitrate: parseAudioBitrate(process.env.DEFAULT_AUDIO_BITRATE, '64k'), audioBitrate: parseAudioBitrate(process.env.DEFAULT_AUDIO_BITRATE, '160k'),
audioChannels: clampInteger(process.env.DEFAULT_AUDIO_CHANNELS, 1, 1, 2), audioChannels: clampInteger(process.env.DEFAULT_AUDIO_CHANNELS, 2, 1, 2),
audioSampleRate: clampInteger(process.env.DEFAULT_AUDIO_SAMPLE_RATE, 44100, 22050, 48000), audioSampleRate: clampInteger(process.env.DEFAULT_AUDIO_SAMPLE_RATE, 48000, 22050, 48000),
}; };
const sessions = new Map(); const sessions = new Map();
@@ -1865,7 +1865,7 @@ function formatFfmpegSeconds(seconds) {
function buildFrameOutputArgs(session, outputUrl) { function buildFrameOutputArgs(session, outputUrl) {
const { fps, quality, width } = session.options; const { fps, quality, width } = session.options;
const videoFilter = `fps=${fps},scale=w='min(${width},iw)':h=-2:flags=fast_bilinear:out_range=pc,format=yuvj420p`; const videoFilter = `fps=${fps},scale=w='min(${width},iw)':h=-2:flags=bicubic:out_range=pc,format=yuvj420p`;
return [ return [
'-an', '-an',