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