diff options
Diffstat (limited to 'server/helpers/ffmpeg-utils.ts')
-rw-r--r-- | server/helpers/ffmpeg-utils.ts | 179 |
1 files changed, 90 insertions, 89 deletions
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts index 7022d3e03..084516e55 100644 --- a/server/helpers/ffmpeg-utils.ts +++ b/server/helpers/ffmpeg-utils.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import * as ffmpeg from 'fluent-ffmpeg' | 1 | import * as ffmpeg from 'fluent-ffmpeg' |
2 | import { dirname, join } from 'path' | 2 | import { dirname, join } from 'path' |
3 | import { getTargetBitrate, getMaxBitrate, VideoResolution } from '../../shared/models/videos' | 3 | import { getMaxBitrate, getTargetBitrate, VideoResolution } from '../../shared/models/videos' |
4 | import { FFMPEG_NICE, VIDEO_TRANSCODING_FPS } from '../initializers/constants' | 4 | import { FFMPEG_NICE, VIDEO_TRANSCODING_FPS } from '../initializers/constants' |
5 | import { processImage } from './image-utils' | 5 | import { processImage } from './image-utils' |
6 | import { logger } from './logger' | 6 | import { logger } from './logger' |
@@ -8,6 +8,71 @@ import { checkFFmpegEncoders } from '../initializers/checker-before-init' | |||
8 | import { readFile, remove, writeFile } from 'fs-extra' | 8 | import { readFile, remove, writeFile } from 'fs-extra' |
9 | import { CONFIG } from '../initializers/config' | 9 | import { CONFIG } from '../initializers/config' |
10 | 10 | ||
11 | /** | ||
12 | * A toolbox to play with audio | ||
13 | */ | ||
14 | namespace audio { | ||
15 | export const get = (videoPath: string) => { | ||
16 | // without position, ffprobe considers the last input only | ||
17 | // we make it consider the first input only | ||
18 | // if you pass a file path to pos, then ffprobe acts on that file directly | ||
19 | return new Promise<{ absolutePath: string, audioStream?: any }>((res, rej) => { | ||
20 | |||
21 | function parseFfprobe (err: any, data: ffmpeg.FfprobeData) { | ||
22 | if (err) return rej(err) | ||
23 | |||
24 | if ('streams' in data) { | ||
25 | const audioStream = data.streams.find(stream => stream['codec_type'] === 'audio') | ||
26 | if (audioStream) { | ||
27 | return res({ | ||
28 | absolutePath: data.format.filename, | ||
29 | audioStream | ||
30 | }) | ||
31 | } | ||
32 | } | ||
33 | |||
34 | return res({ absolutePath: data.format.filename }) | ||
35 | } | ||
36 | |||
37 | return ffmpeg.ffprobe(videoPath, parseFfprobe) | ||
38 | }) | ||
39 | } | ||
40 | |||
41 | export namespace bitrate { | ||
42 | const baseKbitrate = 384 | ||
43 | |||
44 | const toBits = (kbits: number) => kbits * 8000 | ||
45 | |||
46 | export const aac = (bitrate: number): number => { | ||
47 | switch (true) { | ||
48 | case bitrate > toBits(baseKbitrate): | ||
49 | return baseKbitrate | ||
50 | |||
51 | default: | ||
52 | return -1 // we interpret it as a signal to copy the audio stream as is | ||
53 | } | ||
54 | } | ||
55 | |||
56 | export const mp3 = (bitrate: number): number => { | ||
57 | /* | ||
58 | a 192kbit/sec mp3 doesn't hold as much information as a 192kbit/sec aac. | ||
59 | That's why, when using aac, we can go to lower kbit/sec. The equivalences | ||
60 | made here are not made to be accurate, especially with good mp3 encoders. | ||
61 | */ | ||
62 | switch (true) { | ||
63 | case bitrate <= toBits(192): | ||
64 | return 128 | ||
65 | |||
66 | case bitrate <= toBits(384): | ||
67 | return 256 | ||
68 | |||
69 | default: | ||
70 | return baseKbitrate | ||
71 | } | ||
72 | } | ||
73 | } | ||
74 | } | ||
75 | |||
11 | function computeResolutionsToTranscode (videoFileHeight: number) { | 76 | function computeResolutionsToTranscode (videoFileHeight: number) { |
12 | const resolutionsEnabled: number[] = [] | 77 | const resolutionsEnabled: number[] = [] |
13 | const configResolutions = CONFIG.TRANSCODING.RESOLUTIONS | 78 | const configResolutions = CONFIG.TRANSCODING.RESOLUTIONS |
@@ -24,7 +89,7 @@ function computeResolutionsToTranscode (videoFileHeight: number) { | |||
24 | ] | 89 | ] |
25 | 90 | ||
26 | for (const resolution of resolutions) { | 91 | for (const resolution of resolutions) { |
27 | if (configResolutions[ resolution + 'p' ] === true && videoFileHeight > resolution) { | 92 | if (configResolutions[resolution + 'p'] === true && videoFileHeight > resolution) { |
28 | resolutionsEnabled.push(resolution) | 93 | resolutionsEnabled.push(resolution) |
29 | } | 94 | } |
30 | } | 95 | } |
@@ -48,9 +113,9 @@ async function getVideoStreamCodec (path: string) { | |||
48 | const videoCodec = videoStream.codec_tag_string | 113 | const videoCodec = videoStream.codec_tag_string |
49 | 114 | ||
50 | const baseProfileMatrix = { | 115 | const baseProfileMatrix = { |
51 | 'High': '6400', | 116 | High: '6400', |
52 | 'Main': '4D40', | 117 | Main: '4D40', |
53 | 'Baseline': '42E0' | 118 | Baseline: '42E0' |
54 | } | 119 | } |
55 | 120 | ||
56 | let baseProfile = baseProfileMatrix[videoStream.profile] | 121 | let baseProfile = baseProfileMatrix[videoStream.profile] |
@@ -91,7 +156,7 @@ async function getVideoFileFPS (path: string) { | |||
91 | if (videoStream === null) return 0 | 156 | if (videoStream === null) return 0 |
92 | 157 | ||
93 | for (const key of [ 'avg_frame_rate', 'r_frame_rate' ]) { | 158 | for (const key of [ 'avg_frame_rate', 'r_frame_rate' ]) { |
94 | const valuesText: string = videoStream[ key ] | 159 | const valuesText: string = videoStream[key] |
95 | if (!valuesText) continue | 160 | if (!valuesText) continue |
96 | 161 | ||
97 | const [ frames, seconds ] = valuesText.split('/') | 162 | const [ frames, seconds ] = valuesText.split('/') |
@@ -191,7 +256,8 @@ interface OnlyAudioTranscodeOptions extends BaseTranscodeOptions { | |||
191 | type: 'only-audio' | 256 | type: 'only-audio' |
192 | } | 257 | } |
193 | 258 | ||
194 | type TranscodeOptions = HLSTranscodeOptions | 259 | type TranscodeOptions = |
260 | HLSTranscodeOptions | ||
195 | | VideoTranscodeOptions | 261 | | VideoTranscodeOptions |
196 | | MergeAudioTranscodeOptions | 262 | | MergeAudioTranscodeOptions |
197 | | OnlyAudioTranscodeOptions | 263 | | OnlyAudioTranscodeOptions |
@@ -204,13 +270,13 @@ function transcode (options: TranscodeOptions) { | |||
204 | .output(options.outputPath) | 270 | .output(options.outputPath) |
205 | 271 | ||
206 | if (options.type === 'quick-transcode') { | 272 | if (options.type === 'quick-transcode') { |
207 | command = await buildQuickTranscodeCommand(command) | 273 | command = buildQuickTranscodeCommand(command) |
208 | } else if (options.type === 'hls') { | 274 | } else if (options.type === 'hls') { |
209 | command = await buildHLSCommand(command, options) | 275 | command = await buildHLSCommand(command, options) |
210 | } else if (options.type === 'merge-audio') { | 276 | } else if (options.type === 'merge-audio') { |
211 | command = await buildAudioMergeCommand(command, options) | 277 | command = await buildAudioMergeCommand(command, options) |
212 | } else if (options.type === 'only-audio') { | 278 | } else if (options.type === 'only-audio') { |
213 | command = await buildOnlyAudioCommand(command, options) | 279 | command = buildOnlyAudioCommand(command, options) |
214 | } else { | 280 | } else { |
215 | command = await buildx264Command(command, options) | 281 | command = await buildx264Command(command, options) |
216 | } | 282 | } |
@@ -247,17 +313,17 @@ async function canDoQuickTranscode (path: string): Promise<boolean> { | |||
247 | 313 | ||
248 | // check video params | 314 | // check video params |
249 | if (videoStream == null) return false | 315 | if (videoStream == null) return false |
250 | if (videoStream[ 'codec_name' ] !== 'h264') return false | 316 | if (videoStream['codec_name'] !== 'h264') return false |
251 | if (videoStream[ 'pix_fmt' ] !== 'yuv420p') return false | 317 | if (videoStream['pix_fmt'] !== 'yuv420p') return false |
252 | if (fps < VIDEO_TRANSCODING_FPS.MIN || fps > VIDEO_TRANSCODING_FPS.MAX) return false | 318 | if (fps < VIDEO_TRANSCODING_FPS.MIN || fps > VIDEO_TRANSCODING_FPS.MAX) return false |
253 | if (bitRate > getMaxBitrate(resolution.videoFileResolution, fps, VIDEO_TRANSCODING_FPS)) return false | 319 | if (bitRate > getMaxBitrate(resolution.videoFileResolution, fps, VIDEO_TRANSCODING_FPS)) return false |
254 | 320 | ||
255 | // check audio params (if audio stream exists) | 321 | // check audio params (if audio stream exists) |
256 | if (parsedAudio.audioStream) { | 322 | if (parsedAudio.audioStream) { |
257 | if (parsedAudio.audioStream[ 'codec_name' ] !== 'aac') return false | 323 | if (parsedAudio.audioStream['codec_name'] !== 'aac') return false |
258 | 324 | ||
259 | const maxAudioBitrate = audio.bitrate[ 'aac' ](parsedAudio.audioStream[ 'bit_rate' ]) | 325 | const maxAudioBitrate = audio.bitrate['aac'](parsedAudio.audioStream['bit_rate']) |
260 | if (maxAudioBitrate !== -1 && parsedAudio.audioStream[ 'bit_rate' ] > maxAudioBitrate) return false | 326 | if (maxAudioBitrate !== -1 && parsedAudio.audioStream['bit_rate'] > maxAudioBitrate) return false |
261 | } | 327 | } |
262 | 328 | ||
263 | return true | 329 | return true |
@@ -333,14 +399,14 @@ async function buildAudioMergeCommand (command: ffmpeg.FfmpegCommand, options: M | |||
333 | return command | 399 | return command |
334 | } | 400 | } |
335 | 401 | ||
336 | async function buildOnlyAudioCommand (command: ffmpeg.FfmpegCommand, options: OnlyAudioTranscodeOptions) { | 402 | function buildOnlyAudioCommand (command: ffmpeg.FfmpegCommand, options: OnlyAudioTranscodeOptions) { |
337 | command = await presetOnlyAudio(command) | 403 | command = presetOnlyAudio(command) |
338 | 404 | ||
339 | return command | 405 | return command |
340 | } | 406 | } |
341 | 407 | ||
342 | async function buildQuickTranscodeCommand (command: ffmpeg.FfmpegCommand) { | 408 | function buildQuickTranscodeCommand (command: ffmpeg.FfmpegCommand) { |
343 | command = await presetCopy(command) | 409 | command = presetCopy(command) |
344 | 410 | ||
345 | command = command.outputOption('-map_metadata -1') // strip all metadata | 411 | command = command.outputOption('-map_metadata -1') // strip all metadata |
346 | .outputOption('-movflags faststart') | 412 | .outputOption('-movflags faststart') |
@@ -351,7 +417,7 @@ async function buildQuickTranscodeCommand (command: ffmpeg.FfmpegCommand) { | |||
351 | async function buildHLSCommand (command: ffmpeg.FfmpegCommand, options: HLSTranscodeOptions) { | 417 | async function buildHLSCommand (command: ffmpeg.FfmpegCommand, options: HLSTranscodeOptions) { |
352 | const videoPath = getHLSVideoPath(options) | 418 | const videoPath = getHLSVideoPath(options) |
353 | 419 | ||
354 | if (options.copyCodecs) command = await presetCopy(command) | 420 | if (options.copyCodecs) command = presetCopy(command) |
355 | else command = await buildx264Command(command, options) | 421 | else command = await buildx264Command(command, options) |
356 | 422 | ||
357 | command = command.outputOption('-hls_time 4') | 423 | command = command.outputOption('-hls_time 4') |
@@ -419,71 +485,6 @@ async function presetH264VeryFast (command: ffmpeg.FfmpegCommand, input: string, | |||
419 | } | 485 | } |
420 | 486 | ||
421 | /** | 487 | /** |
422 | * A toolbox to play with audio | ||
423 | */ | ||
424 | namespace audio { | ||
425 | export const get = (videoPath: string) => { | ||
426 | // without position, ffprobe considers the last input only | ||
427 | // we make it consider the first input only | ||
428 | // if you pass a file path to pos, then ffprobe acts on that file directly | ||
429 | return new Promise<{ absolutePath: string, audioStream?: any }>((res, rej) => { | ||
430 | |||
431 | function parseFfprobe (err: any, data: ffmpeg.FfprobeData) { | ||
432 | if (err) return rej(err) | ||
433 | |||
434 | if ('streams' in data) { | ||
435 | const audioStream = data.streams.find(stream => stream[ 'codec_type' ] === 'audio') | ||
436 | if (audioStream) { | ||
437 | return res({ | ||
438 | absolutePath: data.format.filename, | ||
439 | audioStream | ||
440 | }) | ||
441 | } | ||
442 | } | ||
443 | |||
444 | return res({ absolutePath: data.format.filename }) | ||
445 | } | ||
446 | |||
447 | return ffmpeg.ffprobe(videoPath, parseFfprobe) | ||
448 | }) | ||
449 | } | ||
450 | |||
451 | export namespace bitrate { | ||
452 | const baseKbitrate = 384 | ||
453 | |||
454 | const toBits = (kbits: number) => kbits * 8000 | ||
455 | |||
456 | export const aac = (bitrate: number): number => { | ||
457 | switch (true) { | ||
458 | case bitrate > toBits(baseKbitrate): | ||
459 | return baseKbitrate | ||
460 | |||
461 | default: | ||
462 | return -1 // we interpret it as a signal to copy the audio stream as is | ||
463 | } | ||
464 | } | ||
465 | |||
466 | export const mp3 = (bitrate: number): number => { | ||
467 | /* | ||
468 | a 192kbit/sec mp3 doesn't hold as much information as a 192kbit/sec aac. | ||
469 | That's why, when using aac, we can go to lower kbit/sec. The equivalences | ||
470 | made here are not made to be accurate, especially with good mp3 encoders. | ||
471 | */ | ||
472 | switch (true) { | ||
473 | case bitrate <= toBits(192): | ||
474 | return 128 | ||
475 | |||
476 | case bitrate <= toBits(384): | ||
477 | return 256 | ||
478 | |||
479 | default: | ||
480 | return baseKbitrate | ||
481 | } | ||
482 | } | ||
483 | } | ||
484 | } | ||
485 | |||
486 | /** | ||
487 | * Standard profile, with variable bitrate audio and faststart. | 488 | * Standard profile, with variable bitrate audio and faststart. |
488 | * | 489 | * |
489 | * As for the audio, quality '5' is the highest and ensures 96-112kbps/channel | 490 | * As for the audio, quality '5' is the highest and ensures 96-112kbps/channel |
@@ -513,10 +514,10 @@ async function presetH264 (command: ffmpeg.FfmpegCommand, input: string, resolut | |||
513 | // of course this is far from perfect, but it might save some space in the end | 514 | // of course this is far from perfect, but it might save some space in the end |
514 | localCommand = localCommand.audioCodec('aac') | 515 | localCommand = localCommand.audioCodec('aac') |
515 | 516 | ||
516 | const audioCodecName = parsedAudio.audioStream[ 'codec_name' ] | 517 | const audioCodecName = parsedAudio.audioStream['codec_name'] |
517 | 518 | ||
518 | if (audio.bitrate[ audioCodecName ]) { | 519 | if (audio.bitrate[audioCodecName]) { |
519 | const bitrate = audio.bitrate[ audioCodecName ](parsedAudio.audioStream[ 'bit_rate' ]) | 520 | const bitrate = audio.bitrate[audioCodecName](parsedAudio.audioStream['bit_rate']) |
520 | if (bitrate !== undefined && bitrate !== -1) localCommand = localCommand.audioBitrate(bitrate) | 521 | if (bitrate !== undefined && bitrate !== -1) localCommand = localCommand.audioBitrate(bitrate) |
521 | } | 522 | } |
522 | } | 523 | } |
@@ -537,14 +538,14 @@ async function presetH264 (command: ffmpeg.FfmpegCommand, input: string, resolut | |||
537 | return localCommand | 538 | return localCommand |
538 | } | 539 | } |
539 | 540 | ||
540 | async function presetCopy (command: ffmpeg.FfmpegCommand): Promise<ffmpeg.FfmpegCommand> { | 541 | function presetCopy (command: ffmpeg.FfmpegCommand): ffmpeg.FfmpegCommand { |
541 | return command | 542 | return command |
542 | .format('mp4') | 543 | .format('mp4') |
543 | .videoCodec('copy') | 544 | .videoCodec('copy') |
544 | .audioCodec('copy') | 545 | .audioCodec('copy') |
545 | } | 546 | } |
546 | 547 | ||
547 | async function presetOnlyAudio (command: ffmpeg.FfmpegCommand): Promise<ffmpeg.FfmpegCommand> { | 548 | function presetOnlyAudio (command: ffmpeg.FfmpegCommand): ffmpeg.FfmpegCommand { |
548 | return command | 549 | return command |
549 | .format('mp4') | 550 | .format('mp4') |
550 | .audioCodec('copy') | 551 | .audioCodec('copy') |