import { logger } from '@server/helpers/logger' ; import { getAverageBitrate , getMinLimitBitrate } from '@shared/core-utils' ; import { AvailableEncoders , EncoderOptionsBuilder , EncoderOptionsBuilderParams , VideoResolution } from '../../../shared/models/videos' ; import { buildStreamSuffix , resetSupportedEncoders } from '../../helpers/ffmpeg-utils' ; import { canDoQuickAudioTranscode , ffprobePromise , getAudioStream , getMaxAudioBitrate } from '../../helpers/ffprobe-utils' ; /**
 *
 * Available encoders and profiles for the transcoding jobs
 * These functions are used by ffmpeg-utils that will get the encoders and options depending on the chosen profile
 *
 * Resources:
 *  * https://slhck.info/video/2017/03/01/rate-control.html
 *  * https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate
 */ const defaultX264VODOptionsBuilder = ( options ) => { const { fps , inputRatio , inputBitrate , resolution } = options ; if ( ! fps ) return { outputOptions : [ ] } ; const targetBitrate = getTargetBitrate ( { inputBitrate , ratio : inputRatio , fps , resolution } ) ; return { outputOptions : [ ... getCommonOutputOptions ( targetBitrate ) , `-r ${ fps }` ] } ; } ; const defaultX264LiveOptionsBuilder = ( options ) => { const { streamNum , fps , inputBitrate , inputRatio , resolution } = options ; const targetBitrate = getTargetBitrate ( { inputBitrate , ratio : inputRatio , fps , resolution } ) ; return { outputOptions : [ ... getCommonOutputOptions ( targetBitrate ) , `${ buildStreamSuffix ( '-r:v' , streamNum ) } ${ fps }` , `${ buildStreamSuffix ( '-b:v' , streamNum ) } ${ targetBitrate }` ] } ; } ; const defaultAACOptionsBuilder = async ( { input , streamNum } ) => { const probe = await ffprobePromise ( input ) ; if ( await canDoQuickAudioTranscode ( input , probe ) ) { logger . debug ( 'Copy audio stream %s by AAC encoder.' , input ) ; return { copy : true , outputOptions : [ ] } ; } const parsedAudio = await getAudioStream ( input , probe ) ; const audioCodecName = parsedAudio . audioStream [ 'codec_name' ] ; const bitrate = getMaxAudioBitrate ( audioCodecName , parsedAudio . bitrate ) ; logger . debug ( 'Calculating audio bitrate of %s by AAC encoder.' , input , { bitrate : parsedAudio . bitrate , audioCodecName } ) ; if ( bitrate !== - 1 ) { return { outputOptions : [ buildStreamSuffix ( '-b:a' , streamNum ) , bitrate + 'k' ] } ; } return { outputOptions : [ ] } ; } ; const defaultLibFDKAACVODOptionsBuilder = ( { streamNum } ) => { return { outputOptions : [ buildStreamSuffix ( '-q:a' , streamNum ) , '5' ] } ; } ; class VideoTranscodingProfilesManager { private static instance : VideoTranscodingProfilesManager ; private readonly encodersPriorities = { vod : this . buildDefaultEncodersPriorities ( ) , live : this . buildDefaultEncodersPriorities ( ) } ; private readonly availableEncoders = { vod : { libx264 : { default : defaultX264VODOptionsBuilder } , aac : { default : defaultAACOptionsBuilder } , libfdk_aac : { default : defaultLibFDKAACVODOptionsBuilder } } , live : { libx264 : { default : defaultX264LiveOptionsBuilder } , aac : { default : defaultAACOptionsBuilder } } } ; private availableProfiles = { vod : [ ] as string [ ] , live : [ ] as string [ ] } ; private constructor ( ) { this . buildAvailableProfiles ( ) ; } getAvailableEncoders ( ) { return { available : this . availableEncoders , encodersToTry : { vod : { video : this . getEncodersByPriority ( 'vod' , 'video' ) , audio : this . getEncodersByPriority ( 'vod' , 'audio' ) } , live : { video : this . getEncodersByPriority ( 'live' , 'video' ) , audio : this . getEncodersByPriority ( 'live' , 'audio' ) } } } ; } getAvailableProfiles ( type ) { return this . availableProfiles [ type ] ; } addProfile ( options ) { const { type , encoder , profile , builder } = options ; const encoders = this . availableEncoders [ type ] ; if ( ! encoders [ encoder ] ) encoders [ encoder ] = { } ; encoders [ encoder ] [ profile ] = builder ; this . buildAvailableProfiles ( ) ; } removeProfile ( options ) { const { type , encoder , profile } = options ; delete this . availableEncoders [ type ] [ encoder ] [ profile ] ; this . buildAvailableProfiles ( ) ; } addEncoderPriority ( type , streamType , encoder , priority ) { this . encodersPriorities [ type ] [ streamType ] . push ( { name : encoder , priority } ) ; resetSupportedEncoders ( ) ; } removeEncoderPriority ( type , streamType , encoder , priority ) { this . encodersPriorities [ type ] [ streamType ] = this . encodersPriorities [ type ] [ streamType ] . filter ( o => o . name !== encoder && o . priority !== priority ) ; resetSupportedEncoders ( ) ; } private getEncodersByPriority ( type , streamType ) { return this . encodersPriorities [ type ] [ streamType ] . sort ( ( e1 , e2 ) => { if ( e1 . priority > e2 . priority ) return - 1 ; else if ( e1 . priority === e2 . priority ) return 0 ; return 1 ; } ) . map ( e => e . name ) ; } private buildAvailableProfiles ( ) { for ( const type of [ 'vod' , 'live' ] ) { const result = new Set ( ) ; const encoders = this . availableEncoders [ type ] ; for ( const encoderName of Object . keys ( encoders ) ) { for ( const profile of Object . keys ( encoders [ encoderName ] ) ) { result . add ( profile ) ; } } this . availableProfiles [ type ] = Array . from ( result ) ; } logger . debug ( 'Available transcoding profiles built.' , { availableProfiles : this . availableProfiles } ) ; } private buildDefaultEncodersPriorities ( ) { return { video : [ { name : 'libx264' , priority : 100 } ] , audio : [ { name : 'libfdk_aac' , priority : 200 } , { name : 'aac' , priority : 100 } ] } ; } static get Instance ( ) { return this . instance || ( this . instance = new this ( ) ) ; } } export { VideoTranscodingProfilesManager } ; function getTargetBitrate ( options ) { const { inputBitrate , resolution , ratio , fps } = options ; const capped = capBitrate ( inputBitrate , getAverageBitrate ( { resolution , fps , ratio } ) ) ; const limit = getMinLimitBitrate ( { resolution , fps , ratio } ) ; return Math . max ( limit , capped ) ; } function capBitrate ( inputBitrate , targetBitrate ) { if ( ! inputBitrate ) return targetBitrate ; const inputBitrateWithMargin = inputBitrate + ( inputBitrate * 0.3 ) ; return Math . min ( targetBitrate , inputBitrateWithMargin ) ; } function getCommonOutputOptions ( targetBitrate ) { return [ `-preset veryfast` , `-maxrate ${ targetBitrate }` , `-bufsize ${ targetBitrate * 2 }` , `-b_strategy 1` , `-bf 16` ] ; }