]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/helpers/ffmpeg-utils.ts
Remove resumable cache after upload success
[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 221async function getLiveTranscodingCommand (options: {
df1db951 222 inputUrl: 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}) {
df1db951 237 const { inputUrl, outPath, resolutions, fps, bitrate, availableEncoders, profile, masterPlaylistName, ratio } = options
5a547f69 238
df1db951 239 const command = getFFmpeg(inputUrl, 'live')
c6c0fa6c
C
240
241 const varStreamMap: string[] = []
242
41fb13c3 243 const complexFilter: FilterSpecification[] = [
c6c0fa6c
C
244 {
245 inputs: '[v:0]',
246 filter: 'split',
247 options: resolutions.length,
248 outputs: resolutions.map(r => `vtemp${r}`)
3e03b961
C
249 }
250 ]
c6c0fa6c 251
49bcdb0d 252 command.outputOption('-sc_threshold 0')
c6c0fa6c 253
ce4a50b9
C
254 addDefaultEncoderGlobalParams({ command })
255
c6c0fa6c
C
256 for (let i = 0; i < resolutions.length; i++) {
257 const resolution = resolutions[i]
884d2c39
C
258 const resolutionFPS = computeFPS(fps, resolution)
259
260 const baseEncoderBuilderParams = {
df1db951 261 input: inputUrl,
529b3752 262
884d2c39
C
263 availableEncoders,
264 profile,
529b3752 265
c826f34a 266 inputBitrate: bitrate,
679c12e6
C
267 inputRatio: ratio,
268
884d2c39 269 resolution,
679c12e6
C
270 fps: resolutionFPS,
271
884d2c39
C
272 streamNum: i,
273 videoType: 'live' as 'live'
274 }
5a547f69
C
275
276 {
529b3752 277 const streamType: StreamType = 'video'
c826f34a 278 const builderResult = await getEncoderBuilderResult({ ...baseEncoderBuilderParams, streamType })
5a547f69
C
279 if (!builderResult) {
280 throw new Error('No available live video encoder found')
281 }
282
283 command.outputOption(`-map [vout${resolution}]`)
284
884d2c39 285 addDefaultEncoderParams({ command, encoder: builderResult.encoder, fps: resolutionFPS, streamNum: i })
5a547f69 286
1896bca0 287 logger.debug('Apply ffmpeg live video params from %s using %s profile.', builderResult.encoder, profile, builderResult)
c6c0fa6c 288
5a547f69 289 command.outputOption(`${buildStreamSuffix('-c:v', i)} ${builderResult.encoder}`)
43f7a43c 290 applyEncoderOptions(command, builderResult.result)
3e03b961
C
291
292 complexFilter.push({
293 inputs: `vtemp${resolution}`,
294 filter: getScaleFilter(builderResult.result),
295 options: `w=-2:h=${resolution}`,
296 outputs: `vout${resolution}`
297 })
5a547f69
C
298 }
299
300 {
529b3752 301 const streamType: StreamType = 'audio'
c826f34a 302 const builderResult = await getEncoderBuilderResult({ ...baseEncoderBuilderParams, streamType })
5a547f69
C
303 if (!builderResult) {
304 throw new Error('No available live audio encoder found')
305 }
c6c0fa6c 306
5a547f69
C
307 command.outputOption('-map a:0')
308
884d2c39 309 addDefaultEncoderParams({ command, encoder: builderResult.encoder, fps: resolutionFPS, streamNum: i })
5a547f69 310
1896bca0 311 logger.debug('Apply ffmpeg live audio params from %s using %s profile.', builderResult.encoder, profile, builderResult)
5a547f69
C
312
313 command.outputOption(`${buildStreamSuffix('-c:a', i)} ${builderResult.encoder}`)
43f7a43c 314 applyEncoderOptions(command, builderResult.result)
5a547f69 315 }
c6c0fa6c
C
316
317 varStreamMap.push(`v:${i},a:${i}`)
318 }
319
3e03b961
C
320 command.complexFilter(complexFilter)
321
764b1a14 322 addDefaultLiveHLSParams(command, outPath, masterPlaylistName)
c6c0fa6c
C
323
324 command.outputOption('-var_stream_map', varStreamMap.join(' '))
325
c6c0fa6c
C
326 return command
327}
328
df1db951
C
329function getLiveMuxingCommand (inputUrl: string, outPath: string, masterPlaylistName: string) {
330 const command = getFFmpeg(inputUrl, 'live')
c6c0fa6c
C
331
332 command.outputOption('-c:v copy')
333 command.outputOption('-c:a copy')
334 command.outputOption('-map 0:a?')
335 command.outputOption('-map 0:v?')
336
764b1a14 337 addDefaultLiveHLSParams(command, outPath, masterPlaylistName)
c6c0fa6c 338
c6c0fa6c
C
339 return command
340}
341
5a547f69
C
342function buildStreamSuffix (base: string, streamNum?: number) {
343 if (streamNum !== undefined) {
344 return `${base}:${streamNum}`
345 }
346
347 return base
348}
349
9252a33d
C
350// ---------------------------------------------------------------------------
351// Default options
352// ---------------------------------------------------------------------------
353
ce4a50b9 354function addDefaultEncoderGlobalParams (options: {
41fb13c3 355 command: FfmpegCommand
ce4a50b9
C
356}) {
357 const { command } = options
358
359 // avoid issues when transcoding some files: https://trac.ffmpeg.org/ticket/6375
360 command.outputOption('-max_muxing_queue_size 1024')
361 // strip all metadata
362 .outputOption('-map_metadata -1')
363 // NOTE: b-strategy 1 - heuristic algorithm, 16 is optimal B-frames for it
364 .outputOption('-b_strategy 1')
365 // NOTE: Why 16: https://github.com/Chocobozzz/PeerTube/pull/774. b-strategy 2 -> B-frames<16
366 .outputOption('-bf 16')
367 // allows import of source material with incompatible pixel formats (e.g. MJPEG video)
368 .outputOption('-pix_fmt yuv420p')
369}
370
5a547f69 371function addDefaultEncoderParams (options: {
41fb13c3 372 command: FfmpegCommand
5a547f69
C
373 encoder: 'libx264' | string
374 streamNum?: number
375 fps?: number
376}) {
377 const { command, encoder, fps, streamNum } = options
378
379 if (encoder === 'libx264') {
380 // 3.1 is the minimal resource allocation for our highest supported resolution
ce4a50b9 381 command.outputOption(buildStreamSuffix('-level:v', streamNum) + ' 3.1')
5a547f69
C
382
383 if (fps) {
384 // Keyframe interval of 2 seconds for faster seeking and resolution switching.
385 // https://streaminglearningcenter.com/blogs/whats-the-right-keyframe-interval.html
386 // https://superuser.com/a/908325
ce4a50b9 387 command.outputOption(buildStreamSuffix('-g:v', streamNum) + ' ' + (fps * 2))
5a547f69
C
388 }
389 }
c6c0fa6c
C
390}
391
41fb13c3 392function addDefaultLiveHLSParams (command: FfmpegCommand, outPath: string, masterPlaylistName: string) {
68e70a74 393 command.outputOption('-hls_time ' + VIDEO_LIVE.SEGMENT_TIME_SECONDS)
fb719404 394 command.outputOption('-hls_list_size ' + VIDEO_LIVE.SEGMENTS_LIST_SIZE)
49bcdb0d 395 command.outputOption('-hls_flags delete_segments+independent_segments')
21226063 396 command.outputOption(`-hls_segment_filename ${join(outPath, '%v-%06d.ts')}`)
764b1a14 397 command.outputOption('-master_pl_name ' + masterPlaylistName)
c6c0fa6c
C
398 command.outputOption(`-f hls`)
399
400 command.output(join(outPath, '%v.m3u8'))
401}
402
9252a33d
C
403// ---------------------------------------------------------------------------
404// Transcode VOD command builders
405// ---------------------------------------------------------------------------
406
41fb13c3 407async function buildx264VODCommand (command: FfmpegCommand, options: TranscodeOptions) {
14aed608 408 let fps = await getVideoFileFPS(options.inputPath)
884d2c39 409 fps = computeFPS(fps, options.resolution)
14aed608 410
3e03b961 411 let scaleFilterValue: string
14aed608
C
412
413 if (options.resolution !== undefined) {
3e03b961 414 scaleFilterValue = options.isPortraitMode === true
a60696ab
C
415 ? `w=${options.resolution}:h=-2`
416 : `w=-2:h=${options.resolution}`
14aed608
C
417 }
418
3e03b961
C
419 command = await presetVideo({ command, input: options.inputPath, transcodeOptions: options, fps, scaleFilterValue })
420
14aed608
C
421 return command
422}
423
41fb13c3 424async function buildAudioMergeCommand (command: FfmpegCommand, options: MergeAudioTranscodeOptions) {
536598cf
C
425 command = command.loop(undefined)
426
318b0bd0 427 const scaleFilterValue = getScaleCleanerValue()
3e03b961 428 command = await presetVideo({ command, input: options.audioPath, transcodeOptions: options, scaleFilterValue })
9252a33d 429
9252a33d 430 command.outputOption('-preset:v veryfast')
536598cf
C
431
432 command = command.input(options.audioPath)
536598cf
C
433 .outputOption('-tune stillimage')
434 .outputOption('-shortest')
435
436 return command
437}
438
41fb13c3 439function buildOnlyAudioCommand (command: FfmpegCommand, _options: OnlyAudioTranscodeOptions) {
a1587156 440 command = presetOnlyAudio(command)
5c7d6508 441
442 return command
443}
444
41fb13c3 445function buildQuickTranscodeCommand (command: FfmpegCommand) {
a1587156 446 command = presetCopy(command)
536598cf
C
447
448 command = command.outputOption('-map_metadata -1') // strip all metadata
449 .outputOption('-movflags faststart')
450
451 return command
452}
453
41fb13c3 454function addCommonHLSVODCommandOptions (command: FfmpegCommand, outputPath: string) {
2650d6d4
C
455 return command.outputOption('-hls_time 4')
456 .outputOption('-hls_list_size 0')
457 .outputOption('-hls_playlist_type vod')
458 .outputOption('-hls_segment_filename ' + outputPath)
459 .outputOption('-hls_segment_type fmp4')
460 .outputOption('-f hls')
461 .outputOption('-hls_flags single_file')
462}
463
41fb13c3 464async function buildHLSVODCommand (command: FfmpegCommand, options: HLSTranscodeOptions) {
14aed608
C
465 const videoPath = getHLSVideoPath(options)
466
a1587156 467 if (options.copyCodecs) command = presetCopy(command)
1c320673 468 else if (options.resolution === VideoResolution.H_NOVIDEO) command = presetOnlyAudio(command)
9252a33d 469 else command = await buildx264VODCommand(command, options)
14aed608 470
2650d6d4
C
471 addCommonHLSVODCommandOptions(command, videoPath)
472
473 return command
474}
475
41fb13c3 476function buildHLSVODFromTSCommand (command: FfmpegCommand, options: HLSFromTSTranscodeOptions) {
2650d6d4
C
477 const videoPath = getHLSVideoPath(options)
478
3851e732 479 command.outputOption('-c copy')
e772bdf1
C
480
481 if (options.isAAC) {
482 // Required for example when copying an AAC stream from an MPEG-TS
483 // Since it's a bitstream filter, we don't need to reencode the audio
484 command.outputOption('-bsf:a aac_adtstoasc')
485 }
2650d6d4
C
486
487 addCommonHLSVODCommandOptions(command, videoPath)
14aed608
C
488
489 return command
490}
491
536598cf 492async function fixHLSPlaylistIfNeeded (options: TranscodeOptions) {
2650d6d4 493 if (options.type !== 'hls' && options.type !== 'hls-from-ts') return
7f8f8bdb 494
7f8f8bdb
C
495 const fileContent = await readFile(options.outputPath)
496
497 const videoFileName = options.hlsPlaylist.videoFilename
498 const videoFilePath = getHLSVideoPath(options)
499
536598cf 500 // Fix wrong mapping with some ffmpeg versions
7f8f8bdb
C
501 const newContent = fileContent.toString()
502 .replace(`#EXT-X-MAP:URI="${videoFilePath}",`, `#EXT-X-MAP:URI="${videoFileName}",`)
503
504 await writeFile(options.outputPath, newContent)
505}
506
2650d6d4 507function getHLSVideoPath (options: HLSTranscodeOptions | HLSFromTSTranscodeOptions) {
9252a33d 508 return `${dirname(options.outputPath)}/${options.hlsPlaylist.videoFilename}`
4176e227
RK
509}
510
9252a33d
C
511// ---------------------------------------------------------------------------
512// Transcoding presets
513// ---------------------------------------------------------------------------
514
529b3752
C
515// Run encoder builder depending on available encoders
516// Try encoders by priority: if the encoder is available, run the chosen profile or fallback to the default one
517// If the default one does not exist, check the next encoder
679c12e6 518async function getEncoderBuilderResult (options: EncoderOptionsBuilderParams & {
529b3752 519 streamType: 'video' | 'audio'
5a547f69
C
520 input: string
521
522 availableEncoders: AvailableEncoders
523 profile: string
524
525 videoType: 'vod' | 'live'
5a547f69 526}) {
679c12e6 527 const { availableEncoders, profile, streamType, videoType } = options
5a547f69 528
1896bca0
C
529 const encodersToTry = availableEncoders.encodersToTry[videoType][streamType]
530 const encoders = availableEncoders.available[videoType]
5a547f69
C
531
532 for (const encoder of encodersToTry) {
1896bca0
C
533 if (!(await checkFFmpegEncoders(availableEncoders)).get(encoder)) {
534 logger.debug('Encoder %s not available in ffmpeg, skipping.', encoder)
535 continue
536 }
537
538 if (!encoders[encoder]) {
539 logger.debug('Encoder %s not available in peertube encoders, skipping.', encoder)
540 continue
541 }
5a547f69 542
529b3752
C
543 // An object containing available profiles for this encoder
544 const builderProfiles: EncoderProfile<EncoderOptionsBuilder> = encoders[encoder]
5a547f69
C
545 let builder = builderProfiles[profile]
546
547 if (!builder) {
548 logger.debug('Profile %s for encoder %s not available. Fallback to default.', profile, encoder)
549 builder = builderProfiles.default
529b3752
C
550
551 if (!builder) {
552 logger.debug('Default profile for encoder %s not available. Try next available encoder.', encoder)
553 continue
554 }
5a547f69
C
555 }
556
679c12e6 557 const result = await builder(pick(options, [ 'input', 'resolution', 'inputBitrate', 'fps', 'inputRatio', 'streamNum' ]))
5a547f69
C
558
559 return {
560 result,
561
562 // If we don't have output options, then copy the input stream
563 encoder: result.copy === true
564 ? 'copy'
565 : encoder
566 }
567 }
568
569 return null
570}
571
3e03b961 572async function presetVideo (options: {
41fb13c3 573 command: FfmpegCommand
3e03b961
C
574 input: string
575 transcodeOptions: TranscodeOptions
9252a33d 576 fps?: number
3e03b961
C
577 scaleFilterValue?: string
578}) {
579 const { command, input, transcodeOptions, fps, scaleFilterValue } = options
580
cdf4cb9e 581 let localCommand = command
4176e227 582 .format('mp4')
4176e227 583 .outputOption('-movflags faststart')
4176e227 584
ce4a50b9
C
585 addDefaultEncoderGlobalParams({ command })
586
c826f34a
C
587 const probe = await ffprobePromise(input)
588
9252a33d 589 // Audio encoder
c826f34a
C
590 const parsedAudio = await getAudioStream(input, probe)
591 const bitrate = await getVideoFileBitrate(input, probe)
679c12e6 592 const { ratio } = await getVideoFileResolution(input, probe)
4176e227 593
529b3752 594 let streamsToProcess: StreamType[] = [ 'audio', 'video' ]
9252a33d 595
cdf4cb9e
C
596 if (!parsedAudio.audioStream) {
597 localCommand = localCommand.noAudio()
1896bca0 598 streamsToProcess = [ 'video' ]
9252a33d 599 }
536598cf 600
5a547f69
C
601 for (const streamType of streamsToProcess) {
602 const { profile, resolution, availableEncoders } = transcodeOptions
603
604 const builderResult = await getEncoderBuilderResult({
605 streamType,
606 input,
607 resolution,
608 availableEncoders,
609 profile,
610 fps,
c826f34a 611 inputBitrate: bitrate,
679c12e6 612 inputRatio: ratio,
5a547f69
C
613 videoType: 'vod' as 'vod'
614 })
9252a33d 615
5a547f69
C
616 if (!builderResult) {
617 throw new Error('No available encoder found for stream ' + streamType)
618 }
9252a33d 619
1896bca0
C
620 logger.debug(
621 'Apply ffmpeg params from %s for %s stream of input %s using %s profile.',
622 builderResult.encoder, streamType, input, profile, builderResult
623 )
9252a33d 624
529b3752 625 if (streamType === 'video') {
5a547f69 626 localCommand.videoCodec(builderResult.encoder)
3e03b961
C
627
628 if (scaleFilterValue) {
629 localCommand.outputOption(`-vf ${getScaleFilter(builderResult.result)}=${scaleFilterValue}`)
630 }
529b3752 631 } else if (streamType === 'audio') {
5a547f69 632 localCommand.audioCodec(builderResult.encoder)
9252a33d 633 }
3e03b961 634
43f7a43c 635 applyEncoderOptions(localCommand, builderResult.result)
5a547f69 636 addDefaultEncoderParams({ command: localCommand, encoder: builderResult.encoder, fps })
536598cf 637 }
bcf21a37 638
cdf4cb9e 639 return localCommand
4176e227 640}
14aed608 641
41fb13c3 642function presetCopy (command: FfmpegCommand): FfmpegCommand {
14aed608
C
643 return command
644 .format('mp4')
645 .videoCodec('copy')
646 .audioCodec('copy')
647}
5c7d6508 648
41fb13c3 649function presetOnlyAudio (command: FfmpegCommand): FfmpegCommand {
5c7d6508 650 return command
651 .format('mp4')
652 .audioCodec('copy')
653 .noVideo()
654}
c6c0fa6c 655
41fb13c3 656function applyEncoderOptions (command: FfmpegCommand, options: EncoderOptions): FfmpegCommand {
43f7a43c
TLC
657 return command
658 .inputOptions(options.inputOptions ?? [])
43f7a43c
TLC
659 .outputOptions(options.outputOptions ?? [])
660}
661
3e03b961
C
662function getScaleFilter (options: EncoderOptions): string {
663 if (options.scaleFilter) return options.scaleFilter.name
664
665 return 'scale'
666}
667
9252a33d
C
668// ---------------------------------------------------------------------------
669// Utils
670// ---------------------------------------------------------------------------
671
55223d65 672function getFFmpeg (input: string, type: 'live' | 'vod') {
c6c0fa6c 673 // We set cwd explicitly because ffmpeg appears to create temporary files when trancoding which fails in read-only file systems
7abb6060
RK
674 const command = ffmpeg(input, {
675 niceness: type === 'live' ? FFMPEG_NICE.LIVE : FFMPEG_NICE.VOD,
676 cwd: CONFIG.STORAGE.TMP_DIR
677 })
c6c0fa6c 678
55223d65
C
679 const threads = type === 'live'
680 ? CONFIG.LIVE.TRANSCODING.THREADS
681 : CONFIG.TRANSCODING.THREADS
682
683 if (threads > 0) {
c6c0fa6c 684 // If we don't set any threads ffmpeg will chose automatically
55223d65 685 command.outputOption('-threads ' + threads)
c6c0fa6c
C
686 }
687
688 return command
689}
9252a33d 690
ae71acca
C
691function getFFmpegVersion () {
692 return new Promise<string>((res, rej) => {
693 (ffmpeg() as any)._getFfmpegPath((err, ffmpegPath) => {
694 if (err) return rej(err)
695 if (!ffmpegPath) return rej(new Error('Could not find ffmpeg path'))
696
697 return execPromise(`${ffmpegPath} -version`)
698 .then(stdout => {
60f1f615 699 const parsed = stdout.match(/ffmpeg version .?(\d+\.\d+(\.\d+)?)/)
ae71acca
C
700 if (!parsed || !parsed[1]) return rej(new Error(`Could not find ffmpeg version in ${stdout}`))
701
60f1f615
C
702 // Fix ffmpeg version that does not include patch version (4.4 for example)
703 let version = parsed[1]
1ff9f1cd 704 if (version.match(/^\d+\.\d+$/)) {
60f1f615
C
705 version += '.0'
706 }
707
708 return res(version)
ae71acca
C
709 })
710 .catch(err => rej(err))
711 })
712 })
713}
714
cd2c3dcd 715async function runCommand (options: {
41fb13c3 716 command: FfmpegCommand
cd2c3dcd
C
717 silent?: boolean // false
718 job?: Job
719}) {
720 const { command, silent = false, job } = options
721
9252a33d 722 return new Promise<void>((res, rej) => {
d0ea3e34
C
723 let shellCommand: string
724
725 command.on('start', cmdline => { shellCommand = cmdline })
726
9252a33d 727 command.on('error', (err, stdout, stderr) => {
cd2c3dcd
C
728 if (silent !== true) logger.error('Error in ffmpeg.', { stdout, stderr })
729
9252a33d
C
730 rej(err)
731 })
732
dd9c7929 733 command.on('end', (stdout, stderr) => {
d0ea3e34 734 logger.debug('FFmpeg command ended.', { stdout, stderr, shellCommand })
dd9c7929 735
9252a33d
C
736 res()
737 })
738
3b01f4c0
C
739 if (job) {
740 command.on('progress', progress => {
741 if (!progress.percent) return
742
743 job.progress(Math.round(progress.percent))
744 .catch(err => logger.warn('Cannot set ffmpeg job progress.', { err }))
745 })
746 }
747
9252a33d
C
748 command.run()
749 })
750}
09849603 751
318b0bd0
C
752// Avoid "height not divisible by 2" error
753function getScaleCleanerValue () {
754 return 'trunc(iw/2)*2:trunc(ih/2)*2'
755}
756
09849603
RK
757// ---------------------------------------------------------------------------
758
759export {
760 getLiveTranscodingCommand,
761 getLiveMuxingCommand,
762 buildStreamSuffix,
763 convertWebPToJPG,
764 processGIF,
765 generateImageFromVideoFile,
766 TranscodeOptions,
767 TranscodeOptionsType,
768 transcode,
769 runCommand,
ae71acca 770 getFFmpegVersion,
09849603 771
1896bca0
C
772 resetSupportedEncoders,
773
09849603
RK
774 // builders
775 buildx264VODCommand
776}