2023-09-03 03:22:42 +02:00
import { CQMode , ToneMapping , TranscodeHWAccel , VideoCodec } from '@app/infra/entities' ;
2023-07-09 04:43:11 +02:00
import { SystemConfigFFmpegDto } from '../system-config/dto' ;
2023-08-02 03:56:10 +02:00
import {
2023-08-29 11:01:42 +02:00
AudioStreamInfo ,
2023-08-02 03:56:10 +02:00
BitrateDistribution ,
TranscodeOptions ,
VideoCodecHWConfig ,
VideoCodecSWConfig ,
VideoStreamInfo ,
} from './media.repository' ;
2023-07-09 04:43:11 +02:00
class BaseConfig implements VideoCodecSWConfig {
2023-09-03 03:22:42 +02:00
presets = [ 'veryslow' , 'slower' , 'slow' , 'medium' , 'fast' , 'faster' , 'veryfast' , 'superfast' , 'ultrafast' ] ;
2023-07-09 04:43:11 +02:00
constructor ( protected config : SystemConfigFFmpegDto ) { }
2023-09-03 03:22:42 +02:00
getOptions ( videoStream : VideoStreamInfo , audioStream? : AudioStreamInfo ) {
2023-07-09 04:43:11 +02:00
const options = {
inputOptions : this.getBaseInputOptions ( ) ,
2023-08-29 11:01:42 +02:00
outputOptions : this.getBaseOutputOptions ( videoStream , audioStream ) . concat ( '-v verbose' ) ,
2023-07-09 04:43:11 +02:00
twoPass : this.eligibleForTwoPass ( ) ,
} as TranscodeOptions ;
2023-08-29 11:01:42 +02:00
const filters = this . getFilterOptions ( videoStream ) ;
2023-07-09 04:43:11 +02:00
if ( filters . length > 0 ) {
options . outputOptions . push ( ` -vf ${ filters . join ( ',' ) } ` ) ;
}
options . outputOptions . push ( . . . this . getPresetOptions ( ) ) ;
options . outputOptions . push ( . . . this . getThreadOptions ( ) ) ;
options . outputOptions . push ( . . . this . getBitrateOptions ( ) ) ;
return options ;
}
getBaseInputOptions ( ) : string [ ] {
return [ ] ;
}
2023-09-03 03:22:42 +02:00
getBaseOutputOptions ( videoStream : VideoStreamInfo , audioStream? : AudioStreamInfo ) {
const options = [
` -c:v ${ this . getVideoCodec ( ) } ` ,
` -c:a ${ this . getAudioCodec ( ) } ` ,
2023-08-07 22:35:25 +02: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-03 03:22:42 +02:00
// explicitly selects the video stream instead of leaving it up to FFmpeg
` -map 0: ${ videoStream . index } ` ,
2023-08-07 22:35:25 +02:00
] ;
2023-09-03 03:22:42 +02: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 ( ) } ` ) ;
}
return options ;
2023-07-09 04:43:11 +02:00
}
2023-08-29 11:01:42 +02:00
getFilterOptions ( videoStream : VideoStreamInfo ) {
2023-07-09 04:43:11 +02:00
const options = [ ] ;
2023-08-29 11:01:42 +02:00
if ( this . shouldScale ( videoStream ) ) {
options . push ( ` scale= ${ this . getScaling ( videoStream ) } ` ) ;
2023-07-09 04:43:11 +02:00
}
2023-08-29 11:01:42 +02:00
if ( this . shouldToneMap ( videoStream ) ) {
2023-08-07 22:35:25 +02:00
options . push ( . . . this . getToneMapping ( ) ) ;
}
options . push ( 'format=yuv420p' ) ;
2023-07-09 04:43:11 +02: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-03 03:22:42 +02:00
` - ${ this . useCQP ( ) ? 'q:v' : 'crf' } ${ this . config . crf } ` ,
2023-07-09 04:43:11 +02:00
` -maxrate ${ bitrates . max } ${ bitrates . unit } ` ,
` -bufsize ${ bitrates . max * 2 } ${ bitrates . unit } ` ,
] ;
} else {
2023-09-03 03:22:42 +02:00
return [ ` - ${ this . useCQP ( ) ? 'q:v' : 'crf' } ${ this . config . crf } ` ] ;
2023-07-09 04:43:11 +02:00
}
}
getThreadOptions ( ) : Array < string > {
if ( this . config . threads <= 0 ) {
return [ ] ;
}
return [ ` -threads ${ this . config . threads } ` ] ;
}
eligibleForTwoPass() {
2023-08-02 03:56:10 +02:00
if ( ! this . config . twoPass || this . config . accel !== TranscodeHWAccel . DISABLED ) {
2023-07-09 04:43:11 +02:00
return false ;
}
2023-08-02 03:56:10 +02:00
return this . isBitrateConstrained ( ) || this . config . targetVideoCodec === VideoCodec . VP9 ;
2023-07-09 04:43:11 +02: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 11:01:42 +02:00
getTargetResolution ( videoStream : VideoStreamInfo ) {
2023-07-09 04:43:11 +02:00
if ( this . config . targetResolution === 'original' ) {
2023-08-29 11:01:42 +02:00
return Math . min ( videoStream . height , videoStream . width ) ;
2023-07-09 04:43:11 +02:00
}
return Number . parseInt ( this . config . targetResolution ) ;
}
2023-08-29 11:01:42 +02:00
shouldScale ( videoStream : VideoStreamInfo ) {
return Math . min ( videoStream . height , videoStream . width ) > this . getTargetResolution ( videoStream ) ;
2023-07-09 04:43:11 +02:00
}
2023-08-29 11:01:42 +02:00
shouldToneMap ( videoStream : VideoStreamInfo ) {
return videoStream . isHDR && this . config . tonemap !== ToneMapping . DISABLED ;
2023-08-07 22:35:25 +02:00
}
2023-08-29 11:01:42 +02:00
getScaling ( videoStream : VideoStreamInfo ) {
const targetResolution = this . getTargetResolution ( videoStream ) ;
2023-08-02 03:56:10 +02:00
const mult = this . config . accel === TranscodeHWAccel . QSV ? 1 : 2 ; // QSV doesn't support scaling numbers below -1
2023-08-29 11:01:42 +02:00
return this . isVideoVertical ( videoStream ) ? ` ${ targetResolution } :- ${ mult } ` : ` - ${ mult } : ${ targetResolution } ` ;
2023-07-09 04:43:11 +02:00
}
2023-08-29 11:01:42 +02:00
isVideoRotated ( videoStream : VideoStreamInfo ) {
return Math . abs ( videoStream . rotation ) === 90 ;
2023-07-09 04:43:11 +02:00
}
2023-08-29 11:01:42 +02:00
isVideoVertical ( videoStream : VideoStreamInfo ) {
return videoStream . height > videoStream . width || this . isVideoRotated ( videoStream ) ;
2023-07-09 04:43:11 +02:00
}
isBitrateConstrained() {
return this . getMaxBitrateValue ( ) > 0 ;
}
getBitrateUnit() {
const maxBitrate = this . getMaxBitrateValue ( ) ;
return this . config . maxBitrate . trim ( ) . substring ( maxBitrate . toString ( ) . length ) ; // use inputted unit if provided
}
getMaxBitrateValue() {
return Number . parseInt ( this . config . maxBitrate ) || 0 ;
}
getPresetIndex() {
2023-09-03 03:22:42 +02:00
return this . presets . indexOf ( this . config . preset ) ;
2023-07-09 04:43:11 +02:00
}
2023-08-07 22:35:25 +02:00
getColors() {
return {
primaries : 'bt709' ,
transfer : 'bt709' ,
matrix : 'bt709' ,
} ;
}
2023-09-03 03:22:42 +02:00
getNPL() {
if ( this . config . npl <= 0 ) {
// since hable already outputs a darker image, we use a lower npl value for it
return this . config . tonemap === ToneMapping . HABLE ? 100 : 250 ;
} else {
return this . config . npl ;
}
}
2023-08-07 22:35:25 +02:00
getToneMapping() {
const colors = this . getColors ( ) ;
2023-09-03 03:22:42 +02:00
2023-08-07 22:35:25 +02:00
return [
2023-09-03 03:22:42 +02:00
` zscale=t=linear:npl= ${ this . getNPL ( ) } ` ,
2023-08-07 22:35:25 +02:00
` tonemap= ${ this . config . tonemap } :desat=0 ` ,
` zscale=p= ${ colors . primaries } :t= ${ colors . transfer } :m= ${ colors . matrix } :range=pc ` ,
] ;
}
2023-08-29 11:01:42 +02:00
getAudioCodec ( ) : string {
return this . config . targetAudioCodec ;
}
getVideoCodec ( ) : string {
return this . config . targetVideoCodec ;
}
2023-09-03 03:22:42 +02: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-09 04:43:11 +02:00
}
2023-08-02 03:56:10 +02:00
export class BaseHWConfig extends BaseConfig implements VideoCodecHWConfig {
protected devices : string [ ] ;
2023-08-28 21:41:57 +02:00
constructor (
protected config : SystemConfigFFmpegDto ,
devices : string [ ] = [ ] ,
) {
2023-08-02 03:56:10 +02:00
super ( config ) ;
this . devices = this . validateDevices ( devices ) ;
}
getSupportedCodecs() {
return [ VideoCodec . H264 , VideoCodec . HEVC , VideoCodec . VP9 ] ;
}
validateDevices ( devices : string [ ] ) {
return devices
. filter ( ( device ) = > device . startsWith ( 'renderD' ) || device . startsWith ( 'card' ) )
. sort ( ( a , b ) = > {
// order GPU devices first
if ( a . startsWith ( 'card' ) && b . startsWith ( 'renderD' ) ) {
return - 1 ;
}
if ( a . startsWith ( 'renderD' ) && b . startsWith ( 'card' ) ) {
return 1 ;
}
return - a . localeCompare ( b ) ;
} ) ;
}
2023-08-29 11:01:42 +02:00
getVideoCodec ( ) : string {
return ` ${ this . config . targetVideoCodec } _ ${ this . config . accel } ` ;
}
2023-09-03 03:22:42 +02:00
getGopSize() {
if ( this . config . gopSize <= 0 ) {
return 256 ;
}
return this . config . gopSize ;
}
2023-08-02 03:56:10 +02:00
}
2023-08-07 22:35:25 +02:00
export class ThumbnailConfig extends BaseConfig {
2023-09-03 08:21:51 +02:00
getBaseInputOptions ( ) : string [ ] {
return [ '-ss 00:00:00' , '-sws_flags accurate_rnd+bitexact+full_chroma_int' ] ;
}
2023-08-07 22:35:25 +02:00
getBaseOutputOptions() {
2023-09-03 08:21:51 +02:00
return [ '-frames:v 1' ] ;
2023-08-07 22:35:25 +02:00
}
getPresetOptions() {
return [ ] ;
}
getBitrateOptions() {
return [ ] ;
}
2023-09-28 14:29:31 +02:00
eligibleForTwoPass() {
return false ;
}
2023-08-29 11:01:42 +02:00
getScaling ( videoStream : VideoStreamInfo ) {
let options = super . getScaling ( videoStream ) ;
2023-09-03 08:21:51 +02:00
options += ':flags=lanczos+accurate_rnd+bitexact+full_chroma_int' ;
2023-08-29 11:01:42 +02:00
if ( ! this . shouldToneMap ( videoStream ) ) {
2023-09-03 08:21:51 +02:00
options += ':out_color_matrix=601:out_range=pc' ;
2023-08-07 22:35:25 +02:00
}
return options ;
}
getColors() {
return {
2023-09-03 08:21:51 +02:00
primaries : 'bt709' ,
2023-08-07 22:35:25 +02:00
transfer : '601' ,
matrix : 'bt470bg' ,
} ;
}
}
2023-07-09 04:43:11 +02:00
export class H264Config extends BaseConfig {
getThreadOptions() {
if ( this . config . threads <= 0 ) {
return [ ] ;
}
return [
. . . super . getThreadOptions ( ) ,
'-x264-params "pools=none"' ,
` -x264-params "frame-threads= ${ this . config . threads } " ` ,
] ;
}
}
export class HEVCConfig extends BaseConfig {
getThreadOptions() {
if ( this . config . threads <= 0 ) {
return [ ] ;
}
return [
. . . super . getThreadOptions ( ) ,
'-x265-params "pools=none"' ,
` -x265-params "frame-threads= ${ this . config . threads } " ` ,
] ;
}
}
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 ( ) ;
if ( this . eligibleForTwoPass ( ) ) {
return [
` -b:v ${ bitrates . target } ${ bitrates . unit } ` ,
` -minrate ${ bitrates . min } ${ bitrates . unit } ` ,
` -maxrate ${ bitrates . max } ${ bitrates . unit } ` ,
] ;
}
2023-09-03 03:22:42 +02:00
return [ ` - ${ this . useCQP ( ) ? 'q:v' : 'crf' } ${ this . config . crf } ` , ` -b:v ${ bitrates . max } ${ bitrates . unit } ` ] ;
2023-07-09 04:43:11 +02:00
}
getThreadOptions() {
return [ '-row-mt 1' , . . . super . getThreadOptions ( ) ] ;
}
}
2023-08-02 03:56:10 +02:00
export class NVENCConfig extends BaseHWConfig {
getSupportedCodecs() {
return [ VideoCodec . H264 , VideoCodec . HEVC ] ;
}
getBaseInputOptions() {
return [ '-init_hw_device cuda=cuda:0' , '-filter_hw_device cuda' ] ;
}
2023-09-03 03:22:42 +02:00
getBaseOutputOptions ( videoStream : VideoStreamInfo , audioStream? : AudioStreamInfo ) {
const options = [
2023-08-02 03:56:10 +02: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' ,
2023-08-29 11:01:42 +02:00
. . . super . getBaseOutputOptions ( videoStream , audioStream ) ,
2023-08-02 03:56:10 +02:00
] ;
2023-09-03 03:22:42 +02:00
if ( this . getBFrames ( ) > 0 ) {
options . push ( '-b_ref_mode middle' ) ;
options . push ( '-b_qfactor 1.1' ) ;
}
if ( this . config . temporalAQ ) {
options . push ( '-temporal-aq 1' ) ;
}
return options ;
2023-08-02 03:56:10 +02:00
}
2023-08-29 11:01:42 +02:00
getFilterOptions ( videoStream : VideoStreamInfo ) {
const options = this . shouldToneMap ( videoStream ) ? this . getToneMapping ( ) : [ ] ;
2023-08-07 22:35:25 +02:00
options . push ( 'format=nv12' , 'hwupload_cuda' ) ;
2023-08-29 11:01:42 +02:00
if ( this . shouldScale ( videoStream ) ) {
options . push ( ` scale_cuda= ${ this . getScaling ( videoStream ) } ` ) ;
2023-08-02 03:56:10 +02: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-03 03:22:42 +02:00
getRefs() {
const bframes = this . getBFrames ( ) ;
if ( bframes > 0 && bframes < 3 && this . config . refs < 3 ) {
return 0 ;
}
return this . config . refs ;
}
2023-08-02 03:56:10 +02:00
}
export class QSVConfig extends BaseHWConfig {
getBaseInputOptions() {
if ( ! this . devices . length ) {
throw Error ( 'No QSV device found' ) ;
}
return [ '-init_hw_device qsv=hw' , '-filter_hw_device hw' ] ;
}
2023-09-03 03:22:42 +02:00
getBaseOutputOptions ( videoStream : VideoStreamInfo , audioStream? : AudioStreamInfo ) {
const options = super . getBaseOutputOptions ( videoStream , audioStream ) ;
2023-08-02 03:56:10 +02: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 11:01:42 +02:00
getFilterOptions ( videoStream : VideoStreamInfo ) {
const options = this . shouldToneMap ( videoStream ) ? this . getToneMapping ( ) : [ ] ;
2023-08-07 22:35:25 +02:00
options . push ( 'format=nv12' , 'hwupload=extra_hw_frames=64' ) ;
2023-08-29 11:01:42 +02:00
if ( this . shouldScale ( videoStream ) ) {
options . push ( ` scale_qsv= ${ this . getScaling ( videoStream ) } ` ) ;
2023-08-02 03:56:10 +02: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 = [ ] ;
2023-09-03 03:22:42 +02:00
options . push ( ` - ${ this . useCQP ( ) ? 'q:v' : 'global_quality' } ${ this . config . crf } ` ) ;
2023-08-02 03:56:10 +02:00
const bitrates = this . getBitrateDistribution ( ) ;
if ( bitrates . max > 0 ) {
options . push ( ` -maxrate ${ bitrates . max } ${ bitrates . unit } ` ) ;
options . push ( ` -bufsize ${ bitrates . max * 2 } ${ bitrates . unit } ` ) ;
}
return options ;
}
2023-09-03 03:22:42 +02: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 ;
}
2023-08-02 03:56:10 +02:00
}
export class VAAPIConfig extends BaseHWConfig {
getBaseInputOptions() {
if ( this . devices . length === 0 ) {
throw Error ( 'No VAAPI device found' ) ;
}
return [ ` -init_hw_device vaapi=accel:/dev/dri/ ${ this . devices [ 0 ] } ` , '-filter_hw_device accel' ] ;
}
2023-08-29 11:01:42 +02:00
getFilterOptions ( videoStream : VideoStreamInfo ) {
const options = this . shouldToneMap ( videoStream ) ? this . getToneMapping ( ) : [ ] ;
2023-08-07 22:35:25 +02:00
options . push ( 'format=nv12' , 'hwupload' ) ;
2023-08-29 11:01:42 +02:00
if ( this . shouldScale ( videoStream ) ) {
options . push ( ` scale_vaapi= ${ this . getScaling ( videoStream ) } ` ) ;
2023-08-02 03:56:10 +02: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-03 03:22:42 +02:00
const options = [ ] ;
if ( this . config . targetVideoCodec === VideoCodec . VP9 ) {
options . push ( '-bsf:v vp9_raw_reorder,vp9_superframe' ) ;
}
2023-08-02 03:56:10 +02:00
// VAAPI doesn't allow setting both quality and max bitrate
if ( bitrates . max > 0 ) {
2023-09-03 03:22:42 +02:00
options . push (
2023-08-02 03:56:10 +02:00
` -b:v ${ bitrates . target } ${ bitrates . unit } ` ,
` -maxrate ${ bitrates . max } ${ bitrates . unit } ` ,
` -minrate ${ bitrates . min } ${ bitrates . unit } ` ,
'-rc_mode 3' ,
2023-09-03 03:22:42 +02:00
) ; // variable bitrate
} else if ( this . useCQP ( ) ) {
options . push ( ` -qp ${ this . config . crf } ` , ` -global_quality ${ this . config . crf } ` , '-rc_mode 1' ) ;
2023-08-02 03:56:10 +02:00
} else {
2023-09-03 03:22:42 +02:00
options . push ( ` -global_quality ${ this . config . crf } ` , '-rc_mode 4' ) ;
2023-08-02 03:56:10 +02:00
}
2023-09-03 03:22:42 +02:00
return options ;
}
useCQP() {
return this . config . cqMode !== CQMode . ICQ || this . config . targetVideoCodec === VideoCodec . VP9 ;
2023-08-02 03:56:10 +02:00
}
}