diff --git a/server.js b/server.js index 7794805..74eb636 100644 --- a/server.js +++ b/server.js @@ -106,6 +106,49 @@ const createFfmpegOptions = (encoderName) => { return options; }; +const parseFpsValue = (fpsText) => { + if (typeof fpsText !== 'string' || !fpsText.trim()) { + return 0; + } + + if (fpsText.includes('/')) { + const [numeratorText, denominatorText] = fpsText.split('/'); + const numerator = parseFloat(numeratorText); + const denominator = parseFloat(denominatorText); + if (Number.isFinite(numerator) && Number.isFinite(denominator) && denominator !== 0) { + return numerator / denominator; + } + return 0; + } + + const numericValue = parseFloat(fpsText); + return Number.isFinite(numericValue) ? numericValue : 0; +}; + +const getSeekFriendlyOutputOptions = (encoderName, metadata) => { + const videoStream = (metadata?.streams || []).find((stream) => stream.codec_type === 'video') || {}; + const parsedFps = parseFpsValue(videoStream.avg_frame_rate) || parseFpsValue(videoStream.r_frame_rate) || 24; + const normalizedFps = Math.min(Math.max(Math.round(parsedFps), 12), 60); + const gopSize = Math.max(24, normalizedFps * 2); + const options = [ + '-movflags', 'frag_keyframe+empty_moov+default_base_moof+faststart', + '-frag_duration', '1000000', + '-min_frag_duration', '1000000', + '-g', gopSize.toString(), + '-keyint_min', gopSize.toString() + ]; + + if (encoderName === 'libx264' || encoderName === 'libx265') { + options.push('-sc_threshold', '0', '-force_key_frames', 'expr:gte(t,n_forced*2)'); + } else if (/_nvenc$/.test(encoderName)) { + options.push('-forced-idr', '1', '-force_key_frames', 'expr:gte(t,n_forced*2)'); + } else if (/_qsv$/.test(encoderName)) { + options.push('-idr_interval', '1'); + } + + return options; +}; + const shouldRetryWithSoftware = (message) => { if (!message) return false; return /Cannot load libcuda\.so\.1|Could not open encoder before EOF|Error while opening encoder|Operation not permitted|Invalid argument/i.test(message); @@ -430,8 +473,10 @@ app.get('/api/stream', async (req, res) => { } // Probe file for duration and broadcast to clients + let sourceMetadata = null; try { const metadata = await probeFile(tmpInputPath); + sourceMetadata = metadata; const duration = metadata.format?.duration || 0; progressMap[progressKey] = { ...(progressMap[progressKey] || {}), @@ -449,7 +494,7 @@ app.get('/api/stream', async (req, res) => { let ffmpegCommand = null; const startStream = (encoderName) => { - const streamingOptions = createFfmpegOptions(encoderName).concat(['-movflags', 'frag_keyframe+empty_moov+faststart']); + const streamingOptions = createFfmpegOptions(encoderName).concat(getSeekFriendlyOutputOptions(encoderName, sourceMetadata)); ffmpegCommand = ffmpeg(tmpInputPath) .videoCodec(encoderName) .audioCodec('aac')