From 4176e227cb18c40e13f30f4634d128cc3169e9d4 Mon Sep 17 00:00:00 2001 From: Rigel Kent Date: Mon, 21 May 2018 13:14:29 +0200 Subject: [PATCH] Fixing #626 with ffmpeg's low default audio bitrate --- server/helpers/ffmpeg-utils.ts | 121 +++++++++++++++++++- server/initializers/checker.ts | 28 ++++- server/tests/api/videos/multiple-servers.ts | 16 +-- server/tests/utils/videos/videos.ts | 4 +- 4 files changed, 153 insertions(+), 16 deletions(-) diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts index f0623c88b..eb1c86ab9 100644 --- a/server/helpers/ffmpeg-utils.ts +++ b/server/helpers/ffmpeg-utils.ts @@ -5,6 +5,7 @@ import { CONFIG, VIDEO_TRANSCODING_FPS } from '../initializers' import { unlinkPromise } from './core-utils' import { processImage } from './image-utils' import { logger } from './logger' +import { checkFFmpegEncoders } from '../initializers/checker' async function getVideoFileResolution (path: string) { const videoStream = await getVideoFileStream(path) @@ -85,12 +86,8 @@ function transcode (options: TranscodeOptions) { return new Promise(async (res, rej) => { let command = ffmpeg(options.inputPath) .output(options.outputPath) - .videoCodec('libx264') .outputOption('-threads ' + CONFIG.TRANSCODING.THREADS) - .outputOption('-movflags faststart') - .outputOption('-b_strategy 1') // NOTE: b-strategy 1 - heuristic algorythm, 16 is optimal B-frames for it - .outputOption('-bf 16') // NOTE: Why 16: https://github.com/Chocobozzz/PeerTube/pull/774. b-strategy 2 -> B-frames<16 - // .outputOption('-crf 18') + .preset(standard) let fps = await getVideoFileFPS(options.inputPath) if (options.resolution !== undefined) { @@ -149,3 +146,117 @@ function getVideoFileStream (path: string) { }) }) } + +/** + * A slightly customised version of the 'veryfast' x264 preset + * + * The veryfast preset is right in the sweet spot of performance + * and quality. Superfast and ultrafast will give you better + * performance, but then quality is noticeably worse. + */ +function veryfast (ffmpeg) { + ffmpeg + .preset(standard) + .outputOption('-preset:v veryfast') + .outputOption(['--aq-mode=2', '--aq-strength=1.3']) + /* + MAIN reference: https://slhck.info/video/2017/03/01/rate-control.html + Our target situation is closer to a livestream than a stream, + since we want to reduce as much a possible the encoding burden, + altough not to the point of a livestream where there is a hard + constraint on the frames per second to be encoded. + + why '--aq-mode=2 --aq-strength=1.3' instead of '-profile:v main'? + Make up for most of the loss of grain and macroblocking + with less computing power. + */ +} + +/** + * A preset optimised for a stillimage audio video + */ +function audio (ffmpeg) { + ffmpeg + .preset(veryfast) + .outputOption('-tune stillimage') +} + +/** + * A toolbox to play with audio + */ +namespace audio { + export const get = (ffmpeg, pos = 0) => { + // without position, ffprobe considers the last input only + // we make it consider the first input only + ffmpeg + .ffprobe(pos, (_,data) => { + return data['streams'].find(stream => { + return stream['codec_type'] === 'audio' + }) + }) + } + + export namespace bitrate { + export const baseKbitrate = 384 + + const toBits = (kbits: number): number => { return kbits * 8000 } + + export const aac = (bitrate: number): number => { + switch (true) { + case bitrate > toBits(384): + return baseKbitrate + default: + return -1 // we interpret it as a signal to copy the audio stream as is + } + } + + export const mp3 = (bitrate: number): number => { + switch (true) { + case bitrate <= toBits(192): + return 128 + case bitrate <= toBits(384): + return 256 + default: + return baseKbitrate + } + } + } +} + +/** + * Standard profile, with variable bitrate audio and faststart. + * + * As for the audio, quality '5' is the highest and ensures 96-112kbps/channel + * See https://trac.ffmpeg.org/wiki/Encode/AAC#fdk_vbr + */ +async function standard (ffmpeg) { + let _bitrate = audio.bitrate.baseKbitrate + let _ffmpeg = ffmpeg + .format('mp4') + .videoCodec('libx264') + .outputOption('-level 3.1') // 3.1 is the minimal ressource allocation for our highest supported resolution + .outputOption('-b_strategy 1') // NOTE: b-strategy 1 - heuristic algorythm, 16 is optimal B-frames for it + .outputOption('-bf 16') // NOTE: Why 16: https://github.com/Chocobozzz/PeerTube/pull/774. b-strategy 2 -> B-frames<16 + .outputOption('-movflags faststart') + let _audio = audio.get(_ffmpeg) + + if (!_audio) return _ffmpeg.noAudio() + + // we try to reduce the ceiling bitrate by making rough correspondances of bitrates + // of course this is far from perfect, but it might save some space in the end + if (audio.bitrate[_audio['codec_name']]) { + _bitrate = audio.bitrate[_audio['codec_name']](_audio['bit_rate']) + if (_bitrate === -1) { + return _ffmpeg.audioCodec('copy') + } + } + + // we favor VBR, if a good AAC encoder is available + if ((await checkFFmpegEncoders()).get('libfdk_aac')) { + return _ffmpeg + .audioCodec('libfdk_aac') + .audioQuality(5) + } + + return _ffmpeg.audioBitrate(_bitrate) +} diff --git a/server/initializers/checker.ts b/server/initializers/checker.ts index 270cbf649..f1c2e80a9 100644 --- a/server/initializers/checker.ts +++ b/server/initializers/checker.ts @@ -84,11 +84,11 @@ function checkMissedConfig () { async function checkFFmpeg (CONFIG: { TRANSCODING: { ENABLED: boolean } }) { const Ffmpeg = require('fluent-ffmpeg') const getAvailableCodecsPromise = promisify0(Ffmpeg.getAvailableCodecs) - const codecs = await getAvailableCodecsPromise() + const canEncode = [ 'libx264' ] + if (CONFIG.TRANSCODING.ENABLED === false) return undefined - const canEncode = [ 'libx264' ] for (const codec of canEncode) { if (codecs[codec] === undefined) { throw new Error('Unknown codec ' + codec + ' in FFmpeg.') @@ -98,6 +98,29 @@ async function checkFFmpeg (CONFIG: { TRANSCODING: { ENABLED: boolean } }) { throw new Error('Unavailable encode codec ' + codec + ' in FFmpeg') } } + + checkFFmpegEncoders() +} + +// Optional encoders, if present, can be used to improve transcoding +// Here we ask ffmpeg if it detects their presence on the system, so that we can later use them +let supportedOptionalEncoders: Map +async function checkFFmpegEncoders (): Promise> { + if (supportedOptionalEncoders !== undefined) { + return supportedOptionalEncoders + } + + const Ffmpeg = require('fluent-ffmpeg') + const getAvailableEncodersPromise = promisify0(Ffmpeg.getAvailableEncoders) + const encoders = await getAvailableEncodersPromise() + const optionalEncoders = [ 'libfdk_aac' ] + supportedOptionalEncoders = new Map() + + for (const encoder of optionalEncoders) { + supportedOptionalEncoders.set(encoder, + encoders[encoder] !== undefined + ) + } } // We get db by param to not import it in this file (import orders) @@ -126,6 +149,7 @@ async function applicationExist () { export { checkConfig, checkFFmpeg, + checkFFmpegEncoders, checkMissedConfig, clientsExist, usersExist, diff --git a/server/tests/api/videos/multiple-servers.ts b/server/tests/api/videos/multiple-servers.ts index cb18898ce..4681deb47 100644 --- a/server/tests/api/videos/multiple-servers.ts +++ b/server/tests/api/videos/multiple-servers.ts @@ -209,19 +209,19 @@ describe('Test multiple servers', function () { files: [ { resolution: 240, - size: 190000 + size: 100000 }, { resolution: 360, - size: 280000 + size: 180000 }, { resolution: 480, - size: 390000 + size: 280000 }, { resolution: 720, - size: 710000 + size: 630000 } ], thumbnailfile: 'thumbnail', @@ -975,19 +975,19 @@ describe('Test multiple servers', function () { files: [ { resolution: 720, - size: 40315 + size: 31000 }, { resolution: 480, - size: 22808 + size: 16000 }, { resolution: 360, - size: 18617 + size: 12000 }, { resolution: 240, - size: 15217 + size: 10000 } ] } diff --git a/server/tests/utils/videos/videos.ts b/server/tests/utils/videos/videos.ts index 8c49eb02b..a9d449c58 100644 --- a/server/tests/utils/videos/videos.ts +++ b/server/tests/utils/videos/videos.ts @@ -522,7 +522,9 @@ async function completeVideoCheck ( const minSize = attributeFile.size - ((10 * attributeFile.size) / 100) const maxSize = attributeFile.size + ((10 * attributeFile.size) / 100) - expect(file.size).to.be.above(minSize).and.below(maxSize) + expect(file.size, + 'File size for resolution ' + file.resolution.label + ' outside confidence interval.') + .to.be.above(minSize).and.below(maxSize) { await testImage(url, attributes.thumbnailfile || attributes.fixture, videoDetails.thumbnailPath) -- 2.25.1