]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/helpers/ffmpeg-utils.ts
Add transcoding module comments
[github/Chocobozzz/PeerTube.git] / server / helpers / ffmpeg-utils.ts
CommitLineData
14d3270f 1import * as ffmpeg from 'fluent-ffmpeg'
053aed43 2import { readFile, remove, writeFile } from 'fs-extra'
09209296 3import { dirname, join } from 'path'
5a547f69
C
4import { FFMPEG_NICE, VIDEO_LIVE, VIDEO_TRANSCODING_ENCODERS, VIDEO_TRANSCODING_FPS } from '@server/initializers/constants'
5import { VideoResolution } from '../../shared/models/videos'
c6c0fa6c
C
6import { checkFFmpegEncoders } from '../initializers/checker-before-init'
7import { CONFIG } from '../initializers/config'
9252a33d 8import { getAudioStream, getClosestFramerateStandard, getVideoFileFPS } from './ffprobe-utils'
26670720 9import { processImage } from './image-utils'
6fdc553a 10import { logger } from './logger'
14d3270f 11
6b67897e
C
12/**
13 *
14 * Functions that run transcoding/muxing ffmpeg processes
15 * Mainly called by lib/video-transcoding.ts and lib/live-manager.ts
16 *
17 */
18
9252a33d
C
19// ---------------------------------------------------------------------------
20// Encoder options
21// ---------------------------------------------------------------------------
22
23// Options builders
24
25export type EncoderOptionsBuilder = (params: {
26 input: string
27 resolution: VideoResolution
28 fps?: number
5a547f69 29 streamNum?: number
9252a33d
C
30}) => Promise<EncoderOptions> | EncoderOptions
31
32// Options types
33
34export interface EncoderOptions {
5a547f69 35 copy?: boolean
9252a33d
C
36 outputOptions: string[]
37}
38
39// All our encoders
40
41export interface EncoderProfile <T> {
42 [ profile: string ]: T
43
44 default: T
45}
46
47export type AvailableEncoders = {
48 [ id in 'live' | 'vod' ]: {
5a547f69 49 [ encoder in 'libx264' | 'aac' | 'libfdk_aac' ]?: EncoderProfile<EncoderOptionsBuilder>
9252a33d
C
50 }
51}
52
53// ---------------------------------------------------------------------------
54// Image manipulation
55// ---------------------------------------------------------------------------
56
57function convertWebPToJPG (path: string, destination: string): Promise<void> {
58 const command = ffmpeg(path)
59 .output(destination)
60
61 return runCommand(command)
62}
63
64function processGIF (
65 path: string,
66 destination: string,
67 newSize: { width: number, height: number },
68 keepOriginal = false
69): Promise<void> {
70 return new Promise<void>(async (res, rej) => {
71 if (path === destination) {
72 throw new Error('FFmpeg needs an input path different that the output path.')
73 }
74
75 logger.debug('Processing gif %s to %s.', path, destination)
76
77 try {
78 const command = ffmpeg(path)
79 .fps(20)
80 .size(`${newSize.width}x${newSize.height}`)
81 .output(destination)
82
83 command.on('error', (err, stdout, stderr) => {
84 logger.error('Error in ffmpeg gif resizing process.', { stdout, stderr })
85 return rej(err)
86 })
87 .on('end', async () => {
88 if (keepOriginal !== true) await remove(path)
89 res()
90 })
91 .run()
92 } catch (err) {
93 return rej(err)
94 }
95 })
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) {
d5b7d911 120 logger.error('Cannot generate image from video %s.', fromPath, { err })
6fdc553a
C
121
122 try {
62689b94 123 await remove(pendingImagePath)
6fdc553a 124 } catch (err) {
d5b7d911 125 logger.debug('Cannot remove pending image path after generation error.', { err })
6fdc553a
C
126 }
127 }
14d3270f
C
128}
129
daf6e480
C
130// ---------------------------------------------------------------------------
131// Transcode meta function
132// ---------------------------------------------------------------------------
133
3a149e9f 134type TranscodeOptionsType = 'hls' | '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
09209296 145 resolution: VideoResolution
9252a33d 146
056aa7f2 147 isPortraitMode?: boolean
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
536598cf
C
158interface QuickTranscodeOptions extends BaseTranscodeOptions {
159 type: 'quick-transcode'
160}
161
162interface VideoTranscodeOptions extends BaseTranscodeOptions {
163 type: 'video'
164}
165
166interface MergeAudioTranscodeOptions extends BaseTranscodeOptions {
167 type: 'merge-audio'
168 audioPath: string
169}
170
3a149e9f
C
171interface OnlyAudioTranscodeOptions extends BaseTranscodeOptions {
172 type: 'only-audio'
5c7d6508 173}
174
a1587156
C
175type TranscodeOptions =
176 HLSTranscodeOptions
3a149e9f
C
177 | VideoTranscodeOptions
178 | MergeAudioTranscodeOptions
179 | OnlyAudioTranscodeOptions
180 | QuickTranscodeOptions
536598cf 181
daf6e480
C
182const builders: {
183 [ type in TranscodeOptionsType ]: (c: ffmpeg.FfmpegCommand, o?: TranscodeOptions) => Promise<ffmpeg.FfmpegCommand> | ffmpeg.FfmpegCommand
184} = {
185 'quick-transcode': buildQuickTranscodeCommand,
186 'hls': buildHLSVODCommand,
187 'merge-audio': buildAudioMergeCommand,
188 'only-audio': buildOnlyAudioCommand,
9252a33d 189 'video': buildx264VODCommand
daf6e480
C
190}
191
192async function transcode (options: TranscodeOptions) {
9e2b2e76
C
193 logger.debug('Will run transcode.', { options })
194
daf6e480
C
195 let command = getFFmpeg(options.inputPath)
196 .output(options.outputPath)
14d3270f 197
daf6e480 198 command = await builders[options.type](command, options)
7ed2c1a4 199
daf6e480 200 await runCommand(command)
5ba49f26 201
daf6e480 202 await fixHLSPlaylistIfNeeded(options)
837666fe
RK
203}
204
9252a33d
C
205// ---------------------------------------------------------------------------
206// Live muxing/transcoding functions
207// ---------------------------------------------------------------------------
123f6193 208
5a547f69
C
209async function getLiveTranscodingCommand (options: {
210 rtmpUrl: string
211 outPath: string
212 resolutions: number[]
213 fps: number
214 deleteSegments: boolean
215
216 availableEncoders: AvailableEncoders
217 profile: string
218}) {
219 const { rtmpUrl, outPath, resolutions, fps, deleteSegments, availableEncoders, profile } = options
220 const input = rtmpUrl
221
222 const command = getFFmpeg(input)
c6c0fa6c
C
223 command.inputOption('-fflags nobuffer')
224
225 const varStreamMap: string[] = []
226
227 command.complexFilter([
228 {
229 inputs: '[v:0]',
230 filter: 'split',
231 options: resolutions.length,
232 outputs: resolutions.map(r => `vtemp${r}`)
233 },
234
235 ...resolutions.map(r => ({
236 inputs: `vtemp${r}`,
237 filter: 'scale',
238 options: `w=-2:h=${r}`,
239 outputs: `vout${r}`
240 }))
241 ])
242
c6c0fa6c 243 command.outputOption('-preset superfast')
c6c0fa6c
C
244
245 for (let i = 0; i < resolutions.length; i++) {
246 const resolution = resolutions[i]
5a547f69
C
247 const baseEncoderBuilderParams = { input, availableEncoders, profile, fps, resolution, streamNum: i, videoType: 'live' as 'live' }
248
249 {
250 const builderResult = await getEncoderBuilderResult(Object.assign({}, baseEncoderBuilderParams, { streamType: 'VIDEO' }))
251 if (!builderResult) {
252 throw new Error('No available live video encoder found')
253 }
254
255 command.outputOption(`-map [vout${resolution}]`)
256
257 addDefaultEncoderParams({ command, encoder: builderResult.encoder, fps, streamNum: i })
258
259 logger.debug('Apply ffmpeg live video params from %s.', builderResult.encoder, builderResult)
c6c0fa6c 260
5a547f69
C
261 command.outputOption(`${buildStreamSuffix('-c:v', i)} ${builderResult.encoder}`)
262 command.addOutputOptions(builderResult.result.outputOptions)
263 }
264
265 {
266 const builderResult = await getEncoderBuilderResult(Object.assign({}, baseEncoderBuilderParams, { streamType: 'AUDIO' }))
267 if (!builderResult) {
268 throw new Error('No available live audio encoder found')
269 }
c6c0fa6c 270
5a547f69
C
271 command.outputOption('-map a:0')
272
273 addDefaultEncoderParams({ command, encoder: builderResult.encoder, fps, streamNum: i })
274
275 logger.debug('Apply ffmpeg live audio params from %s.', builderResult.encoder, builderResult)
276
277 command.outputOption(`${buildStreamSuffix('-c:a', i)} ${builderResult.encoder}`)
278 command.addOutputOptions(builderResult.result.outputOptions)
279 }
c6c0fa6c
C
280
281 varStreamMap.push(`v:${i},a:${i}`)
282 }
283
fb719404 284 addDefaultLiveHLSParams(command, outPath, deleteSegments)
c6c0fa6c
C
285
286 command.outputOption('-var_stream_map', varStreamMap.join(' '))
287
c6c0fa6c
C
288 return command
289}
290
9252a33d 291function getLiveMuxingCommand (rtmpUrl: string, outPath: string, deleteSegments: boolean) {
c6c0fa6c
C
292 const command = getFFmpeg(rtmpUrl)
293 command.inputOption('-fflags nobuffer')
294
295 command.outputOption('-c:v copy')
296 command.outputOption('-c:a copy')
297 command.outputOption('-map 0:a?')
298 command.outputOption('-map 0:v?')
299
fb719404 300 addDefaultLiveHLSParams(command, outPath, deleteSegments)
c6c0fa6c 301
c6c0fa6c
C
302 return command
303}
304
31c82cd9 305async function hlsPlaylistToFragmentedMP4 (hlsDirectory: string, segmentFiles: string[], outputPath: string) {
3bc68dfd
C
306 const concatFilePath = join(hlsDirectory, 'concat.txt')
307
308 function cleaner () {
309 remove(concatFilePath)
310 .catch(err => logger.error('Cannot remove concat file in %s.', hlsDirectory, { err }))
311 }
312
313 // First concat the ts files to a mp4 file
31c82cd9
C
314 const content = segmentFiles.map(f => 'file ' + f)
315 .join('\n')
316
317 await writeFile(concatFilePath, content + '\n')
318
319 const command = getFFmpeg(concatFilePath)
320 command.inputOption('-safe 0')
321 command.inputOption('-f concat')
b5b68755 322
3bc68dfd
C
323 command.outputOption('-c:v copy')
324 command.audioFilter('aresample=async=1:first_pts=0')
b5b68755
C
325 command.output(outputPath)
326
3bc68dfd
C
327 return runCommand(command, cleaner)
328}
b5b68755 329
5a547f69
C
330function buildStreamSuffix (base: string, streamNum?: number) {
331 if (streamNum !== undefined) {
332 return `${base}:${streamNum}`
333 }
334
335 return base
336}
337
14d3270f
C
338// ---------------------------------------------------------------------------
339
340export {
9252a33d
C
341 getLiveTranscodingCommand,
342 getLiveMuxingCommand,
5a547f69 343 buildStreamSuffix,
18782242 344 convertWebPToJPG,
123f6193 345 processGIF,
14d3270f 346 generateImageFromVideoFile,
536598cf
C
347 TranscodeOptions,
348 TranscodeOptionsType,
73c69591 349 transcode,
daf6e480 350 hlsPlaylistToFragmentedMP4
73c69591
C
351}
352
353// ---------------------------------------------------------------------------
354
9252a33d
C
355// ---------------------------------------------------------------------------
356// Default options
357// ---------------------------------------------------------------------------
358
5a547f69
C
359function addDefaultEncoderParams (options: {
360 command: ffmpeg.FfmpegCommand
361 encoder: 'libx264' | string
362 streamNum?: number
363 fps?: number
364}) {
365 const { command, encoder, fps, streamNum } = options
366
367 if (encoder === 'libx264') {
368 // 3.1 is the minimal resource allocation for our highest supported resolution
369 command.outputOption('-level 3.1')
370 // NOTE: b-strategy 1 - heuristic algorithm, 16 is optimal B-frames for it
371 .outputOption('-b_strategy 1')
372 // NOTE: Why 16: https://github.com/Chocobozzz/PeerTube/pull/774. b-strategy 2 -> B-frames<16
373 .outputOption('-bf 16')
374 // allows import of source material with incompatible pixel formats (e.g. MJPEG video)
375 .outputOption(buildStreamSuffix('-pix_fmt', streamNum) + ' yuv420p')
376 // strip all metadata
377 .outputOption('-map_metadata -1')
378 // avoid issues when transcoding some files: https://trac.ffmpeg.org/ticket/6375
379 .outputOption(buildStreamSuffix('-max_muxing_queue_size', streamNum) + ' 1024')
380
381 if (fps) {
382 // Keyframe interval of 2 seconds for faster seeking and resolution switching.
383 // https://streaminglearningcenter.com/blogs/whats-the-right-keyframe-interval.html
384 // https://superuser.com/a/908325
385 command.outputOption('-g ' + (fps * 2))
386 }
387 }
c6c0fa6c
C
388}
389
fb719404 390function addDefaultLiveHLSParams (command: ffmpeg.FfmpegCommand, outPath: string, deleteSegments: boolean) {
68e70a74 391 command.outputOption('-hls_time ' + VIDEO_LIVE.SEGMENT_TIME_SECONDS)
fb719404
C
392 command.outputOption('-hls_list_size ' + VIDEO_LIVE.SEGMENTS_LIST_SIZE)
393
394 if (deleteSegments === true) {
395 command.outputOption('-hls_flags delete_segments')
396 }
397
21226063 398 command.outputOption(`-hls_segment_filename ${join(outPath, '%v-%06d.ts')}`)
c6c0fa6c
C
399 command.outputOption('-master_pl_name master.m3u8')
400 command.outputOption(`-f hls`)
401
402 command.output(join(outPath, '%v.m3u8'))
403}
404
9252a33d
C
405// ---------------------------------------------------------------------------
406// Transcode VOD command builders
407// ---------------------------------------------------------------------------
408
409async function buildx264VODCommand (command: ffmpeg.FfmpegCommand, options: TranscodeOptions) {
14aed608 410 let fps = await getVideoFileFPS(options.inputPath)
14aed608 411 if (
06bcfbd9 412 // On small/medium resolutions, limit FPS
14aed608
C
413 options.resolution !== undefined &&
414 options.resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN &&
c7f36e4f 415 fps > VIDEO_TRANSCODING_FPS.AVERAGE
14aed608 416 ) {
06bcfbd9 417 // Get closest standard framerate by modulo: downsampling has to be done to a divisor of the nominal fps value
c7f36e4f 418 fps = getClosestFramerateStandard(fps, 'STANDARD')
14aed608
C
419 }
420
9252a33d 421 command = await presetVideo(command, options.inputPath, options, fps)
14aed608
C
422
423 if (options.resolution !== undefined) {
424 // '?x720' or '720x?' for example
5a547f69
C
425 const size = options.isPortraitMode === true
426 ? `${options.resolution}x?`
427 : `?x${options.resolution}`
428
14aed608
C
429 command = command.size(size)
430 }
431
5a547f69
C
432 // Hard FPS limits
433 if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = getClosestFramerateStandard(fps, 'HD_STANDARD')
434 else if (fps < VIDEO_TRANSCODING_FPS.MIN) fps = VIDEO_TRANSCODING_FPS.MIN
14aed608 435
5a547f69 436 command = command.withFPS(fps)
14aed608
C
437
438 return command
439}
440
536598cf
C
441async function buildAudioMergeCommand (command: ffmpeg.FfmpegCommand, options: MergeAudioTranscodeOptions) {
442 command = command.loop(undefined)
443
9252a33d
C
444 command = await presetVideo(command, options.audioPath, options)
445
446 /*
447 MAIN reference: https://slhck.info/video/2017/03/01/rate-control.html
448 Our target situation is closer to a livestream than a stream,
449 since we want to reduce as much a possible the encoding burden,
450 although not to the point of a livestream where there is a hard
451 constraint on the frames per second to be encoded.
452 */
453 command.outputOption('-preset:v veryfast')
536598cf
C
454
455 command = command.input(options.audioPath)
456 .videoFilter('scale=trunc(iw/2)*2:trunc(ih/2)*2') // Avoid "height not divisible by 2" error
457 .outputOption('-tune stillimage')
458 .outputOption('-shortest')
459
460 return command
461}
462
daf6e480 463function buildOnlyAudioCommand (command: ffmpeg.FfmpegCommand, _options: OnlyAudioTranscodeOptions) {
a1587156 464 command = presetOnlyAudio(command)
5c7d6508 465
466 return command
467}
468
a1587156
C
469function buildQuickTranscodeCommand (command: ffmpeg.FfmpegCommand) {
470 command = presetCopy(command)
536598cf
C
471
472 command = command.outputOption('-map_metadata -1') // strip all metadata
473 .outputOption('-movflags faststart')
474
475 return command
476}
477
c6c0fa6c 478async function buildHLSVODCommand (command: ffmpeg.FfmpegCommand, options: HLSTranscodeOptions) {
14aed608
C
479 const videoPath = getHLSVideoPath(options)
480
a1587156 481 if (options.copyCodecs) command = presetCopy(command)
1c320673 482 else if (options.resolution === VideoResolution.H_NOVIDEO) command = presetOnlyAudio(command)
9252a33d 483 else command = await buildx264VODCommand(command, options)
14aed608
C
484
485 command = command.outputOption('-hls_time 4')
486 .outputOption('-hls_list_size 0')
487 .outputOption('-hls_playlist_type vod')
488 .outputOption('-hls_segment_filename ' + videoPath)
489 .outputOption('-hls_segment_type fmp4')
490 .outputOption('-f hls')
491 .outputOption('-hls_flags single_file')
492
493 return command
494}
495
536598cf
C
496async function fixHLSPlaylistIfNeeded (options: TranscodeOptions) {
497 if (options.type !== 'hls') 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
9252a33d
C
511function getHLSVideoPath (options: HLSTranscodeOptions) {
512 return `${dirname(options.outputPath)}/${options.hlsPlaylist.videoFilename}`
4176e227
RK
513}
514
9252a33d
C
515// ---------------------------------------------------------------------------
516// Transcoding presets
517// ---------------------------------------------------------------------------
518
5a547f69
C
519async function getEncoderBuilderResult (options: {
520 streamType: string
521 input: string
522
523 availableEncoders: AvailableEncoders
524 profile: string
525
526 videoType: 'vod' | 'live'
527
528 resolution: number
529 fps?: number
530 streamNum?: number
531}) {
532 const { availableEncoders, input, profile, resolution, streamType, fps, streamNum, videoType } = options
533
534 const encodersToTry: string[] = VIDEO_TRANSCODING_ENCODERS[streamType]
535
536 for (const encoder of encodersToTry) {
537 if (!(await checkFFmpegEncoders()).get(encoder) || !availableEncoders[videoType][encoder]) continue
538
539 const builderProfiles: EncoderProfile<EncoderOptionsBuilder> = availableEncoders[videoType][encoder]
540 let builder = builderProfiles[profile]
541
542 if (!builder) {
543 logger.debug('Profile %s for encoder %s not available. Fallback to default.', profile, encoder)
544 builder = builderProfiles.default
545 }
546
547 const result = await builder({ input, resolution: resolution, fps, streamNum })
548
549 return {
550 result,
551
552 // If we don't have output options, then copy the input stream
553 encoder: result.copy === true
554 ? 'copy'
555 : encoder
556 }
557 }
558
559 return null
560}
561
9252a33d
C
562async function presetVideo (
563 command: ffmpeg.FfmpegCommand,
564 input: string,
565 transcodeOptions: TranscodeOptions,
566 fps?: number
567) {
cdf4cb9e 568 let localCommand = command
4176e227 569 .format('mp4')
4176e227 570 .outputOption('-movflags faststart')
4176e227 571
9252a33d 572 // Audio encoder
daf6e480 573 const parsedAudio = await getAudioStream(input)
4176e227 574
9252a33d 575 let streamsToProcess = [ 'AUDIO', 'VIDEO' ]
9252a33d 576
cdf4cb9e
C
577 if (!parsedAudio.audioStream) {
578 localCommand = localCommand.noAudio()
9252a33d
C
579 streamsToProcess = [ 'VIDEO' ]
580 }
536598cf 581
5a547f69
C
582 for (const streamType of streamsToProcess) {
583 const { profile, resolution, availableEncoders } = transcodeOptions
584
585 const builderResult = await getEncoderBuilderResult({
586 streamType,
587 input,
588 resolution,
589 availableEncoders,
590 profile,
591 fps,
592 videoType: 'vod' as 'vod'
593 })
9252a33d 594
5a547f69
C
595 if (!builderResult) {
596 throw new Error('No available encoder found for stream ' + streamType)
597 }
9252a33d 598
5a547f69 599 logger.debug('Apply ffmpeg params from %s.', builderResult.encoder, builderResult)
9252a33d 600
5a547f69
C
601 if (streamType === 'VIDEO') {
602 localCommand.videoCodec(builderResult.encoder)
603 } else if (streamType === 'AUDIO') {
604 localCommand.audioCodec(builderResult.encoder)
9252a33d
C
605 }
606
5a547f69
C
607 command.addOutputOptions(builderResult.result.outputOptions)
608 addDefaultEncoderParams({ command: localCommand, encoder: builderResult.encoder, fps })
536598cf 609 }
bcf21a37 610
cdf4cb9e 611 return localCommand
4176e227 612}
14aed608 613
a1587156 614function presetCopy (command: ffmpeg.FfmpegCommand): ffmpeg.FfmpegCommand {
14aed608
C
615 return command
616 .format('mp4')
617 .videoCodec('copy')
618 .audioCodec('copy')
619}
5c7d6508 620
a1587156 621function presetOnlyAudio (command: ffmpeg.FfmpegCommand): ffmpeg.FfmpegCommand {
5c7d6508 622 return command
623 .format('mp4')
624 .audioCodec('copy')
625 .noVideo()
626}
c6c0fa6c 627
9252a33d
C
628// ---------------------------------------------------------------------------
629// Utils
630// ---------------------------------------------------------------------------
631
c6c0fa6c
C
632function getFFmpeg (input: string) {
633 // We set cwd explicitly because ffmpeg appears to create temporary files when trancoding which fails in read-only file systems
634 const command = ffmpeg(input, { niceness: FFMPEG_NICE.TRANSCODING, cwd: CONFIG.STORAGE.TMP_DIR })
635
636 if (CONFIG.TRANSCODING.THREADS > 0) {
637 // If we don't set any threads ffmpeg will chose automatically
638 command.outputOption('-threads ' + CONFIG.TRANSCODING.THREADS)
639 }
640
641 return command
642}
9252a33d
C
643
644async function runCommand (command: ffmpeg.FfmpegCommand, onEnd?: Function) {
645 return new Promise<void>((res, rej) => {
646 command.on('error', (err, stdout, stderr) => {
647 if (onEnd) onEnd()
648
649 logger.error('Error in transcoding job.', { stdout, stderr })
650 rej(err)
651 })
652
653 command.on('end', () => {
654 if (onEnd) onEnd()
655
656 res()
657 })
658
659 command.run()
660 })
661}