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