]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/helpers/ffmpeg-utils.ts
Fix audio sync after saving replay
[github/Chocobozzz/PeerTube.git] / server / helpers / ffmpeg-utils.ts
1 import * as ffmpeg from 'fluent-ffmpeg'
2 import { outputFile, readFile, remove, writeFile } from 'fs-extra'
3 import { dirname, join } from 'path'
4 import { VideoFileMetadata } from '@shared/models/videos/video-file-metadata'
5 import { getMaxBitrate, getTargetBitrate, VideoResolution } from '../../shared/models/videos'
6 import { checkFFmpegEncoders } from '../initializers/checker-before-init'
7 import { CONFIG } from '../initializers/config'
8 import { FFMPEG_NICE, VIDEO_LIVE, VIDEO_TRANSCODING_FPS } from '../initializers/constants'
9 import { processImage } from './image-utils'
10 import { logger } from './logger'
11
12 /**
13 * A toolbox to play with audio
14 */
15 namespace audio {
16 export const get = (videoPath: string) => {
17 // without position, ffprobe considers the last input only
18 // we make it consider the first input only
19 // if you pass a file path to pos, then ffprobe acts on that file directly
20 return new Promise<{ absolutePath: string, audioStream?: any }>((res, rej) => {
21
22 function parseFfprobe (err: any, data: ffmpeg.FfprobeData) {
23 if (err) return rej(err)
24
25 if ('streams' in data) {
26 const audioStream = data.streams.find(stream => stream['codec_type'] === 'audio')
27 if (audioStream) {
28 return res({
29 absolutePath: data.format.filename,
30 audioStream
31 })
32 }
33 }
34
35 return res({ absolutePath: data.format.filename })
36 }
37
38 return ffmpeg.ffprobe(videoPath, parseFfprobe)
39 })
40 }
41
42 export namespace bitrate {
43 const baseKbitrate = 384
44
45 const toBits = (kbits: number) => kbits * 8000
46
47 export const aac = (bitrate: number): number => {
48 switch (true) {
49 case bitrate > toBits(baseKbitrate):
50 return baseKbitrate
51
52 default:
53 return -1 // we interpret it as a signal to copy the audio stream as is
54 }
55 }
56
57 export const mp3 = (bitrate: number): number => {
58 /*
59 a 192kbit/sec mp3 doesn't hold as much information as a 192kbit/sec aac.
60 That's why, when using aac, we can go to lower kbit/sec. The equivalences
61 made here are not made to be accurate, especially with good mp3 encoders.
62 */
63 switch (true) {
64 case bitrate <= toBits(192):
65 return 128
66
67 case bitrate <= toBits(384):
68 return 256
69
70 default:
71 return baseKbitrate
72 }
73 }
74 }
75 }
76
77 function computeResolutionsToTranscode (videoFileResolution: number, type: 'vod' | 'live') {
78 const configResolutions = type === 'vod'
79 ? CONFIG.TRANSCODING.RESOLUTIONS
80 : CONFIG.LIVE.TRANSCODING.RESOLUTIONS
81
82 const resolutionsEnabled: number[] = []
83
84 // Put in the order we want to proceed jobs
85 const resolutions = [
86 VideoResolution.H_NOVIDEO,
87 VideoResolution.H_480P,
88 VideoResolution.H_360P,
89 VideoResolution.H_720P,
90 VideoResolution.H_240P,
91 VideoResolution.H_1080P,
92 VideoResolution.H_4K
93 ]
94
95 for (const resolution of resolutions) {
96 if (configResolutions[resolution + 'p'] === true && videoFileResolution > resolution) {
97 resolutionsEnabled.push(resolution)
98 }
99 }
100
101 return resolutionsEnabled
102 }
103
104 async function getVideoStreamSize (path: string) {
105 const videoStream = await getVideoStreamFromFile(path)
106
107 return videoStream === null
108 ? { width: 0, height: 0 }
109 : { width: videoStream.width, height: videoStream.height }
110 }
111
112 async function getVideoStreamCodec (path: string) {
113 const videoStream = await getVideoStreamFromFile(path)
114
115 if (!videoStream) return ''
116
117 const videoCodec = videoStream.codec_tag_string
118
119 const baseProfileMatrix = {
120 High: '6400',
121 Main: '4D40',
122 Baseline: '42E0'
123 }
124
125 let baseProfile = baseProfileMatrix[videoStream.profile]
126 if (!baseProfile) {
127 logger.warn('Cannot get video profile codec of %s.', path, { videoStream })
128 baseProfile = baseProfileMatrix['High'] // Fallback
129 }
130
131 let level = videoStream.level.toString(16)
132 if (level.length === 1) level = `0${level}`
133
134 return `${videoCodec}.${baseProfile}${level}`
135 }
136
137 async function getAudioStreamCodec (path: string) {
138 const { audioStream } = await audio.get(path)
139
140 if (!audioStream) return ''
141
142 const audioCodec = audioStream.codec_name
143 if (audioCodec === 'aac') return 'mp4a.40.2'
144
145 logger.warn('Cannot get audio codec of %s.', path, { audioStream })
146
147 return 'mp4a.40.2' // Fallback
148 }
149
150 async function getVideoFileResolution (path: string) {
151 const size = await getVideoStreamSize(path)
152
153 return {
154 videoFileResolution: Math.min(size.height, size.width),
155 isPortraitMode: size.height > size.width
156 }
157 }
158
159 async function getVideoFileFPS (path: string) {
160 const videoStream = await getVideoStreamFromFile(path)
161 if (videoStream === null) return 0
162
163 for (const key of [ 'avg_frame_rate', 'r_frame_rate' ]) {
164 const valuesText: string = videoStream[key]
165 if (!valuesText) continue
166
167 const [ frames, seconds ] = valuesText.split('/')
168 if (!frames || !seconds) continue
169
170 const result = parseInt(frames, 10) / parseInt(seconds, 10)
171 if (result > 0) return Math.round(result)
172 }
173
174 return 0
175 }
176
177 async function getMetadataFromFile <T> (path: string, cb = metadata => metadata) {
178 return new Promise<T>((res, rej) => {
179 ffmpeg.ffprobe(path, (err, metadata) => {
180 if (err) return rej(err)
181
182 return res(cb(new VideoFileMetadata(metadata)))
183 })
184 })
185 }
186
187 async function getVideoFileBitrate (path: string) {
188 return getMetadataFromFile<number>(path, metadata => metadata.format.bit_rate)
189 }
190
191 function getDurationFromVideoFile (path: string) {
192 return getMetadataFromFile<number>(path, metadata => Math.floor(metadata.format.duration))
193 }
194
195 function getVideoStreamFromFile (path: string) {
196 return getMetadataFromFile<any>(path, metadata => metadata.streams.find(s => s.codec_type === 'video') || null)
197 }
198
199 async function generateImageFromVideoFile (fromPath: string, folder: string, imageName: string, size: { width: number, height: number }) {
200 const pendingImageName = 'pending-' + imageName
201
202 const options = {
203 filename: pendingImageName,
204 count: 1,
205 folder
206 }
207
208 const pendingImagePath = join(folder, pendingImageName)
209
210 try {
211 await new Promise<string>((res, rej) => {
212 ffmpeg(fromPath, { niceness: FFMPEG_NICE.THUMBNAIL })
213 .on('error', rej)
214 .on('end', () => res(imageName))
215 .thumbnail(options)
216 })
217
218 const destination = join(folder, imageName)
219 await processImage(pendingImagePath, destination, size)
220 } catch (err) {
221 logger.error('Cannot generate image from video %s.', fromPath, { err })
222
223 try {
224 await remove(pendingImagePath)
225 } catch (err) {
226 logger.debug('Cannot remove pending image path after generation error.', { err })
227 }
228 }
229 }
230
231 type TranscodeOptionsType = 'hls' | 'quick-transcode' | 'video' | 'merge-audio' | 'only-audio'
232
233 interface BaseTranscodeOptions {
234 type: TranscodeOptionsType
235 inputPath: string
236 outputPath: string
237 resolution: VideoResolution
238 isPortraitMode?: boolean
239 }
240
241 interface HLSTranscodeOptions extends BaseTranscodeOptions {
242 type: 'hls'
243 copyCodecs: boolean
244 hlsPlaylist: {
245 videoFilename: string
246 }
247 }
248
249 interface QuickTranscodeOptions extends BaseTranscodeOptions {
250 type: 'quick-transcode'
251 }
252
253 interface VideoTranscodeOptions extends BaseTranscodeOptions {
254 type: 'video'
255 }
256
257 interface MergeAudioTranscodeOptions extends BaseTranscodeOptions {
258 type: 'merge-audio'
259 audioPath: string
260 }
261
262 interface OnlyAudioTranscodeOptions extends BaseTranscodeOptions {
263 type: 'only-audio'
264 }
265
266 type TranscodeOptions =
267 HLSTranscodeOptions
268 | VideoTranscodeOptions
269 | MergeAudioTranscodeOptions
270 | OnlyAudioTranscodeOptions
271 | QuickTranscodeOptions
272
273 function transcode (options: TranscodeOptions) {
274 return new Promise<void>(async (res, rej) => {
275 try {
276 let command = getFFmpeg(options.inputPath)
277 .output(options.outputPath)
278
279 if (options.type === 'quick-transcode') {
280 command = buildQuickTranscodeCommand(command)
281 } else if (options.type === 'hls') {
282 command = await buildHLSVODCommand(command, options)
283 } else if (options.type === 'merge-audio') {
284 command = await buildAudioMergeCommand(command, options)
285 } else if (options.type === 'only-audio') {
286 command = buildOnlyAudioCommand(command, options)
287 } else {
288 command = await buildx264Command(command, options)
289 }
290
291 command
292 .on('error', (err, stdout, stderr) => {
293 logger.error('Error in transcoding job.', { stdout, stderr })
294 return rej(err)
295 })
296 .on('end', () => {
297 return fixHLSPlaylistIfNeeded(options)
298 .then(() => res())
299 .catch(err => rej(err))
300 })
301 .run()
302 } catch (err) {
303 return rej(err)
304 }
305 })
306 }
307
308 async function canDoQuickTranscode (path: string): Promise<boolean> {
309 // NOTE: This could be optimized by running ffprobe only once (but it runs fast anyway)
310 const videoStream = await getVideoStreamFromFile(path)
311 const parsedAudio = await audio.get(path)
312 const fps = await getVideoFileFPS(path)
313 const bitRate = await getVideoFileBitrate(path)
314 const resolution = await getVideoFileResolution(path)
315
316 // check video params
317 if (videoStream == null) return false
318 if (videoStream['codec_name'] !== 'h264') return false
319 if (videoStream['pix_fmt'] !== 'yuv420p') return false
320 if (fps < VIDEO_TRANSCODING_FPS.MIN || fps > VIDEO_TRANSCODING_FPS.MAX) return false
321 if (bitRate > getMaxBitrate(resolution.videoFileResolution, fps, VIDEO_TRANSCODING_FPS)) return false
322
323 // check audio params (if audio stream exists)
324 if (parsedAudio.audioStream) {
325 if (parsedAudio.audioStream['codec_name'] !== 'aac') return false
326
327 const maxAudioBitrate = audio.bitrate['aac'](parsedAudio.audioStream['bit_rate'])
328 if (maxAudioBitrate !== -1 && parsedAudio.audioStream['bit_rate'] > maxAudioBitrate) return false
329 }
330
331 return true
332 }
333
334 function getClosestFramerateStandard (fps: number, type: 'HD_STANDARD' | 'STANDARD'): number {
335 return VIDEO_TRANSCODING_FPS[type].slice(0)
336 .sort((a, b) => fps % a - fps % b)[0]
337 }
338
339 function convertWebPToJPG (path: string, destination: string): Promise<void> {
340 return new Promise<void>(async (res, rej) => {
341 try {
342 const command = ffmpeg(path).output(destination)
343
344 command.on('error', (err, stdout, stderr) => {
345 logger.error('Error in ffmpeg webp convert process.', { stdout, stderr })
346 return rej(err)
347 })
348 .on('end', () => res())
349 .run()
350 } catch (err) {
351 return rej(err)
352 }
353 })
354 }
355
356 function runLiveTranscoding (rtmpUrl: string, outPath: string, resolutions: number[], fps, deleteSegments: boolean) {
357 const command = getFFmpeg(rtmpUrl)
358 command.inputOption('-fflags nobuffer')
359
360 const varStreamMap: string[] = []
361
362 command.complexFilter([
363 {
364 inputs: '[v:0]',
365 filter: 'split',
366 options: resolutions.length,
367 outputs: resolutions.map(r => `vtemp${r}`)
368 },
369
370 ...resolutions.map(r => ({
371 inputs: `vtemp${r}`,
372 filter: 'scale',
373 options: `w=-2:h=${r}`,
374 outputs: `vout${r}`
375 }))
376 ])
377
378 command.outputOption('-b_strategy 1')
379 command.outputOption('-bf 16')
380 command.outputOption('-preset superfast')
381 command.outputOption('-level 3.1')
382 command.outputOption('-map_metadata -1')
383 command.outputOption('-pix_fmt yuv420p')
384 command.outputOption('-max_muxing_queue_size 1024')
385 command.outputOption('-g ' + (fps * 2))
386
387 for (let i = 0; i < resolutions.length; i++) {
388 const resolution = resolutions[i]
389
390 command.outputOption(`-map [vout${resolution}]`)
391 command.outputOption(`-c:v:${i} libx264`)
392 command.outputOption(`-b:v:${i} ${getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS)}`)
393
394 command.outputOption(`-map a:0`)
395 command.outputOption(`-c:a:${i} aac`)
396
397 varStreamMap.push(`v:${i},a:${i}`)
398 }
399
400 addDefaultLiveHLSParams(command, outPath, deleteSegments)
401
402 command.outputOption('-var_stream_map', varStreamMap.join(' '))
403
404 command.run()
405
406 return command
407 }
408
409 function runLiveMuxing (rtmpUrl: string, outPath: string, deleteSegments: boolean) {
410 const command = getFFmpeg(rtmpUrl)
411 command.inputOption('-fflags nobuffer')
412
413 command.outputOption('-c:v copy')
414 command.outputOption('-c:a copy')
415 command.outputOption('-map 0:a?')
416 command.outputOption('-map 0:v?')
417
418 addDefaultLiveHLSParams(command, outPath, deleteSegments)
419
420 command.run()
421
422 return command
423 }
424
425 async function hlsPlaylistToFragmentedMP4 (hlsDirectory: string, segmentFiles: string[], outputPath: string) {
426 const concatFilePath = join(hlsDirectory, 'concat.txt')
427
428 function cleaner () {
429 remove(concatFilePath)
430 .catch(err => logger.error('Cannot remove concat file in %s.', hlsDirectory, { err }))
431 }
432
433 // First concat the ts files to a mp4 file
434 const content = segmentFiles.map(f => 'file ' + f)
435 .join('\n')
436
437 await writeFile(concatFilePath, content + '\n')
438
439 const command = getFFmpeg(concatFilePath)
440 command.inputOption('-safe 0')
441 command.inputOption('-f concat')
442
443 command.outputOption('-c:v copy')
444 command.audioFilter('aresample=async=1:first_pts=0')
445 command.output(outputPath)
446
447 return runCommand(command, cleaner)
448 }
449
450 async function runCommand (command: ffmpeg.FfmpegCommand, onEnd?: Function) {
451 command.run()
452
453 return new Promise<string>((res, rej) => {
454 command.on('error', err => {
455 if (onEnd) onEnd()
456
457 rej(err)
458 })
459
460 command.on('end', () => {
461 if (onEnd) onEnd()
462
463 res()
464 })
465 })
466 }
467
468 // ---------------------------------------------------------------------------
469
470 export {
471 getVideoStreamCodec,
472 getAudioStreamCodec,
473 runLiveMuxing,
474 convertWebPToJPG,
475 getVideoStreamSize,
476 getVideoFileResolution,
477 getMetadataFromFile,
478 getDurationFromVideoFile,
479 runLiveTranscoding,
480 generateImageFromVideoFile,
481 TranscodeOptions,
482 TranscodeOptionsType,
483 transcode,
484 getVideoFileFPS,
485 computeResolutionsToTranscode,
486 audio,
487 hlsPlaylistToFragmentedMP4,
488 getVideoFileBitrate,
489 canDoQuickTranscode
490 }
491
492 // ---------------------------------------------------------------------------
493
494 function addDefaultX264Params (command: ffmpeg.FfmpegCommand) {
495 command.outputOption('-level 3.1') // 3.1 is the minimal resource allocation for our highest supported resolution
496 .outputOption('-b_strategy 1') // NOTE: b-strategy 1 - heuristic algorithm, 16 is optimal B-frames for it
497 .outputOption('-bf 16') // NOTE: Why 16: https://github.com/Chocobozzz/PeerTube/pull/774. b-strategy 2 -> B-frames<16
498 .outputOption('-pix_fmt yuv420p') // allows import of source material with incompatible pixel formats (e.g. MJPEG video)
499 .outputOption('-map_metadata -1') // strip all metadata
500 }
501
502 function addDefaultLiveHLSParams (command: ffmpeg.FfmpegCommand, outPath: string, deleteSegments: boolean) {
503 command.outputOption('-hls_time ' + VIDEO_LIVE.SEGMENT_TIME_SECONDS)
504 command.outputOption('-hls_list_size ' + VIDEO_LIVE.SEGMENTS_LIST_SIZE)
505
506 if (deleteSegments === true) {
507 command.outputOption('-hls_flags delete_segments')
508 }
509
510 command.outputOption(`-hls_segment_filename ${join(outPath, '%v-%04d.ts')}`)
511 command.outputOption('-master_pl_name master.m3u8')
512 command.outputOption(`-f hls`)
513
514 command.output(join(outPath, '%v.m3u8'))
515 }
516
517 async function buildx264Command (command: ffmpeg.FfmpegCommand, options: TranscodeOptions) {
518 let fps = await getVideoFileFPS(options.inputPath)
519 if (
520 // On small/medium resolutions, limit FPS
521 options.resolution !== undefined &&
522 options.resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN &&
523 fps > VIDEO_TRANSCODING_FPS.AVERAGE
524 ) {
525 // Get closest standard framerate by modulo: downsampling has to be done to a divisor of the nominal fps value
526 fps = getClosestFramerateStandard(fps, 'STANDARD')
527 }
528
529 command = await presetH264(command, options.inputPath, options.resolution, fps)
530
531 if (options.resolution !== undefined) {
532 // '?x720' or '720x?' for example
533 const size = options.isPortraitMode === true ? `${options.resolution}x?` : `?x${options.resolution}`
534 command = command.size(size)
535 }
536
537 if (fps) {
538 // Hard FPS limits
539 if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = getClosestFramerateStandard(fps, 'HD_STANDARD')
540 else if (fps < VIDEO_TRANSCODING_FPS.MIN) fps = VIDEO_TRANSCODING_FPS.MIN
541
542 command = command.withFPS(fps)
543 }
544
545 return command
546 }
547
548 async function buildAudioMergeCommand (command: ffmpeg.FfmpegCommand, options: MergeAudioTranscodeOptions) {
549 command = command.loop(undefined)
550
551 command = await presetH264VeryFast(command, options.audioPath, options.resolution)
552
553 command = command.input(options.audioPath)
554 .videoFilter('scale=trunc(iw/2)*2:trunc(ih/2)*2') // Avoid "height not divisible by 2" error
555 .outputOption('-tune stillimage')
556 .outputOption('-shortest')
557
558 return command
559 }
560
561 function buildOnlyAudioCommand (command: ffmpeg.FfmpegCommand, options: OnlyAudioTranscodeOptions) {
562 command = presetOnlyAudio(command)
563
564 return command
565 }
566
567 function buildQuickTranscodeCommand (command: ffmpeg.FfmpegCommand) {
568 command = presetCopy(command)
569
570 command = command.outputOption('-map_metadata -1') // strip all metadata
571 .outputOption('-movflags faststart')
572
573 return command
574 }
575
576 async function buildHLSVODCommand (command: ffmpeg.FfmpegCommand, options: HLSTranscodeOptions) {
577 const videoPath = getHLSVideoPath(options)
578
579 if (options.copyCodecs) command = presetCopy(command)
580 else if (options.resolution === VideoResolution.H_NOVIDEO) command = presetOnlyAudio(command)
581 else command = await buildx264Command(command, options)
582
583 command = command.outputOption('-hls_time 4')
584 .outputOption('-hls_list_size 0')
585 .outputOption('-hls_playlist_type vod')
586 .outputOption('-hls_segment_filename ' + videoPath)
587 .outputOption('-hls_segment_type fmp4')
588 .outputOption('-f hls')
589 .outputOption('-hls_flags single_file')
590
591 return command
592 }
593
594 function getHLSVideoPath (options: HLSTranscodeOptions) {
595 return `${dirname(options.outputPath)}/${options.hlsPlaylist.videoFilename}`
596 }
597
598 async function fixHLSPlaylistIfNeeded (options: TranscodeOptions) {
599 if (options.type !== 'hls') return
600
601 const fileContent = await readFile(options.outputPath)
602
603 const videoFileName = options.hlsPlaylist.videoFilename
604 const videoFilePath = getHLSVideoPath(options)
605
606 // Fix wrong mapping with some ffmpeg versions
607 const newContent = fileContent.toString()
608 .replace(`#EXT-X-MAP:URI="${videoFilePath}",`, `#EXT-X-MAP:URI="${videoFileName}",`)
609
610 await writeFile(options.outputPath, newContent)
611 }
612
613 /**
614 * A slightly customised version of the 'veryfast' x264 preset
615 *
616 * The veryfast preset is right in the sweet spot of performance
617 * and quality. Superfast and ultrafast will give you better
618 * performance, but then quality is noticeably worse.
619 */
620 async function presetH264VeryFast (command: ffmpeg.FfmpegCommand, input: string, resolution: VideoResolution, fps?: number) {
621 let localCommand = await presetH264(command, input, resolution, fps)
622
623 localCommand = localCommand.outputOption('-preset:v veryfast')
624
625 /*
626 MAIN reference: https://slhck.info/video/2017/03/01/rate-control.html
627 Our target situation is closer to a livestream than a stream,
628 since we want to reduce as much a possible the encoding burden,
629 although not to the point of a livestream where there is a hard
630 constraint on the frames per second to be encoded.
631 */
632
633 return localCommand
634 }
635
636 /**
637 * Standard profile, with variable bitrate audio and faststart.
638 *
639 * As for the audio, quality '5' is the highest and ensures 96-112kbps/channel
640 * See https://trac.ffmpeg.org/wiki/Encode/AAC#fdk_vbr
641 */
642 async function presetH264 (command: ffmpeg.FfmpegCommand, input: string, resolution: VideoResolution, fps?: number) {
643 let localCommand = command
644 .format('mp4')
645 .videoCodec('libx264')
646 .outputOption('-movflags faststart')
647
648 addDefaultX264Params(localCommand)
649
650 const parsedAudio = await audio.get(input)
651
652 if (!parsedAudio.audioStream) {
653 localCommand = localCommand.noAudio()
654 } else if ((await checkFFmpegEncoders()).get('libfdk_aac')) { // we favor VBR, if a good AAC encoder is available
655 localCommand = localCommand
656 .audioCodec('libfdk_aac')
657 .audioQuality(5)
658 } else {
659 // we try to reduce the ceiling bitrate by making rough matches of bitrates
660 // of course this is far from perfect, but it might save some space in the end
661 localCommand = localCommand.audioCodec('aac')
662
663 const audioCodecName = parsedAudio.audioStream['codec_name']
664
665 if (audio.bitrate[audioCodecName]) {
666 const bitrate = audio.bitrate[audioCodecName](parsedAudio.audioStream['bit_rate'])
667 if (bitrate !== undefined && bitrate !== -1) localCommand = localCommand.audioBitrate(bitrate)
668 }
669 }
670
671 if (fps) {
672 // Constrained Encoding (VBV)
673 // https://slhck.info/video/2017/03/01/rate-control.html
674 // https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate
675 const targetBitrate = getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS)
676 localCommand = localCommand.outputOptions([ `-maxrate ${targetBitrate}`, `-bufsize ${targetBitrate * 2}` ])
677
678 // Keyframe interval of 2 seconds for faster seeking and resolution switching.
679 // https://streaminglearningcenter.com/blogs/whats-the-right-keyframe-interval.html
680 // https://superuser.com/a/908325
681 localCommand = localCommand.outputOption(`-g ${fps * 2}`)
682 }
683
684 return localCommand
685 }
686
687 function presetCopy (command: ffmpeg.FfmpegCommand): ffmpeg.FfmpegCommand {
688 return command
689 .format('mp4')
690 .videoCodec('copy')
691 .audioCodec('copy')
692 }
693
694 function presetOnlyAudio (command: ffmpeg.FfmpegCommand): ffmpeg.FfmpegCommand {
695 return command
696 .format('mp4')
697 .audioCodec('copy')
698 .noVideo()
699 }
700
701 function getFFmpeg (input: string) {
702 // We set cwd explicitly because ffmpeg appears to create temporary files when trancoding which fails in read-only file systems
703 const command = ffmpeg(input, { niceness: FFMPEG_NICE.TRANSCODING, cwd: CONFIG.STORAGE.TMP_DIR })
704
705 if (CONFIG.TRANSCODING.THREADS > 0) {
706 // If we don't set any threads ffmpeg will chose automatically
707 command.outputOption('-threads ' + CONFIG.TRANSCODING.THREADS)
708 }
709
710 return command
711 }