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