aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/helpers
diff options
context:
space:
mode:
Diffstat (limited to 'server/helpers')
-rw-r--r--server/helpers/ffmpeg-utils.ts223
-rw-r--r--server/helpers/ffprobe-utils.ts19
2 files changed, 177 insertions, 65 deletions
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts
index e297108df..712ec757e 100644
--- a/server/helpers/ffmpeg-utils.ts
+++ b/server/helpers/ffmpeg-utils.ts
@@ -1,10 +1,10 @@
1import * as ffmpeg from 'fluent-ffmpeg' 1import * as ffmpeg from 'fluent-ffmpeg'
2import { readFile, remove, writeFile } from 'fs-extra' 2import { readFile, remove, writeFile } from 'fs-extra'
3import { dirname, join } from 'path' 3import { dirname, join } from 'path'
4import { getTargetBitrate, VideoResolution } from '../../shared/models/videos' 4import { FFMPEG_NICE, VIDEO_LIVE, VIDEO_TRANSCODING_ENCODERS, VIDEO_TRANSCODING_FPS } from '@server/initializers/constants'
5import { VideoResolution } from '../../shared/models/videos'
5import { checkFFmpegEncoders } from '../initializers/checker-before-init' 6import { checkFFmpegEncoders } from '../initializers/checker-before-init'
6import { CONFIG } from '../initializers/config' 7import { CONFIG } from '../initializers/config'
7import { FFMPEG_NICE, VIDEO_LIVE, VIDEO_TRANSCODING_ENCODERS, VIDEO_TRANSCODING_FPS } from '../initializers/constants'
8import { getAudioStream, getClosestFramerateStandard, getVideoFileFPS } from './ffprobe-utils' 8import { getAudioStream, getClosestFramerateStandard, getVideoFileFPS } from './ffprobe-utils'
9import { processImage } from './image-utils' 9import { processImage } from './image-utils'
10import { logger } from './logger' 10import { logger } from './logger'
@@ -19,11 +19,13 @@ export type EncoderOptionsBuilder = (params: {
19 input: string 19 input: string
20 resolution: VideoResolution 20 resolution: VideoResolution
21 fps?: number 21 fps?: number
22 streamNum?: number
22}) => Promise<EncoderOptions> | EncoderOptions 23}) => Promise<EncoderOptions> | EncoderOptions
23 24
24// Options types 25// Options types
25 26
26export interface EncoderOptions { 27export interface EncoderOptions {
28 copy?: boolean
27 outputOptions: string[] 29 outputOptions: string[]
28} 30}
29 31
@@ -37,7 +39,7 @@ export interface EncoderProfile <T> {
37 39
38export type AvailableEncoders = { 40export type AvailableEncoders = {
39 [ id in 'live' | 'vod' ]: { 41 [ id in 'live' | 'vod' ]: {
40 [ encoder in 'libx264' | 'aac' | 'libfdkAAC' ]: EncoderProfile<EncoderOptionsBuilder> 42 [ encoder in 'libx264' | 'aac' | 'libfdk_aac' ]?: EncoderProfile<EncoderOptionsBuilder>
41 } 43 }
42} 44}
43 45
@@ -197,8 +199,20 @@ async function transcode (options: TranscodeOptions) {
197// Live muxing/transcoding functions 199// Live muxing/transcoding functions
198// --------------------------------------------------------------------------- 200// ---------------------------------------------------------------------------
199 201
200function getLiveTranscodingCommand (rtmpUrl: string, outPath: string, resolutions: number[], fps: number, deleteSegments: boolean) { 202async function getLiveTranscodingCommand (options: {
201 const command = getFFmpeg(rtmpUrl) 203 rtmpUrl: string
204 outPath: string
205 resolutions: number[]
206 fps: number
207 deleteSegments: boolean
208
209 availableEncoders: AvailableEncoders
210 profile: string
211}) {
212 const { rtmpUrl, outPath, resolutions, fps, deleteSegments, availableEncoders, profile } = options
213 const input = rtmpUrl
214
215 const command = getFFmpeg(input)
202 command.inputOption('-fflags nobuffer') 216 command.inputOption('-fflags nobuffer')
203 217
204 const varStreamMap: string[] = [] 218 const varStreamMap: string[] = []
@@ -219,19 +233,43 @@ function getLiveTranscodingCommand (rtmpUrl: string, outPath: string, resolution
219 })) 233 }))
220 ]) 234 ])
221 235
222 addEncoderDefaultParams(command, 'libx264', fps)
223
224 command.outputOption('-preset superfast') 236 command.outputOption('-preset superfast')
225 237
226 for (let i = 0; i < resolutions.length; i++) { 238 for (let i = 0; i < resolutions.length; i++) {
227 const resolution = resolutions[i] 239 const resolution = resolutions[i]
240 const baseEncoderBuilderParams = { input, availableEncoders, profile, fps, resolution, streamNum: i, videoType: 'live' as 'live' }
241
242 {
243 const builderResult = await getEncoderBuilderResult(Object.assign({}, baseEncoderBuilderParams, { streamType: 'VIDEO' }))
244 if (!builderResult) {
245 throw new Error('No available live video encoder found')
246 }
247
248 command.outputOption(`-map [vout${resolution}]`)
249
250 addDefaultEncoderParams({ command, encoder: builderResult.encoder, fps, streamNum: i })
251
252 logger.debug('Apply ffmpeg live video params from %s.', builderResult.encoder, builderResult)
228 253
229 command.outputOption(`-map [vout${resolution}]`) 254 command.outputOption(`${buildStreamSuffix('-c:v', i)} ${builderResult.encoder}`)
230 command.outputOption(`-c:v:${i} libx264`) 255 command.addOutputOptions(builderResult.result.outputOptions)
231 command.outputOption(`-b:v:${i} ${getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS)}`) 256 }
257
258 {
259 const builderResult = await getEncoderBuilderResult(Object.assign({}, baseEncoderBuilderParams, { streamType: 'AUDIO' }))
260 if (!builderResult) {
261 throw new Error('No available live audio encoder found')
262 }
232 263
233 command.outputOption(`-map a:0`) 264 command.outputOption('-map a:0')
234 command.outputOption(`-c:a:${i} aac`) 265
266 addDefaultEncoderParams({ command, encoder: builderResult.encoder, fps, streamNum: i })
267
268 logger.debug('Apply ffmpeg live audio params from %s.', builderResult.encoder, builderResult)
269
270 command.outputOption(`${buildStreamSuffix('-c:a', i)} ${builderResult.encoder}`)
271 command.addOutputOptions(builderResult.result.outputOptions)
272 }
235 273
236 varStreamMap.push(`v:${i},a:${i}`) 274 varStreamMap.push(`v:${i},a:${i}`)
237 } 275 }
@@ -282,11 +320,20 @@ async function hlsPlaylistToFragmentedMP4 (hlsDirectory: string, segmentFiles: s
282 return runCommand(command, cleaner) 320 return runCommand(command, cleaner)
283} 321}
284 322
323function buildStreamSuffix (base: string, streamNum?: number) {
324 if (streamNum !== undefined) {
325 return `${base}:${streamNum}`
326 }
327
328 return base
329}
330
285// --------------------------------------------------------------------------- 331// ---------------------------------------------------------------------------
286 332
287export { 333export {
288 getLiveTranscodingCommand, 334 getLiveTranscodingCommand,
289 getLiveMuxingCommand, 335 getLiveMuxingCommand,
336 buildStreamSuffix,
290 convertWebPToJPG, 337 convertWebPToJPG,
291 processGIF, 338 processGIF,
292 generateImageFromVideoFile, 339 generateImageFromVideoFile,
@@ -302,19 +349,35 @@ export {
302// Default options 349// Default options
303// --------------------------------------------------------------------------- 350// ---------------------------------------------------------------------------
304 351
305function addEncoderDefaultParams (command: ffmpeg.FfmpegCommand, encoder: 'libx264' | string, fps?: number) { 352function addDefaultEncoderParams (options: {
306 if (encoder !== 'libx264') return 353 command: ffmpeg.FfmpegCommand
307 354 encoder: 'libx264' | string
308 command.outputOption('-level 3.1') // 3.1 is the minimal resource allocation for our highest supported resolution 355 streamNum?: number
309 .outputOption('-b_strategy 1') // NOTE: b-strategy 1 - heuristic algorithm, 16 is optimal B-frames for it 356 fps?: number
310 .outputOption('-bf 16') // NOTE: Why 16: https://github.com/Chocobozzz/PeerTube/pull/774. b-strategy 2 -> B-frames<16 357}) {
311 .outputOption('-pix_fmt yuv420p') // allows import of source material with incompatible pixel formats (e.g. MJPEG video) 358 const { command, encoder, fps, streamNum } = options
312 .outputOption('-map_metadata -1') // strip all metadata 359
313 .outputOption('-max_muxing_queue_size 1024') // avoid issues when transcoding some files: https://trac.ffmpeg.org/ticket/6375 360 if (encoder === 'libx264') {
314 // Keyframe interval of 2 seconds for faster seeking and resolution switching. 361 // 3.1 is the minimal resource allocation for our highest supported resolution
315 // https://streaminglearningcenter.com/blogs/whats-the-right-keyframe-interval.html 362 command.outputOption('-level 3.1')
316 // https://superuser.com/a/908325 363 // NOTE: b-strategy 1 - heuristic algorithm, 16 is optimal B-frames for it
317 .outputOption('-g ' + (fps * 2)) 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(buildStreamSuffix('-pix_fmt', streamNum) + ' yuv420p')
369 // strip all metadata
370 .outputOption('-map_metadata -1')
371 // avoid issues when transcoding some files: https://trac.ffmpeg.org/ticket/6375
372 .outputOption(buildStreamSuffix('-max_muxing_queue_size', streamNum) + ' 1024')
373
374 if (fps) {
375 // Keyframe interval of 2 seconds for faster seeking and resolution switching.
376 // https://streaminglearningcenter.com/blogs/whats-the-right-keyframe-interval.html
377 // https://superuser.com/a/908325
378 command.outputOption('-g ' + (fps * 2))
379 }
380 }
318} 381}
319 382
320function addDefaultLiveHLSParams (command: ffmpeg.FfmpegCommand, outPath: string, deleteSegments: boolean) { 383function addDefaultLiveHLSParams (command: ffmpeg.FfmpegCommand, outPath: string, deleteSegments: boolean) {
@@ -352,17 +415,18 @@ async function buildx264VODCommand (command: ffmpeg.FfmpegCommand, options: Tran
352 415
353 if (options.resolution !== undefined) { 416 if (options.resolution !== undefined) {
354 // '?x720' or '720x?' for example 417 // '?x720' or '720x?' for example
355 const size = options.isPortraitMode === true ? `${options.resolution}x?` : `?x${options.resolution}` 418 const size = options.isPortraitMode === true
419 ? `${options.resolution}x?`
420 : `?x${options.resolution}`
421
356 command = command.size(size) 422 command = command.size(size)
357 } 423 }
358 424
359 if (fps) { 425 // Hard FPS limits
360 // Hard FPS limits 426 if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = getClosestFramerateStandard(fps, 'HD_STANDARD')
361 if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = getClosestFramerateStandard(fps, 'HD_STANDARD') 427 else if (fps < VIDEO_TRANSCODING_FPS.MIN) fps = VIDEO_TRANSCODING_FPS.MIN
362 else if (fps < VIDEO_TRANSCODING_FPS.MIN) fps = VIDEO_TRANSCODING_FPS.MIN
363 428
364 command = command.withFPS(fps) 429 command = command.withFPS(fps)
365 }
366 430
367 return command 431 return command
368} 432}
@@ -445,6 +509,49 @@ function getHLSVideoPath (options: HLSTranscodeOptions) {
445// Transcoding presets 509// Transcoding presets
446// --------------------------------------------------------------------------- 510// ---------------------------------------------------------------------------
447 511
512async function getEncoderBuilderResult (options: {
513 streamType: string
514 input: string
515
516 availableEncoders: AvailableEncoders
517 profile: string
518
519 videoType: 'vod' | 'live'
520
521 resolution: number
522 fps?: number
523 streamNum?: number
524}) {
525 const { availableEncoders, input, profile, resolution, streamType, fps, streamNum, videoType } = options
526
527 const encodersToTry: string[] = VIDEO_TRANSCODING_ENCODERS[streamType]
528
529 for (const encoder of encodersToTry) {
530 if (!(await checkFFmpegEncoders()).get(encoder) || !availableEncoders[videoType][encoder]) continue
531
532 const builderProfiles: EncoderProfile<EncoderOptionsBuilder> = availableEncoders[videoType][encoder]
533 let builder = builderProfiles[profile]
534
535 if (!builder) {
536 logger.debug('Profile %s for encoder %s not available. Fallback to default.', profile, encoder)
537 builder = builderProfiles.default
538 }
539
540 const result = await builder({ input, resolution: resolution, fps, streamNum })
541
542 return {
543 result,
544
545 // If we don't have output options, then copy the input stream
546 encoder: result.copy === true
547 ? 'copy'
548 : encoder
549 }
550 }
551
552 return null
553}
554
448async function presetVideo ( 555async function presetVideo (
449 command: ffmpeg.FfmpegCommand, 556 command: ffmpeg.FfmpegCommand,
450 input: string, 557 input: string,
@@ -459,49 +566,41 @@ async function presetVideo (
459 const parsedAudio = await getAudioStream(input) 566 const parsedAudio = await getAudioStream(input)
460 567
461 let streamsToProcess = [ 'AUDIO', 'VIDEO' ] 568 let streamsToProcess = [ 'AUDIO', 'VIDEO' ]
462 const streamsFound = {
463 AUDIO: '',
464 VIDEO: ''
465 }
466 569
467 if (!parsedAudio.audioStream) { 570 if (!parsedAudio.audioStream) {
468 localCommand = localCommand.noAudio() 571 localCommand = localCommand.noAudio()
469 streamsToProcess = [ 'VIDEO' ] 572 streamsToProcess = [ 'VIDEO' ]
470 } 573 }
471 574
472 for (const stream of streamsToProcess) { 575 for (const streamType of streamsToProcess) {
473 const encodersToTry: string[] = VIDEO_TRANSCODING_ENCODERS[stream] 576 const { profile, resolution, availableEncoders } = transcodeOptions
474 577
475 for (const encoder of encodersToTry) { 578 const builderResult = await getEncoderBuilderResult({
476 if (!(await checkFFmpegEncoders()).get(encoder)) continue 579 streamType,
477 580 input,
478 const builderProfiles: EncoderProfile<EncoderOptionsBuilder> = transcodeOptions.availableEncoders.vod[encoder] 581 resolution,
479 let builder = builderProfiles[transcodeOptions.profile] 582 availableEncoders,
480 583 profile,
481 if (!builder) { 584 fps,
482 logger.debug('Profile %s for encoder %s not available. Fallback to default.', transcodeOptions.profile, encoder) 585 videoType: 'vod' as 'vod'
483 builder = builderProfiles.default 586 })
484 }
485
486 const builderResult = await builder({ input, resolution: transcodeOptions.resolution, fps })
487
488 logger.debug('Apply ffmpeg params from %s.', encoder, builderResult)
489 587
490 localCommand.outputOptions(builderResult.outputOptions) 588 if (!builderResult) {
589 throw new Error('No available encoder found for stream ' + streamType)
590 }
491 591
492 addEncoderDefaultParams(localCommand, encoder) 592 logger.debug('Apply ffmpeg params from %s.', builderResult.encoder, builderResult)
493 593
494 streamsFound[stream] = encoder 594 if (streamType === 'VIDEO') {
495 break 595 localCommand.videoCodec(builderResult.encoder)
596 } else if (streamType === 'AUDIO') {
597 localCommand.audioCodec(builderResult.encoder)
496 } 598 }
497 599
498 if (!streamsFound[stream]) { 600 command.addOutputOptions(builderResult.result.outputOptions)
499 throw new Error('No available encoder found ' + encodersToTry.join(', ')) 601 addDefaultEncoderParams({ command: localCommand, encoder: builderResult.encoder, fps })
500 }
501 } 602 }
502 603
503 localCommand.videoCodec(streamsFound.VIDEO)
504
505 return localCommand 604 return localCommand
506} 605}
507 606
diff --git a/server/helpers/ffprobe-utils.ts b/server/helpers/ffprobe-utils.ts
index 6159d3963..5545ddbbf 100644
--- a/server/helpers/ffprobe-utils.ts
+++ b/server/helpers/ffprobe-utils.ts
@@ -198,9 +198,12 @@ function computeResolutionsToTranscode (videoFileResolution: number, type: 'vod'
198async function canDoQuickTranscode (path: string): Promise<boolean> { 198async function canDoQuickTranscode (path: string): Promise<boolean> {
199 const probe = await ffprobePromise(path) 199 const probe = await ffprobePromise(path)
200 200
201 // NOTE: This could be optimized by running ffprobe only once (but it runs fast anyway) 201 return await canDoQuickVideoTranscode(path, probe) &&
202 await canDoQuickAudioTranscode(path, probe)
203}
204
205async function canDoQuickVideoTranscode (path: string, probe?: ffmpeg.FfprobeData): Promise<boolean> {
202 const videoStream = await getVideoStreamFromFile(path, probe) 206 const videoStream = await getVideoStreamFromFile(path, probe)
203 const parsedAudio = await getAudioStream(path, probe)
204 const fps = await getVideoFileFPS(path, probe) 207 const fps = await getVideoFileFPS(path, probe)
205 const bitRate = await getVideoFileBitrate(path, probe) 208 const bitRate = await getVideoFileBitrate(path, probe)
206 const resolution = await getVideoFileResolution(path, probe) 209 const resolution = await getVideoFileResolution(path, probe)
@@ -212,6 +215,12 @@ async function canDoQuickTranscode (path: string): Promise<boolean> {
212 if (fps < VIDEO_TRANSCODING_FPS.MIN || fps > VIDEO_TRANSCODING_FPS.MAX) return false 215 if (fps < VIDEO_TRANSCODING_FPS.MIN || fps > VIDEO_TRANSCODING_FPS.MAX) return false
213 if (bitRate > getMaxBitrate(resolution.videoFileResolution, fps, VIDEO_TRANSCODING_FPS)) return false 216 if (bitRate > getMaxBitrate(resolution.videoFileResolution, fps, VIDEO_TRANSCODING_FPS)) return false
214 217
218 return true
219}
220
221async function canDoQuickAudioTranscode (path: string, probe?: ffmpeg.FfprobeData): Promise<boolean> {
222 const parsedAudio = await getAudioStream(path, probe)
223
215 // check audio params (if audio stream exists) 224 // check audio params (if audio stream exists)
216 if (parsedAudio.audioStream) { 225 if (parsedAudio.audioStream) {
217 if (parsedAudio.audioStream['codec_name'] !== 'aac') return false 226 if (parsedAudio.audioStream['codec_name'] !== 'aac') return false
@@ -239,11 +248,15 @@ export {
239 getVideoFileResolution, 248 getVideoFileResolution,
240 getMetadataFromFile, 249 getMetadataFromFile,
241 getMaxAudioBitrate, 250 getMaxAudioBitrate,
251 getVideoStreamFromFile,
242 getDurationFromVideoFile, 252 getDurationFromVideoFile,
243 getAudioStream, 253 getAudioStream,
244 getVideoFileFPS, 254 getVideoFileFPS,
255 ffprobePromise,
245 getClosestFramerateStandard, 256 getClosestFramerateStandard,
246 computeResolutionsToTranscode, 257 computeResolutionsToTranscode,
247 getVideoFileBitrate, 258 getVideoFileBitrate,
248 canDoQuickTranscode 259 canDoQuickTranscode,
260 canDoQuickVideoTranscode,
261 canDoQuickAudioTranscode
249} 262}