Fix live FPS limit
This commit is contained in:
parent
0151c41c65
commit
884d2c39ae
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue