diff --git a/public/js/components/VideoPlayer.js b/public/js/components/VideoPlayer.js index 5e81fce..0cd2eef 100644 --- a/public/js/components/VideoPlayer.js +++ b/public/js/components/VideoPlayer.js @@ -920,7 +920,8 @@ class VideoPlayer { videoMode, videoCodec: info.video, audioCodec: info.audio, - audioChannels: info.audioChannels + audioChannels: info.audioChannels, + videoHeight: info.height }); this.currentUrl = playlistUrl; // Update currentUrl for HLS reload diff --git a/public/js/pages/WatchPage.js b/public/js/pages/WatchPage.js index 6e46bab..2a38c69 100644 --- a/public/js/pages/WatchPage.js +++ b/public/js/pages/WatchPage.js @@ -460,7 +460,8 @@ class WatchPage { seekOffset: this.resumeTime, // Ensure seekOffset is passed videoCodec: info.video, audioCodec: info.audio, - audioChannels: info.audioChannels + audioChannels: info.audioChannels, + videoHeight: info.height }); this.playHls(playlistUrl); this.setVolumeFromStorage(); diff --git a/server/routes/probe.js b/server/routes/probe.js index 751ef43..c344e86 100644 --- a/server/routes/probe.js +++ b/server/routes/probe.js @@ -33,6 +33,11 @@ function probeStream(url, ffprobePath, userAgent = null, timeout = 15000) { const args = [ '-v', 'error', '-user_agent', userAgent || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36', + '-http_persistent', '0', + '-reconnect', '1', + '-reconnect_streamed', '1', + '-reconnect_on_http_error', '4xx,5xx', + '-reconnect_delay_max', '10', '-print_format', 'json', '-show_streams', '-show_format', @@ -186,7 +191,7 @@ router.get('/', async (req, res) => { console.error('[Probe] Failed:', err.message); // On error, assume transcode needed to be safe - res.json({ + const fallback = { video: 'unknown', audio: 'unknown', container: 'unknown', @@ -194,7 +199,11 @@ router.get('/', async (req, res) => { needsRemux: false, needsTranscode: true, error: err.message - }); + }; + + probeCache.set(cacheKey, { result: fallback, timestamp: Date.now() - (CACHE_TTL - 30000) }); + + res.json(fallback); } }); diff --git a/server/routes/remux.js b/server/routes/remux.js index a30a61a..4f699a1 100644 --- a/server/routes/remux.js +++ b/server/routes/remux.js @@ -44,10 +44,12 @@ router.get('/', async (req, res) => { '-err_detect', 'ignore_err', // Limit max demux delay to prevent buffering issues with bad timestamps '-max_delay', '5000000', + '-http_persistent', '0', // Reconnect settings for network drops '-reconnect', '1', '-reconnect_streamed', '1', - '-reconnect_delay_max', '5', + '-reconnect_on_http_error', '4xx,5xx', + '-reconnect_delay_max', '10', // Prevent Range/HEAD requests that some providers reject with 405 '-seekable', '0', '-i', url, diff --git a/server/routes/transcode.js b/server/routes/transcode.js index 3f82ec9..051d679 100644 --- a/server/routes/transcode.js +++ b/server/routes/transcode.js @@ -29,7 +29,7 @@ transcodeSession.startCleanupInterval(); * Body: { url: string, seekOffset?: number } */ router.post('/session', async (req, res) => { - const { url, seekOffset, videoMode, videoCodec, audioCodec, audioChannels } = req.body; + const { url, seekOffset, videoMode, videoCodec, audioCodec, audioChannels, videoHeight } = req.body; if (!url) { return res.status(400).json({ error: 'URL is required' }); @@ -55,7 +55,8 @@ router.post('/session', async (req, res) => { videoMode: videoMode, // 'copy' or 'encode' videoCodec: videoCodec, // 'h264', 'hevc', etc. audioCodec: audioCodec, // 'aac', 'ac3', etc. - audioChannels: audioChannels // number of channels (2=stereo) + audioChannels: audioChannels, // number of channels (2=stereo) + videoHeight: videoHeight // source height from probe; used to cap max-resolution in JS }); await session.start(); @@ -191,10 +192,12 @@ router.get('/', async (req, res) => { '-err_detect', 'ignore_err', // Limit max demux delay to prevent buffering issues '-max_delay', '2000000', + '-http_persistent', '0', // Reconnect settings for network drops (useful for live streams) '-reconnect', '1', '-reconnect_streamed', '1', - '-reconnect_delay_max', '3', + '-reconnect_on_http_error', '4xx,5xx', + '-reconnect_delay_max', '10', // Prevent Range/HEAD requests that some providers reject with 405 '-seekable', '0', '-i', url, diff --git a/server/services/transcodeSession.js b/server/services/transcodeSession.js index 4d6c2f7..f48a816 100644 --- a/server/services/transcodeSession.js +++ b/server/services/transcodeSession.js @@ -198,9 +198,11 @@ class TranscodeSession extends EventEmitter { '-analyzeduration', '5000000', '-fflags', '+genpts+discardcorrupt', '-err_detect', 'ignore_err', + '-http_persistent', '0', '-reconnect', '1', '-reconnect_streamed', '1', - '-reconnect_delay_max', '3' + '-reconnect_on_http_error', '4xx,5xx', + '-reconnect_delay_max', '10' ); args.push('-i', this.url); @@ -390,34 +392,43 @@ class TranscodeSession extends EventEmitter { buildScaleFilter(encoder, height) { const useUpscale = this.options.upscaleEnabled; const upscaleMethod = this.options.upscaleMethod || 'hardware'; + const sourceHeight = this.options.videoHeight | 0; - // Log upscaling status + const effectiveHeight = useUpscale + ? height + : (sourceHeight > 0 ? Math.min(height, sourceHeight) : height); + + // Log upscaling / passthrough decision if (useUpscale) { - console.log(`[TranscodeSession ${this.id}] Upscaling: ${upscaleMethod} method to ${height}p`); + console.log(`[TranscodeSession ${this.id}] Upscaling: ${upscaleMethod} method to ${effectiveHeight}p`); + } else if (sourceHeight > 0 && sourceHeight <= height) { + console.log(`[TranscodeSession ${this.id}] Source ${sourceHeight}p is within cap ${height}p, passing through`); } + const h = effectiveHeight; + // Hardware scaling filters (for both upscale and downscale) if (upscaleMethod === 'hardware' || !useUpscale) { switch (encoder) { case 'nvenc': // NVIDIA CUDA scaling with Lanczos // Force nv12 (8-bit) output to handle 10-bit inputs (fixes "10 bit encode not supported") - return `scale_cuda=-2:${height}:interp_algo=lanczos:format=nv12`; + return `scale_cuda=-2:${h}:interp_algo=lanczos:format=nv12`; case 'vaapi': - return `scale_vaapi=w=-2:h=${height}:format=nv12`; + return `scale_vaapi=w=-2:h=${h}:format=nv12`; case 'qsv': - return `scale_qsv=w=-2:h=${height}:format=nv12`; + return `scale_qsv=w=-2:h=${h}:format=nv12`; case 'amf': // AMF uses CPU decode, so use software scale - return useUpscale ? `scale=-2:${height}:flags=lanczos` : `scale=-2:${height}`; + return useUpscale ? `scale=-2:${h}:flags=lanczos` : `scale=-2:${h}`; case 'software': default: - return useUpscale ? `scale=-2:${height}:flags=lanczos` : `scale=-2:${height}`; + return useUpscale ? `scale=-2:${h}:flags=lanczos` : `scale=-2:${h}`; } } // Software Lanczos scaling (high quality, slower) - return `scale=-2:${height}:flags=lanczos`; + return `scale=-2:${h}:flags=lanczos`; } /**