]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/helpers/ffmpeg/ffmpeg-vod.ts
Fix unregister default value
[github/Chocobozzz/PeerTube.git] / server / helpers / ffmpeg / ffmpeg-vod.ts
CommitLineData
3545e72c 1import { MutexInterface } from 'async-mutex'
5a921e7b 2import { Job } from 'bullmq'
c729caf6
C
3import { FfmpegCommand } from 'fluent-ffmpeg'
4import { readFile, writeFile } from 'fs-extra'
5import { dirname } from 'path'
3545e72c 6import { VIDEO_TRANSCODING_FPS } from '@server/initializers/constants'
c729caf6
C
7import { pick } from '@shared/core-utils'
8import { AvailableEncoders, VideoResolution } from '@shared/models'
9import { logger, loggerTagsFactory } from '../logger'
10import { getFFmpeg, runCommand } from './ffmpeg-commons'
11import { presetCopy, presetOnlyAudio, presetVOD } from './ffmpeg-presets'
84cae54e 12import { computeFPS, ffprobePromise, getVideoStreamDimensionsInfo, getVideoStreamFPS } from './ffprobe-utils'
c729caf6
C
13
14const lTags = loggerTagsFactory('ffmpeg')
15
16// ---------------------------------------------------------------------------
17
18type TranscodeVODOptionsType = 'hls' | 'hls-from-ts' | 'quick-transcode' | 'video' | 'merge-audio' | 'only-audio'
19
20interface BaseTranscodeVODOptions {
21 type: TranscodeVODOptionsType
22
23 inputPath: string
24 outputPath: string
25
3545e72c
C
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
c729caf6
C
30 availableEncoders: AvailableEncoders
31 profile: string
32
33 resolution: number
34
c729caf6
C
35 job?: Job
36}
37
38interface HLSTranscodeOptions extends BaseTranscodeVODOptions {
39 type: 'hls'
40 copyCodecs: boolean
41 hlsPlaylist: {
42 videoFilename: string
43 }
44}
45
46interface HLSFromTSTranscodeOptions extends BaseTranscodeVODOptions {
47 type: 'hls-from-ts'
48
49 isAAC: boolean
50
51 hlsPlaylist: {
52 videoFilename: string
53 }
54}
55
56interface QuickTranscodeOptions extends BaseTranscodeVODOptions {
57 type: 'quick-transcode'
58}
59
60interface VideoTranscodeOptions extends BaseTranscodeVODOptions {
61 type: 'video'
62}
63
64interface MergeAudioTranscodeOptions extends BaseTranscodeVODOptions {
65 type: 'merge-audio'
66 audioPath: string
67}
68
69interface OnlyAudioTranscodeOptions extends BaseTranscodeVODOptions {
70 type: 'only-audio'
71}
72
73type TranscodeVODOptions =
74 HLSTranscodeOptions
75 | HLSFromTSTranscodeOptions
76 | VideoTranscodeOptions
77 | MergeAudioTranscodeOptions
78 | OnlyAudioTranscodeOptions
79 | QuickTranscodeOptions
80
81// ---------------------------------------------------------------------------
82
83const 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
94async 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
3545e72c
C
102 command.on('start', () => {
103 setTimeout(() => {
104 options.inputFileMutexReleaser()
105 }, 1000)
106 })
107
c729caf6
C
108 await runCommand({ command, job: options.job })
109
110 await fixHLSPlaylistIfNeeded(options)
111}
112
113// ---------------------------------------------------------------------------
114
115export {
116 transcodeVOD,
117
118 buildVODCommand,
119
120 TranscodeVODOptions,
121 TranscodeVODOptionsType
122}
123
124// ---------------------------------------------------------------------------
125
126async function buildVODCommand (command: FfmpegCommand, options: TranscodeVODOptions) {
84cae54e
C
127 const probe = await ffprobePromise(options.inputPath)
128
129 let fps = await getVideoStreamFPS(options.inputPath, probe)
c729caf6
C
130 fps = computeFPS(fps, options.resolution)
131
132 let scaleFilterValue: string
133
134 if (options.resolution !== undefined) {
84cae54e
C
135 const videoStreamInfo = await getVideoStreamDimensionsInfo(options.inputPath, probe)
136
137 scaleFilterValue = videoStreamInfo?.isPortraitMode === true
c729caf6
C
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
156function 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
169async 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
193function buildOnlyAudioCommand (command: FfmpegCommand, _options: OnlyAudioTranscodeOptions) {
194 command = presetOnlyAudio(command)
195
196 return command
197}
198
199// ---------------------------------------------------------------------------
200// HLS transcoding
201// ---------------------------------------------------------------------------
202
203async 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
215function 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
231function 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
241async 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
260function getHLSVideoPath (options: HLSTranscodeOptions | HLSFromTSTranscodeOptions) {
261 return `${dirname(options.outputPath)}/${options.hlsPlaylist.videoFilename}`
262}
263
264// Avoid "height not divisible by 2" error
265function getMergeAudioScaleFilterValue () {
266 return 'trunc(iw/2)*2:trunc(ih/2)*2'
267}