From 92634dba13cf83994bef569fd0354863732a56d8 Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Wed, 10 Jun 2026 11:23:32 +0200 Subject: [PATCH] perf: faster stream startup - Stop sending no-store on vendored player libraries: hls.min.js and mpegts.min.js (~750 KB) were re-downloaded on every player open - hls.js: start with 1 live segment instead of 3, lower buffer-first threshold 1.5s -> 0.8s, poll buffer every 100ms - mpegts.js: initial stash 512KB -> 128KB, start at 0.5s buffered - Live FFmpeg: -hls_init_time 1 closes the first segment after ~1s Co-Authored-By: Claude Fable 5 --- .claude/settings.json | 5 +++++ bin/api/streaming_handler.dart | 5 ++++- bin/server.dart | 14 +++++++++----- web/player.html | 6 +++--- web/player_lite.html | 14 +++++++------- 5 files changed, 28 insertions(+), 16 deletions(-) create mode 100644 .claude/settings.json diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..9030888 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,5 @@ +{ + "enabledPlugins": { + "frontend-design@claude-plugins-official": true + } +} diff --git a/bin/api/streaming_handler.dart b/bin/api/streaming_handler.dart index 055e2d9..6d8303c 100644 --- a/bin/api/streaming_handler.dart +++ b/bin/api/streaming_handler.dart @@ -220,8 +220,11 @@ Handler createLiveStreamHandler( ..._liveVideoArgs(quality, useNvidiaGpu), ..._audioArgs(withFilters: false), // HLS sliding window: 10 x 2s segments (lower live latency than the - // previous 20-segment window while still safe for iOS) + // previous 20-segment window while still safe for iOS). + // hls_init_time 1: first segment closes after ~1s so playback can + // start sooner; subsequent segments use hls_time. '-f', 'hls', + '-hls_init_time', '1', '-hls_time', '2', '-hls_list_size', '10', '-hls_flags', 'delete_segments+independent_segments', diff --git a/bin/server.dart b/bin/server.dart index 5f7dfbc..52cc2cc 100644 --- a/bin/server.dart +++ b/bin/server.dart @@ -245,12 +245,16 @@ void main(List args) async { FutureOr staticHandler(Request request) async { final response = await baseStaticHandler(request); - // Disable cache for entry points to ensure updates are seen immediately + // Disable cache for entry points to ensure updates are seen immediately. + // Vendored player libraries (hls.js/mpegts.js, ~750 KB) are pinned + // versions: keep them cacheable or every player open re-downloads them. final path = request.url.path; - if (path.isEmpty || - path == 'index.html' || - path.endsWith('.js') || - path.endsWith('.json')) { + final isVendored = path.startsWith('vendor/'); + if (!isVendored && + (path.isEmpty || + path == 'index.html' || + path.endsWith('.js') || + path.endsWith('.json'))) { return response.change( headers: { 'Cache-Control': 'no-store, no-cache, must-revalidate, max-age=0', diff --git a/web/player.html b/web/player.html index a86ae47..1869f3c 100644 --- a/web/player.html +++ b/web/player.html @@ -264,7 +264,7 @@ nudgeOffset: 0.8, // More aggressive jump over corrupted frames nudgeMaxRetries: 30, maxLiveSyncPlaybackRate: 1.1, // Slower catch-up to avoid chaos - initialLiveManifestSize: 3, // Wait for more segments before starting + initialLiveManifestSize: 1, // Start as soon as one segment exists abrEwmaDefaultEstimate: 1000000, manifestLoadingRetryDelay: 1000, levelLoadingRetryDelay: 1000 @@ -286,12 +286,12 @@ const checkBuffer = setInterval(() => { if (video.buffered.length > 0) { const bufferEnd = video.buffered.end(0); - if (bufferEnd > 1.5) { // 1.5s buffer-first + if (bufferEnd > 0.8) { // Start fast, hls.js keeps buffering clearInterval(checkBuffer); video.play().catch(() => playOverlay.style.display = 'flex'); } } - }, 200); + }, 100); }); hls.on(Hls.Events.LEVEL_LOADED, (e, d) => { if (d.details && d.details.totalduration > injectedDuration) window.currentHlsDuration = d.details.totalduration; diff --git a/web/player_lite.html b/web/player_lite.html index c4de264..c4012b7 100644 --- a/web/player_lite.html +++ b/web/player_lite.html @@ -258,7 +258,7 @@ nudgeOffset: 0.8, nudgeMaxRetries: 30, maxLiveSyncPlaybackRate: 1.1, - initialLiveManifestSize: 3, + initialLiveManifestSize: 1, abrEwmaDefaultEstimate: 1000000, manifestLoadingRetryDelay: 1000, levelLoadingRetryDelay: 1000 @@ -271,11 +271,11 @@ hls.on(Hls.Events.MANIFEST_PARSED, () => { hideLoading(); const checkBuffer = setInterval(() => { - if (video.buffered.length > 0 && video.buffered.end(0) > 1.5) { + if (video.buffered.length > 0 && video.buffered.end(0) > 0.8) { clearInterval(checkBuffer); video.play().catch(() => playOverlay.style.display = 'flex'); } - }, 200); + }, 100); }); hls.on(Hls.Events.ERROR, (event, data) => { @@ -302,7 +302,7 @@ url: url }, { enableWorker: true, - stashInitialSize: 512 * 1024, + stashInitialSize: 128 * 1024, enableStashBuffer: true, liveBufferLatencyChasing: false, maxStashSize: 30 * 1024 * 1024 @@ -310,13 +310,13 @@ player.attachMediaElement(video); player.load(); - + const checkBuffer = setInterval(() => { - if (video.buffered.length > 0 && video.buffered.end(0) > 1.0) { + if (video.buffered.length > 0 && video.buffered.end(0) > 0.5) { clearInterval(checkBuffer); player.play().catch(() => playOverlay.style.display = 'flex'); } - }, 200); + }, 100); player.on(mpegts.Events.ERROR, (type, detail, info) => { log('MpegTS Error: ' + type + ' - ' + detail);