]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/helpers/ffmpeg/ffmpeg-vod.ts
Merge branch 'release/4.1.0' into develop
[github/Chocobozzz/PeerTube.git] / server / helpers / ffmpeg / ffmpeg-vod.ts
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'
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 isPortraitMode?: boolean
31
32 job?: Job
33 }
34
35 interface HLSTranscodeOptions extends BaseTranscodeVODOptions {
36 type: 'hls'
37 copyCodecs: boolean
38 hlsPlaylist: {
39 videoFilename: string
40 }
41 }
42
43 interface HLSFromTSTranscodeOptions extends BaseTranscodeVODOptions {
44 type: 'hls-from-ts'
45
46 isAAC: boolean
47
48 hlsPlaylist: {
49 videoFilename: string
50 }
51 }
52
53 interface QuickTranscodeOptions extends BaseTranscodeVODOptions {
54 type: 'quick-transcode'
55 }
56
57 interface VideoTranscodeOptions extends BaseTranscodeVODOptions {
58 type: 'video'
59 }
60
61 interface MergeAudioTranscodeOptions extends BaseTranscodeVODOptions {
62 type: 'merge-audio'
63 audioPath: string
64 }
65
66 interface OnlyAudioTranscodeOptions extends BaseTranscodeVODOptions {
67 type: 'only-audio'
68 }
69
70 type TranscodeVODOptions =
71 HLSTranscodeOptions
72 | HLSFromTSTranscodeOptions
73 | VideoTranscodeOptions
74 | MergeAudioTranscodeOptions
75 | OnlyAudioTranscodeOptions
76 | QuickTranscodeOptions
77
78 // ---------------------------------------------------------------------------
79
80 const builders: {
81 [ type in TranscodeVODOptionsType ]: (c: FfmpegCommand, o?: TranscodeVODOptions) => Promise<FfmpegCommand> | FfmpegCommand
82 } = {
83 'quick-transcode': buildQuickTranscodeCommand,
84 'hls': buildHLSVODCommand,
85 'hls-from-ts': buildHLSVODFromTSCommand,
86 'merge-audio': buildAudioMergeCommand,
87 'only-audio': buildOnlyAudioCommand,
88 'video': buildVODCommand
89 }
90
91 async function transcodeVOD (options: TranscodeVODOptions) {
92 logger.debug('Will run transcode.', { options, ...lTags() })
93
94 let command = getFFmpeg(options.inputPath, 'vod')
95 .output(options.outputPath)
96
97 command = await builders[options.type](command, options)
98
99 await runCommand({ command, job: options.job })
100
101 await fixHLSPlaylistIfNeeded(options)
102 }
103
104 // ---------------------------------------------------------------------------
105
106 export {
107 transcodeVOD,
108
109 buildVODCommand,
110
111 TranscodeVODOptions,
112 TranscodeVODOptionsType
113 }
114
115 // ---------------------------------------------------------------------------
116
117 async function buildVODCommand (command: FfmpegCommand, options: TranscodeVODOptions) {
118 let fps = await getVideoStreamFPS(options.inputPath)
119 fps = computeFPS(fps, options.resolution)
120
121 let scaleFilterValue: string
122
123 if (options.resolution !== undefined) {
124 scaleFilterValue = options.isPortraitMode === true
125 ? `w=${options.resolution}:h=-2`
126 : `w=-2:h=${options.resolution}`
127 }
128
129 command = await presetVOD({
130 ...pick(options, [ 'resolution', 'availableEncoders', 'profile' ]),
131
132 command,
133 input: options.inputPath,
134 canCopyAudio: true,
135 canCopyVideo: true,
136 fps,
137 scaleFilterValue
138 })
139
140 return command
141 }
142
143 function buildQuickTranscodeCommand (command: FfmpegCommand) {
144 command = presetCopy(command)
145
146 command = command.outputOption('-map_metadata -1') // strip all metadata
147 .outputOption('-movflags faststart')
148
149 return command
150 }
151
152 // ---------------------------------------------------------------------------
153 // Audio transcoding
154 // ---------------------------------------------------------------------------
155
156 async function buildAudioMergeCommand (command: FfmpegCommand, options: MergeAudioTranscodeOptions) {
157 command = command.loop(undefined)
158
159 const scaleFilterValue = getMergeAudioScaleFilterValue()
160 command = await presetVOD({
161 ...pick(options, [ 'resolution', 'availableEncoders', 'profile' ]),
162
163 command,
164 input: options.audioPath,
165 canCopyAudio: true,
166 canCopyVideo: true,
167 fps: VIDEO_TRANSCODING_FPS.AUDIO_MERGE,
168 scaleFilterValue
169 })
170
171 command.outputOption('-preset:v veryfast')
172
173 command = command.input(options.audioPath)
174 .outputOption('-tune stillimage')
175 .outputOption('-shortest')
176
177 return command
178 }
179
180 function buildOnlyAudioCommand (command: FfmpegCommand, _options: OnlyAudioTranscodeOptions) {
181 command = presetOnlyAudio(command)
182
183 return command
184 }
185
186 // ---------------------------------------------------------------------------
187 // HLS transcoding
188 // ---------------------------------------------------------------------------
189
190 async function buildHLSVODCommand (command: FfmpegCommand, options: HLSTranscodeOptions) {
191 const videoPath = getHLSVideoPath(options)
192
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)
196
197 addCommonHLSVODCommandOptions(command, videoPath)
198
199 return command
200 }
201
202 function buildHLSVODFromTSCommand (command: FfmpegCommand, options: HLSFromTSTranscodeOptions) {
203 const videoPath = getHLSVideoPath(options)
204
205 command.outputOption('-c copy')
206
207 if (options.isAAC) {
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')
211 }
212
213 addCommonHLSVODCommandOptions(command, videoPath)
214
215 return command
216 }
217
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')
226 }
227
228 async function fixHLSPlaylistIfNeeded (options: TranscodeVODOptions) {
229 if (options.type !== 'hls' && options.type !== 'hls-from-ts') return
230
231 const fileContent = await readFile(options.outputPath)
232
233 const videoFileName = options.hlsPlaylist.videoFilename
234 const videoFilePath = getHLSVideoPath(options)
235
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}",`)
239
240 await writeFile(options.outputPath, newContent)
241 }
242
243 // ---------------------------------------------------------------------------
244 // Helpers
245 // ---------------------------------------------------------------------------
246
247 function getHLSVideoPath (options: HLSTranscodeOptions | HLSFromTSTranscodeOptions) {
248 return `${dirname(options.outputPath)}/${options.hlsPlaylist.videoFilename}`
249 }
250
251 // Avoid "height not divisible by 2" error
252 function getMergeAudioScaleFilterValue () {
253 return 'trunc(iw/2)*2:trunc(ih/2)*2'
254 }