2024-03-22 16:36:20 -04:00
import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto' ;
2024-09-27 10:28:42 -04:00
import { CQMode , ToneMapping , TranscodeHWAccel , TranscodeTarget , VideoCodec } from 'src/enum' ;
2024-03-20 21:42:58 +01:00
import {
AudioStreamInfo ,
BitrateDistribution ,
2024-05-27 15:20:07 -04:00
TranscodeCommand ,
2024-03-20 21:42:58 +01:00
VideoCodecHWConfig ,
VideoCodecSWConfig ,
2024-11-14 02:07:04 -05:00
VideoFormat ,
2024-12-10 12:11:19 -05:00
VideoInterfaces ,
2024-03-20 21:42:58 +01:00
VideoStreamInfo ,
2025-01-22 17:11:07 -05:00
} from 'src/types' ;
2024-03-20 19:32:04 +01:00
2024-05-27 15:20:07 -04:00
export class BaseConfig implements VideoCodecSWConfig {
readonly presets = [ 'veryslow' , 'slower' , 'slow' , 'medium' , 'fast' , 'faster' , 'veryfast' , 'superfast' , 'ultrafast' ] ;
protected constructor ( protected config : SystemConfigFFmpegDto ) { }
2023-07-08 22:43:11 -04:00
2024-12-10 12:11:19 -05:00
static create ( config : SystemConfigFFmpegDto , interfaces : VideoInterfaces ) : VideoCodecSWConfig {
2024-05-27 15:20:07 -04:00
if ( config . accel === TranscodeHWAccel . DISABLED ) {
return this . getSWCodecConfig ( config ) ;
}
2024-12-10 12:11:19 -05:00
return this . getHWCodecConfig ( config , interfaces ) ;
2024-05-27 15:20:07 -04:00
}
private static getSWCodecConfig ( config : SystemConfigFFmpegDto ) {
switch ( config . targetVideoCodec ) {
case VideoCodec . H264 : {
return new H264Config ( config ) ;
}
case VideoCodec . HEVC : {
return new HEVCConfig ( config ) ;
}
case VideoCodec . VP9 : {
return new VP9Config ( config ) ;
}
case VideoCodec . AV1 : {
return new AV1Config ( config ) ;
}
default : {
throw new Error ( ` Codec ' ${ config . targetVideoCodec } ' is unsupported ` ) ;
}
}
}
2024-12-10 12:11:19 -05:00
private static getHWCodecConfig ( config : SystemConfigFFmpegDto , interfaces : VideoInterfaces ) {
2024-05-27 15:20:07 -04:00
let handler : VideoCodecHWConfig ;
switch ( config . accel ) {
case TranscodeHWAccel . NVENC : {
2024-12-10 12:11:19 -05:00
handler = config . accelDecode
? new NvencHwDecodeConfig ( config , interfaces )
: new NvencSwDecodeConfig ( config , interfaces ) ;
2024-05-27 15:20:07 -04:00
break ;
}
case TranscodeHWAccel . QSV : {
2024-12-10 12:11:19 -05:00
handler = config . accelDecode
? new QsvHwDecodeConfig ( config , interfaces )
: new QsvSwDecodeConfig ( config , interfaces ) ;
2024-05-27 15:20:07 -04:00
break ;
}
case TranscodeHWAccel . VAAPI : {
2024-10-17 18:02:41 -04:00
handler = config . accelDecode
2024-12-10 12:11:19 -05:00
? new VaapiHwDecodeConfig ( config , interfaces )
: new VaapiSwDecodeConfig ( config , interfaces ) ;
2024-05-27 15:20:07 -04:00
break ;
}
case TranscodeHWAccel . RKMPP : {
2024-11-22 16:08:49 +08:00
handler = config . accelDecode
2024-12-10 12:11:19 -05:00
? new RkmppHwDecodeConfig ( config , interfaces )
: new RkmppSwDecodeConfig ( config , interfaces ) ;
2024-05-27 15:20:07 -04:00
break ;
}
default : {
throw new Error ( ` ${ config . accel . toUpperCase ( ) } acceleration is unsupported ` ) ;
}
}
if ( ! handler . getSupportedCodecs ( ) . includes ( config . targetVideoCodec ) ) {
throw new Error (
` ${ config . accel . toUpperCase ( ) } acceleration does not support codec ' ${ config . targetVideoCodec . toUpperCase ( ) } '. Supported codecs: ${ handler . getSupportedCodecs ( ) } ` ,
) ;
}
return handler ;
}
2024-11-14 02:07:04 -05:00
getCommand (
target : TranscodeTarget ,
videoStream : VideoStreamInfo ,
audioStream? : AudioStreamInfo ,
format? : VideoFormat ,
) {
2023-07-08 22:43:11 -04:00
const options = {
2024-11-14 02:07:04 -05:00
inputOptions : this.getBaseInputOptions ( videoStream , format ) ,
2024-02-14 11:24:39 -05:00
outputOptions : [ . . . this . getBaseOutputOptions ( target , videoStream , audioStream ) , '-v verbose' ] ,
2023-07-08 22:43:11 -04:00
twoPass : this.eligibleForTwoPass ( ) ,
2024-09-27 18:10:39 -04:00
progress : { frameCount : videoStream.frameCount , percentInterval : 5 } ,
2024-05-27 15:20:07 -04:00
} as TranscodeCommand ;
2024-02-14 11:24:39 -05:00
if ( [ TranscodeTarget . ALL , TranscodeTarget . VIDEO ] . includes ( target ) ) {
const filters = this . getFilterOptions ( videoStream ) ;
if ( filters . length > 0 ) {
options . outputOptions . push ( ` -vf ${ filters . join ( ',' ) } ` ) ;
}
2023-07-08 22:43:11 -04:00
}
2024-02-14 11:24:39 -05:00
2024-05-16 13:30:26 -04:00
options . outputOptions . push (
. . . this . getPresetOptions ( ) ,
. . . this . getOutputThreadOptions ( ) ,
. . . this . getBitrateOptions ( ) ,
) ;
2023-07-08 22:43:11 -04:00
return options ;
}
2024-02-28 02:32:07 +01:00
// eslint-disable-next-line @typescript-eslint/no-unused-vars
2024-11-14 02:07:04 -05:00
getBaseInputOptions ( videoStream : VideoStreamInfo , format? : VideoFormat ) : string [ ] {
2024-05-16 13:30:26 -04:00
return this . getInputThreadOptions ( ) ;
2023-07-08 22:43:11 -04:00
}
2024-02-14 11:24:39 -05:00
getBaseOutputOptions ( target : TranscodeTarget , videoStream : VideoStreamInfo , audioStream? : AudioStreamInfo ) {
2024-04-07 12:44:09 -04:00
const videoCodec = [ TranscodeTarget . ALL , TranscodeTarget . VIDEO ] . includes ( target ) ? this . getVideoCodec ( ) : 'copy' ;
const audioCodec = [ TranscodeTarget . ALL , TranscodeTarget . AUDIO ] . includes ( target ) ? this . getAudioCodec ( ) : 'copy' ;
2023-09-02 21:22:42 -04:00
const options = [
2024-04-07 12:44:09 -04:00
` -c:v ${ videoCodec } ` ,
` -c:a ${ audioCodec } ` ,
2023-08-07 16:35:25 -04:00
// Makes a second pass moving the moov atom to the
// beginning of the file for improved playback speed.
'-movflags faststart' ,
'-fps_mode passthrough' ,
2023-09-02 21:22:42 -04:00
// explicitly selects the video stream instead of leaving it up to FFmpeg
` -map 0: ${ videoStream . index } ` ,
2023-08-07 16:35:25 -04:00
] ;
2024-01-28 21:17:20 -05:00
2023-09-02 21:22:42 -04:00
if ( audioStream ) {
options . push ( ` -map 0: ${ audioStream . index } ` ) ;
}
if ( this . getBFrames ( ) > - 1 ) {
options . push ( ` -bf ${ this . getBFrames ( ) } ` ) ;
}
if ( this . getRefs ( ) > 0 ) {
options . push ( ` -refs ${ this . getRefs ( ) } ` ) ;
}
if ( this . getGopSize ( ) > 0 ) {
options . push ( ` -g ${ this . getGopSize ( ) } ` ) ;
}
2024-01-28 21:17:20 -05:00
2024-04-07 12:44:09 -04:00
if (
this . config . targetVideoCodec === VideoCodec . HEVC &&
( videoCodec !== 'copy' || videoStream . codecName === 'hevc' )
) {
2024-01-28 21:17:20 -05:00
options . push ( '-tag:v hvc1' ) ;
}
2023-09-02 21:22:42 -04:00
return options ;
2023-07-08 22:43:11 -04:00
}
2023-08-29 05:01:42 -04:00
getFilterOptions ( videoStream : VideoStreamInfo ) {
2023-07-08 22:43:11 -04:00
const options = [ ] ;
2023-08-29 05:01:42 -04:00
if ( this . shouldScale ( videoStream ) ) {
options . push ( ` scale= ${ this . getScaling ( videoStream ) } ` ) ;
2023-07-08 22:43:11 -04:00
}
2025-03-18 12:42:09 -04:00
const tonemapOptions = this . getToneMapping ( videoStream ) ;
if ( tonemapOptions . length > 0 ) {
options . push ( . . . tonemapOptions ) ;
} else if ( ! videoStream . pixelFormat . endsWith ( '420p' ) ) {
options . push ( 'format=yuv420p' ) ;
2024-10-31 20:48:23 -04:00
}
2023-07-08 22:43:11 -04:00
return options ;
}
getPresetOptions() {
return [ ` -preset ${ this . config . preset } ` ] ;
}
getBitrateOptions() {
const bitrates = this . getBitrateDistribution ( ) ;
if ( this . eligibleForTwoPass ( ) ) {
return [
` -b:v ${ bitrates . target } ${ bitrates . unit } ` ,
` -minrate ${ bitrates . min } ${ bitrates . unit } ` ,
` -maxrate ${ bitrates . max } ${ bitrates . unit } ` ,
] ;
} else if ( bitrates . max > 0 ) {
// -bufsize is the peak possible bitrate at any moment, while -maxrate is the max rolling average bitrate
return [
2023-09-02 21:22:42 -04:00
` - ${ this . useCQP ( ) ? 'q:v' : 'crf' } ${ this . config . crf } ` ,
2023-07-08 22:43:11 -04:00
` -maxrate ${ bitrates . max } ${ bitrates . unit } ` ,
` -bufsize ${ bitrates . max * 2 } ${ bitrates . unit } ` ,
] ;
} else {
2023-09-02 21:22:42 -04:00
return [ ` - ${ this . useCQP ( ) ? 'q:v' : 'crf' } ${ this . config . crf } ` ] ;
2023-07-08 22:43:11 -04:00
}
}
2024-05-16 13:30:26 -04:00
getInputThreadOptions ( ) : Array < string > {
return [ ] ;
}
getOutputThreadOptions ( ) : Array < string > {
2023-07-08 22:43:11 -04:00
if ( this . config . threads <= 0 ) {
return [ ] ;
}
return [ ` -threads ${ this . config . threads } ` ] ;
}
eligibleForTwoPass() {
2023-08-01 21:56:10 -04:00
if ( ! this . config . twoPass || this . config . accel !== TranscodeHWAccel . DISABLED ) {
2023-07-08 22:43:11 -04:00
return false ;
}
2024-04-11 07:26:27 +02:00
return this . isBitrateConstrained ( ) ;
2023-07-08 22:43:11 -04:00
}
getBitrateDistribution() {
const max = this . getMaxBitrateValue ( ) ;
const target = Math . ceil ( max / 1.45 ) ; // recommended by https://developers.google.com/media/vp9/settings/vod
const min = target / 2 ;
const unit = this . getBitrateUnit ( ) ;
return { max , target , min , unit } as BitrateDistribution ;
}
2023-08-29 05:01:42 -04:00
getTargetResolution ( videoStream : VideoStreamInfo ) {
2024-01-17 22:16:44 -05:00
let target ;
2024-02-02 04:18:00 +01:00
target =
this . config . targetResolution === 'original'
? Math . min ( videoStream . height , videoStream . width )
: Number . parseInt ( this . config . targetResolution ) ;
2024-01-17 22:16:44 -05:00
if ( target % 2 !== 0 ) {
target -= 1 ;
2023-07-08 22:43:11 -04:00
}
2024-01-17 22:16:44 -05:00
return target ;
2023-07-08 22:43:11 -04:00
}
2023-08-29 05:01:42 -04:00
shouldScale ( videoStream : VideoStreamInfo ) {
2024-01-17 22:16:44 -05:00
const oddDimensions = videoStream . height % 2 !== 0 || videoStream . width % 2 !== 0 ;
const largerThanTarget = Math . min ( videoStream . height , videoStream . width ) > this . getTargetResolution ( videoStream ) ;
return oddDimensions || largerThanTarget ;
2023-07-08 22:43:11 -04:00
}
2023-08-29 05:01:42 -04:00
shouldToneMap ( videoStream : VideoStreamInfo ) {
return videoStream . isHDR && this . config . tonemap !== ToneMapping . DISABLED ;
2023-08-07 16:35:25 -04:00
}
2024-05-28 04:47:41 -04:00
getScaling ( videoStream : VideoStreamInfo , mult = 2 ) {
2023-08-29 05:01:42 -04:00
const targetResolution = this . getTargetResolution ( videoStream ) ;
return this . isVideoVertical ( videoStream ) ? ` ${ targetResolution } :- ${ mult } ` : ` - ${ mult } : ${ targetResolution } ` ;
2023-07-08 22:43:11 -04:00
}
2023-10-30 15:39:37 +01:00
getSize ( videoStream : VideoStreamInfo ) {
const smaller = this . getTargetResolution ( videoStream ) ;
const factor = Math . max ( videoStream . height , videoStream . width ) / Math . min ( videoStream . height , videoStream . width ) ;
2024-01-17 22:16:44 -05:00
let larger = Math . round ( smaller * factor ) ;
if ( larger % 2 !== 0 ) {
larger -= 1 ;
}
2023-10-30 15:39:37 +01:00
return this . isVideoVertical ( videoStream ) ? { width : smaller , height : larger } : { width : larger , height : smaller } ;
}
2023-08-29 05:01:42 -04:00
isVideoRotated ( videoStream : VideoStreamInfo ) {
return Math . abs ( videoStream . rotation ) === 90 ;
2023-07-08 22:43:11 -04:00
}
2023-08-29 05:01:42 -04:00
isVideoVertical ( videoStream : VideoStreamInfo ) {
return videoStream . height > videoStream . width || this . isVideoRotated ( videoStream ) ;
2023-07-08 22:43:11 -04:00
}
isBitrateConstrained() {
return this . getMaxBitrateValue ( ) > 0 ;
}
getBitrateUnit() {
const maxBitrate = this . getMaxBitrateValue ( ) ;
2025-01-11 23:09:19 -05:00
return this . config . maxBitrate . trim ( ) . slice ( maxBitrate . toString ( ) . length ) || 'k' ; // use inputted unit if provided, else default to kbps
2023-07-08 22:43:11 -04:00
}
getMaxBitrateValue() {
return Number . parseInt ( this . config . maxBitrate ) || 0 ;
}
getPresetIndex() {
2023-09-02 21:22:42 -04:00
return this . presets . indexOf ( this . config . preset ) ;
2023-07-08 22:43:11 -04:00
}
2023-08-07 16:35:25 -04:00
getColors() {
return {
2024-10-31 20:48:23 -04:00
primaries : 'bt709' ,
transfer : 'bt709' ,
matrix : 'bt709' ,
2023-08-07 16:35:25 -04:00
} ;
}
2024-05-16 13:30:26 -04:00
getToneMapping ( videoStream : VideoStreamInfo ) {
if ( ! this . shouldToneMap ( videoStream ) ) {
return [ ] ;
}
2024-10-31 20:48:23 -04:00
const { primaries , transfer , matrix } = this . getColors ( ) ;
const options = ` tonemapx=tonemap= ${ this . config . tonemap } :desat=0:p= ${ primaries } :t= ${ transfer } :m= ${ matrix } :r=pc:peak=100:format=yuv420p ` ;
return [ options ] ;
2023-08-07 16:35:25 -04:00
}
2023-08-29 05:01:42 -04:00
getAudioCodec ( ) : string {
return this . config . targetAudioCodec ;
}
getVideoCodec ( ) : string {
return this . config . targetVideoCodec ;
}
2023-09-02 21:22:42 -04:00
getBFrames() {
return this . config . bframes ;
}
getRefs() {
return this . config . refs ;
}
getGopSize() {
return this . config . gopSize ;
}
useCQP() {
return this . config . cqMode === CQMode . CQP ;
}
2023-07-08 22:43:11 -04:00
}
2023-08-01 21:56:10 -04:00
export class BaseHWConfig extends BaseConfig implements VideoCodecHWConfig {
2024-12-02 20:28:50 -05:00
protected device : string ;
2024-12-10 12:11:19 -05:00
protected interfaces : VideoInterfaces ;
2023-08-01 21:56:10 -04:00
2023-08-28 14:41:57 -05:00
constructor (
protected config : SystemConfigFFmpegDto ,
2024-12-10 12:11:19 -05:00
interfaces : VideoInterfaces ,
2023-08-28 14:41:57 -05:00
) {
2023-08-01 21:56:10 -04:00
super ( config ) ;
2024-12-10 12:11:19 -05:00
this . interfaces = interfaces ;
this . device = this . getDevice ( interfaces ) ;
2023-08-01 21:56:10 -04:00
}
getSupportedCodecs() {
2024-04-11 07:26:27 +02:00
return [ VideoCodec . H264 , VideoCodec . HEVC ] ;
2023-08-01 21:56:10 -04:00
}
validateDevices ( devices : string [ ] ) {
2024-12-02 20:28:50 -05:00
if ( devices . length === 0 ) {
throw new Error ( 'No /dev/dri devices found. If using Docker, make sure at least one /dev/dri device is mounted' ) ;
}
return devices . filter ( function ( device ) {
return device . startsWith ( 'renderD' ) || device . startsWith ( 'card' ) ;
} ) ;
}
2024-12-10 12:11:19 -05:00
getDevice ( { dri } : VideoInterfaces ) {
2024-12-02 20:28:50 -05:00
if ( this . config . preferredHwDevice === 'auto' ) {
// eslint-disable-next-line unicorn/no-array-reduce
2024-12-10 12:11:19 -05:00
return ` /dev/dri/ ${ this . validateDevices ( dri ) . reduce ( function ( a , b ) {
2024-12-02 20:28:50 -05:00
return a . localeCompare ( b ) < 0 ? b : a ;
} ) } ` ;
}
const deviceName = this . config . preferredHwDevice . replace ( '/dev/dri/' , '' ) ;
2024-12-10 12:11:19 -05:00
if ( ! dri . includes ( deviceName ) ) {
2024-12-02 20:28:50 -05:00
throw new Error ( ` Device ' ${ deviceName } ' does not exist. If using Docker, make sure this device is mounted ` ) ;
}
return ` /dev/dri/ ${ deviceName } ` ;
2023-08-01 21:56:10 -04:00
}
2023-08-29 05:01:42 -04:00
getVideoCodec ( ) : string {
return ` ${ this . config . targetVideoCodec } _ ${ this . config . accel } ` ;
}
2023-09-02 21:22:42 -04:00
getGopSize() {
if ( this . config . gopSize <= 0 ) {
return 256 ;
}
return this . config . gopSize ;
}
2023-08-01 21:56:10 -04:00
}
2023-08-07 16:35:25 -04:00
export class ThumbnailConfig extends BaseConfig {
2024-05-27 15:20:07 -04:00
static create ( config : SystemConfigFFmpegDto ) : VideoCodecSWConfig {
return new ThumbnailConfig ( config ) ;
}
2024-11-14 02:07:04 -05:00
getBaseInputOptions ( videoStream : VideoStreamInfo , format? : VideoFormat ) : string [ ] {
// skip_frame nointra skips all frames for some MPEG-TS files. Look at ffmpeg tickets 7950 and 7895 for more details.
return format ? . formatName === 'mpegts'
? [ '-sws_flags accurate_rnd+full_chroma_int' ]
: [ '-skip_frame nointra' , '-sws_flags accurate_rnd+full_chroma_int' ] ;
2023-09-03 02:21:51 -04:00
}
2024-05-27 12:08:38 -04:00
2023-08-07 16:35:25 -04:00
getBaseOutputOptions() {
2024-05-27 12:08:38 -04:00
return [ '-fps_mode vfr' , '-frames:v 1' , '-update 1' ] ;
}
getFilterOptions ( videoStream : VideoStreamInfo ) : string [ ] {
2024-10-31 20:48:23 -04:00
return [
2024-06-19 10:50:25 -04:00
'fps=12:eof_action=pass:round=down' ,
2024-06-08 05:55:05 -04:00
'thumbnail=12' ,
String . raw ` select=gt(scene \ ,0.1)-eq(prev_selected_n \ ,n)+isnan(prev_selected_n)+gt(n \ ,20) ` ,
'trim=end_frame=2' ,
'reverse' ,
2024-10-31 20:48:23 -04:00
. . . super . getFilterOptions ( videoStream ) ,
2024-06-08 05:55:05 -04:00
] ;
2023-08-07 16:35:25 -04:00
}
getPresetOptions() {
return [ ] ;
}
getBitrateOptions() {
return [ ] ;
}
2023-09-28 08:29:31 -04:00
eligibleForTwoPass() {
return false ;
}
2023-08-29 05:01:42 -04:00
getScaling ( videoStream : VideoStreamInfo ) {
2024-10-31 20:48:23 -04:00
return super . getScaling ( videoStream ) + ':flags=lanczos+accurate_rnd+full_chroma_int:out_range=pc' ;
2023-08-07 16:35:25 -04:00
}
}
2023-07-08 22:43:11 -04:00
export class H264Config extends BaseConfig {
2024-05-16 13:30:26 -04:00
getOutputThreadOptions() {
const options = super . getOutputThreadOptions ( ) ;
2024-04-07 12:43:50 -04:00
if ( this . config . threads === 1 ) {
options . push ( '-x264-params frame-threads=1:pools=none' ) ;
2023-07-08 22:43:11 -04:00
}
2024-04-07 12:43:50 -04:00
return options ;
2023-07-08 22:43:11 -04:00
}
}
export class HEVCConfig extends BaseConfig {
2024-05-16 13:30:26 -04:00
getOutputThreadOptions() {
const options = super . getOutputThreadOptions ( ) ;
2024-04-07 12:43:50 -04:00
if ( this . config . threads === 1 ) {
options . push ( '-x265-params frame-threads=1:pools=none' ) ;
2023-07-08 22:43:11 -04:00
}
2024-04-07 12:43:50 -04:00
return options ;
2023-07-08 22:43:11 -04:00
}
}
export class VP9Config extends BaseConfig {
getPresetOptions() {
const speed = Math . min ( this . getPresetIndex ( ) , 5 ) ; // values over 5 require realtime mode, which is its own can of worms since it overrides -crf and -threads
if ( speed >= 0 ) {
return [ ` -cpu-used ${ speed } ` ] ;
}
return [ ] ;
}
getBitrateOptions() {
const bitrates = this . getBitrateDistribution ( ) ;
2024-01-20 15:05:08 -05:00
if ( bitrates . max > 0 && this . eligibleForTwoPass ( ) ) {
2023-07-08 22:43:11 -04:00
return [
` -b:v ${ bitrates . target } ${ bitrates . unit } ` ,
` -minrate ${ bitrates . min } ${ bitrates . unit } ` ,
` -maxrate ${ bitrates . max } ${ bitrates . unit } ` ,
] ;
}
2023-09-02 21:22:42 -04:00
return [ ` - ${ this . useCQP ( ) ? 'q:v' : 'crf' } ${ this . config . crf } ` , ` -b:v ${ bitrates . max } ${ bitrates . unit } ` ] ;
2023-07-08 22:43:11 -04:00
}
2024-05-16 13:30:26 -04:00
getOutputThreadOptions() {
return [ '-row-mt 1' , . . . super . getOutputThreadOptions ( ) ] ;
2023-07-08 22:43:11 -04:00
}
2024-04-11 07:26:27 +02:00
eligibleForTwoPass() {
return this . config . twoPass ;
}
}
export class AV1Config extends BaseConfig {
2024-10-15 14:53:18 -04:00
getVideoCodec ( ) : string {
return 'libsvtav1' ;
}
2024-04-11 07:26:27 +02:00
getPresetOptions() {
const speed = this . getPresetIndex ( ) + 4 ; // Use 4 as slowest, giving us an effective range of 4-12 which is far more useful than 0-8
if ( speed >= 0 ) {
return [ ` -preset ${ speed } ` ] ;
}
return [ ] ;
}
getBitrateOptions() {
const options = [ ` -crf ${ this . config . crf } ` ] ;
const bitrates = this . getBitrateDistribution ( ) ;
const svtparams = [ ] ;
if ( this . config . threads > 0 ) {
svtparams . push ( ` lp= ${ this . config . threads } ` ) ;
}
if ( bitrates . max > 0 ) {
svtparams . push ( ` mbr= ${ bitrates . max } ${ bitrates . unit } ` ) ;
}
if ( svtparams . length > 0 ) {
options . push ( ` -svtav1-params ${ svtparams . join ( ':' ) } ` ) ;
}
return options ;
}
2024-05-16 13:30:26 -04:00
getOutputThreadOptions() {
2024-04-11 07:26:27 +02:00
return [ ] ; // Already set above with svtav1-params
}
eligibleForTwoPass() {
return this . config . twoPass ;
}
2023-07-08 22:43:11 -04:00
}
2023-08-01 21:56:10 -04:00
2024-05-16 13:30:26 -04:00
export class NvencSwDecodeConfig extends BaseHWConfig {
2024-12-02 20:28:50 -05:00
getDevice() {
return '0' ;
}
2023-08-01 21:56:10 -04:00
getSupportedCodecs() {
2024-04-20 14:52:50 -04:00
return [ VideoCodec . H264 , VideoCodec . HEVC , VideoCodec . AV1 ] ;
2023-08-01 21:56:10 -04:00
}
getBaseInputOptions() {
2024-12-02 20:28:50 -05:00
return [ ` -init_hw_device cuda=cuda: ${ this . device } ` , '-filter_hw_device cuda' ] ;
2023-08-01 21:56:10 -04:00
}
2024-02-14 11:24:39 -05:00
getBaseOutputOptions ( target : TranscodeTarget , videoStream : VideoStreamInfo , audioStream? : AudioStreamInfo ) {
2023-09-02 21:22:42 -04:00
const options = [
2023-08-01 21:56:10 -04:00
// below settings recommended from https://docs.nvidia.com/video-technologies/video-codec-sdk/12.0/ffmpeg-with-nvidia-gpu/index.html#command-line-for-latency-tolerant-high-quality-transcoding
'-tune hq' ,
'-qmin 0' ,
'-rc-lookahead 20' ,
'-i_qfactor 0.75' ,
2024-02-14 11:24:39 -05:00
. . . super . getBaseOutputOptions ( target , videoStream , audioStream ) ,
2023-08-01 21:56:10 -04:00
] ;
2023-09-02 21:22:42 -04:00
if ( this . getBFrames ( ) > 0 ) {
2024-02-02 04:18:00 +01:00
options . push ( '-b_ref_mode middle' , '-b_qfactor 1.1' ) ;
2023-09-02 21:22:42 -04:00
}
if ( this . config . temporalAQ ) {
options . push ( '-temporal-aq 1' ) ;
}
return options ;
2023-08-01 21:56:10 -04:00
}
2023-08-29 05:01:42 -04:00
getFilterOptions ( videoStream : VideoStreamInfo ) {
2024-05-16 13:30:26 -04:00
const options = this . getToneMapping ( videoStream ) ;
2024-10-31 20:48:23 -04:00
options . push ( 'hwupload_cuda' ) ;
2023-08-29 05:01:42 -04:00
if ( this . shouldScale ( videoStream ) ) {
2024-10-31 20:48:23 -04:00
options . push ( ` scale_cuda= ${ this . getScaling ( videoStream ) } :format=nv12 ` ) ;
2023-08-01 21:56:10 -04:00
}
return options ;
}
getPresetOptions() {
let presetIndex = this . getPresetIndex ( ) ;
if ( presetIndex < 0 ) {
return [ ] ;
}
presetIndex = 7 - Math . min ( 6 , presetIndex ) ; // map to p1-p7; p7 is the highest quality, so reverse index
return [ ` -preset p ${ presetIndex } ` ] ;
}
getBitrateOptions() {
const bitrates = this . getBitrateDistribution ( ) ;
if ( bitrates . max > 0 && this . config . twoPass ) {
return [
` -b:v ${ bitrates . target } ${ bitrates . unit } ` ,
` -maxrate ${ bitrates . max } ${ bitrates . unit } ` ,
` -bufsize ${ bitrates . target } ${ bitrates . unit } ` ,
'-multipass 2' ,
] ;
} else if ( bitrates . max > 0 ) {
return [
` -cq:v ${ this . config . crf } ` ,
` -maxrate ${ bitrates . max } ${ bitrates . unit } ` ,
` -bufsize ${ bitrates . target } ${ bitrates . unit } ` ,
] ;
} else {
return [ ` -cq:v ${ this . config . crf } ` ] ;
}
}
getThreadOptions() {
return [ ] ;
}
2023-09-02 21:22:42 -04:00
getRefs() {
const bframes = this . getBFrames ( ) ;
if ( bframes > 0 && bframes < 3 && this . config . refs < 3 ) {
return 0 ;
}
return this . config . refs ;
}
2023-08-01 21:56:10 -04:00
}
2024-05-16 13:30:26 -04:00
export class NvencHwDecodeConfig extends NvencSwDecodeConfig {
getBaseInputOptions() {
return [ '-hwaccel cuda' , '-hwaccel_output_format cuda' , '-noautorotate' , . . . this . getInputThreadOptions ( ) ] ;
}
getFilterOptions ( videoStream : VideoStreamInfo ) {
const options = [ ] ;
2025-03-18 12:42:09 -04:00
const tonemapOptions = this . getToneMapping ( videoStream ) ;
if ( this . shouldScale ( videoStream ) || ( tonemapOptions . length === 0 && ! videoStream . pixelFormat . endsWith ( '420p' ) ) ) {
2024-05-16 13:30:26 -04:00
options . push ( ` scale_cuda= ${ this . getScaling ( videoStream ) } ` ) ;
}
2025-03-18 12:42:09 -04:00
options . push ( . . . tonemapOptions ) ;
2024-05-16 13:30:26 -04:00
if ( options . length > 0 ) {
options [ options . length - 1 ] += ':format=nv12' ;
}
return options ;
}
getToneMapping ( videoStream : VideoStreamInfo ) {
if ( ! this . shouldToneMap ( videoStream ) ) {
return [ ] ;
}
2024-10-31 20:48:23 -04:00
const { matrix , primaries , transfer } = this . getColors ( ) ;
2024-05-16 13:30:26 -04:00
const tonemapOptions = [
'desat=0' ,
2024-10-31 20:48:23 -04:00
` matrix= ${ matrix } ` ,
` primaries= ${ primaries } ` ,
2024-05-16 13:30:26 -04:00
'range=pc' ,
` tonemap= ${ this . config . tonemap } ` ,
2024-10-31 20:48:23 -04:00
'tonemap_mode=lum' ,
` transfer= ${ transfer } ` ,
'peak=100' ,
2024-05-16 13:30:26 -04:00
] ;
return [ ` tonemap_cuda= ${ tonemapOptions . join ( ':' ) } ` ] ;
}
getInputThreadOptions() {
2024-05-27 15:18:01 -04:00
return [ ` -threads 1 ` ] ;
2024-05-16 13:30:26 -04:00
}
getOutputThreadOptions() {
return [ ] ;
}
}
2024-05-22 23:58:29 -04:00
export class QsvSwDecodeConfig extends BaseHWConfig {
2023-08-01 21:56:10 -04:00
getBaseInputOptions() {
2024-12-02 20:28:50 -05:00
return [ ` -init_hw_device qsv=hw,child_device= ${ this . device } ` , '-filter_hw_device hw' ] ;
2023-08-01 21:56:10 -04:00
}
2024-02-14 11:24:39 -05:00
getBaseOutputOptions ( target : TranscodeTarget , videoStream : VideoStreamInfo , audioStream? : AudioStreamInfo ) {
const options = super . getBaseOutputOptions ( target , videoStream , audioStream ) ;
2023-08-01 21:56:10 -04:00
// VP9 requires enabling low power mode https://git.ffmpeg.org/gitweb/ffmpeg.git/commit/33583803e107b6d532def0f9d949364b01b6ad5a
if ( this . config . targetVideoCodec === VideoCodec . VP9 ) {
options . push ( '-low_power 1' ) ;
}
return options ;
}
2023-08-29 05:01:42 -04:00
getFilterOptions ( videoStream : VideoStreamInfo ) {
2024-05-16 13:30:26 -04:00
const options = this . getToneMapping ( videoStream ) ;
2024-10-31 20:48:23 -04:00
options . push ( 'hwupload=extra_hw_frames=64' ) ;
2023-08-29 05:01:42 -04:00
if ( this . shouldScale ( videoStream ) ) {
2024-10-31 20:48:23 -04:00
options . push ( ` scale_qsv= ${ this . getScaling ( videoStream ) } :mode=hq:format=nv12 ` ) ;
2023-08-01 21:56:10 -04:00
}
return options ;
}
getPresetOptions() {
let presetIndex = this . getPresetIndex ( ) ;
if ( presetIndex < 0 ) {
return [ ] ;
}
presetIndex = Math . min ( 6 , presetIndex ) + 1 ; // 1 to 7
return [ ` -preset ${ presetIndex } ` ] ;
}
getBitrateOptions() {
const options = [ ] ;
2024-05-28 04:49:51 -04:00
options . push ( ` - ${ this . useCQP ( ) ? 'q:v' : 'global_quality:v' } ${ this . config . crf } ` ) ;
2023-08-01 21:56:10 -04:00
const bitrates = this . getBitrateDistribution ( ) ;
if ( bitrates . max > 0 ) {
2024-02-02 04:18:00 +01:00
options . push ( ` -maxrate ${ bitrates . max } ${ bitrates . unit } ` , ` -bufsize ${ bitrates . max * 2 } ${ bitrates . unit } ` ) ;
2023-08-01 21:56:10 -04:00
}
return options ;
}
2023-09-02 21:22:42 -04:00
2024-04-11 07:26:27 +02:00
getSupportedCodecs() {
2024-04-20 22:36:00 +08:00
return [ VideoCodec . H264 , VideoCodec . HEVC , VideoCodec . VP9 , VideoCodec . AV1 ] ;
2024-04-11 07:26:27 +02:00
}
2023-09-02 21:22:42 -04:00
// recommended from https://github.com/intel/media-delivery/blob/master/doc/benchmarks/intel-iris-xe-max-graphics/intel-iris-xe-max-graphics.md
getBFrames() {
if ( this . config . bframes < 0 ) {
return 7 ;
}
return this . config . bframes ;
}
getRefs() {
if ( this . config . refs <= 0 ) {
return 5 ;
}
return this . config . refs ;
}
useCQP() {
return this . config . cqMode === CQMode . CQP || this . config . targetVideoCodec === VideoCodec . VP9 ;
}
2024-05-28 04:47:41 -04:00
getScaling ( videoStream : VideoStreamInfo ) : string {
return super . getScaling ( videoStream , 1 ) ;
}
2023-08-01 21:56:10 -04:00
}
2024-05-22 23:58:29 -04:00
export class QsvHwDecodeConfig extends QsvSwDecodeConfig {
getBaseInputOptions() {
2024-12-02 20:28:50 -05:00
return [
2024-07-09 17:18:49 +02:00
'-hwaccel qsv' ,
'-hwaccel_output_format qsv' ,
'-async_depth 4' ,
'-noautorotate' ,
2024-12-02 20:28:50 -05:00
` -qsv_device ${ this . device } ` ,
2024-07-09 17:18:49 +02:00
. . . this . getInputThreadOptions ( ) ,
] ;
2024-05-22 23:58:29 -04:00
}
getFilterOptions ( videoStream : VideoStreamInfo ) {
const options = [ ] ;
2024-10-31 20:48:23 -04:00
const tonemapOptions = this . getToneMapping ( videoStream ) ;
2025-03-18 12:42:09 -04:00
if ( tonemapOptions . length === 0 && ! videoStream . pixelFormat . endsWith ( '420p' ) ) {
options . push ( ` scale_qsv= ${ this . getScaling ( videoStream ) } :async_depth=4:mode=hq:format=nv12 ` ) ;
} else if ( this . shouldScale ( videoStream ) ) {
options . push ( ` scale_qsv= ${ this . getScaling ( videoStream ) } :async_depth=4:mode=hq ` ) ;
2024-05-22 23:58:29 -04:00
}
2024-10-31 20:48:23 -04:00
options . push ( . . . tonemapOptions ) ;
2024-05-22 23:58:29 -04:00
return options ;
}
getToneMapping ( videoStream : VideoStreamInfo ) : string [ ] {
if ( ! this . shouldToneMap ( videoStream ) ) {
return [ ] ;
}
2024-10-31 20:48:23 -04:00
const { matrix , primaries , transfer } = this . getColors ( ) ;
2024-05-22 23:58:29 -04:00
const tonemapOptions = [
'desat=0' ,
'format=nv12' ,
2024-10-31 20:48:23 -04:00
` matrix= ${ matrix } ` ,
` primaries= ${ primaries } ` ,
` transfer= ${ transfer } ` ,
2024-05-22 23:58:29 -04:00
'range=pc' ,
` tonemap= ${ this . config . tonemap } ` ,
2024-10-31 20:48:23 -04:00
'tonemap_mode=lum' ,
'peak=100' ,
2024-05-22 23:58:29 -04:00
] ;
return [
'hwmap=derive_device=opencl' ,
` tonemap_opencl= ${ tonemapOptions . join ( ':' ) } ` ,
2024-05-24 04:50:28 -04:00
'hwmap=derive_device=qsv:reverse=1,format=qsv' ,
2024-05-22 23:58:29 -04:00
] ;
}
2024-05-27 15:18:01 -04:00
getInputThreadOptions() {
return [ ` -threads 1 ` ] ;
}
2024-05-22 23:58:29 -04:00
}
2024-10-17 18:02:41 -04:00
export class VaapiSwDecodeConfig extends BaseHWConfig {
2023-08-01 21:56:10 -04:00
getBaseInputOptions() {
2024-12-02 20:28:50 -05:00
return [ ` -init_hw_device vaapi=accel: ${ this . device } ` , '-filter_hw_device accel' ] ;
2023-08-01 21:56:10 -04:00
}
2023-08-29 05:01:42 -04:00
getFilterOptions ( videoStream : VideoStreamInfo ) {
2024-05-16 13:30:26 -04:00
const options = this . getToneMapping ( videoStream ) ;
2024-10-31 20:48:23 -04:00
options . push ( 'hwupload=extra_hw_frames=64' ) ;
2023-08-29 05:01:42 -04:00
if ( this . shouldScale ( videoStream ) ) {
2024-10-31 20:48:23 -04:00
options . push ( ` scale_vaapi= ${ this . getScaling ( videoStream ) } :mode=hq:out_range=pc:format=nv12 ` ) ;
2023-08-01 21:56:10 -04:00
}
return options ;
}
getPresetOptions() {
let presetIndex = this . getPresetIndex ( ) ;
if ( presetIndex < 0 ) {
return [ ] ;
}
presetIndex = Math . min ( 6 , presetIndex ) + 1 ; // 1 to 7
return [ ` -compression_level ${ presetIndex } ` ] ;
}
getBitrateOptions() {
const bitrates = this . getBitrateDistribution ( ) ;
2023-09-02 21:22:42 -04:00
const options = [ ] ;
if ( this . config . targetVideoCodec === VideoCodec . VP9 ) {
options . push ( '-bsf:v vp9_raw_reorder,vp9_superframe' ) ;
}
2023-08-01 21:56:10 -04:00
// VAAPI doesn't allow setting both quality and max bitrate
if ( bitrates . max > 0 ) {
2023-09-02 21:22:42 -04:00
options . push (
2023-08-01 21:56:10 -04:00
` -b:v ${ bitrates . target } ${ bitrates . unit } ` ,
` -maxrate ${ bitrates . max } ${ bitrates . unit } ` ,
` -minrate ${ bitrates . min } ${ bitrates . unit } ` ,
'-rc_mode 3' ,
2023-09-02 21:22:42 -04:00
) ; // variable bitrate
} else if ( this . useCQP ( ) ) {
2024-05-28 04:49:51 -04:00
options . push ( ` -qp:v ${ this . config . crf } ` , ` -global_quality:v ${ this . config . crf } ` , '-rc_mode 1' ) ;
2023-08-01 21:56:10 -04:00
} else {
2024-05-28 04:49:51 -04:00
options . push ( ` -global_quality:v ${ this . config . crf } ` , '-rc_mode 4' ) ;
2023-08-01 21:56:10 -04:00
}
2023-09-02 21:22:42 -04:00
return options ;
}
2024-04-11 07:26:27 +02:00
getSupportedCodecs() {
2024-06-11 20:24:06 -04:00
return [ VideoCodec . H264 , VideoCodec . HEVC , VideoCodec . VP9 , VideoCodec . AV1 ] ;
2024-04-11 07:26:27 +02:00
}
2023-09-02 21:22:42 -04:00
useCQP() {
return this . config . cqMode !== CQMode . ICQ || this . config . targetVideoCodec === VideoCodec . VP9 ;
2023-08-01 21:56:10 -04:00
}
}
2023-10-30 15:39:37 +01:00
2024-10-17 18:02:41 -04:00
export class VaapiHwDecodeConfig extends VaapiSwDecodeConfig {
getBaseInputOptions() {
2024-12-02 20:28:50 -05:00
return [
2024-10-17 18:02:41 -04:00
'-hwaccel vaapi' ,
'-hwaccel_output_format vaapi' ,
'-noautorotate' ,
2024-12-02 20:28:50 -05:00
` -hwaccel_device ${ this . device } ` ,
2024-10-17 18:02:41 -04:00
. . . this . getInputThreadOptions ( ) ,
] ;
}
getFilterOptions ( videoStream : VideoStreamInfo ) {
const options = [ ] ;
2024-10-31 20:48:23 -04:00
const tonemapOptions = this . getToneMapping ( videoStream ) ;
2025-03-18 12:42:09 -04:00
if ( tonemapOptions . length === 0 && ! videoStream . pixelFormat . endsWith ( '420p' ) ) {
options . push ( ` scale_vaapi= ${ this . getScaling ( videoStream ) } :mode=hq:out_range=pc:format=nv12 ` ) ;
} else if ( this . shouldScale ( videoStream ) ) {
options . push ( ` scale_vaapi= ${ this . getScaling ( videoStream ) } :mode=hq:out_range=pc ` ) ;
2024-10-17 18:02:41 -04:00
}
2024-10-31 20:48:23 -04:00
options . push ( . . . tonemapOptions ) ;
2024-10-17 18:02:41 -04:00
return options ;
}
getToneMapping ( videoStream : VideoStreamInfo ) : string [ ] {
if ( ! this . shouldToneMap ( videoStream ) ) {
return [ ] ;
}
2024-10-31 20:48:23 -04:00
const { matrix , primaries , transfer } = this . getColors ( ) ;
2024-10-17 18:02:41 -04:00
const tonemapOptions = [
'desat=0' ,
'format=nv12' ,
2024-10-31 20:48:23 -04:00
` matrix= ${ matrix } ` ,
` primaries= ${ primaries } ` ,
` transfer= ${ transfer } ` ,
2024-10-17 18:02:41 -04:00
'range=pc' ,
` tonemap= ${ this . config . tonemap } ` ,
2024-10-31 20:48:23 -04:00
'tonemap_mode=lum' ,
'peak=100' ,
2024-10-17 18:02:41 -04:00
] ;
return [
'hwmap=derive_device=opencl' ,
` tonemap_opencl= ${ tonemapOptions . join ( ':' ) } ` ,
'hwmap=derive_device=vaapi:reverse=1,format=vaapi' ,
] ;
}
getInputThreadOptions() {
return [ ` -threads 1 ` ] ;
}
}
2024-05-16 13:30:26 -04:00
export class RkmppSwDecodeConfig extends BaseHWConfig {
2023-10-30 15:39:37 +01:00
eligibleForTwoPass ( ) : boolean {
return false ;
}
2024-05-16 13:30:26 -04:00
getBaseInputOptions ( ) : string [ ] {
2023-10-30 15:39:37 +01:00
return [ ] ;
}
getPresetOptions() {
switch ( this . config . targetVideoCodec ) {
2024-02-02 04:18:00 +01:00
case VideoCodec . H264 : {
2023-10-30 15:39:37 +01:00
// from ffmpeg_mpp help, commonly referred to as H264 level 5.1
return [ '-level 51' ] ;
2024-02-02 04:18:00 +01:00
}
case VideoCodec . HEVC : {
2023-10-30 15:39:37 +01:00
// from ffmpeg_mpp help, commonly referred to as HEVC level 5.1
return [ '-level 153' ] ;
2024-02-02 04:18:00 +01:00
}
default : {
throw new Error ( ` Incompatible video codec for RKMPP: ${ this . config . targetVideoCodec } ` ) ;
}
2023-10-30 15:39:37 +01:00
}
}
getBitrateOptions() {
const bitrate = this . getMaxBitrateValue ( ) ;
if ( bitrate > 0 ) {
2024-02-27 16:47:04 +01:00
// -b:v specifies max bitrate, average bitrate is derived automatically...
2024-02-28 02:32:07 +01:00
return [ '-rc_mode AVBR' , ` -b:v ${ bitrate } ${ this . getBitrateUnit ( ) } ` ] ;
2023-10-30 15:39:37 +01:00
}
2024-02-27 16:47:04 +01:00
// use CRF value as QP value
2024-02-28 02:32:07 +01:00
return [ '-rc_mode CQP' , ` -qp_init ${ this . config . crf } ` ] ;
2023-10-30 15:39:37 +01:00
}
getSupportedCodecs() {
return [ VideoCodec . H264 , VideoCodec . HEVC ] ;
}
getVideoCodec ( ) : string {
2024-02-27 16:47:04 +01:00
return ` ${ this . config . targetVideoCodec } _rkmpp ` ;
2023-10-30 15:39:37 +01:00
}
}
2024-05-16 13:30:26 -04:00
export class RkmppHwDecodeConfig extends RkmppSwDecodeConfig {
getBaseInputOptions() {
2024-07-09 17:18:49 +02:00
return [ '-hwaccel rkmpp' , '-hwaccel_output_format drm_prime' , '-afbc rga' , '-noautorotate' ] ;
2024-05-16 13:30:26 -04:00
}
getFilterOptions ( videoStream : VideoStreamInfo ) {
if ( this . shouldToneMap ( videoStream ) ) {
2024-10-31 20:48:23 -04:00
const { primaries , transfer , matrix } = this . getColors ( ) ;
2024-12-10 12:11:19 -05:00
if ( this . interfaces . mali ) {
2024-11-22 16:08:49 +08:00
return [
// use RKMPP for scaling, OpenCL for tone mapping
` scale_rkrga= ${ this . getScaling ( videoStream ) } :format=p010:afbc=1:async_depth=4 ` ,
'hwmap=derive_device=opencl:mode=read' ,
` tonemap_opencl=format=nv12:r=pc:p= ${ primaries } :t= ${ transfer } :m= ${ matrix } :tonemap= ${ this . config . tonemap } :desat=0:tonemap_mode=lum:peak=100 ` ,
'hwmap=derive_device=rkmpp:mode=write:reverse=1' ,
'format=drm_prime' ,
] ;
}
2024-05-16 13:30:26 -04:00
return [
2024-11-22 16:08:49 +08:00
// use RKMPP for scaling, CPU for tone mapping (only works on RK3588, which supports 10-bit output)
` scale_rkrga= ${ this . getScaling ( videoStream ) } :format=p010:afbc=1:async_depth=4 ` ,
'hwdownload' ,
'format=p010' ,
` tonemapx=tonemap= ${ this . config . tonemap } :desat=0:p= ${ primaries } :t= ${ transfer } :m= ${ matrix } :r=pc:peak=100:format=yuv420p ` ,
'hwupload' ,
2024-05-16 13:30:26 -04:00
] ;
} else if ( this . shouldScale ( videoStream ) ) {
2024-11-22 16:08:49 +08:00
return [ ` scale_rkrga= ${ this . getScaling ( videoStream ) } :format=nv12:afbc=1:async_depth=4 ` ] ;
2024-05-16 13:30:26 -04:00
}
return [ ] ;
}
}