+async function buildAudioMergeCommand (command: FfmpegCommand, options: MergeAudioTranscodeOptions) {
+ command = command.loop(undefined)
+
+ const scaleFilterValue = getScaleCleanerValue()
+ command = await presetVideo({ command, input: options.audioPath, transcodeOptions: options, scaleFilterValue })
+
+ command.outputOption('-preset:v veryfast')
+
+ command = command.input(options.audioPath)
+ .outputOption('-tune stillimage')
+ .outputOption('-shortest')
+
+ return command
+}
+
+function buildOnlyAudioCommand (command: FfmpegCommand, _options: OnlyAudioTranscodeOptions) {
+ command = presetOnlyAudio(command)
+
+ return command
+}
+
+function buildQuickTranscodeCommand (command: FfmpegCommand) {
+ command = presetCopy(command)
+
+ command = command.outputOption('-map_metadata -1') // strip all metadata
+ .outputOption('-movflags faststart')
+
+ return command
+}
+
+function addCommonHLSVODCommandOptions (command: FfmpegCommand, outputPath: string) {
+ return command.outputOption('-hls_time 4')
+ .outputOption('-hls_list_size 0')
+ .outputOption('-hls_playlist_type vod')
+ .outputOption('-hls_segment_filename ' + outputPath)
+ .outputOption('-hls_segment_type fmp4')
+ .outputOption('-f hls')
+ .outputOption('-hls_flags single_file')
+}
+
+async function buildHLSVODCommand (command: FfmpegCommand, options: HLSTranscodeOptions) {
+ const videoPath = getHLSVideoPath(options)
+
+ if (options.copyCodecs) command = presetCopy(command)
+ else if (options.resolution === VideoResolution.H_NOVIDEO) command = presetOnlyAudio(command)
+ else command = await buildx264VODCommand(command, options)
+
+ addCommonHLSVODCommandOptions(command, videoPath)
+
+ return command
+}
+
+function buildHLSVODFromTSCommand (command: FfmpegCommand, options: HLSFromTSTranscodeOptions) {
+ const videoPath = getHLSVideoPath(options)
+
+ command.outputOption('-c copy')
+
+ if (options.isAAC) {
+ // Required for example when copying an AAC stream from an MPEG-TS
+ // Since it's a bitstream filter, we don't need to reencode the audio
+ command.outputOption('-bsf:a aac_adtstoasc')
+ }
+
+ addCommonHLSVODCommandOptions(command, videoPath)
+
+ return command
+}
+
+async function fixHLSPlaylistIfNeeded (options: TranscodeOptions) {
+ if (options.type !== 'hls' && options.type !== 'hls-from-ts') return
+
+ const fileContent = await readFile(options.outputPath)
+
+ const videoFileName = options.hlsPlaylist.videoFilename
+ const videoFilePath = getHLSVideoPath(options)
+
+ // Fix wrong mapping with some ffmpeg versions
+ const newContent = fileContent.toString()
+ .replace(`#EXT-X-MAP:URI="${videoFilePath}",`, `#EXT-X-MAP:URI="${videoFileName}",`)
+
+ await writeFile(options.outputPath, newContent)
+}
+
+function getHLSVideoPath (options: HLSTranscodeOptions | HLSFromTSTranscodeOptions) {
+ return `${dirname(options.outputPath)}/${options.hlsPlaylist.videoFilename}`
+}
+
+// ---------------------------------------------------------------------------
+// Transcoding presets
+// ---------------------------------------------------------------------------
+
+// Run encoder builder depending on available encoders
+// Try encoders by priority: if the encoder is available, run the chosen profile or fallback to the default one
+// If the default one does not exist, check the next encoder
+async function getEncoderBuilderResult (options: EncoderOptionsBuilderParams & {
+ streamType: 'video' | 'audio'
+ input: string
+
+ availableEncoders: AvailableEncoders
+ profile: string
+
+ videoType: 'vod' | 'live'
+}) {
+ const { availableEncoders, profile, streamType, videoType } = options
+
+ const encodersToTry = availableEncoders.encodersToTry[videoType][streamType]
+ const encoders = availableEncoders.available[videoType]
+
+ for (const encoder of encodersToTry) {
+ if (!(await checkFFmpegEncoders(availableEncoders)).get(encoder)) {
+ logger.debug('Encoder %s not available in ffmpeg, skipping.', encoder)
+ continue
+ }
+
+ if (!encoders[encoder]) {
+ logger.debug('Encoder %s not available in peertube encoders, skipping.', encoder)
+ continue
+ }
+
+ // An object containing available profiles for this encoder
+ const builderProfiles: EncoderProfile<EncoderOptionsBuilder> = encoders[encoder]
+ let builder = builderProfiles[profile]
+
+ if (!builder) {
+ logger.debug('Profile %s for encoder %s not available. Fallback to default.', profile, encoder)
+ builder = builderProfiles.default
+
+ if (!builder) {
+ logger.debug('Default profile for encoder %s not available. Try next available encoder.', encoder)
+ continue
+ }
+ }
+
+ const result = await builder(pick(options, [ 'input', 'resolution', 'inputBitrate', 'fps', 'inputRatio', 'streamNum' ]))
+
+ return {
+ result,
+
+ // If we don't have output options, then copy the input stream
+ encoder: result.copy === true
+ ? 'copy'
+ : encoder
+ }
+ }
+
+ return null
+}
+
+async function presetVideo (options: {
+ command: FfmpegCommand
+ input: string
+ transcodeOptions: TranscodeOptions
+ fps?: number
+ scaleFilterValue?: string
+}) {
+ const { command, input, transcodeOptions, fps, scaleFilterValue } = options
+
+ let localCommand = command
+ .format('mp4')
+ .outputOption('-movflags faststart')
+
+ addDefaultEncoderGlobalParams({ command })
+
+ const probe = await ffprobePromise(input)
+
+ // Audio encoder
+ const parsedAudio = await getAudioStream(input, probe)
+ const bitrate = await getVideoFileBitrate(input, probe)
+ const { ratio } = await getVideoFileResolution(input, probe)
+
+ let streamsToProcess: StreamType[] = [ 'audio', 'video' ]
+
+ if (!parsedAudio.audioStream) {
+ localCommand = localCommand.noAudio()
+ streamsToProcess = [ 'video' ]
+ }
+
+ for (const streamType of streamsToProcess) {
+ const { profile, resolution, availableEncoders } = transcodeOptions
+
+ const builderResult = await getEncoderBuilderResult({
+ streamType,
+ input,
+ resolution,
+ availableEncoders,
+ profile,
+ fps,
+ inputBitrate: bitrate,
+ inputRatio: ratio,
+ 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
+ )
+
+ 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 ?? [])
+}
+
+function getScaleFilter (options: EncoderOptions): string {
+ if (options.scaleFilter) return options.scaleFilter.name
+
+ return 'scale'
+}
+
+// ---------------------------------------------------------------------------
+// Utils
+// ---------------------------------------------------------------------------
+
+function getFFmpeg (input: string, type: 'live' | 'vod') {
+ // We set cwd explicitly because ffmpeg appears to create temporary files when trancoding which fails in read-only file systems
+ const command = ffmpeg(input, {
+ niceness: type === 'live' ? FFMPEG_NICE.LIVE : FFMPEG_NICE.VOD,
+ cwd: CONFIG.STORAGE.TMP_DIR
+ })
+
+ const threads = type === 'live'
+ ? CONFIG.LIVE.TRANSCODING.THREADS
+ : CONFIG.TRANSCODING.THREADS
+
+ if (threads > 0) {
+ // If we don't set any threads ffmpeg will chose automatically
+ command.outputOption('-threads ' + threads)
+ }
+
+ return command
+}
+
+function getFFmpegVersion () {
+ return new Promise<string>((res, rej) => {
+ (ffmpeg() as any)._getFfmpegPath((err, ffmpegPath) => {