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:
parent
0ffc9737c4
commit
f73416a6fe
11
index.js
11
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) {
|
||||
|
||||
@ -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,
|
||||
}
|
||||
@ -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`
|
||||
);
|
||||
|
||||
@ -199,7 +199,6 @@ function video(db) {
|
||||
|
||||
let streamStats = stream.streamStats;
|
||||
streamStats.duration = lineupItem.streamDuration;
|
||||
console.log("timeElapsed=" + prog.timeElapsed );
|
||||
|
||||
this.backup = {
|
||||
stream: stream,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user