From f73416a6fe81710c8d25a264c8f83129cf23e5a7 Mon Sep 17 00:00:00 2001 From: vexorian Date: Sat, 4 Jul 2020 17:53:05 -0400 Subject: [PATCH] Audio channel/sampleRate normalization. Fix ffmpeg config load issues. Raise beep volume a bit. ffmpeg config migration to version 4. --- index.js | 11 ++++- src/defaultSettings.js | 54 +++++++++++++++++++--- src/ffmpeg.js | 55 ++++++++++++++++++----- src/video.js | 1 - web/directives/ffmpeg-settings.js | 2 +- web/public/templates/ffmpeg-settings.html | 18 ++++++-- 6 files changed, 116 insertions(+), 25 deletions(-) diff --git a/index.js b/index.js index 157540d..4bde234 100644 --- a/index.js +++ b/index.js @@ -109,8 +109,15 @@ function initDB(db) { fs.writeFileSync(process.env.DATABASE + '/images/generic-error-screen.png', data) } - if ( (ffmpegSettings.length === 0) || typeof(ffmpegSettings.configVersion) === 'undefined' ) { - db['ffmpeg-settings'].save( defaultSettings.ffmpeg() ) + var ffmpegRepaired = defaultSettings.repairFFmpeg(ffmpegSettings); + if (ffmpegRepaired.hasBeenRepaired) { + var fixed = ffmpegRepaired.fixedConfig; + var i = fixed._id; + if ( i == null || typeof(i) == 'undefined') { + db['ffmpeg-settings'].save(fixed); + } else { + db['ffmpeg-settings'].update( { _id: i } , fixed ); + } } if (plexSettings.length === 0) { diff --git a/src/defaultSettings.js b/src/defaultSettings.js index bc19291..27ad297 100644 --- a/src/defaultSettings.js +++ b/src/defaultSettings.js @@ -1,6 +1,4 @@ -module.exports = { - - ffmpeg: () => { + function ffmpeg() { return { //a record of the config version will help migrating between versions // in the future. Always increase the version when new ffmpeg configs @@ -8,7 +6,7 @@ module.exports = { // // configVersion 3: First versioned config. // - configVersion: 3, + configVersion: 4, ffmpegPath: "/usr/bin/ffmpeg", threads: 4, concatMuxDelay: "0", @@ -20,12 +18,56 @@ module.exports = { targetResolution: "1920x1080", videoBitrate: 10000, videoBufSize: 2000, + audioBitrate: 192, + audioBufSize: 50, + audioSampleRate: 48, + audioChannels: 2, errorScreen: "pic", errorAudio: "silent", normalizeVideoCodec: false, normalizeAudioCodec: false, normalizeResolution: false, - alignAudio: false, + normalizeAudio: false, } } -} + + function repairFFmpeg(existingConfigs) { + var hasBeenRepaired = false; + var currentConfig = {}; + var _id = null; + if (existingConfigs.length === 0) { + currentConfig = {}; + } else { + currentConfig = existingConfigs[0]; + _id = currentConfig._id; + } + if ( + (typeof(currentConfig.configVersion) === 'undefined') + || (currentConfig.configVersion < 3) + ) { + hasBeenRepaired = true; + currentConfig = ffmpeg(); + currentConfig._id = _id; + } + if (currentConfig.configVersion == 3) { + //migrate from version 3 to 4 + hasBeenRepaired = true; + //new settings: + currentConfig.audioBitrate = 192; + currentConfig.audioBufSize = 50; + currentConfig.audioChannels = 2; + currentConfig.audioSampleRate = 48; + //this one has been renamed: + currentConfig.normalizeAudio = currentConfig.alignAudio; + currentConfig.configVersion = 4; + } + return { + hasBeenRepaired: hasBeenRepaired, + fixedConfig : currentConfig, + }; + } + +module.exports = { + ffmpeg: ffmpeg, + repairFFmpeg: repairFFmpeg, +} \ No newline at end of file diff --git a/src/ffmpeg.js b/src/ffmpeg.js index 033a62c..eee282f 100644 --- a/src/ffmpeg.js +++ b/src/ffmpeg.js @@ -10,6 +10,16 @@ class FFMPEG extends events.EventEmitter { constructor(opts, channel) { super() this.opts = opts + if (! this.opts.enableFFMPEGTranscoding) { + //this ensures transcoding is completely disabled even if + // some settings are true + this.opts.normalizeAudio = false; + this.opts.normalizeAudioCodec = false; + this.opts.normalizeVideoCodec = false; + this.opts.errorScreen = 'kill'; + this.opts.normalizeResolution = false; + this.opts.audioVolumePercent = 100; + } this.channel = channel this.ffmpegPath = opts.ffmpegPath @@ -18,7 +28,8 @@ class FFMPEG extends events.EventEmitter { this.wantedH = parsed.h; this.sentData = false; - this.alignAudio = this.opts.alignAudio; + this.apad = this.opts.normalizeAudio; + this.audioChannelsSampleRate = this.opts.normalizeAudio; this.ensureResolution = this.opts.normalizeResolution; this.volumePercent = this.opts.audioVolumePercent; } @@ -31,7 +42,7 @@ class FFMPEG extends events.EventEmitter { async spawnError(title, subtitle, streamStats, enableIcon, type) { if (! this.opts.enableFFMPEGTranscoding || this.opts.errorScreen == 'kill') { console.log("error: " + title + " ; " + subtitle); - this.emit('error', { code: -1, cmd: `error stream disabled` }) + this.emit('error', { code: -1, cmd: `error stream disabled. ${title} ${subtitles}`} ) return; } // since this is from an error situation, streamStats may have issues. @@ -97,7 +108,8 @@ class FFMPEG extends events.EventEmitter { if ( typeof(streamUrl.errorTitle) !== 'undefined') { doOverlay = false; //never show icon in the error screen // for error stream, we have to generate the input as well - this.alignAudio = false; //all of these generate audio correctly-aligned to video so there is no need for apad + this.apad = false; //all of these generate audio correctly-aligned to video so there is no need for apad + this.audioChannelsSampleRate = true; //we'll need these if (this.ensureResolution) { //all of the error strings already choose the resolution to @@ -149,7 +161,7 @@ class FFMPEG extends events.EventEmitter { if (this.opts.errorAudio == 'whitenoise') { audioComplex = `;aevalsrc=-2+0.1*random(0):${durstr}[audioy]`; } else if (this.opts.errorAudio == 'sine') { - audioComplex = `;sine=f=440:${durstr}[audiox];[audiox]volume=-65dB[audioy]`; + audioComplex = `;sine=f=440:${durstr}[audiox];[audiox]volume=-35dB[audioy]`; } else { //silent audioComplex = `;aevalsrc=0:${durstr}[audioy]`; } @@ -192,26 +204,29 @@ class FFMPEG extends events.EventEmitter { currentAudio = '[boosted]'; } // Align audio is just the apad filter applied to audio stream - if (this.alignAudio) { + if (this.apad) { audioComplex += `;${currentAudio}apad=whole_dur=${streamStats.duration}ms[padded]`; currentAudio = '[padded]'; + } else if (this.audioChannelsSampleRate) { + //TODO: Do not set this to true if audio channels and sample rate are already good + transcodeAudio = true; } // If no filters have been applied, then the stream will still be // [video] , in that case, we do not actually add the video stuff to // filter_complex and this allows us to avoid transcoding. - var changeVideoCodec = (this.opts.normalizeVideoCodec && isDifferentVideoCodec( streamStats.videoCodec, this.opts.videoEncoder) ); - var changeAudioCodec = (this.opts.normalizeAudioCodec && isDifferentAudioCodec( streamStats.audioCodec, this.opts.audioEncoder) ); + var transcodeVideo = (this.opts.normalizeVideoCodec && isDifferentVideoCodec( streamStats.videoCodec, this.opts.videoEncoder) ); + var transcodeAudio = (this.opts.normalizeAudioCodec && isDifferentAudioCodec( streamStats.audioCodec, this.opts.audioEncoder) ); var filterComplex = ''; if (currentVideo != '[video]') { - changeVideoCodec = true; //this is useful so that it adds some lines below + transcodeVideo = true; //this is useful so that it adds some lines below filterComplex += videoComplex; } else { currentVideo = `0:${videoIndex}`; } // same with audio: if (currentAudio != '[audio]') { - changeAudioCodec = true; + transcodeAudio = true; filterComplex += audioComplex; } else { currentAudio = `0:${audioIndex}`; @@ -228,11 +243,11 @@ class FFMPEG extends events.EventEmitter { ffmpegArgs.push( '-map', currentVideo, '-map', currentAudio, - `-c:v`, (changeVideoCodec ? this.opts.videoEncoder : 'copy'), + `-c:v`, (transcodeVideo ? this.opts.videoEncoder : 'copy'), `-flags`, `cgop+ilme`, `-sc_threshold`, `1000000000` ); - if ( changeVideoCodec ) { + if ( transcodeVideo ) { // add the video encoder flags ffmpegArgs.push( `-b:v`, `${this.opts.videoBitrate}k`, @@ -241,8 +256,24 @@ class FFMPEG extends events.EventEmitter { `-bufsize:v`, `${this.opts.videoBufSize}k` ); } + if ( transcodeAudio ) { + // add the audio encoder flags + ffmpegArgs.push( + `-b:a`, `${this.opts.audioBitrate}k`, + `-minrate:a`, `${this.opts.audioBitrate}k`, + `-maxrate:a`, `${this.opts.audioBitrate}k`, + `-bufsize:a`, `${this.opts.videoBufSize}k` + ); + if (this.audioChannelsSampleRate) { + ffmpegArgs.push( + `-ac`, `${this.opts.audioChannels}`, + `-ar`, `${this.opts.audioSampleRate}k` + ); + } + } ffmpegArgs.push( - `-c:a`, (changeAudioCodec ? this.opts.audioEncoder : 'copy'), + `-c:a`, (transcodeAudio ? this.opts.audioEncoder : 'copy'), + '-movflags', '+faststart', `-muxdelay`, `0`, `-muxpreload`, `0` ); diff --git a/src/video.js b/src/video.js index f09bc60..c1f1e4e 100644 --- a/src/video.js +++ b/src/video.js @@ -199,7 +199,6 @@ function video(db) { let streamStats = stream.streamStats; streamStats.duration = lineupItem.streamDuration; - console.log("timeElapsed=" + prog.timeElapsed ); this.backup = { stream: stream, diff --git a/web/directives/ffmpeg-settings.js b/web/directives/ffmpeg-settings.js index 1ceb042..a1f766d 100644 --- a/web/directives/ffmpeg-settings.js +++ b/web/directives/ffmpeg-settings.js @@ -20,7 +20,7 @@ }) } scope.isTranscodingNotNeeded = () => { - return ! (scope.settings.enableFFMPEGTranscoding) + return (typeof(scope.settings) ==='undefined') || ! (scope.settings.enableFFMPEGTranscoding); }; scope.hideIfNotAutoPlay = () => { return scope.settings.enableAutoPlay != true diff --git a/web/public/templates/ffmpeg-settings.html b/web/public/templates/ffmpeg-settings.html index e400170..c3e4b6c 100644 --- a/web/public/templates/ffmpeg-settings.html +++ b/web/public/templates/ffmpeg-settings.html @@ -93,9 +93,21 @@
+ + +
+ + +
Values higher than 100 will boost the audio. +
+ + +
+ +
@@ -146,9 +158,9 @@
- - - In rare situations, video and audio streams in a video may have different lengths. This can cause desync issues in some clients. This transcodes audio in all videos to ensure the lengths stay the same. + + + This will force the preferred number of audio channels and sample rate, in addition it will align the lengths of the audio and video channels. This will prevent audio-related episode transition issues in many clients. Audio will always be transcoded.