]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/helpers/ffmpeg/ffmpeg-vod.ts
Add basic video editor support
[github/Chocobozzz/PeerTube.git] / server / helpers / ffmpeg / ffmpeg-vod.ts
CommitLineData
c729caf6
C
1import { Job } from 'bull'
2import { FfmpegCommand } from 'fluent-ffmpeg'
3import { readFile, writeFile } from 'fs-extra'
4import { dirname } from 'path'
5import { pick } from '@shared/core-utils'
6import { AvailableEncoders, VideoResolution } from '@shared/models'
7import { logger, loggerTagsFactory } from '../logger'
8import { getFFmpeg, runCommand } from './ffmpeg-commons'
9import { presetCopy, presetOnlyAudio, presetVOD } from './ffmpeg-presets'
10import { computeFPS, getVideoStreamFPS } from './ffprobe-utils'
11import { VIDEO_TRANSCODING_FPS } from '@server/initializers/constants'
12
13const lTags = loggerTagsFactory('ffmpeg')
14
15// ---------------------------------------------------------------------------
16
17type TranscodeVODOptionsType = 'hls' | 'hls-from-ts' | 'quick-transcode' | 'video' | 'merge-audio' | 'only-audio'
18
19interface 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
35interface HLSTranscodeOptions extends BaseTranscodeVODOptions {
36 type: 'hls'
37 copyCodecs: boolean
38 hlsPlaylist: {
39 videoFilename: string
40 }
41}
42
43interface HLSFromTSTranscodeOptions extends BaseTranscodeVODOptions {
44 type: 'hls-from-ts'
45
46 isAAC: boolean
47
48 hlsPlaylist: {
49 videoFilename: string
50 }
51}
52
53interface QuickTranscodeOptions extends BaseTranscodeVODOptions {
54 type: 'quick-transcode'
55}
56
57interface VideoTranscodeOptions extends BaseTranscodeVODOptions {
58 type: 'video'
59}
60
61interface MergeAudioTranscodeOptions extends BaseTranscodeVODOptions {
62 type: 'merge-audio'
63 audioPath: string
64}
65
66interface OnlyAudioTranscodeOptions extends BaseTranscodeVODOptions {
67 type: 'only-audio'
68}
69
70type TranscodeVODOptions =
71 HLSTranscodeOptions
72 | HLSFromTSTranscodeOptions
73 | VideoTranscodeOptions
74 | MergeAudioTranscodeOptions
75 | OnlyAudioTranscodeOptions
76 | QuickTranscodeOptions
77
78// ---------------------------------------------------------------------------
79
80const 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
91async 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
106export {
107 transcodeVOD,
108
109 buildVODCommand,
110
111 TranscodeVODOptions,
112 TranscodeVODOptionsType
113}
114
115// ---------------------------------------------------------------------------
116
117async 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
143function 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
156async 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
180function buildOnlyAudioCommand (command: FfmpegCommand, _options: OnlyAudioTranscodeOptions) {
181 command = presetOnlyAudio(command)
182
183 return command
184}
185
186// ---------------------------------------------------------------------------
187// HLS transcoding
188// ---------------------------------------------------------------------------
189
190async 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
202function 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
218function 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
228async 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
247function getHLSVideoPath (options: HLSTranscodeOptions | HLSFromTSTranscodeOptions) {
248 return `${dirname(options.outputPath)}/${options.hlsPlaylist.videoFilename}`
249}
250
251// Avoid "height not divisible by 2" error
252function getMergeAudioScaleFilterValue () {
253 return 'trunc(iw/2)*2:trunc(ih/2)*2'
254}