From 884d2c39ae23b44d0d037aaff0f66ad9ae0807ba Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Thu, 26 Nov 2020 11:29:50 +0100 Subject: Fix live FPS limit --- server/helpers/ffmpeg-utils.ts | 43 ++++++++++++-------------------- server/helpers/ffprobe-utils.ts | 21 ++++++++++++++++ server/lib/video-transcoding-profiles.ts | 36 ++++++++++++++++---------- server/tests/api/live/live.ts | 9 +++++-- shared/extra-utils/videos/live.ts | 1 + 5 files changed, 68 insertions(+), 42 deletions(-) diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts index 9755dd67c..3cc062b8c 100644 --- a/server/helpers/ffmpeg-utils.ts +++ b/server/helpers/ffmpeg-utils.ts @@ -1,11 +1,11 @@ import * as ffmpeg from 'fluent-ffmpeg' import { readFile, remove, writeFile } from 'fs-extra' import { dirname, join } from 'path' -import { FFMPEG_NICE, VIDEO_LIVE, VIDEO_TRANSCODING_ENCODERS, VIDEO_TRANSCODING_FPS } from '@server/initializers/constants' +import { FFMPEG_NICE, VIDEO_LIVE, VIDEO_TRANSCODING_ENCODERS } from '@server/initializers/constants' import { VideoResolution } from '../../shared/models/videos' import { checkFFmpegEncoders } from '../initializers/checker-before-init' import { CONFIG } from '../initializers/config' -import { getAudioStream, getClosestFramerateStandard, getVideoFileFPS } from './ffprobe-utils' +import { computeFPS, getAudioStream, getVideoFileFPS } from './ffprobe-utils' import { processImage } from './image-utils' import { logger } from './logger' @@ -223,7 +223,17 @@ async function getLiveTranscodingCommand (options: { for (let i = 0; i < resolutions.length; i++) { const resolution = resolutions[i] - const baseEncoderBuilderParams = { input, availableEncoders, profile, fps, resolution, streamNum: i, videoType: 'live' as 'live' } + const resolutionFPS = computeFPS(fps, resolution) + + const baseEncoderBuilderParams = { + input, + availableEncoders, + profile, + fps: resolutionFPS, + resolution, + streamNum: i, + videoType: 'live' as 'live' + } { const builderResult = await getEncoderBuilderResult(Object.assign({}, baseEncoderBuilderParams, { streamType: 'VIDEO' })) @@ -233,7 +243,7 @@ async function getLiveTranscodingCommand (options: { command.outputOption(`-map [vout${resolution}]`) - addDefaultEncoderParams({ command, encoder: builderResult.encoder, fps, streamNum: i }) + addDefaultEncoderParams({ command, encoder: builderResult.encoder, fps: resolutionFPS, streamNum: i }) logger.debug('Apply ffmpeg live video params from %s.', builderResult.encoder, builderResult) @@ -249,7 +259,7 @@ async function getLiveTranscodingCommand (options: { command.outputOption('-map a:0') - addDefaultEncoderParams({ command, encoder: builderResult.encoder, fps, streamNum: i }) + addDefaultEncoderParams({ command, encoder: builderResult.encoder, fps: resolutionFPS, streamNum: i }) logger.debug('Apply ffmpeg live audio params from %s.', builderResult.encoder, builderResult) @@ -387,15 +397,7 @@ function addDefaultLiveHLSParams (command: ffmpeg.FfmpegCommand, outPath: string async function buildx264VODCommand (command: ffmpeg.FfmpegCommand, options: TranscodeOptions) { let fps = await getVideoFileFPS(options.inputPath) - if ( - // On small/medium resolutions, limit FPS - options.resolution !== undefined && - options.resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN && - fps > VIDEO_TRANSCODING_FPS.AVERAGE - ) { - // Get closest standard framerate by modulo: downsampling has to be done to a divisor of the nominal fps value - fps = getClosestFramerateStandard(fps, 'STANDARD') - } + fps = computeFPS(fps, options.resolution) command = await presetVideo(command, options.inputPath, options, fps) @@ -408,12 +410,6 @@ async function buildx264VODCommand (command: ffmpeg.FfmpegCommand, options: Tran command = command.size(size) } - // Hard FPS limits - if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = getClosestFramerateStandard(fps, 'HD_STANDARD') - else if (fps < VIDEO_TRANSCODING_FPS.MIN) fps = VIDEO_TRANSCODING_FPS.MIN - - command = command.withFPS(fps) - return command } @@ -422,13 +418,6 @@ async function buildAudioMergeCommand (command: ffmpeg.FfmpegCommand, options: M command = await presetVideo(command, options.audioPath, options) - /* - MAIN reference: https://slhck.info/video/2017/03/01/rate-control.html - Our target situation is closer to a livestream than a stream, - since we want to reduce as much a possible the encoding burden, - although not to the point of a livestream where there is a hard - constraint on the frames per second to be encoded. - */ command.outputOption('-preset:v veryfast') command = command.input(options.audioPath) diff --git a/server/helpers/ffprobe-utils.ts b/server/helpers/ffprobe-utils.ts index 16b295bbd..1cf397767 100644 --- a/server/helpers/ffprobe-utils.ts +++ b/server/helpers/ffprobe-utils.ts @@ -247,6 +247,26 @@ function getClosestFramerateStandard (fps: number, type: 'HD_STANDARD' | 'STANDA .sort((a, b) => fps % a - fps % b)[0] } +function computeFPS (fpsArg: number, resolution: VideoResolution) { + let fps = fpsArg + + if ( + // On small/medium resolutions, limit FPS + resolution !== undefined && + resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN && + fps > VIDEO_TRANSCODING_FPS.AVERAGE + ) { + // Get closest standard framerate by modulo: downsampling has to be done to a divisor of the nominal fps value + fps = getClosestFramerateStandard(fps, 'STANDARD') + } + + // Hard FPS limits + if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = getClosestFramerateStandard(fps, 'HD_STANDARD') + else if (fps < VIDEO_TRANSCODING_FPS.MIN) fps = VIDEO_TRANSCODING_FPS.MIN + + return fps +} + // --------------------------------------------------------------------------- export { @@ -259,6 +279,7 @@ export { getVideoStreamFromFile, getDurationFromVideoFile, getAudioStream, + computeFPS, getVideoFileFPS, ffprobePromise, getClosestFramerateStandard, diff --git a/server/lib/video-transcoding-profiles.ts b/server/lib/video-transcoding-profiles.ts index 03c26f236..3bf83d6a8 100644 --- a/server/lib/video-transcoding-profiles.ts +++ b/server/lib/video-transcoding-profiles.ts @@ -1,5 +1,5 @@ import { logger } from '@server/helpers/logger' -import { getTargetBitrate } from '../../shared/models/videos' +import { getTargetBitrate, VideoResolution } from '../../shared/models/videos' import { AvailableEncoders, buildStreamSuffix, EncoderOptionsBuilder } from '../helpers/ffmpeg-utils' import { canDoQuickAudioTranscode, @@ -23,21 +23,12 @@ import { VIDEO_TRANSCODING_FPS } from '../initializers/constants' // * https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate const defaultX264VODOptionsBuilder: EncoderOptionsBuilder = async ({ input, resolution, fps }) => { - let targetBitrate = getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS) - - const probe = await ffprobePromise(input) - - const videoStream = await getVideoStreamFromFile(input, probe) - if (!videoStream) { - return { outputOptions: [ ] } - } - - // Don't transcode to an higher bitrate than the original file - const fileBitrate = await getVideoFileBitrate(input, probe) - targetBitrate = Math.min(targetBitrate, fileBitrate) + const targetBitrate = await buildTargetBitrate({ input, resolution, fps }) + if (!targetBitrate) return { outputOptions: [ ] } return { outputOptions: [ + `-r ${fps}`, `-maxrate ${targetBitrate}`, `-bufsize ${targetBitrate * 2}` ] @@ -49,6 +40,7 @@ const defaultX264LiveOptionsBuilder: EncoderOptionsBuilder = async ({ resolution return { outputOptions: [ + `${buildStreamSuffix('-r:v', streamNum)} ${fps}`, `${buildStreamSuffix('-b:v', streamNum)} ${targetBitrate}`, `-maxrate ${targetBitrate}`, `-bufsize ${targetBitrate * 2}` @@ -115,3 +107,21 @@ export { } // --------------------------------------------------------------------------- +async function buildTargetBitrate (options: { + input: string + resolution: VideoResolution + fps: number + +}) { + const { input, resolution, fps } = options + const probe = await ffprobePromise(input) + + const videoStream = await getVideoStreamFromFile(input, probe) + if (!videoStream) return undefined + + const targetBitrate = getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS) + + // Don't transcode to an higher bitrate than the original file + const fileBitrate = await getVideoFileBitrate(input, probe) + return Math.min(targetBitrate, fileBitrate) +} diff --git a/server/tests/api/live/live.ts b/server/tests/api/live/live.ts index 0786db554..4f84882ff 100644 --- a/server/tests/api/live/live.ts +++ b/server/tests/api/live/live.ts @@ -416,7 +416,7 @@ describe('Test live', function () { await waitJobs(servers) const bitrateLimits = { - 720: 3000 * 1000, + 720: 4000 * 1000, // 60FPS 360: 1100 * 1000, 240: 600 * 1000 } @@ -436,9 +436,14 @@ describe('Test live', function () { const file = hlsPlaylist.files.find(f => f.resolution.id === resolution) expect(file).to.exist - expect(file.fps).to.be.approximately(30, 5) expect(file.size).to.be.greaterThan(1) + if (resolution >= 720) { + expect(file.fps).to.be.approximately(60, 2) + } else { + expect(file.fps).to.be.approximately(30, 2) + } + const filename = `${video.uuid}-${resolution}-fragmented.mp4` const segmentPath = buildServerDirectory(servers[0], join('streaming-playlists', 'hls', video.uuid, filename)) diff --git a/shared/extra-utils/videos/live.ts b/shared/extra-utils/videos/live.ts index c8acb90da..266baaed3 100644 --- a/shared/extra-utils/videos/live.ts +++ b/shared/extra-utils/videos/live.ts @@ -69,6 +69,7 @@ function sendRTMPStream (rtmpBaseUrl: string, streamKey: string, fixtureName = ' command.outputOption('-c:v libx264') command.outputOption('-g 50') command.outputOption('-keyint_min 2') + command.outputOption('-r 60') command.outputOption('-f flv') const rtmpUrl = rtmpBaseUrl + '/' + streamKey -- cgit v1.2.3