Audio channel/sampleRate normalization. Fix ffmpeg config load issues. Raise beep volume a bit. ffmpeg config migration to version 4.

This commit is contained in:
vexorian 2020-07-04 17:53:05 -04:00
parent 0ffc9737c4
commit f73416a6fe
6 changed files with 116 additions and 25 deletions

View File

@ -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) {

View File

@ -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,
}

View File

@ -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`
);

View File

@ -199,7 +199,6 @@ function video(db) {
let streamStats = stream.streamStats;
streamStats.duration = lineupItem.streamDuration;
console.log("timeElapsed=" + prog.timeElapsed );
this.backup = {
stream: stream,

View File

@ -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

View File

@ -93,9 +93,21 @@
</div>
<div class="form-group">
<label>Audio Bitrate (k)</label>
<input type="number" class="form-control form-control-sm" ng-model="settings.audioBitrate"/>
<br />
<label>Audio Buffer Size (k)</label>
<input type="number" class="form-control form-control-sm" ng-model="settings.audioBufSize"/>
<br />
<label>Audio Volume (%)</label>
<input type="number" ria-describedby="volumeHelp" class="form-control form-control-sm" ng-model="settings.audioVolumePercent"/>
<small id="volumeHelp" class="form-text text-muted">Values higher than 100 will boost the audio.</small>
<br />
<label>Audio Channels</label>
<input type="number" class="form-control form-control-sm" ng-model="settings.audioChannels"/>
<br />
<label>Audio Sample Rate (k)</label>
<input type="number" class="form-control form-control-sm" ng-model="settings.audioSampleRate"/>
</div>
@ -146,9 +158,9 @@
<div class="row">
<div class="col-sm-9">
<div class="form-group">
<input id="enableAlignAudio" type="checkbox" ng-model="settings.alignAudio" ng-disabled="isTranscodingNotNeeded()" />
<label for="enableAlignAudio">Align Audio and Video lengths</label>
<small class="form-text text-muted">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.
<input id="enableAlignAudio" type="checkbox" ng-model="settings.normalizeAudio" ng-disabled="isTranscodingNotNeeded()" />
<label for="enableAlignAudio">Normalize Audio</label>
<small class="form-text text-muted">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.
</small>
</div>
</div>