1 import { MutexInterface } from 'async-mutex'
2 import { Job } from 'bullmq'
3 import { FfmpegCommand } from 'fluent-ffmpeg'
4 import { readFile, writeFile } from 'fs-extra'
5 import { dirname } from 'path'
6 import { VIDEO_TRANSCODING_FPS } from '@server/initializers/constants'
7 import { pick } from '@shared/core-utils'
8 import { AvailableEncoders, VideoResolution } from '@shared/models'
9 import { logger, loggerTagsFactory } from '../logger'
10 import { getFFmpeg, runCommand } from './ffmpeg-commons'
11 import { presetCopy, presetOnlyAudio, presetVOD } from './ffmpeg-presets'
12 import { computeFPS, ffprobePromise, getVideoStreamDimensionsInfo, getVideoStreamFPS } from './ffprobe-utils'
14 const lTags = loggerTagsFactory('ffmpeg')
16 // ---------------------------------------------------------------------------
18 type TranscodeVODOptionsType = 'hls' | 'hls-from-ts' | 'quick-transcode' | 'video' | 'merge-audio' | 'only-audio'
20 interface BaseTranscodeVODOptions {
21 type: TranscodeVODOptionsType
26 // Will be released after the ffmpeg started
27 // To prevent a bug where the input file does not exist anymore when running ffmpeg
28 inputFileMutexReleaser: MutexInterface.Releaser
30 availableEncoders: AvailableEncoders
38 interface HLSTranscodeOptions extends BaseTranscodeVODOptions {
46 interface HLSFromTSTranscodeOptions extends BaseTranscodeVODOptions {
56 interface QuickTranscodeOptions extends BaseTranscodeVODOptions {
57 type: 'quick-transcode'
60 interface VideoTranscodeOptions extends BaseTranscodeVODOptions {
64 interface MergeAudioTranscodeOptions extends BaseTranscodeVODOptions {
69 interface OnlyAudioTranscodeOptions extends BaseTranscodeVODOptions {
73 type TranscodeVODOptions =
75 | HLSFromTSTranscodeOptions
76 | VideoTranscodeOptions
77 | MergeAudioTranscodeOptions
78 | OnlyAudioTranscodeOptions
79 | QuickTranscodeOptions
81 // ---------------------------------------------------------------------------
84 [ type in TranscodeVODOptionsType ]: (c: FfmpegCommand, o?: TranscodeVODOptions) => Promise<FfmpegCommand> | FfmpegCommand
86 'quick-transcode': buildQuickTranscodeCommand,
87 'hls': buildHLSVODCommand,
88 'hls-from-ts': buildHLSVODFromTSCommand,
89 'merge-audio': buildAudioMergeCommand,
90 'only-audio': buildOnlyAudioCommand,
91 'video': buildVODCommand
94 async function transcodeVOD (options: TranscodeVODOptions) {
95 logger.debug('Will run transcode.', { options, ...lTags() })
97 let command = getFFmpeg(options.inputPath, 'vod')
98 .output(options.outputPath)
100 command = await builders[options.type](command, options)
102 command.on('start', () => {
104 options.inputFileMutexReleaser()
108 await runCommand({ command, job: options.job })
110 await fixHLSPlaylistIfNeeded(options)
113 // ---------------------------------------------------------------------------
121 TranscodeVODOptionsType
124 // ---------------------------------------------------------------------------
126 async function buildVODCommand (command: FfmpegCommand, options: TranscodeVODOptions) {
127 const probe = await ffprobePromise(options.inputPath)
129 let fps = await getVideoStreamFPS(options.inputPath, probe)
130 fps = computeFPS(fps, options.resolution)
132 let scaleFilterValue: string
134 if (options.resolution !== undefined) {
135 const videoStreamInfo = await getVideoStreamDimensionsInfo(options.inputPath, probe)
137 scaleFilterValue = videoStreamInfo?.isPortraitMode === true
138 ? `w=${options.resolution}:h=-2`
139 : `w=-2:h=${options.resolution}`
142 command = await presetVOD({
143 ...pick(options, [ 'resolution', 'availableEncoders', 'profile' ]),
146 input: options.inputPath,
156 function buildQuickTranscodeCommand (command: FfmpegCommand) {
157 command = presetCopy(command)
159 command = command.outputOption('-map_metadata -1') // strip all metadata
160 .outputOption('-movflags faststart')
165 // ---------------------------------------------------------------------------
167 // ---------------------------------------------------------------------------
169 async function buildAudioMergeCommand (command: FfmpegCommand, options: MergeAudioTranscodeOptions) {
170 command = command.loop(undefined)
172 const scaleFilterValue = getMergeAudioScaleFilterValue()
173 command = await presetVOD({
174 ...pick(options, [ 'resolution', 'availableEncoders', 'profile' ]),
177 input: options.audioPath,
180 fps: VIDEO_TRANSCODING_FPS.AUDIO_MERGE,
184 command.outputOption('-preset:v veryfast')
186 command = command.input(options.audioPath)
187 .outputOption('-tune stillimage')
188 .outputOption('-shortest')
193 function buildOnlyAudioCommand (command: FfmpegCommand, _options: OnlyAudioTranscodeOptions) {
194 command = presetOnlyAudio(command)
199 // ---------------------------------------------------------------------------
201 // ---------------------------------------------------------------------------
203 async function buildHLSVODCommand (command: FfmpegCommand, options: HLSTranscodeOptions) {
204 const videoPath = getHLSVideoPath(options)
206 if (options.copyCodecs) command = presetCopy(command)
207 else if (options.resolution === VideoResolution.H_NOVIDEO) command = presetOnlyAudio(command)
208 else command = await buildVODCommand(command, options)
210 addCommonHLSVODCommandOptions(command, videoPath)
215 function buildHLSVODFromTSCommand (command: FfmpegCommand, options: HLSFromTSTranscodeOptions) {
216 const videoPath = getHLSVideoPath(options)
218 command.outputOption('-c copy')
221 // Required for example when copying an AAC stream from an MPEG-TS
222 // Since it's a bitstream filter, we don't need to reencode the audio
223 command.outputOption('-bsf:a aac_adtstoasc')
226 addCommonHLSVODCommandOptions(command, videoPath)
231 function addCommonHLSVODCommandOptions (command: FfmpegCommand, outputPath: string) {
232 return command.outputOption('-hls_time 4')
233 .outputOption('-hls_list_size 0')
234 .outputOption('-hls_playlist_type vod')
235 .outputOption('-hls_segment_filename ' + outputPath)
236 .outputOption('-hls_segment_type fmp4')
237 .outputOption('-f hls')
238 .outputOption('-hls_flags single_file')
241 async function fixHLSPlaylistIfNeeded (options: TranscodeVODOptions) {
242 if (options.type !== 'hls' && options.type !== 'hls-from-ts') return
244 const fileContent = await readFile(options.outputPath)
246 const videoFileName = options.hlsPlaylist.videoFilename
247 const videoFilePath = getHLSVideoPath(options)
249 // Fix wrong mapping with some ffmpeg versions
250 const newContent = fileContent.toString()
251 .replace(`#EXT-X-MAP:URI="${videoFilePath}",`, `#EXT-X-MAP:URI="${videoFileName}",`)
253 await writeFile(options.outputPath, newContent)
256 // ---------------------------------------------------------------------------
258 // ---------------------------------------------------------------------------
260 function getHLSVideoPath (options: HLSTranscodeOptions | HLSFromTSTranscodeOptions) {
261 return `${dirname(options.outputPath)}/${options.hlsPlaylist.videoFilename}`
264 // Avoid "height not divisible by 2" error
265 function getMergeAudioScaleFilterValue () {
266 return 'trunc(iw/2)*2:trunc(ih/2)*2'