aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/helpers/ffmpeg/ffmpeg-presets.ts
blob: d1160a4a265e316ce2891c0f57d51af8ec0c40c1 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
import { FfmpegCommand } from 'fluent-ffmpeg'
import { logger, loggerTagsFactory } from '@server/helpers/logger'
import { pick } from '@shared/core-utils'
import { AvailableEncoders, EncoderOptions } from '@shared/models'
import { buildStreamSuffix, getScaleFilter, StreamType } from './ffmpeg-commons'
import { getEncoderBuilderResult } from './ffmpeg-encoders'
import { ffprobePromise, getVideoStreamBitrate, getVideoStreamDimensionsInfo, hasAudioStream } from './ffprobe-utils'

const lTags = loggerTagsFactory('ffmpeg')

// ---------------------------------------------------------------------------

function addDefaultEncoderGlobalParams (command: FfmpegCommand) {
  // avoid issues when transcoding some files: https://trac.ffmpeg.org/ticket/6375
  command.outputOption('-max_muxing_queue_size 1024')
         // strip all metadata
         .outputOption('-map_metadata -1')
         // allows import of source material with incompatible pixel formats (e.g. MJPEG video)
         .outputOption('-pix_fmt yuv420p')
}

function addDefaultEncoderParams (options: {
  command: FfmpegCommand
  encoder: 'libx264' | string
  fps: number

  streamNum?: number
}) {
  const { command, encoder, fps, streamNum } = options

  if (encoder === 'libx264') {
    // 3.1 is the minimal resource allocation for our highest supported resolution
    command.outputOption(buildStreamSuffix('-level:v', streamNum) + ' 3.1')

    if (fps) {
      // Keyframe interval of 2 seconds for faster seeking and resolution switching.
      // https://streaminglearningcenter.com/blogs/whats-the-right-keyframe-interval.html
      // https://superuser.com/a/908325
      command.outputOption(buildStreamSuffix('-g:v', streamNum) + ' ' + (fps * 2))
    }
  }
}

// ---------------------------------------------------------------------------

async function presetVOD (options: {
  command: FfmpegCommand
  input: string

  availableEncoders: AvailableEncoders
  profile: string

  canCopyAudio: boolean
  canCopyVideo: boolean

  resolution: number
  fps: number

  scaleFilterValue?: string
}) {
  const { command, input, profile, resolution, fps, scaleFilterValue } = options

  let localCommand = command
    .format('mp4')
    .outputOption('-movflags faststart')

  addDefaultEncoderGlobalParams(command)

  const probe = await ffprobePromise(input)

  // Audio encoder
  const bitrate = await getVideoStreamBitrate(input, probe)
  const videoStreamDimensions = await getVideoStreamDimensionsInfo(input, probe)

  let streamsToProcess: StreamType[] = [ 'audio', 'video' ]

  if (!await hasAudioStream(input, probe)) {
    localCommand = localCommand.noAudio()
    streamsToProcess = [ 'video' ]
  }

  for (const streamType of streamsToProcess) {
    const builderResult = await getEncoderBuilderResult({
      ...pick(options, [ 'availableEncoders', 'canCopyAudio', 'canCopyVideo' ]),

      input,
      inputBitrate: bitrate,
      inputRatio: videoStreamDimensions?.ratio || 0,

      profile,
      resolution,
      fps,
      streamType,

      videoType: 'vod' as 'vod'
    })

    if (!builderResult) {
      throw new Error('No available encoder found for stream ' + streamType)
    }

    logger.debug(
      'Apply ffmpeg params from %s for %s stream of input %s using %s profile.',
      builderResult.encoder, streamType, input, profile,
      { builderResult, resolution, fps, ...lTags() }
    )

    if (streamType === 'video') {
      localCommand.videoCodec(builderResult.encoder)

      if (scaleFilterValue) {
        localCommand.outputOption(`-vf ${getScaleFilter(builderResult.result)}=${scaleFilterValue}`)
      }
    } else if (streamType === 'audio') {
      localCommand.audioCodec(builderResult.encoder)
    }

    applyEncoderOptions(localCommand, builderResult.result)
    addDefaultEncoderParams({ command: localCommand, encoder: builderResult.encoder, fps })
  }

  return localCommand
}

function presetCopy (command: FfmpegCommand): FfmpegCommand {
  return command
    .format('mp4')
    .videoCodec('copy')
    .audioCodec('copy')
}

function presetOnlyAudio (command: FfmpegCommand): FfmpegCommand {
  return command
    .format('mp4')
    .audioCodec('copy')
    .noVideo()
}

function applyEncoderOptions (command: FfmpegCommand, options: EncoderOptions): FfmpegCommand {
  return command
    .inputOptions(options.inputOptions ?? [])
    .outputOptions(options.outputOptions ?? [])
}

// ---------------------------------------------------------------------------

export {
  presetVOD,
  presetCopy,
  presetOnlyAudio,

  addDefaultEncoderGlobalParams,
  addDefaultEncoderParams,

  applyEncoderOptions
}