]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/helpers/ffmpeg/ffmpeg-vod.ts
Merge branch 'release/4.3.0' into develop
[github/Chocobozzz/PeerTube.git] / server / helpers / ffmpeg / ffmpeg-vod.ts
1 import { Job } from 'bullmq'
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, ffprobePromise, getVideoStreamDimensionsInfo, getVideoStreamFPS } from './ffprobe-utils'
11 import { VIDEO_TRANSCODING_FPS } from '@server/initializers/constants'
12
13 const lTags = loggerTagsFactory('ffmpeg')
14
15 // ---------------------------------------------------------------------------
16
17 type TranscodeVODOptionsType = 'hls' | 'hls-from-ts' | 'quick-transcode' | 'video' | 'merge-audio' | 'only-audio'
18
19 interface BaseTranscodeVODOptions {
20 type: TranscodeVODOptionsType
21
22 inputPath: string
23 outputPath: string
24
25 availableEncoders: AvailableEncoders
26 profile: string
27
28 resolution: number
29
30 job?: Job
31 }
32
33 interface HLSTranscodeOptions extends BaseTranscodeVODOptions {
34 type: 'hls'
35 copyCodecs: boolean
36 hlsPlaylist: {
37 videoFilename: string
38 }
39 }
40
41 interface HLSFromTSTranscodeOptions extends BaseTranscodeVODOptions {
42 type: 'hls-from-ts'
43
44 isAAC: boolean
45
46 hlsPlaylist: {
47 videoFilename: string
48 }
49 }
50
51 interface QuickTranscodeOptions extends BaseTranscodeVODOptions {
52 type: 'quick-transcode'
53 }
54
55 interface VideoTranscodeOptions extends BaseTranscodeVODOptions {
56 type: 'video'
57 }
58
59 interface MergeAudioTranscodeOptions extends BaseTranscodeVODOptions {
60 type: 'merge-audio'
61 audioPath: string
62 }
63
64 interface OnlyAudioTranscodeOptions extends BaseTranscodeVODOptions {
65 type: 'only-audio'
66 }
67
68 type TranscodeVODOptions =
69 HLSTranscodeOptions
70 | HLSFromTSTranscodeOptions
71 | VideoTranscodeOptions
72 | MergeAudioTranscodeOptions
73 | OnlyAudioTranscodeOptions
74 | QuickTranscodeOptions
75
76 // ---------------------------------------------------------------------------
77
78 const builders: {
79 [ type in TranscodeVODOptionsType ]: (c: FfmpegCommand, o?: TranscodeVODOptions) => Promise<FfmpegCommand> | FfmpegCommand
80 } = {
81 'quick-transcode': buildQuickTranscodeCommand,
82 'hls': buildHLSVODCommand,
83 'hls-from-ts': buildHLSVODFromTSCommand,
84 'merge-audio': buildAudioMergeCommand,
85 'only-audio': buildOnlyAudioCommand,
86 'video': buildVODCommand
87 }
88
89 async function transcodeVOD (options: TranscodeVODOptions) {
90 logger.debug('Will run transcode.', { options, ...lTags() })
91
92 let command = getFFmpeg(options.inputPath, 'vod')
93 .output(options.outputPath)
94
95 command = await builders[options.type](command, options)
96
97 await runCommand({ command, job: options.job })
98
99 await fixHLSPlaylistIfNeeded(options)
100 }
101
102 // ---------------------------------------------------------------------------
103
104 export {
105 transcodeVOD,
106
107 buildVODCommand,
108
109 TranscodeVODOptions,
110 TranscodeVODOptionsType
111 }
112
113 // ---------------------------------------------------------------------------
114
115 async function buildVODCommand (command: FfmpegCommand, options: TranscodeVODOptions) {
116 const probe = await ffprobePromise(options.inputPath)
117
118 let fps = await getVideoStreamFPS(options.inputPath, probe)
119 fps = computeFPS(fps, options.resolution)
120
121 let scaleFilterValue: string
122
123 if (options.resolution !== undefined) {
124 const videoStreamInfo = await getVideoStreamDimensionsInfo(options.inputPath, probe)
125
126 scaleFilterValue = videoStreamInfo?.isPortraitMode === true
127 ? `w=${options.resolution}:h=-2`
128 : `w=-2:h=${options.resolution}`
129 }
130
131 command = await presetVOD({
132 ...pick(options, [ 'resolution', 'availableEncoders', 'profile' ]),
133
134 command,
135 input: options.inputPath,
136 canCopyAudio: true,
137 canCopyVideo: true,
138 fps,
139 scaleFilterValue
140 })
141
142 return command
143 }
144
145 function buildQuickTranscodeCommand (command: FfmpegCommand) {
146 command = presetCopy(command)
147
148 command = command.outputOption('-map_metadata -1') // strip all metadata
149 .outputOption('-movflags faststart')
150
151 return command
152 }
153
154 // ---------------------------------------------------------------------------
155 // Audio transcoding
156 // ---------------------------------------------------------------------------
157
158 async function buildAudioMergeCommand (command: FfmpegCommand, options: MergeAudioTranscodeOptions) {
159 command = command.loop(undefined)
160
161 const scaleFilterValue = getMergeAudioScaleFilterValue()
162 command = await presetVOD({
163 ...pick(options, [ 'resolution', 'availableEncoders', 'profile' ]),
164
165 command,
166 input: options.audioPath,
167 canCopyAudio: true,
168 canCopyVideo: true,
169 fps: VIDEO_TRANSCODING_FPS.AUDIO_MERGE,
170 scaleFilterValue
171 })
172
173 command.outputOption('-preset:v veryfast')
174
175 command = command.input(options.audioPath)
176 .outputOption('-tune stillimage')
177 .outputOption('-shortest')
178
179 return command
180 }
181
182 function buildOnlyAudioCommand (command: FfmpegCommand, _options: OnlyAudioTranscodeOptions) {
183 command = presetOnlyAudio(command)
184
185 return command
186 }
187
188 // ---------------------------------------------------------------------------
189 // HLS transcoding
190 // ---------------------------------------------------------------------------
191
192 async function buildHLSVODCommand (command: FfmpegCommand, options: HLSTranscodeOptions) {
193 const videoPath = getHLSVideoPath(options)
194
195 if (options.copyCodecs) command = presetCopy(command)
196 else if (options.resolution === VideoResolution.H_NOVIDEO) command = presetOnlyAudio(command)
197 else command = await buildVODCommand(command, options)
198
199 addCommonHLSVODCommandOptions(command, videoPath)
200
201 return command
202 }
203
204 function buildHLSVODFromTSCommand (command: FfmpegCommand, options: HLSFromTSTranscodeOptions) {
205 const videoPath = getHLSVideoPath(options)
206
207 command.outputOption('-c copy')
208
209 if (options.isAAC) {
210 // Required for example when copying an AAC stream from an MPEG-TS
211 // Since it's a bitstream filter, we don't need to reencode the audio
212 command.outputOption('-bsf:a aac_adtstoasc')
213 }
214
215 addCommonHLSVODCommandOptions(command, videoPath)
216
217 return command
218 }
219
220 function addCommonHLSVODCommandOptions (command: FfmpegCommand, outputPath: string) {
221 return command.outputOption('-hls_time 4')
222 .outputOption('-hls_list_size 0')
223 .outputOption('-hls_playlist_type vod')
224 .outputOption('-hls_segment_filename ' + outputPath)
225 .outputOption('-hls_segment_type fmp4')
226 .outputOption('-f hls')
227 .outputOption('-hls_flags single_file')
228 }
229
230 async function fixHLSPlaylistIfNeeded (options: TranscodeVODOptions) {
231 if (options.type !== 'hls' && options.type !== 'hls-from-ts') return
232
233 const fileContent = await readFile(options.outputPath)
234
235 const videoFileName = options.hlsPlaylist.videoFilename
236 const videoFilePath = getHLSVideoPath(options)
237
238 // Fix wrong mapping with some ffmpeg versions
239 const newContent = fileContent.toString()
240 .replace(`#EXT-X-MAP:URI="${videoFilePath}",`, `#EXT-X-MAP:URI="${videoFileName}",`)
241
242 await writeFile(options.outputPath, newContent)
243 }
244
245 // ---------------------------------------------------------------------------
246 // Helpers
247 // ---------------------------------------------------------------------------
248
249 function getHLSVideoPath (options: HLSTranscodeOptions | HLSFromTSTranscodeOptions) {
250 return `${dirname(options.outputPath)}/${options.hlsPlaylist.videoFilename}`
251 }
252
253 // Avoid "height not divisible by 2" error
254 function getMergeAudioScaleFilterValue () {
255 return 'trunc(iw/2)*2:trunc(ih/2)*2'
256 }