aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/helpers/ffmpeg-utils.ts
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2020-09-17 09:20:52 +0200
committerChocobozzz <chocobozzz@cpy.re>2020-11-09 15:33:04 +0100
commitc6c0fa6cd8fe8f752463d8982c3dbcd448739c4e (patch)
tree79304b0152b0a38d33b26e65d4acdad0da4032a7 /server/helpers/ffmpeg-utils.ts
parent110d463fece85e87a26aca48a6048ae0017a27b3 (diff)
downloadPeerTube-c6c0fa6cd8fe8f752463d8982c3dbcd448739c4e.tar.gz
PeerTube-c6c0fa6cd8fe8f752463d8982c3dbcd448739c4e.tar.zst
PeerTube-c6c0fa6cd8fe8f752463d8982c3dbcd448739c4e.zip
Live streaming implementation first step
Diffstat (limited to 'server/helpers/ffmpeg-utils.ts')
-rw-r--r--server/helpers/ffmpeg-utils.ts138
1 files changed, 118 insertions, 20 deletions
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts
index 02c66cd01..fac2595f1 100644
--- a/server/helpers/ffmpeg-utils.ts
+++ b/server/helpers/ffmpeg-utils.ts
@@ -1,13 +1,13 @@
1import * as ffmpeg from 'fluent-ffmpeg' 1import * as ffmpeg from 'fluent-ffmpeg'
2import { readFile, remove, writeFile } from 'fs-extra'
2import { dirname, join } from 'path' 3import { dirname, join } from 'path'
4import { VideoFileMetadata } from '@shared/models/videos/video-file-metadata'
3import { getMaxBitrate, getTargetBitrate, VideoResolution } from '../../shared/models/videos' 5import { getMaxBitrate, getTargetBitrate, VideoResolution } from '../../shared/models/videos'
6import { checkFFmpegEncoders } from '../initializers/checker-before-init'
7import { CONFIG } from '../initializers/config'
4import { FFMPEG_NICE, VIDEO_TRANSCODING_FPS } from '../initializers/constants' 8import { FFMPEG_NICE, VIDEO_TRANSCODING_FPS } from '../initializers/constants'
5import { processImage } from './image-utils' 9import { processImage } from './image-utils'
6import { logger } from './logger' 10import { logger } from './logger'
7import { checkFFmpegEncoders } from '../initializers/checker-before-init'
8import { readFile, remove, writeFile } from 'fs-extra'
9import { CONFIG } from '../initializers/config'
10import { VideoFileMetadata } from '@shared/models/videos/video-file-metadata'
11 11
12/** 12/**
13 * A toolbox to play with audio 13 * A toolbox to play with audio
@@ -74,9 +74,12 @@ namespace audio {
74 } 74 }
75} 75}
76 76
77function computeResolutionsToTranscode (videoFileResolution: number) { 77function computeResolutionsToTranscode (videoFileResolution: number, type: 'vod' | 'live') {
78 const configResolutions = type === 'vod'
79 ? CONFIG.TRANSCODING.RESOLUTIONS
80 : CONFIG.LIVE.TRANSCODING.RESOLUTIONS
81
78 const resolutionsEnabled: number[] = [] 82 const resolutionsEnabled: number[] = []
79 const configResolutions = CONFIG.TRANSCODING.RESOLUTIONS
80 83
81 // Put in the order we want to proceed jobs 84 // Put in the order we want to proceed jobs
82 const resolutions = [ 85 const resolutions = [
@@ -270,14 +273,13 @@ type TranscodeOptions =
270function transcode (options: TranscodeOptions) { 273function transcode (options: TranscodeOptions) {
271 return new Promise<void>(async (res, rej) => { 274 return new Promise<void>(async (res, rej) => {
272 try { 275 try {
273 // we set cwd explicitly because ffmpeg appears to create temporary files when trancoding which fails in read-only file systems 276 let command = getFFmpeg(options.inputPath)
274 let command = ffmpeg(options.inputPath, { niceness: FFMPEG_NICE.TRANSCODING, cwd: CONFIG.STORAGE.TMP_DIR })
275 .output(options.outputPath) 277 .output(options.outputPath)
276 278
277 if (options.type === 'quick-transcode') { 279 if (options.type === 'quick-transcode') {
278 command = buildQuickTranscodeCommand(command) 280 command = buildQuickTranscodeCommand(command)
279 } else if (options.type === 'hls') { 281 } else if (options.type === 'hls') {
280 command = await buildHLSCommand(command, options) 282 command = await buildHLSVODCommand(command, options)
281 } else if (options.type === 'merge-audio') { 283 } else if (options.type === 'merge-audio') {
282 command = await buildAudioMergeCommand(command, options) 284 command = await buildAudioMergeCommand(command, options)
283 } else if (options.type === 'only-audio') { 285 } else if (options.type === 'only-audio') {
@@ -286,11 +288,6 @@ function transcode (options: TranscodeOptions) {
286 command = await buildx264Command(command, options) 288 command = await buildx264Command(command, options)
287 } 289 }
288 290
289 if (CONFIG.TRANSCODING.THREADS > 0) {
290 // if we don't set any threads ffmpeg will chose automatically
291 command = command.outputOption('-threads ' + CONFIG.TRANSCODING.THREADS)
292 }
293
294 command 291 command
295 .on('error', (err, stdout, stderr) => { 292 .on('error', (err, stdout, stderr) => {
296 logger.error('Error in transcoding job.', { stdout, stderr }) 293 logger.error('Error in transcoding job.', { stdout, stderr })
@@ -356,16 +353,89 @@ function convertWebPToJPG (path: string, destination: string): Promise<void> {
356 }) 353 })
357} 354}
358 355
356function runLiveTranscoding (rtmpUrl: string, outPath: string, resolutions: number[]) {
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 const liveFPS = VIDEO_TRANSCODING_FPS.AVERAGE
379
380 command.withFps(liveFPS)
381
382 command.outputOption('-b_strategy 1')
383 command.outputOption('-bf 16')
384 command.outputOption('-preset superfast')
385 command.outputOption('-level 3.1')
386 command.outputOption('-map_metadata -1')
387 command.outputOption('-pix_fmt yuv420p')
388
389 for (let i = 0; i < resolutions.length; i++) {
390 const resolution = resolutions[i]
391
392 command.outputOption(`-map [vout${resolution}]`)
393 command.outputOption(`-c:v:${i} libx264`)
394 command.outputOption(`-b:v:${i} ${getTargetBitrate(resolution, liveFPS, VIDEO_TRANSCODING_FPS)}`)
395
396 command.outputOption(`-map a:0`)
397 command.outputOption(`-c:a:${i} aac`)
398
399 varStreamMap.push(`v:${i},a:${i}`)
400 }
401
402 addDefaultLiveHLSParams(command, outPath)
403
404 command.outputOption('-var_stream_map', varStreamMap.join(' '))
405
406 command.run()
407
408 return command
409}
410
411function runLiveMuxing (rtmpUrl: string, outPath: string) {
412 const command = getFFmpeg(rtmpUrl)
413 command.inputOption('-fflags nobuffer')
414
415 command.outputOption('-c:v copy')
416 command.outputOption('-c:a copy')
417 command.outputOption('-map 0:a?')
418 command.outputOption('-map 0:v?')
419
420 addDefaultLiveHLSParams(command, outPath)
421
422 command.run()
423
424 return command
425}
426
359// --------------------------------------------------------------------------- 427// ---------------------------------------------------------------------------
360 428
361export { 429export {
362 getVideoStreamCodec, 430 getVideoStreamCodec,
363 getAudioStreamCodec, 431 getAudioStreamCodec,
432 runLiveMuxing,
364 convertWebPToJPG, 433 convertWebPToJPG,
365 getVideoStreamSize, 434 getVideoStreamSize,
366 getVideoFileResolution, 435 getVideoFileResolution,
367 getMetadataFromFile, 436 getMetadataFromFile,
368 getDurationFromVideoFile, 437 getDurationFromVideoFile,
438 runLiveTranscoding,
369 generateImageFromVideoFile, 439 generateImageFromVideoFile,
370 TranscodeOptions, 440 TranscodeOptions,
371 TranscodeOptionsType, 441 TranscodeOptionsType,
@@ -379,6 +449,25 @@ export {
379 449
380// --------------------------------------------------------------------------- 450// ---------------------------------------------------------------------------
381 451
452function addDefaultX264Params (command: ffmpeg.FfmpegCommand) {
453 command.outputOption('-level 3.1') // 3.1 is the minimal resource allocation for our highest supported resolution
454 .outputOption('-b_strategy 1') // NOTE: b-strategy 1 - heuristic algorithm, 16 is optimal B-frames for it
455 .outputOption('-bf 16') // NOTE: Why 16: https://github.com/Chocobozzz/PeerTube/pull/774. b-strategy 2 -> B-frames<16
456 .outputOption('-pix_fmt yuv420p') // allows import of source material with incompatible pixel formats (e.g. MJPEG video)
457 .outputOption('-map_metadata -1') // strip all metadata
458}
459
460function addDefaultLiveHLSParams (command: ffmpeg.FfmpegCommand, outPath: string) {
461 command.outputOption('-hls_time 4')
462 command.outputOption('-hls_list_size 15')
463 command.outputOption('-hls_flags delete_segments')
464 command.outputOption(`-hls_segment_filename ${join(outPath, '%v-%d.ts')}`)
465 command.outputOption('-master_pl_name master.m3u8')
466 command.outputOption(`-f hls`)
467
468 command.output(join(outPath, '%v.m3u8'))
469}
470
382async function buildx264Command (command: ffmpeg.FfmpegCommand, options: TranscodeOptions) { 471async function buildx264Command (command: ffmpeg.FfmpegCommand, options: TranscodeOptions) {
383 let fps = await getVideoFileFPS(options.inputPath) 472 let fps = await getVideoFileFPS(options.inputPath)
384 if ( 473 if (
@@ -438,7 +527,7 @@ function buildQuickTranscodeCommand (command: ffmpeg.FfmpegCommand) {
438 return command 527 return command
439} 528}
440 529
441async function buildHLSCommand (command: ffmpeg.FfmpegCommand, options: HLSTranscodeOptions) { 530async function buildHLSVODCommand (command: ffmpeg.FfmpegCommand, options: HLSTranscodeOptions) {
442 const videoPath = getHLSVideoPath(options) 531 const videoPath = getHLSVideoPath(options)
443 532
444 if (options.copyCodecs) command = presetCopy(command) 533 if (options.copyCodecs) command = presetCopy(command)
@@ -508,13 +597,10 @@ async function presetH264 (command: ffmpeg.FfmpegCommand, input: string, resolut
508 let localCommand = command 597 let localCommand = command
509 .format('mp4') 598 .format('mp4')
510 .videoCodec('libx264') 599 .videoCodec('libx264')
511 .outputOption('-level 3.1') // 3.1 is the minimal resource allocation for our highest supported resolution
512 .outputOption('-b_strategy 1') // NOTE: b-strategy 1 - heuristic algorithm, 16 is optimal B-frames for it
513 .outputOption('-bf 16') // NOTE: Why 16: https://github.com/Chocobozzz/PeerTube/pull/774. b-strategy 2 -> B-frames<16
514 .outputOption('-pix_fmt yuv420p') // allows import of source material with incompatible pixel formats (e.g. MJPEG video)
515 .outputOption('-map_metadata -1') // strip all metadata
516 .outputOption('-movflags faststart') 600 .outputOption('-movflags faststart')
517 601
602 addDefaultX264Params(localCommand)
603
518 const parsedAudio = await audio.get(input) 604 const parsedAudio = await audio.get(input)
519 605
520 if (!parsedAudio.audioStream) { 606 if (!parsedAudio.audioStream) {
@@ -565,3 +651,15 @@ function presetOnlyAudio (command: ffmpeg.FfmpegCommand): ffmpeg.FfmpegCommand {
565 .audioCodec('copy') 651 .audioCodec('copy')
566 .noVideo() 652 .noVideo()
567} 653}
654
655function getFFmpeg (input: string) {
656 // We set cwd explicitly because ffmpeg appears to create temporary files when trancoding which fails in read-only file systems
657 const command = ffmpeg(input, { niceness: FFMPEG_NICE.TRANSCODING, cwd: CONFIG.STORAGE.TMP_DIR })
658
659 if (CONFIG.TRANSCODING.THREADS > 0) {
660 // If we don't set any threads ffmpeg will chose automatically
661 command.outputOption('-threads ' + CONFIG.TRANSCODING.THREADS)
662 }
663
664 return command
665}