Fix live FPS limit

This commit is contained in:
Chocobozzz 2020-11-26 11:29:50 +01:00
parent 0151c41c65
commit 884d2c39ae
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
5 changed files with 68 additions and 42 deletions

View File

@ -1,11 +1,11 @@
import * as ffmpeg from 'fluent-ffmpeg' import * as ffmpeg from 'fluent-ffmpeg'
import { readFile, remove, writeFile } from 'fs-extra' import { readFile, remove, writeFile } from 'fs-extra'
import { dirname, join } from 'path' 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 { VideoResolution } from '../../shared/models/videos'
import { checkFFmpegEncoders } from '../initializers/checker-before-init' import { checkFFmpegEncoders } from '../initializers/checker-before-init'
import { CONFIG } from '../initializers/config' 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 { processImage } from './image-utils'
import { logger } from './logger' import { logger } from './logger'
@ -223,7 +223,17 @@ async function getLiveTranscodingCommand (options: {
for (let i = 0; i < resolutions.length; i++) { for (let i = 0; i < resolutions.length; i++) {
const resolution = resolutions[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' })) const builderResult = await getEncoderBuilderResult(Object.assign({}, baseEncoderBuilderParams, { streamType: 'VIDEO' }))
@ -233,7 +243,7 @@ async function getLiveTranscodingCommand (options: {
command.outputOption(`-map [vout${resolution}]`) 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) 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') 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) 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) { async function buildx264VODCommand (command: ffmpeg.FfmpegCommand, options: TranscodeOptions) {
let fps = await getVideoFileFPS(options.inputPath) let fps = await getVideoFileFPS(options.inputPath)
if ( fps = computeFPS(fps, options.resolution)
// 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')
}
command = await presetVideo(command, options.inputPath, options, fps) command = await presetVideo(command, options.inputPath, options, fps)
@ -408,12 +410,6 @@ async function buildx264VODCommand (command: ffmpeg.FfmpegCommand, options: Tran
command = command.size(size) 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 return command
} }
@ -422,13 +418,6 @@ async function buildAudioMergeCommand (command: ffmpeg.FfmpegCommand, options: M
command = await presetVideo(command, options.audioPath, options) 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.outputOption('-preset:v veryfast')
command = command.input(options.audioPath) command = command.input(options.audioPath)

View File

@ -247,6 +247,26 @@ function getClosestFramerateStandard (fps: number, type: 'HD_STANDARD' | 'STANDA
.sort((a, b) => fps % a - fps % b)[0] .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 { export {
@ -259,6 +279,7 @@ export {
getVideoStreamFromFile, getVideoStreamFromFile,
getDurationFromVideoFile, getDurationFromVideoFile,
getAudioStream, getAudioStream,
computeFPS,
getVideoFileFPS, getVideoFileFPS,
ffprobePromise, ffprobePromise,
getClosestFramerateStandard, getClosestFramerateStandard,

View File

@ -1,5 +1,5 @@
import { logger } from '@server/helpers/logger' 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 { AvailableEncoders, buildStreamSuffix, EncoderOptionsBuilder } from '../helpers/ffmpeg-utils'
import { import {
canDoQuickAudioTranscode, canDoQuickAudioTranscode,
@ -23,21 +23,12 @@ import { VIDEO_TRANSCODING_FPS } from '../initializers/constants'
// * https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate // * https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate
const defaultX264VODOptionsBuilder: EncoderOptionsBuilder = async ({ input, resolution, fps }) => { const defaultX264VODOptionsBuilder: EncoderOptionsBuilder = async ({ input, resolution, fps }) => {
let targetBitrate = getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS) const targetBitrate = await buildTargetBitrate({ input, resolution, fps })
if (!targetBitrate) return { outputOptions: [ ] }
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)
return { return {
outputOptions: [ outputOptions: [
`-r ${fps}`,
`-maxrate ${targetBitrate}`, `-maxrate ${targetBitrate}`,
`-bufsize ${targetBitrate * 2}` `-bufsize ${targetBitrate * 2}`
] ]
@ -49,6 +40,7 @@ const defaultX264LiveOptionsBuilder: EncoderOptionsBuilder = async ({ resolution
return { return {
outputOptions: [ outputOptions: [
`${buildStreamSuffix('-r:v', streamNum)} ${fps}`,
`${buildStreamSuffix('-b:v', streamNum)} ${targetBitrate}`, `${buildStreamSuffix('-b:v', streamNum)} ${targetBitrate}`,
`-maxrate ${targetBitrate}`, `-maxrate ${targetBitrate}`,
`-bufsize ${targetBitrate * 2}` `-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)
}

View File

@ -416,7 +416,7 @@ describe('Test live', function () {
await waitJobs(servers) await waitJobs(servers)
const bitrateLimits = { const bitrateLimits = {
720: 3000 * 1000, 720: 4000 * 1000, // 60FPS
360: 1100 * 1000, 360: 1100 * 1000,
240: 600 * 1000 240: 600 * 1000
} }
@ -436,9 +436,14 @@ describe('Test live', function () {
const file = hlsPlaylist.files.find(f => f.resolution.id === resolution) const file = hlsPlaylist.files.find(f => f.resolution.id === resolution)
expect(file).to.exist expect(file).to.exist
expect(file.fps).to.be.approximately(30, 5)
expect(file.size).to.be.greaterThan(1) 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 filename = `${video.uuid}-${resolution}-fragmented.mp4`
const segmentPath = buildServerDirectory(servers[0], join('streaming-playlists', 'hls', video.uuid, filename)) const segmentPath = buildServerDirectory(servers[0], join('streaming-playlists', 'hls', video.uuid, filename))

View File

@ -69,6 +69,7 @@ function sendRTMPStream (rtmpBaseUrl: string, streamKey: string, fixtureName = '
command.outputOption('-c:v libx264') command.outputOption('-c:v libx264')
command.outputOption('-g 50') command.outputOption('-g 50')
command.outputOption('-keyint_min 2') command.outputOption('-keyint_min 2')
command.outputOption('-r 60')
command.outputOption('-f flv') command.outputOption('-f flv')
const rtmpUrl = rtmpBaseUrl + '/' + streamKey const rtmpUrl = rtmpBaseUrl + '/' + streamKey