]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/helpers/ffmpeg-utils.ts
Add ability to run transcoding jobs
[github/Chocobozzz/PeerTube.git] / server / helpers / ffmpeg-utils.ts
CommitLineData
3b01f4c0 1import { Job } from 'bull'
41fb13c3 2import ffmpeg, { FfmpegCommand, FilterSpecification, getAvailableEncoders } from 'fluent-ffmpeg'
053aed43 3import { readFile, remove, writeFile } from 'fs-extra'
09209296 4import { dirname, join } from 'path'
529b3752 5import { FFMPEG_NICE, VIDEO_LIVE } from '@server/initializers/constants'
679c12e6
C
6import { pick } from '@shared/core-utils'
7import {
8 AvailableEncoders,
9 EncoderOptions,
10 EncoderOptionsBuilder,
11 EncoderOptionsBuilderParams,
12 EncoderProfile,
13 VideoResolution
14} from '../../shared/models/videos'
c6c0fa6c 15import { CONFIG } from '../initializers/config'
b7c8304c 16import { execPromise, promisify0 } from './core-utils'
679c12e6 17import { computeFPS, ffprobePromise, getAudioStream, getVideoFileBitrate, getVideoFileFPS, getVideoFileResolution } from './ffprobe-utils'
26670720 18import { processImage } from './image-utils'
93904032
C
19import { logger, loggerTagsFactory } from './logger'
20
21const lTags = loggerTagsFactory('ffmpeg')
14d3270f 22
6b67897e
C
23/**
24 *
25 * Functions that run transcoding/muxing ffmpeg processes
26 * Mainly called by lib/video-transcoding.ts and lib/live-manager.ts
27 *
28 */
29
9252a33d
C
30// ---------------------------------------------------------------------------
31// Encoder options
32// ---------------------------------------------------------------------------
33
1896bca0 34type StreamType = 'audio' | 'video'
9252a33d 35
1896bca0
C
36// ---------------------------------------------------------------------------
37// Encoders support
38// ---------------------------------------------------------------------------
9252a33d 39
1896bca0
C
40// Detect supported encoders by ffmpeg
41let supportedEncoders: Map<string, boolean>
42async function checkFFmpegEncoders (peertubeAvailableEncoders: AvailableEncoders): Promise<Map<string, boolean>> {
43 if (supportedEncoders !== undefined) {
44 return supportedEncoders
45 }
9252a33d 46
41fb13c3 47 const getAvailableEncodersPromise = promisify0(getAvailableEncoders)
1896bca0 48 const availableFFmpegEncoders = await getAvailableEncodersPromise()
9252a33d 49
1896bca0
C
50 const searchEncoders = new Set<string>()
51 for (const type of [ 'live', 'vod' ]) {
52 for (const streamType of [ 'audio', 'video' ]) {
53 for (const encoder of peertubeAvailableEncoders.encodersToTry[type][streamType]) {
54 searchEncoders.add(encoder)
55 }
56 }
57 }
9252a33d 58
1896bca0 59 supportedEncoders = new Map<string, boolean>()
9252a33d 60
1896bca0
C
61 for (const searchEncoder of searchEncoders) {
62 supportedEncoders.set(searchEncoder, availableFFmpegEncoders[searchEncoder] !== undefined)
529b3752
C
63 }
64
93904032 65 logger.info('Built supported ffmpeg encoders.', { supportedEncoders, searchEncoders, ...lTags() })
529b3752 66
1896bca0 67 return supportedEncoders
9252a33d
C
68}
69
1896bca0
C
70function resetSupportedEncoders () {
71 supportedEncoders = undefined
72}
529b3752 73
9252a33d
C
74// ---------------------------------------------------------------------------
75// Image manipulation
76// ---------------------------------------------------------------------------
77
78function convertWebPToJPG (path: string, destination: string): Promise<void> {
a4a8cd39 79 const command = ffmpeg(path, { niceness: FFMPEG_NICE.THUMBNAIL })
9252a33d
C
80 .output(destination)
81
cd2c3dcd 82 return runCommand({ command, silent: true })
9252a33d
C
83}
84
85function processGIF (
86 path: string,
87 destination: string,
f619de0e 88 newSize: { width: number, height: number }
9252a33d 89): Promise<void> {
a4a8cd39 90 const command = ffmpeg(path, { niceness: FFMPEG_NICE.THUMBNAIL })
f619de0e
C
91 .fps(20)
92 .size(`${newSize.width}x${newSize.height}`)
93 .output(destination)
9252a33d 94
cd2c3dcd 95 return runCommand({ command })
9252a33d
C
96}
97
26670720
C
98async function generateImageFromVideoFile (fromPath: string, folder: string, imageName: string, size: { width: number, height: number }) {
99 const pendingImageName = 'pending-' + imageName
100
14d3270f 101 const options = {
26670720 102 filename: pendingImageName,
14d3270f
C
103 count: 1,
104 folder
105 }
106
26670720 107 const pendingImagePath = join(folder, pendingImageName)
6fdc553a
C
108
109 try {
110 await new Promise<string>((res, rej) => {
7160878c 111 ffmpeg(fromPath, { niceness: FFMPEG_NICE.THUMBNAIL })
6fdc553a
C
112 .on('error', rej)
113 .on('end', () => res(imageName))
114 .thumbnail(options)
115 })
116
117 const destination = join(folder, imageName)
2fb5b3a5 118 await processImage(pendingImagePath, destination, size)
6fdc553a 119 } catch (err) {
93904032 120 logger.error('Cannot generate image from video %s.', fromPath, { err, ...lTags() })
6fdc553a
C
121
122 try {
62689b94 123 await remove(pendingImagePath)
6fdc553a 124 } catch (err) {
93904032 125 logger.debug('Cannot remove pending image path after generation error.', { err, ...lTags() })
6fdc553a
C
126 }
127 }
14d3270f
C
128}
129
daf6e480
C
130// ---------------------------------------------------------------------------
131// Transcode meta function
132// ---------------------------------------------------------------------------
133
2650d6d4 134type TranscodeOptionsType = 'hls' | 'hls-from-ts' | 'quick-transcode' | 'video' | 'merge-audio' | 'only-audio'
536598cf
C
135
136interface BaseTranscodeOptions {
137 type: TranscodeOptionsType
9252a33d 138
14d3270f
C
139 inputPath: string
140 outputPath: string
9252a33d
C
141
142 availableEncoders: AvailableEncoders
143 profile: string
144
318b0bd0 145 resolution: number
9252a33d 146
056aa7f2 147 isPortraitMode?: boolean
3b01f4c0
C
148
149 job?: Job
536598cf 150}
09209296 151
536598cf
C
152interface HLSTranscodeOptions extends BaseTranscodeOptions {
153 type: 'hls'
d7a25329 154 copyCodecs: boolean
536598cf 155 hlsPlaylist: {
4c280004
C
156 videoFilename: string
157 }
14d3270f
C
158}
159
2650d6d4
C
160interface HLSFromTSTranscodeOptions extends BaseTranscodeOptions {
161 type: 'hls-from-ts'
162
e772bdf1
C
163 isAAC: boolean
164
2650d6d4
C
165 hlsPlaylist: {
166 videoFilename: string
167 }
168}
169
536598cf
C
170interface QuickTranscodeOptions extends BaseTranscodeOptions {
171 type: 'quick-transcode'
172}
173
174interface VideoTranscodeOptions extends BaseTranscodeOptions {
175 type: 'video'
176}
177
178interface MergeAudioTranscodeOptions extends BaseTranscodeOptions {
179 type: 'merge-audio'
180 audioPath: string
181}
182
3a149e9f
C
183interface OnlyAudioTranscodeOptions extends BaseTranscodeOptions {
184 type: 'only-audio'
5c7d6508 185}
186
a1587156
C
187type TranscodeOptions =
188 HLSTranscodeOptions
2650d6d4 189 | HLSFromTSTranscodeOptions
3a149e9f
C
190 | VideoTranscodeOptions
191 | MergeAudioTranscodeOptions
192 | OnlyAudioTranscodeOptions
193 | QuickTranscodeOptions
536598cf 194
daf6e480 195const builders: {
41fb13c3 196 [ type in TranscodeOptionsType ]: (c: FfmpegCommand, o?: TranscodeOptions) => Promise<FfmpegCommand> | FfmpegCommand
daf6e480
C
197} = {
198 'quick-transcode': buildQuickTranscodeCommand,
199 'hls': buildHLSVODCommand,
2650d6d4 200 'hls-from-ts': buildHLSVODFromTSCommand,
daf6e480
C
201 'merge-audio': buildAudioMergeCommand,
202 'only-audio': buildOnlyAudioCommand,
9252a33d 203 'video': buildx264VODCommand
daf6e480
C
204}
205
206async function transcode (options: TranscodeOptions) {
93904032 207 logger.debug('Will run transcode.', { options, ...lTags() })
9e2b2e76 208
55223d65 209 let command = getFFmpeg(options.inputPath, 'vod')
daf6e480 210 .output(options.outputPath)
14d3270f 211
daf6e480 212 command = await builders[options.type](command, options)
7ed2c1a4 213
cd2c3dcd 214 await runCommand({ command, job: options.job })
5ba49f26 215
daf6e480 216 await fixHLSPlaylistIfNeeded(options)
837666fe
RK
217}
218
9252a33d
C
219// ---------------------------------------------------------------------------
220// Live muxing/transcoding functions
221// ---------------------------------------------------------------------------
123f6193 222
5a547f69 223async function getLiveTranscodingCommand (options: {
df1db951 224 inputUrl: string
764b1a14 225
5a547f69 226 outPath: string
764b1a14
C
227 masterPlaylistName: string
228
5a547f69 229 resolutions: number[]
679c12e6
C
230
231 // Input information
5a547f69 232 fps: number
c826f34a 233 bitrate: number
679c12e6 234 ratio: number
5a547f69
C
235
236 availableEncoders: AvailableEncoders
237 profile: string
238}) {
df1db951 239 const { inputUrl, outPath, resolutions, fps, bitrate, availableEncoders, profile, masterPlaylistName, ratio } = options
5a547f69 240
df1db951 241 const command = getFFmpeg(inputUrl, 'live')
c6c0fa6c
C
242
243 const varStreamMap: string[] = []
244
41fb13c3 245 const complexFilter: FilterSpecification[] = [
c6c0fa6c
C
246 {
247 inputs: '[v:0]',
248 filter: 'split',
249 options: resolutions.length,
250 outputs: resolutions.map(r => `vtemp${r}`)
3e03b961
C
251 }
252 ]
c6c0fa6c 253
49bcdb0d 254 command.outputOption('-sc_threshold 0')
c6c0fa6c 255
ce4a50b9
C
256 addDefaultEncoderGlobalParams({ command })
257
c6c0fa6c
C
258 for (let i = 0; i < resolutions.length; i++) {
259 const resolution = resolutions[i]
884d2c39
C
260 const resolutionFPS = computeFPS(fps, resolution)
261
262 const baseEncoderBuilderParams = {
df1db951 263 input: inputUrl,
529b3752 264
884d2c39
C
265 availableEncoders,
266 profile,
529b3752 267
c826f34a 268 inputBitrate: bitrate,
679c12e6
C
269 inputRatio: ratio,
270
884d2c39 271 resolution,
679c12e6
C
272 fps: resolutionFPS,
273
884d2c39
C
274 streamNum: i,
275 videoType: 'live' as 'live'
276 }
5a547f69
C
277
278 {
529b3752 279 const streamType: StreamType = 'video'
c826f34a 280 const builderResult = await getEncoderBuilderResult({ ...baseEncoderBuilderParams, streamType })
5a547f69
C
281 if (!builderResult) {
282 throw new Error('No available live video encoder found')
283 }
284
285 command.outputOption(`-map [vout${resolution}]`)
286
884d2c39 287 addDefaultEncoderParams({ command, encoder: builderResult.encoder, fps: resolutionFPS, streamNum: i })
5a547f69 288
67eeec8b
C
289 logger.debug(
290 'Apply ffmpeg live video params from %s using %s profile.', builderResult.encoder, profile, builderResult,
291 { fps: resolutionFPS, resolution, ...lTags() }
292 )
c6c0fa6c 293
5a547f69 294 command.outputOption(`${buildStreamSuffix('-c:v', i)} ${builderResult.encoder}`)
43f7a43c 295 applyEncoderOptions(command, builderResult.result)
3e03b961
C
296
297 complexFilter.push({
298 inputs: `vtemp${resolution}`,
299 filter: getScaleFilter(builderResult.result),
300 options: `w=-2:h=${resolution}`,
301 outputs: `vout${resolution}`
302 })
5a547f69
C
303 }
304
305 {
529b3752 306 const streamType: StreamType = 'audio'
c826f34a 307 const builderResult = await getEncoderBuilderResult({ ...baseEncoderBuilderParams, streamType })
5a547f69
C
308 if (!builderResult) {
309 throw new Error('No available live audio encoder found')
310 }
c6c0fa6c 311
5a547f69
C
312 command.outputOption('-map a:0')
313
884d2c39 314 addDefaultEncoderParams({ command, encoder: builderResult.encoder, fps: resolutionFPS, streamNum: i })
5a547f69 315
67eeec8b
C
316 logger.debug(
317 'Apply ffmpeg live audio params from %s using %s profile.', builderResult.encoder, profile, builderResult,
318 { fps: resolutionFPS, resolution, ...lTags() }
319 )
5a547f69
C
320
321 command.outputOption(`${buildStreamSuffix('-c:a', i)} ${builderResult.encoder}`)
43f7a43c 322 applyEncoderOptions(command, builderResult.result)
5a547f69 323 }
c6c0fa6c
C
324
325 varStreamMap.push(`v:${i},a:${i}`)
326 }
327
3e03b961
C
328 command.complexFilter(complexFilter)
329
764b1a14 330 addDefaultLiveHLSParams(command, outPath, masterPlaylistName)
c6c0fa6c
C
331
332 command.outputOption('-var_stream_map', varStreamMap.join(' '))
333
c6c0fa6c
C
334 return command
335}
336
df1db951
C
337function getLiveMuxingCommand (inputUrl: string, outPath: string, masterPlaylistName: string) {
338 const command = getFFmpeg(inputUrl, 'live')
c6c0fa6c
C
339
340 command.outputOption('-c:v copy')
341 command.outputOption('-c:a copy')
342 command.outputOption('-map 0:a?')
343 command.outputOption('-map 0:v?')
344
764b1a14 345 addDefaultLiveHLSParams(command, outPath, masterPlaylistName)
c6c0fa6c 346
c6c0fa6c
C
347 return command
348}
349
5a547f69
C
350function buildStreamSuffix (base: string, streamNum?: number) {
351 if (streamNum !== undefined) {
352 return `${base}:${streamNum}`
353 }
354
355 return base
356}
357
9252a33d
C
358// ---------------------------------------------------------------------------
359// Default options
360// ---------------------------------------------------------------------------
361
ce4a50b9 362function addDefaultEncoderGlobalParams (options: {
41fb13c3 363 command: FfmpegCommand
ce4a50b9
C
364}) {
365 const { command } = options
366
367 // avoid issues when transcoding some files: https://trac.ffmpeg.org/ticket/6375
368 command.outputOption('-max_muxing_queue_size 1024')
369 // strip all metadata
370 .outputOption('-map_metadata -1')
371 // NOTE: b-strategy 1 - heuristic algorithm, 16 is optimal B-frames for it
372 .outputOption('-b_strategy 1')
373 // NOTE: Why 16: https://github.com/Chocobozzz/PeerTube/pull/774. b-strategy 2 -> B-frames<16
374 .outputOption('-bf 16')
375 // allows import of source material with incompatible pixel formats (e.g. MJPEG video)
376 .outputOption('-pix_fmt yuv420p')
377}
378
5a547f69 379function addDefaultEncoderParams (options: {
41fb13c3 380 command: FfmpegCommand
5a547f69
C
381 encoder: 'libx264' | string
382 streamNum?: number
383 fps?: number
384}) {
385 const { command, encoder, fps, streamNum } = options
386
387 if (encoder === 'libx264') {
388 // 3.1 is the minimal resource allocation for our highest supported resolution
ce4a50b9 389 command.outputOption(buildStreamSuffix('-level:v', streamNum) + ' 3.1')
5a547f69
C
390
391 if (fps) {
392 // Keyframe interval of 2 seconds for faster seeking and resolution switching.
393 // https://streaminglearningcenter.com/blogs/whats-the-right-keyframe-interval.html
394 // https://superuser.com/a/908325
ce4a50b9 395 command.outputOption(buildStreamSuffix('-g:v', streamNum) + ' ' + (fps * 2))
5a547f69
C
396 }
397 }
c6c0fa6c
C
398}
399
41fb13c3 400function addDefaultLiveHLSParams (command: FfmpegCommand, outPath: string, masterPlaylistName: string) {
68e70a74 401 command.outputOption('-hls_time ' + VIDEO_LIVE.SEGMENT_TIME_SECONDS)
fb719404 402 command.outputOption('-hls_list_size ' + VIDEO_LIVE.SEGMENTS_LIST_SIZE)
49bcdb0d 403 command.outputOption('-hls_flags delete_segments+independent_segments')
21226063 404 command.outputOption(`-hls_segment_filename ${join(outPath, '%v-%06d.ts')}`)
764b1a14 405 command.outputOption('-master_pl_name ' + masterPlaylistName)
c6c0fa6c
C
406 command.outputOption(`-f hls`)
407
408 command.output(join(outPath, '%v.m3u8'))
409}
410
9252a33d
C
411// ---------------------------------------------------------------------------
412// Transcode VOD command builders
413// ---------------------------------------------------------------------------
414
41fb13c3 415async function buildx264VODCommand (command: FfmpegCommand, options: TranscodeOptions) {
14aed608 416 let fps = await getVideoFileFPS(options.inputPath)
884d2c39 417 fps = computeFPS(fps, options.resolution)
14aed608 418
3e03b961 419 let scaleFilterValue: string
14aed608
C
420
421 if (options.resolution !== undefined) {
3e03b961 422 scaleFilterValue = options.isPortraitMode === true
a60696ab
C
423 ? `w=${options.resolution}:h=-2`
424 : `w=-2:h=${options.resolution}`
14aed608
C
425 }
426
3e03b961
C
427 command = await presetVideo({ command, input: options.inputPath, transcodeOptions: options, fps, scaleFilterValue })
428
14aed608
C
429 return command
430}
431
41fb13c3 432async function buildAudioMergeCommand (command: FfmpegCommand, options: MergeAudioTranscodeOptions) {
536598cf
C
433 command = command.loop(undefined)
434
318b0bd0 435 const scaleFilterValue = getScaleCleanerValue()
3e03b961 436 command = await presetVideo({ command, input: options.audioPath, transcodeOptions: options, scaleFilterValue })
9252a33d 437
9252a33d 438 command.outputOption('-preset:v veryfast')
536598cf
C
439
440 command = command.input(options.audioPath)
536598cf
C
441 .outputOption('-tune stillimage')
442 .outputOption('-shortest')
443
444 return command
445}
446
41fb13c3 447function buildOnlyAudioCommand (command: FfmpegCommand, _options: OnlyAudioTranscodeOptions) {
a1587156 448 command = presetOnlyAudio(command)
5c7d6508 449
450 return command
451}
452
41fb13c3 453function buildQuickTranscodeCommand (command: FfmpegCommand) {
a1587156 454 command = presetCopy(command)
536598cf
C
455
456 command = command.outputOption('-map_metadata -1') // strip all metadata
457 .outputOption('-movflags faststart')
458
459 return command
460}
461
41fb13c3 462function addCommonHLSVODCommandOptions (command: FfmpegCommand, outputPath: string) {
2650d6d4
C
463 return command.outputOption('-hls_time 4')
464 .outputOption('-hls_list_size 0')
465 .outputOption('-hls_playlist_type vod')
466 .outputOption('-hls_segment_filename ' + outputPath)
467 .outputOption('-hls_segment_type fmp4')
468 .outputOption('-f hls')
469 .outputOption('-hls_flags single_file')
470}
471
41fb13c3 472async function buildHLSVODCommand (command: FfmpegCommand, options: HLSTranscodeOptions) {
14aed608
C
473 const videoPath = getHLSVideoPath(options)
474
a1587156 475 if (options.copyCodecs) command = presetCopy(command)
1c320673 476 else if (options.resolution === VideoResolution.H_NOVIDEO) command = presetOnlyAudio(command)
9252a33d 477 else command = await buildx264VODCommand(command, options)
14aed608 478
2650d6d4
C
479 addCommonHLSVODCommandOptions(command, videoPath)
480
481 return command
482}
483
41fb13c3 484function buildHLSVODFromTSCommand (command: FfmpegCommand, options: HLSFromTSTranscodeOptions) {
2650d6d4
C
485 const videoPath = getHLSVideoPath(options)
486
3851e732 487 command.outputOption('-c copy')
e772bdf1
C
488
489 if (options.isAAC) {
490 // Required for example when copying an AAC stream from an MPEG-TS
491 // Since it's a bitstream filter, we don't need to reencode the audio
492 command.outputOption('-bsf:a aac_adtstoasc')
493 }
2650d6d4
C
494
495 addCommonHLSVODCommandOptions(command, videoPath)
14aed608
C
496
497 return command
498}
499
536598cf 500async function fixHLSPlaylistIfNeeded (options: TranscodeOptions) {
2650d6d4 501 if (options.type !== 'hls' && options.type !== 'hls-from-ts') return
7f8f8bdb 502
7f8f8bdb
C
503 const fileContent = await readFile(options.outputPath)
504
505 const videoFileName = options.hlsPlaylist.videoFilename
506 const videoFilePath = getHLSVideoPath(options)
507
536598cf 508 // Fix wrong mapping with some ffmpeg versions
7f8f8bdb
C
509 const newContent = fileContent.toString()
510 .replace(`#EXT-X-MAP:URI="${videoFilePath}",`, `#EXT-X-MAP:URI="${videoFileName}",`)
511
512 await writeFile(options.outputPath, newContent)
513}
514
2650d6d4 515function getHLSVideoPath (options: HLSTranscodeOptions | HLSFromTSTranscodeOptions) {
9252a33d 516 return `${dirname(options.outputPath)}/${options.hlsPlaylist.videoFilename}`
4176e227
RK
517}
518
9252a33d
C
519// ---------------------------------------------------------------------------
520// Transcoding presets
521// ---------------------------------------------------------------------------
522
529b3752
C
523// Run encoder builder depending on available encoders
524// Try encoders by priority: if the encoder is available, run the chosen profile or fallback to the default one
525// If the default one does not exist, check the next encoder
679c12e6 526async function getEncoderBuilderResult (options: EncoderOptionsBuilderParams & {
529b3752 527 streamType: 'video' | 'audio'
5a547f69
C
528 input: string
529
530 availableEncoders: AvailableEncoders
531 profile: string
532
533 videoType: 'vod' | 'live'
5a547f69 534}) {
679c12e6 535 const { availableEncoders, profile, streamType, videoType } = options
5a547f69 536
1896bca0
C
537 const encodersToTry = availableEncoders.encodersToTry[videoType][streamType]
538 const encoders = availableEncoders.available[videoType]
5a547f69
C
539
540 for (const encoder of encodersToTry) {
1896bca0 541 if (!(await checkFFmpegEncoders(availableEncoders)).get(encoder)) {
93904032 542 logger.debug('Encoder %s not available in ffmpeg, skipping.', encoder, lTags())
1896bca0
C
543 continue
544 }
545
546 if (!encoders[encoder]) {
93904032 547 logger.debug('Encoder %s not available in peertube encoders, skipping.', encoder, lTags())
1896bca0
C
548 continue
549 }
5a547f69 550
529b3752
C
551 // An object containing available profiles for this encoder
552 const builderProfiles: EncoderProfile<EncoderOptionsBuilder> = encoders[encoder]
5a547f69
C
553 let builder = builderProfiles[profile]
554
555 if (!builder) {
93904032 556 logger.debug('Profile %s for encoder %s not available. Fallback to default.', profile, encoder, lTags())
5a547f69 557 builder = builderProfiles.default
529b3752
C
558
559 if (!builder) {
93904032 560 logger.debug('Default profile for encoder %s not available. Try next available encoder.', encoder, lTags())
529b3752
C
561 continue
562 }
5a547f69
C
563 }
564
679c12e6 565 const result = await builder(pick(options, [ 'input', 'resolution', 'inputBitrate', 'fps', 'inputRatio', 'streamNum' ]))
5a547f69
C
566
567 return {
568 result,
569
570 // If we don't have output options, then copy the input stream
571 encoder: result.copy === true
572 ? 'copy'
573 : encoder
574 }
575 }
576
577 return null
578}
579
3e03b961 580async function presetVideo (options: {
41fb13c3 581 command: FfmpegCommand
3e03b961
C
582 input: string
583 transcodeOptions: TranscodeOptions
9252a33d 584 fps?: number
3e03b961
C
585 scaleFilterValue?: string
586}) {
587 const { command, input, transcodeOptions, fps, scaleFilterValue } = options
588
cdf4cb9e 589 let localCommand = command
4176e227 590 .format('mp4')
4176e227 591 .outputOption('-movflags faststart')
4176e227 592
ce4a50b9
C
593 addDefaultEncoderGlobalParams({ command })
594
c826f34a
C
595 const probe = await ffprobePromise(input)
596
9252a33d 597 // Audio encoder
c826f34a
C
598 const parsedAudio = await getAudioStream(input, probe)
599 const bitrate = await getVideoFileBitrate(input, probe)
679c12e6 600 const { ratio } = await getVideoFileResolution(input, probe)
4176e227 601
529b3752 602 let streamsToProcess: StreamType[] = [ 'audio', 'video' ]
9252a33d 603
cdf4cb9e
C
604 if (!parsedAudio.audioStream) {
605 localCommand = localCommand.noAudio()
1896bca0 606 streamsToProcess = [ 'video' ]
9252a33d 607 }
536598cf 608
5a547f69
C
609 for (const streamType of streamsToProcess) {
610 const { profile, resolution, availableEncoders } = transcodeOptions
611
612 const builderResult = await getEncoderBuilderResult({
613 streamType,
614 input,
615 resolution,
616 availableEncoders,
617 profile,
618 fps,
c826f34a 619 inputBitrate: bitrate,
679c12e6 620 inputRatio: ratio,
5a547f69
C
621 videoType: 'vod' as 'vod'
622 })
9252a33d 623
5a547f69
C
624 if (!builderResult) {
625 throw new Error('No available encoder found for stream ' + streamType)
626 }
9252a33d 627
1896bca0
C
628 logger.debug(
629 'Apply ffmpeg params from %s for %s stream of input %s using %s profile.',
93904032 630 builderResult.encoder, streamType, input, profile, builderResult,
67eeec8b 631 { resolution, fps, ...lTags() }
1896bca0 632 )
9252a33d 633
529b3752 634 if (streamType === 'video') {
5a547f69 635 localCommand.videoCodec(builderResult.encoder)
3e03b961
C
636
637 if (scaleFilterValue) {
638 localCommand.outputOption(`-vf ${getScaleFilter(builderResult.result)}=${scaleFilterValue}`)
639 }
529b3752 640 } else if (streamType === 'audio') {
5a547f69 641 localCommand.audioCodec(builderResult.encoder)
9252a33d 642 }
3e03b961 643
43f7a43c 644 applyEncoderOptions(localCommand, builderResult.result)
5a547f69 645 addDefaultEncoderParams({ command: localCommand, encoder: builderResult.encoder, fps })
536598cf 646 }
bcf21a37 647
cdf4cb9e 648 return localCommand
4176e227 649}
14aed608 650
41fb13c3 651function presetCopy (command: FfmpegCommand): FfmpegCommand {
14aed608
C
652 return command
653 .format('mp4')
654 .videoCodec('copy')
655 .audioCodec('copy')
656}
5c7d6508 657
41fb13c3 658function presetOnlyAudio (command: FfmpegCommand): FfmpegCommand {
5c7d6508 659 return command
660 .format('mp4')
661 .audioCodec('copy')
662 .noVideo()
663}
c6c0fa6c 664
41fb13c3 665function applyEncoderOptions (command: FfmpegCommand, options: EncoderOptions): FfmpegCommand {
43f7a43c
TLC
666 return command
667 .inputOptions(options.inputOptions ?? [])
43f7a43c
TLC
668 .outputOptions(options.outputOptions ?? [])
669}
670
3e03b961
C
671function getScaleFilter (options: EncoderOptions): string {
672 if (options.scaleFilter) return options.scaleFilter.name
673
674 return 'scale'
675}
676
9252a33d
C
677// ---------------------------------------------------------------------------
678// Utils
679// ---------------------------------------------------------------------------
680
55223d65 681function getFFmpeg (input: string, type: 'live' | 'vod') {
c6c0fa6c 682 // We set cwd explicitly because ffmpeg appears to create temporary files when trancoding which fails in read-only file systems
7abb6060
RK
683 const command = ffmpeg(input, {
684 niceness: type === 'live' ? FFMPEG_NICE.LIVE : FFMPEG_NICE.VOD,
685 cwd: CONFIG.STORAGE.TMP_DIR
686 })
c6c0fa6c 687
55223d65
C
688 const threads = type === 'live'
689 ? CONFIG.LIVE.TRANSCODING.THREADS
690 : CONFIG.TRANSCODING.THREADS
691
692 if (threads > 0) {
c6c0fa6c 693 // If we don't set any threads ffmpeg will chose automatically
55223d65 694 command.outputOption('-threads ' + threads)
c6c0fa6c
C
695 }
696
697 return command
698}
9252a33d 699
ae71acca
C
700function getFFmpegVersion () {
701 return new Promise<string>((res, rej) => {
702 (ffmpeg() as any)._getFfmpegPath((err, ffmpegPath) => {
703 if (err) return rej(err)
704 if (!ffmpegPath) return rej(new Error('Could not find ffmpeg path'))
705
706 return execPromise(`${ffmpegPath} -version`)
707 .then(stdout => {
60f1f615 708 const parsed = stdout.match(/ffmpeg version .?(\d+\.\d+(\.\d+)?)/)
ae71acca
C
709 if (!parsed || !parsed[1]) return rej(new Error(`Could not find ffmpeg version in ${stdout}`))
710
60f1f615
C
711 // Fix ffmpeg version that does not include patch version (4.4 for example)
712 let version = parsed[1]
1ff9f1cd 713 if (version.match(/^\d+\.\d+$/)) {
60f1f615
C
714 version += '.0'
715 }
716
717 return res(version)
ae71acca
C
718 })
719 .catch(err => rej(err))
720 })
721 })
722}
723
cd2c3dcd 724async function runCommand (options: {
41fb13c3 725 command: FfmpegCommand
cd2c3dcd
C
726 silent?: boolean // false
727 job?: Job
728}) {
729 const { command, silent = false, job } = options
730
9252a33d 731 return new Promise<void>((res, rej) => {
d0ea3e34
C
732 let shellCommand: string
733
734 command.on('start', cmdline => { shellCommand = cmdline })
735
9252a33d 736 command.on('error', (err, stdout, stderr) => {
93904032 737 if (silent !== true) logger.error('Error in ffmpeg.', { stdout, stderr, ...lTags() })
cd2c3dcd 738
9252a33d
C
739 rej(err)
740 })
741
dd9c7929 742 command.on('end', (stdout, stderr) => {
93904032 743 logger.debug('FFmpeg command ended.', { stdout, stderr, shellCommand, ...lTags() })
dd9c7929 744
9252a33d
C
745 res()
746 })
747
3b01f4c0
C
748 if (job) {
749 command.on('progress', progress => {
750 if (!progress.percent) return
751
752 job.progress(Math.round(progress.percent))
93904032 753 .catch(err => logger.warn('Cannot set ffmpeg job progress.', { err, ...lTags() }))
3b01f4c0
C
754 })
755 }
756
9252a33d
C
757 command.run()
758 })
759}
09849603 760
318b0bd0
C
761// Avoid "height not divisible by 2" error
762function getScaleCleanerValue () {
763 return 'trunc(iw/2)*2:trunc(ih/2)*2'
764}
765
09849603
RK
766// ---------------------------------------------------------------------------
767
768export {
769 getLiveTranscodingCommand,
770 getLiveMuxingCommand,
771 buildStreamSuffix,
772 convertWebPToJPG,
773 processGIF,
774 generateImageFromVideoFile,
775 TranscodeOptions,
776 TranscodeOptionsType,
777 transcode,
778 runCommand,
ae71acca 779 getFFmpegVersion,
09849603 780
1896bca0
C
781 resetSupportedEncoders,
782
09849603
RK
783 // builders
784 buildx264VODCommand
785}