1 import { Job } from 'bull'
2 import { FfmpegCommand } from 'fluent-ffmpeg'
3 import { readFile, writeFile } from 'fs-extra'
4 import { dirname } from 'path'
5 import { pick } from '@shared/core-utils'
6 import { AvailableEncoders, VideoResolution } from '@shared/models'
7 import { logger, loggerTagsFactory } from '../logger'
8 import { getFFmpeg, runCommand } from './ffmpeg-commons'
9 import { presetCopy, presetOnlyAudio, presetVOD } from './ffmpeg-presets'
10 import { computeFPS, getVideoStreamFPS } from './ffprobe-utils'
11 import { VIDEO_TRANSCODING_FPS } from '@server/initializers/constants'
13 const lTags = loggerTagsFactory('ffmpeg')
15 // ---------------------------------------------------------------------------
17 type TranscodeVODOptionsType = 'hls' | 'hls-from-ts' | 'quick-transcode' | 'video' | 'merge-audio' | 'only-audio'
19 interface BaseTranscodeVODOptions {
20 type: TranscodeVODOptionsType
25 availableEncoders: AvailableEncoders
30 isPortraitMode?: boolean
35 interface HLSTranscodeOptions extends BaseTranscodeVODOptions {
43 interface HLSFromTSTranscodeOptions extends BaseTranscodeVODOptions {
53 interface QuickTranscodeOptions extends BaseTranscodeVODOptions {
54 type: 'quick-transcode'
57 interface VideoTranscodeOptions extends BaseTranscodeVODOptions {
61 interface MergeAudioTranscodeOptions extends BaseTranscodeVODOptions {
66 interface OnlyAudioTranscodeOptions extends BaseTranscodeVODOptions {
70 type TranscodeVODOptions =
72 | HLSFromTSTranscodeOptions
73 | VideoTranscodeOptions
74 | MergeAudioTranscodeOptions
75 | OnlyAudioTranscodeOptions
76 | QuickTranscodeOptions
78 // ---------------------------------------------------------------------------
81 [ type in TranscodeVODOptionsType ]: (c: FfmpegCommand, o?: TranscodeVODOptions) => Promise<FfmpegCommand> | FfmpegCommand
83 'quick-transcode': buildQuickTranscodeCommand,
84 'hls': buildHLSVODCommand,
85 'hls-from-ts': buildHLSVODFromTSCommand,
86 'merge-audio': buildAudioMergeCommand,
87 'only-audio': buildOnlyAudioCommand,
88 'video': buildVODCommand
91 async function transcodeVOD (options: TranscodeVODOptions) {
92 logger.debug('Will run transcode.', { options, ...lTags() })
94 let command = getFFmpeg(options.inputPath, 'vod')
95 .output(options.outputPath)
97 command = await builders[options.type](command, options)
99 await runCommand({ command, job: options.job })
101 await fixHLSPlaylistIfNeeded(options)
104 // ---------------------------------------------------------------------------
112 TranscodeVODOptionsType
115 // ---------------------------------------------------------------------------
117 async function buildVODCommand (command: FfmpegCommand, options: TranscodeVODOptions) {
118 let fps = await getVideoStreamFPS(options.inputPath)
119 fps = computeFPS(fps, options.resolution)
121 let scaleFilterValue: string
123 if (options.resolution !== undefined) {
124 scaleFilterValue = options.isPortraitMode === true
125 ? `w=${options.resolution}:h=-2`
126 : `w=-2:h=${options.resolution}`
129 command = await presetVOD({
130 ...pick(options, [ 'resolution', 'availableEncoders', 'profile' ]),
133 input: options.inputPath,
143 function buildQuickTranscodeCommand (command: FfmpegCommand) {
144 command = presetCopy(command)
146 command = command.outputOption('-map_metadata -1') // strip all metadata
147 .outputOption('-movflags faststart')
152 // ---------------------------------------------------------------------------
154 // ---------------------------------------------------------------------------
156 async function buildAudioMergeCommand (command: FfmpegCommand, options: MergeAudioTranscodeOptions) {
157 command = command.loop(undefined)
159 const scaleFilterValue = getMergeAudioScaleFilterValue()
160 command = await presetVOD({
161 ...pick(options, [ 'resolution', 'availableEncoders', 'profile' ]),
164 input: options.audioPath,
167 fps: VIDEO_TRANSCODING_FPS.AUDIO_MERGE,
171 command.outputOption('-preset:v veryfast')
173 command = command.input(options.audioPath)
174 .outputOption('-tune stillimage')
175 .outputOption('-shortest')
180 function buildOnlyAudioCommand (command: FfmpegCommand, _options: OnlyAudioTranscodeOptions) {
181 command = presetOnlyAudio(command)
186 // ---------------------------------------------------------------------------
188 // ---------------------------------------------------------------------------
190 async function buildHLSVODCommand (command: FfmpegCommand, options: HLSTranscodeOptions) {
191 const videoPath = getHLSVideoPath(options)
193 if (options.copyCodecs) command = presetCopy(command)
194 else if (options.resolution === VideoResolution.H_NOVIDEO) command = presetOnlyAudio(command)
195 else command = await buildVODCommand(command, options)
197 addCommonHLSVODCommandOptions(command, videoPath)
202 function buildHLSVODFromTSCommand (command: FfmpegCommand, options: HLSFromTSTranscodeOptions) {
203 const videoPath = getHLSVideoPath(options)
205 command.outputOption('-c copy')
208 // Required for example when copying an AAC stream from an MPEG-TS
209 // Since it's a bitstream filter, we don't need to reencode the audio
210 command.outputOption('-bsf:a aac_adtstoasc')
213 addCommonHLSVODCommandOptions(command, videoPath)
218 function addCommonHLSVODCommandOptions (command: FfmpegCommand, outputPath: string) {
219 return command.outputOption('-hls_time 4')
220 .outputOption('-hls_list_size 0')
221 .outputOption('-hls_playlist_type vod')
222 .outputOption('-hls_segment_filename ' + outputPath)
223 .outputOption('-hls_segment_type fmp4')
224 .outputOption('-f hls')
225 .outputOption('-hls_flags single_file')
228 async function fixHLSPlaylistIfNeeded (options: TranscodeVODOptions) {
229 if (options.type !== 'hls' && options.type !== 'hls-from-ts') return
231 const fileContent = await readFile(options.outputPath)
233 const videoFileName = options.hlsPlaylist.videoFilename
234 const videoFilePath = getHLSVideoPath(options)
236 // Fix wrong mapping with some ffmpeg versions
237 const newContent = fileContent.toString()
238 .replace(`#EXT-X-MAP:URI="${videoFilePath}",`, `#EXT-X-MAP:URI="${videoFileName}",`)
240 await writeFile(options.outputPath, newContent)
243 // ---------------------------------------------------------------------------
245 // ---------------------------------------------------------------------------
247 function getHLSVideoPath (options: HLSTranscodeOptions | HLSFromTSTranscodeOptions) {
248 return `${dirname(options.outputPath)}/${options.hlsPlaylist.videoFilename}`
251 // Avoid "height not divisible by 2" error
252 function getMergeAudioScaleFilterValue () {
253 return 'trunc(iw/2)*2:trunc(ih/2)*2'