Optimize stream defaults for weak connections

This commit is contained in:
2026-06-05 21:26:31 -07:00
parent 5bf59b2436
commit 2c2b0fdf78
5 changed files with 106 additions and 36 deletions

View File

@@ -14,13 +14,17 @@ const publicDir = path.join(__dirname, '..', 'public');
const app = express();
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 FFMPEG_PATH = process.env.FFMPEG_PATH ?? 'ffmpeg';
const FFPROBE_PATH = process.env.FFPROBE_PATH ?? 'ffprobe';
const FFMPEG_LOG_LEVEL = process.env.FFMPEG_LOG_LEVEL ?? 'warning';
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 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);
@@ -29,7 +33,7 @@ const FAVORITES_LIMIT = clampInteger(process.env.FAVORITES_LIMIT, 50, 1, 200);
const SESSION_TTL_MS = 60 * 60 * 1000;
const METADATA_PROBE_TIMEOUT_MS = 8 * 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_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);
@@ -37,10 +41,12 @@ const JPEG_SOI = Buffer.from([0xff, 0xd8]);
const JPEG_EOI = Buffer.from([0xff, 0xd9]);
const defaults = {
fps: 24,
width: 960,
quality: clampInteger(process.env.JPEG_QUALITY, 7, 2, 18),
audioBitrate: '160k',
fps: clampInteger(process.env.DEFAULT_FPS, 8, 1, 30),
width: clampInteger(process.env.DEFAULT_FRAME_WIDTH, 480, 160, 1920),
quality: clampInteger(process.env.JPEG_QUALITY, 12, 2, 18),
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();
@@ -363,7 +369,9 @@ function parsePlaybackOptions(body) {
fps: clampInteger(body?.fps, defaults.fps, 1, 30),
width: clampInteger(body?.width, defaults.width, 160, 1920),
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)));
}
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) {
return Math.min(max, Math.max(min, value));
}
function parseAudioBitrate(value) {
function parseAudioBitrate(value, fallback = '64k') {
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) {
@@ -1654,7 +1678,7 @@ function sendFramePacket(websocket, timestamp, jpeg, onError) {
const packet = Buffer.allocUnsafe(packetBytes);
packet.writeDoubleLE(timestamp, 0);
copyJpegFrame(jpeg, packet, 8);
websocket.send(packet, { binary: true }, (error) => {
websocket.send(packet, { binary: true, compress: false }, (error) => {
if (error) {
onError(error);
}
@@ -1728,9 +1752,9 @@ function buildPlaybackArgs(session, inputUrl) {
'0:a:0?',
'-vn',
'-ac',
'2',
String(session.options.audioChannels),
'-ar',
'48000',
String(session.options.audioSampleRate),
'-codec:a',
'libmp3lame',
'-b:a',
@@ -1751,9 +1775,9 @@ function buildAudioArgs(session, inputUrl, inputOptions) {
'0:a:0?',
'-vn',
'-ac',
'2',
String(session.options.audioChannels),
'-ar',
'48000',
String(session.options.audioSampleRate),
'-codec:a',
'libmp3lame',
'-b:a',
@@ -1790,17 +1814,40 @@ function buildInputArgs(inputUrl, { seekable = true, startTime = 0 } = {}) {
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);
return args;
}
function isHttpInputUrl(inputUrl) {
return inputUrl.startsWith('http://') || inputUrl.startsWith('https://');
}
function formatFfmpegSeconds(seconds) {
return clampNumber(seconds, 0, Number.MAX_SAFE_INTEGER).toFixed(3);
}
function buildFrameOutputArgs(session, outputUrl) {
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 [
'-an',