Optimize stream defaults for weak connections
This commit is contained in:
30
AGENTS.md
30
AGENTS.md
@@ -88,6 +88,7 @@ Common HTTP input args:
|
|||||||
- `-loglevel ${FFMPEG_LOG_LEVEL}`
|
- `-loglevel ${FFMPEG_LOG_LEVEL}`
|
||||||
- `-nostats`
|
- `-nostats`
|
||||||
- `-seekable ${FFMPEG_INPUT_SEEKABLE}`
|
- `-seekable ${FFMPEG_INPUT_SEEKABLE}`
|
||||||
|
- HTTP reconnect options when `FFMPEG_HTTP_RECONNECT=1` and the input is HTTP(S)
|
||||||
- `-re`
|
- `-re`
|
||||||
- `-i <inputUrl>`
|
- `-i <inputUrl>`
|
||||||
|
|
||||||
@@ -97,15 +98,17 @@ Audio output:
|
|||||||
|
|
||||||
- Maps `0:a:0?`
|
- Maps `0:a:0?`
|
||||||
- Disables video with `-vn`
|
- Disables video with `-vn`
|
||||||
- Converts to stereo 48 kHz MP3 with `libmp3lame`
|
- Converts to MP3 with `libmp3lame`
|
||||||
- Uses `session.options.audioBitrate`, default `160k`
|
- Uses `session.options.audioBitrate`, default `64k`
|
||||||
|
- Uses `session.options.audioChannels`, default `1`
|
||||||
|
- Uses `session.options.audioSampleRate`, default `44100`
|
||||||
- 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=bicubic:out_range=pc,format=yuvj420p`
|
- Applies `fps=<fps>,scale=w='min(<width>,iw)':h=-2:flags=fast_bilinear: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`
|
||||||
@@ -171,22 +174,33 @@ Runtime:
|
|||||||
- `FFMPEG_PATH`: ffmpeg binary path, default `ffmpeg`.
|
- `FFMPEG_PATH`: ffmpeg binary path, default `ffmpeg`.
|
||||||
- `FFMPEG_LOG_LEVEL`: ffmpeg log level, default `warning`.
|
- `FFMPEG_LOG_LEVEL`: ffmpeg log level, default `warning`.
|
||||||
- `FFMPEG_INPUT_SEEKABLE`: HTTP input seekable option, default `0`.
|
- `FFMPEG_INPUT_SEEKABLE`: HTTP input seekable option, default `0`.
|
||||||
|
- `FFMPEG_HTTP_RECONNECT`: enable ffmpeg HTTP reconnect options for HTTP inputs, default `1`.
|
||||||
|
- `FFMPEG_HTTP_RECONNECT_DELAY_MAX`: max ffmpeg reconnect delay, default `2`.
|
||||||
|
- `FFMPEG_HTTP_RECONNECT_MAX_RETRIES`: max ffmpeg reconnect retries, default `4`.
|
||||||
|
- `FFMPEG_HTTP_RECONNECT_ON_HTTP_ERROR`: HTTP status list for reconnect, default `5xx`.
|
||||||
- `PLAYBACK_CONNECTION_MODE`: `split`, `relay`, or `single`.
|
- `PLAYBACK_CONNECTION_MODE`: `split`, `relay`, or `single`.
|
||||||
- `RECENT_URLS_PATH`: recent URL JSON path.
|
- `RECENT_URLS_PATH`: recent URL JSON path.
|
||||||
- `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`.
|
||||||
- `JPEG_QUALITY`: default JPEG quality, fallback `7`, clamped `2..18`; lower is better for ffmpeg `-q:v`.
|
- `DEFAULT_FPS`: default frame rate, fallback `8`, clamped `1..30`.
|
||||||
- `MAX_WS_BUFFER_BYTES`: server-side WebSocket JPEG frame backlog cap, default `2097152`.
|
- `DEFAULT_FRAME_WIDTH`: default maximum frame width, fallback `480`, clamped `160..1920`.
|
||||||
|
- `JPEG_QUALITY`: default JPEG quality, fallback `12`, clamped `2..18`; lower is better for ffmpeg `-q:v`.
|
||||||
|
- `DEFAULT_AUDIO_BITRATE`: default MP3 audio bitrate, fallback `64k`.
|
||||||
|
- `DEFAULT_AUDIO_CHANNELS`: default MP3 audio channels, fallback `1`, clamped `1..2`.
|
||||||
|
- `DEFAULT_AUDIO_SAMPLE_RATE`: default MP3 audio sample rate, fallback `44100`, clamped `22050..48000`.
|
||||||
|
- `MAX_WS_BUFFER_BYTES`: server-side WebSocket JPEG frame backlog cap, default `524288`.
|
||||||
- `MAX_AUDIO_QUEUE_BYTES`: single-mode audio output queue cap, default `16777216`.
|
- `MAX_AUDIO_QUEUE_BYTES`: single-mode audio output queue cap, default `16777216`.
|
||||||
- `MAX_RELAY_BRANCH_QUEUE_BYTES`: relay per-branch compressed-input queue cap, default `16777216`.
|
- `MAX_RELAY_BRANCH_QUEUE_BYTES`: relay per-branch compressed-input queue cap, default `16777216`.
|
||||||
|
|
||||||
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 `24`, clamped `1..30`.
|
- `fps`: default `8`, clamped `1..30`.
|
||||||
- `width`: default `960`, clamped `160..1920`.
|
- `width`: default `480`, 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 `160k`, accepts two or three digits followed by `k`.
|
- `audioBitrate`: default `64k`, accepts two or three digits followed by `k`.
|
||||||
|
- `audioChannels`: default `1`, clamped `1..2`.
|
||||||
|
- `audioSampleRate`: default `44100`, clamped `22050..48000`.
|
||||||
|
|
||||||
## Docker Notes
|
## Docker Notes
|
||||||
|
|
||||||
|
|||||||
12
README.md
12
README.md
@@ -47,9 +47,9 @@ Recently played URLs and favorites are stored globally by the backend. In Docker
|
|||||||
docker logs -f frame-stream-player
|
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.
|
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 `2097152`.
|
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`.
|
||||||
|
|
||||||
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 `16777216`.
|
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 `16777216`.
|
||||||
|
|
||||||
@@ -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 `24fps`. Lower it if the client cannot keep up.
|
- Frame rate defaults to `8fps`. Set `DEFAULT_FPS` higher only when clients and links can keep up.
|
||||||
- Max width defaults to `960px`. Lower it first if bandwidth or image decode is the bottleneck.
|
- Max width defaults to `480px`. Set `DEFAULT_FRAME_WIDTH` higher for better detail at the cost of bandwidth and browser image decode work.
|
||||||
- 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.
|
- 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.
|
||||||
- Audio defaults to MP3 at `160k`.
|
- Audio defaults to mono MP3 at `64k`. Tune with `DEFAULT_AUDIO_BITRATE`, `DEFAULT_AUDIO_CHANNELS`, and `DEFAULT_AUDIO_SAMPLE_RATE`.
|
||||||
|
|
||||||
## Tradeoffs
|
## Tradeoffs
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,17 @@ services:
|
|||||||
PLAYBACK_CONNECTION_MODE: relay
|
PLAYBACK_CONNECTION_MODE: relay
|
||||||
FFMPEG_LOG_LEVEL: warning
|
FFMPEG_LOG_LEVEL: warning
|
||||||
FFMPEG_INPUT_SEEKABLE: "0"
|
FFMPEG_INPUT_SEEKABLE: "0"
|
||||||
JPEG_QUALITY: "7"
|
FFMPEG_HTTP_RECONNECT: "1"
|
||||||
MAX_WS_BUFFER_BYTES: "2097152"
|
FFMPEG_HTTP_RECONNECT_DELAY_MAX: "2"
|
||||||
|
FFMPEG_HTTP_RECONNECT_MAX_RETRIES: "4"
|
||||||
|
FFMPEG_HTTP_RECONNECT_ON_HTTP_ERROR: "5xx"
|
||||||
|
DEFAULT_FPS: "8"
|
||||||
|
DEFAULT_FRAME_WIDTH: "480"
|
||||||
|
JPEG_QUALITY: "12"
|
||||||
|
DEFAULT_AUDIO_BITRATE: "64k"
|
||||||
|
DEFAULT_AUDIO_CHANNELS: "1"
|
||||||
|
DEFAULT_AUDIO_SAMPLE_RATE: "44100"
|
||||||
|
MAX_WS_BUFFER_BYTES: "524288"
|
||||||
MAX_AUDIO_QUEUE_BYTES: "16777216"
|
MAX_AUDIO_QUEUE_BYTES: "16777216"
|
||||||
MAX_RELAY_BRANCH_QUEUE_BYTES: "16777216"
|
MAX_RELAY_BRANCH_QUEUE_BYTES: "16777216"
|
||||||
RECENT_URLS_PATH: /app/data/recent-urls.json
|
RECENT_URLS_PATH: /app/data/recent-urls.json
|
||||||
|
|||||||
@@ -36,10 +36,10 @@ const elements = {
|
|||||||
|
|
||||||
const context = elements.canvas.getContext('2d', { alpha: false });
|
const context = elements.canvas.getContext('2d', { alpha: false });
|
||||||
const FRAME_LATE_GRACE_SECONDS = 0.25;
|
const FRAME_LATE_GRACE_SECONDS = 0.25;
|
||||||
const MAX_PENDING_FRAME_QUEUE_SECONDS = 2;
|
const MAX_PENDING_FRAME_QUEUE_SECONDS = 1.5;
|
||||||
const MAX_DECODED_FRAME_QUEUE_SECONDS = 3;
|
const MAX_DECODED_FRAME_QUEUE_SECONDS = 1.5;
|
||||||
const MIN_PENDING_FRAME_QUEUE = 12;
|
const MIN_PENDING_FRAME_QUEUE = 6;
|
||||||
const MIN_DECODED_FRAME_QUEUE = 24;
|
const MIN_DECODED_FRAME_QUEUE = 8;
|
||||||
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;
|
||||||
|
|||||||
@@ -14,13 +14,17 @@ const publicDir = path.join(__dirname, '..', 'public');
|
|||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const server = createServer(app);
|
const server = createServer(app);
|
||||||
const wss = new WebSocketServer({ noServer: true });
|
const wss = new WebSocketServer({ noServer: true, perMessageDeflate: false });
|
||||||
|
|
||||||
const PORT = Number(process.env.PORT ?? 3000);
|
const PORT = Number(process.env.PORT ?? 3000);
|
||||||
const FFMPEG_PATH = process.env.FFMPEG_PATH ?? 'ffmpeg';
|
const FFMPEG_PATH = process.env.FFMPEG_PATH ?? 'ffmpeg';
|
||||||
const FFPROBE_PATH = process.env.FFPROBE_PATH ?? 'ffprobe';
|
const FFPROBE_PATH = process.env.FFPROBE_PATH ?? 'ffprobe';
|
||||||
const FFMPEG_LOG_LEVEL = process.env.FFMPEG_LOG_LEVEL ?? 'warning';
|
const FFMPEG_LOG_LEVEL = process.env.FFMPEG_LOG_LEVEL ?? 'warning';
|
||||||
const FFMPEG_INPUT_SEEKABLE = process.env.FFMPEG_INPUT_SEEKABLE ?? '0';
|
const FFMPEG_INPUT_SEEKABLE = process.env.FFMPEG_INPUT_SEEKABLE ?? '0';
|
||||||
|
const FFMPEG_HTTP_RECONNECT = parseBoolean(process.env.FFMPEG_HTTP_RECONNECT, true);
|
||||||
|
const FFMPEG_HTTP_RECONNECT_DELAY_MAX = clampInteger(process.env.FFMPEG_HTTP_RECONNECT_DELAY_MAX, 2, 0, 30);
|
||||||
|
const FFMPEG_HTTP_RECONNECT_MAX_RETRIES = clampInteger(process.env.FFMPEG_HTTP_RECONNECT_MAX_RETRIES, 4, 0, 20);
|
||||||
|
const FFMPEG_HTTP_RECONNECT_ON_HTTP_ERROR = process.env.FFMPEG_HTTP_RECONNECT_ON_HTTP_ERROR ?? '5xx';
|
||||||
const PLAYBACK_CONNECTION_MODE = parsePlaybackConnectionMode(process.env.PLAYBACK_CONNECTION_MODE ?? process.env.PLAYBACK_MODE);
|
const PLAYBACK_CONNECTION_MODE = parsePlaybackConnectionMode(process.env.PLAYBACK_CONNECTION_MODE ?? process.env.PLAYBACK_MODE);
|
||||||
const RECENT_URLS_PATH = process.env.RECENT_URLS_PATH ?? path.join(__dirname, '..', 'data', 'recent-urls.json');
|
const RECENT_URLS_PATH = process.env.RECENT_URLS_PATH ?? path.join(__dirname, '..', 'data', 'recent-urls.json');
|
||||||
const RECENT_URL_LIMIT = clampInteger(process.env.RECENT_URL_LIMIT, 12, 1, 50);
|
const RECENT_URL_LIMIT = clampInteger(process.env.RECENT_URL_LIMIT, 12, 1, 50);
|
||||||
@@ -29,7 +33,7 @@ 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 METADATA_PROBE_TIMEOUT_MS = 8 * 1000;
|
const METADATA_PROBE_TIMEOUT_MS = 8 * 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, 2 * 1024 * 1024, 128 * 1024, 64 * 1024 * 1024);
|
const MAX_WS_BUFFER_BYTES = clampInteger(process.env.MAX_WS_BUFFER_BYTES, 512 * 1024, 128 * 1024, 64 * 1024 * 1024);
|
||||||
const MAX_AUDIO_QUEUE_BYTES = clampInteger(process.env.MAX_AUDIO_QUEUE_BYTES, 16 * 1024 * 1024, 256 * 1024, 128 * 1024 * 1024);
|
const MAX_AUDIO_QUEUE_BYTES = clampInteger(process.env.MAX_AUDIO_QUEUE_BYTES, 16 * 1024 * 1024, 256 * 1024, 128 * 1024 * 1024);
|
||||||
const MAX_RELAY_BRANCH_QUEUE_BYTES = clampInteger(process.env.MAX_RELAY_BRANCH_QUEUE_BYTES, 16 * 1024 * 1024, 512 * 1024, 256 * 1024 * 1024);
|
const MAX_RELAY_BRANCH_QUEUE_BYTES = clampInteger(process.env.MAX_RELAY_BRANCH_QUEUE_BYTES, 16 * 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);
|
||||||
@@ -37,10 +41,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: 24,
|
fps: clampInteger(process.env.DEFAULT_FPS, 8, 1, 30),
|
||||||
width: 960,
|
width: clampInteger(process.env.DEFAULT_FRAME_WIDTH, 480, 160, 1920),
|
||||||
quality: clampInteger(process.env.JPEG_QUALITY, 7, 2, 18),
|
quality: clampInteger(process.env.JPEG_QUALITY, 12, 2, 18),
|
||||||
audioBitrate: '160k',
|
audioBitrate: parseAudioBitrate(process.env.DEFAULT_AUDIO_BITRATE, '64k'),
|
||||||
|
audioChannels: clampInteger(process.env.DEFAULT_AUDIO_CHANNELS, 1, 1, 2),
|
||||||
|
audioSampleRate: clampInteger(process.env.DEFAULT_AUDIO_SAMPLE_RATE, 44100, 22050, 48000),
|
||||||
};
|
};
|
||||||
|
|
||||||
const sessions = new Map();
|
const sessions = new Map();
|
||||||
@@ -363,7 +369,9 @@ function parsePlaybackOptions(body) {
|
|||||||
fps: clampInteger(body?.fps, defaults.fps, 1, 30),
|
fps: clampInteger(body?.fps, defaults.fps, 1, 30),
|
||||||
width: clampInteger(body?.width, defaults.width, 160, 1920),
|
width: clampInteger(body?.width, defaults.width, 160, 1920),
|
||||||
quality: clampInteger(body?.quality, defaults.quality, 2, 18),
|
quality: clampInteger(body?.quality, defaults.quality, 2, 18),
|
||||||
audioBitrate: parseAudioBitrate(body?.audioBitrate),
|
audioBitrate: parseAudioBitrate(body?.audioBitrate, defaults.audioBitrate),
|
||||||
|
audioChannels: clampInteger(body?.audioChannels, defaults.audioChannels, 1, 2),
|
||||||
|
audioSampleRate: clampInteger(body?.audioSampleRate, defaults.audioSampleRate, 22050, 48000),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -394,16 +402,32 @@ function clampInteger(value, fallback, min, max) {
|
|||||||
return Math.min(max, Math.max(min, Math.round(parsed)));
|
return Math.min(max, Math.max(min, Math.round(parsed)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseBoolean(value, fallback) {
|
||||||
|
if (value === undefined || value === '') {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['1', 'true', 'yes', 'on'].includes(String(value).toLowerCase())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['0', 'false', 'no', 'off'].includes(String(value).toLowerCase())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
function clampNumber(value, min, max) {
|
function clampNumber(value, min, max) {
|
||||||
return Math.min(max, Math.max(min, value));
|
return Math.min(max, Math.max(min, value));
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseAudioBitrate(value) {
|
function parseAudioBitrate(value, fallback = '64k') {
|
||||||
if (typeof value !== 'string') {
|
if (typeof value !== 'string') {
|
||||||
return defaults.audioBitrate;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
return /^\d{2,3}k$/i.test(value) ? value.toLowerCase() : defaults.audioBitrate;
|
return /^\d{2,3}k$/i.test(value) ? value.toLowerCase() : fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatSessionPayload(session) {
|
function formatSessionPayload(session) {
|
||||||
@@ -1654,7 +1678,7 @@ function sendFramePacket(websocket, timestamp, jpeg, onError) {
|
|||||||
const packet = Buffer.allocUnsafe(packetBytes);
|
const packet = Buffer.allocUnsafe(packetBytes);
|
||||||
packet.writeDoubleLE(timestamp, 0);
|
packet.writeDoubleLE(timestamp, 0);
|
||||||
copyJpegFrame(jpeg, packet, 8);
|
copyJpegFrame(jpeg, packet, 8);
|
||||||
websocket.send(packet, { binary: true }, (error) => {
|
websocket.send(packet, { binary: true, compress: false }, (error) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
onError(error);
|
onError(error);
|
||||||
}
|
}
|
||||||
@@ -1728,9 +1752,9 @@ function buildPlaybackArgs(session, inputUrl) {
|
|||||||
'0:a:0?',
|
'0:a:0?',
|
||||||
'-vn',
|
'-vn',
|
||||||
'-ac',
|
'-ac',
|
||||||
'2',
|
String(session.options.audioChannels),
|
||||||
'-ar',
|
'-ar',
|
||||||
'48000',
|
String(session.options.audioSampleRate),
|
||||||
'-codec:a',
|
'-codec:a',
|
||||||
'libmp3lame',
|
'libmp3lame',
|
||||||
'-b:a',
|
'-b:a',
|
||||||
@@ -1751,9 +1775,9 @@ function buildAudioArgs(session, inputUrl, inputOptions) {
|
|||||||
'0:a:0?',
|
'0:a:0?',
|
||||||
'-vn',
|
'-vn',
|
||||||
'-ac',
|
'-ac',
|
||||||
'2',
|
String(session.options.audioChannels),
|
||||||
'-ar',
|
'-ar',
|
||||||
'48000',
|
String(session.options.audioSampleRate),
|
||||||
'-codec:a',
|
'-codec:a',
|
||||||
'libmp3lame',
|
'libmp3lame',
|
||||||
'-b:a',
|
'-b:a',
|
||||||
@@ -1790,17 +1814,40 @@ function buildInputArgs(inputUrl, { seekable = true, startTime = 0 } = {}) {
|
|||||||
args.push('-ss', formatFfmpegSeconds(startTime));
|
args.push('-ss', formatFfmpegSeconds(startTime));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (FFMPEG_HTTP_RECONNECT && isHttpInputUrl(inputUrl)) {
|
||||||
|
args.push(
|
||||||
|
'-reconnect',
|
||||||
|
'1',
|
||||||
|
'-reconnect_on_network_error',
|
||||||
|
'1',
|
||||||
|
'-reconnect_streamed',
|
||||||
|
'1',
|
||||||
|
'-reconnect_delay_max',
|
||||||
|
String(FFMPEG_HTTP_RECONNECT_DELAY_MAX),
|
||||||
|
'-reconnect_max_retries',
|
||||||
|
String(FFMPEG_HTTP_RECONNECT_MAX_RETRIES),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (FFMPEG_HTTP_RECONNECT_ON_HTTP_ERROR) {
|
||||||
|
args.push('-reconnect_on_http_error', FFMPEG_HTTP_RECONNECT_ON_HTTP_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
args.push('-re', '-i', inputUrl);
|
args.push('-re', '-i', inputUrl);
|
||||||
return args;
|
return args;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isHttpInputUrl(inputUrl) {
|
||||||
|
return inputUrl.startsWith('http://') || inputUrl.startsWith('https://');
|
||||||
|
}
|
||||||
|
|
||||||
function formatFfmpegSeconds(seconds) {
|
function formatFfmpegSeconds(seconds) {
|
||||||
return clampNumber(seconds, 0, Number.MAX_SAFE_INTEGER).toFixed(3);
|
return clampNumber(seconds, 0, Number.MAX_SAFE_INTEGER).toFixed(3);
|
||||||
}
|
}
|
||||||
|
|
||||||
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=bicubic:out_range=pc,format=yuvj420p`;
|
const videoFilter = `fps=${fps},scale=w='min(${width},iw)':h=-2:flags=fast_bilinear:out_range=pc,format=yuvj420p`;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'-an',
|
'-an',
|
||||||
|
|||||||
Reference in New Issue
Block a user