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