From 94aa908e0b7b64a80329fa9dd4d0aa5c6c632fe1 Mon Sep 17 00:00:00 2001 From: vexorian Date: Sat, 27 Jun 2020 11:43:19 -0400 Subject: [PATCH] UI and config for normalization changes. Audio Volume Boost. Error streams. Various fixes. Fix bug where normalize codecs logic to pick when to transcode was the reverse of what we wanted. Beep and white noise are less loud, which prevent issues with ac3 encoder. Channel Icon Overlay doesn't appear during error screen. --- Dockerfile | 36 ++ index.js | 19 +- resources/generic-error-screen.png | Bin 0 -> 77550 bytes src/api.js | 17 +- src/defaultSettings.js | 31 + src/ffmpeg.js | 195 ++++-- src/helperFuncs.js | 15 +- src/svg/generic-error-screen.svg | 731 ++++++++++++++++++++++ src/video.js | 36 +- web/directives/ffmpeg-settings.js | 37 +- web/public/templates/ffmpeg-settings.html | 154 ++++- 11 files changed, 1159 insertions(+), 112 deletions(-) create mode 100644 resources/generic-error-screen.png create mode 100644 src/defaultSettings.js create mode 100644 src/svg/generic-error-screen.svg diff --git a/Dockerfile b/Dockerfile index f173afc..6e3f458 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,42 @@ FROM node:12.18-alpine3.12 # Should be ffmpeg v4.2.3 RUN apk add --no-cache ffmpeg && ffmpeg -version + +# Remove the previous line and uncommenting the following lines will allow the +# ffmpeg version to support draw_text filter, but it makes the docker build take +# a long time and it's only used for minor features at the moment. +#RUN apk add --update \ +# curl yasm build-base gcc zlib-dev libc-dev openssl-dev yasm-dev lame-dev libogg-dev x264-dev libvpx-dev libvorbis-dev x265-dev freetype-dev libass-dev libwebp-dev rtmpdump-dev libtheora-dev opus-dev && \ +# DIR=$(mktemp -d) && cd ${DIR} && \ +# curl -s http://ffmpeg.org/releases/ffmpeg-4.2.3.tar.gz | tar zxvf - -C . && \ +# cd ffmpeg-4.2.3 && \ +# ./configure \ +# --enable-version3 \ +# --enable-gpl \ +# --enable-nonfree \ +# --enable-small \ +# --enable-libmp3lame \ +# --enable-libx264 \ +# --enable-libx265 \ +# --enable-libvpx \ +# --enable-libtheora \ +# --enable-libvorbis \ +# --enable-libopus \ +# --enable-libass \ +# --enable-libwebp \ +# --enable-librtmp \ +# --enable-postproc \ +# --enable-avresample \ +# --enable-libfreetype \ +# --enable-openssl \ +# --enable-filter=drawtext \ +# --disable-debug && \ +# make && \ +# make install && \ +# make distclean && \ +# rm -rf ${DIR} && \ +# mv /usr/local/bin/ffmpeg /usr/bin/ffmpeg && \ +# apk del build-base curl tar bzip2 x264 openssl nasm openssl xz gnupg && rm -rf /v WORKDIR /home/node/app COPY package*.json ./ RUN npm install diff --git a/index.js b/index.js index 4e8a08b..157540d 100644 --- a/index.js +++ b/index.js @@ -7,6 +7,7 @@ const express = require('express') const bodyParser = require('body-parser') const api = require('./src/api') +const defaultSettings = require('./src/defaultSettings') const video = require('./src/video') const HDHR = require('./src/hdhr') @@ -103,19 +104,13 @@ function initDB(db) { let data = fs.readFileSync(path.resolve(path.join(__dirname, 'resources/pseudotv.png'))) fs.writeFileSync(process.env.DATABASE + '/images/pseudotv.png', data) } + if (!fs.existsSync(process.env.DATABASE + '/images/generic-error-screen.png')) { + let data = fs.readFileSync(path.resolve(path.join(__dirname, 'resources/generic-error-screen.png'))) + fs.writeFileSync(process.env.DATABASE + '/images/generic-error-screen.png', data) + } - if (ffmpegSettings.length === 0) { - db['ffmpeg-settings'].save({ - ffmpegPath: '/usr/bin/ffmpeg', - enableChannelOverlay: false, - threads: 4, - videoEncoder: 'mpeg2video', - videoResolutionHeight: 'unchanged', - videoBitrate: 10000, - videoBufSize: 2000, - concatMuxDelay: '0', - logFfmpeg: true - }) + if ( (ffmpegSettings.length === 0) || typeof(ffmpegSettings.configVersion) === 'undefined' ) { + db['ffmpeg-settings'].save( defaultSettings.ffmpeg() ) } if (plexSettings.length === 0) { diff --git a/resources/generic-error-screen.png b/resources/generic-error-screen.png new file mode 100644 index 0000000000000000000000000000000000000000..fbd261f825852c7415df086b66c0381c2a3eb2f7 GIT binary patch literal 77550 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%0AyWZ_>BI13UTc4uY^w_r~K?m1_oB!ML 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 literal 0 HcmV?d00001 diff --git a/src/api.js b/src/api.js index 5bff62b..0b4b004 100644 --- a/src/api.js +++ b/src/api.js @@ -1,6 +1,7 @@ const express = require('express') const fs = require('fs') +const defaultSettings = require('./defaultSettings') module.exports = { router: api } function api(db, xmltvInterval) { @@ -62,18 +63,10 @@ function api(db, xmltvInterval) { res.send(ffmpeg) }) router.post('/api/ffmpeg-settings', (req, res) => { // RESET - db['ffmpeg-settings'].update({ _id: req.body._id }, { - ffmpegPath: req.body.ffmpegPath, - enableChannelOverlay: false, - threads: 4, - videoEncoder: 'mpeg2video', - videoResolutionHeight: 'unchanged', - videoBitrate: 10000, - videoBufSize: 2000, - concatMuxDelay: '0', - logFfmpeg: true - }) - let ffmpeg = db['ffmpeg-settings'].find()[0] + let ffmpeg = defaultSettings.ffmpeg(); + ffmpeg.ffmpegPath = req.body.ffmpegPath; + db['ffmpeg-settings'].update({ _id: req.body._id }, ffmpeg) + ffmpeg = db['ffmpeg-settings'].find()[0] res.send(ffmpeg) }) diff --git a/src/defaultSettings.js b/src/defaultSettings.js new file mode 100644 index 0000000..bc19291 --- /dev/null +++ b/src/defaultSettings.js @@ -0,0 +1,31 @@ +module.exports = { + + ffmpeg: () => { + return { + //a record of the config version will help migrating between versions + // in the future. Always increase the version when new ffmpeg configs + // are added. + // + // configVersion 3: First versioned config. + // + configVersion: 3, + ffmpegPath: "/usr/bin/ffmpeg", + threads: 4, + concatMuxDelay: "0", + logFfmpeg: false, + enableFFMPEGTranscoding: false, + audioVolumePercent: 100, + videoEncoder: "mpeg2video", + audioEncoder: "ac3", + targetResolution: "1920x1080", + videoBitrate: 10000, + videoBufSize: 2000, + errorScreen: "pic", + errorAudio: "silent", + normalizeVideoCodec: false, + normalizeAudioCodec: false, + normalizeResolution: false, + alignAudio: false, + } + } +} diff --git a/src/ffmpeg.js b/src/ffmpeg.js index 681233b..033a62c 100644 --- a/src/ffmpeg.js +++ b/src/ffmpeg.js @@ -1,31 +1,26 @@ const spawn = require('child_process').spawn const events = require('events') -const fs = require('fs') - -// For now these options can be enabled with constants, must also enable overlay in settings: - -// Normalize resoltion to WxH: -const FIX_RESOLUTION = false; - const W = 1920; - const H = 1080; -// Normalize codecs, video codec is in ffmpeg settings: -const FIX_CODECS = false; - -// Align audio and video channels -const ALIGN_AUDIO = false; - -// What audio encoder to use: -const AUDIO_ENCODER = 'aac'; +//they can customize this by modifying the picture in .pseudotv folder const ERROR_PICTURE_PATH = 'http://localhost:8000/images/generic-error-screen.png' +const MAXIMUM_ERROR_DURATION_MS = 60000; + class FFMPEG extends events.EventEmitter { constructor(opts, channel) { super() this.opts = opts this.channel = channel this.ffmpegPath = opts.ffmpegPath - this.alignAudio = ALIGN_AUDIO; + + var parsed = parseResolutionString(opts.targetResolution); + this.wantedW = parsed.w; + this.wantedH = parsed.h; + + this.sentData = false; + this.alignAudio = this.opts.alignAudio; + this.ensureResolution = this.opts.normalizeResolution; + this.volumePercent = this.opts.audioVolumePercent; } async spawnConcat(streamUrl) { this.spawn(streamUrl, undefined, undefined, undefined, false, false, undefined, true) @@ -34,13 +29,27 @@ class FFMPEG extends events.EventEmitter { this.spawn(streamUrl, streamStats, startTime, duration, true, enableIcon, type, false); } async spawnError(title, subtitle, streamStats, enableIcon, type) { - // currently the error stream feature is not implemented + if (! this.opts.enableFFMPEGTranscoding || this.opts.errorScreen == 'kill') { console.log("error: " + title + " ; " + subtitle); this.emit('error', { code: -1, cmd: `error stream disabled` }) return; + } + // since this is from an error situation, streamStats may have issues. + if ( (streamStats == null) || (typeof(streamStats) === 'undefined') ) { + streamStats = {}; + } + streamStats.videoWidth = this.wantedW; + streamStats.videoHeight = this.wantedH; + if ( (typeof(streamStats.duration) === 'undefined') || isNaN(streamStats.duration) || (streamStats.duration > MAXIMUM_ERROR_DURATION_MS) ) { + // it's possible that whatever issue there was when attempting to download the video from plex + // could be temporary, so it'd be better to retry after a minute + streamStats.duration = MAXIMUM_ERROR_DURATION_MS; + } + this.spawn({ errorTitle: title , subtitle: subtitle }, streamStats, undefined, `${streamStats.duration}ms`, true, enableIcon, type, false) } async spawn(streamUrl, streamStats, startTime, duration, limitRead, enableIcon, type, isConcatPlaylist) { - let ffmpegArgs = [`-threads`, this.opts.threads, + let ffmpegArgs = [ + `-threads`, this.opts.threads, `-fflags`, `+genpts+discardcorrupt+igndts`]; if (limitRead === true) @@ -73,8 +82,9 @@ class FFMPEG extends events.EventEmitter { var currentAudio = "[audio]"; // Initially, videoComplex does nothing besides assigning the label // to the input stream + var videoIndex = 'v'; var audioComplex = `;[0:${audioIndex}]anull[audio]`; - var videoComplex = `;[0:v]null[video]`; + var videoComplex = `;[0:${videoIndex}]null[video]`; // Depending on the options we will apply multiple filters // each filter modifies the current video stream. Adds a filter to // the videoComplex variable. The result of the filter becomes the @@ -83,17 +93,83 @@ class FFMPEG extends events.EventEmitter { // When adding filters, make sure that // videoComplex always begins wiht ; and doesn't end with ; - // prepare input files, overlay adds another input file - ffmpegArgs.push(`-i`, streamUrl); + // prepare input streams + 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 + + if (this.ensureResolution) { + //all of the error strings already choose the resolution to + //match iW x iH , so with this we save ourselves a second + // scale filter + iW = this.wantedW; + iH = this.wantedH; + } + + ffmpegArgs.push("-r" , "24"); + if (this.opts.errorScreen == 'static') { + ffmpegArgs.push( + '-f', 'lavfi', + '-i', `nullsrc=s=64x36`); + videoComplex = `;geq=random(1)*255:128:128[videoz];[videoz]scale=${iW}:${iH}[videoy];[videoy]realtime[videox]`; + } else if (this.opts.errorScreen == 'testsrc') { + ffmpegArgs.push( + '-f', 'lavfi', + '-i', `testsrc=size=${iW}x${iH}`, + '-pix_fmt' , 'yuv420p' + ); + videoComplex = `;realtime[videox]`; + } else if (this.opts.errorScreen == 'text') { + var sz2 = Math.ceil( (iH) / 33.0); + var sz1 = Math.ceil( sz2 * 3. / 2. ); + var sz3 = 2*sz2; + + ffmpegArgs.push( + '-f', 'lavfi', + '-i', `color=c=black:s=${iW}x${iH}` + ); + + videoComplex = `;drawtext=fontfile=${process.env.DATABASE}/font.ttf:fontsize=${sz1}:fontcolor=white:x=(w-text_w)/2:y=(h-text_h)/2:text='${streamUrl.errorTitle}',drawtext=fontfile=${process.env.DATABASE}/font.ttf:fontsize=${sz2}:fontcolor=white:x=(w-text_w)/2:y=(h+text_h+${sz3})/2:text='${streamUrl.subtitle}'[videoy];[videoy]realtime[videox]`; + } else if (this.opts.errorScreen == 'blank') { + ffmpegArgs.push( + '-f', 'lavfi', + '-i', `color=c=black:s=${iW}x${iH}` + ); + videoComplex = `;realtime[videox]`; + } else {//'pic' + ffmpegArgs.push( + '-loop', '1', + '-i', `${ERROR_PICTURE_PATH}`, + '-pix_fmt' , 'yuv420p' + ); + videoComplex = `;[0:0]scale=${iW}:${iH}[videoy];[videoy]realtime[videox]`; + } + let durstr = `duration=${streamStats.duration}ms`; + 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]`; + } else { //silent + audioComplex = `;aevalsrc=0:${durstr}[audioy]`; + } + audioComplex += ';[audioy]arealtime[audiox]'; + currentVideo = "[videox]"; + currentAudio = "[audiox]"; + } else { + ffmpegArgs.push(`-i`, streamUrl); + } if (doOverlay) { ffmpegArgs.push(`-i`, `${this.channel.icon}` ); } // Resolution fix: Add scale filter, current stream becomes [siz] - if (FIX_RESOLUTION && (iW != W || iH != H) ) { + if (this.ensureResolution && (iW != this.wantedW || iH != this.wantedH) ) { //Maybe the scaling algorithm could be configurable. bicubic seems good though - videoComplex += `;${currentVideo}scale=${W}:${H}:flags=bicubic:force_original_aspect_ratio=decrease,pad=${W}:${H}:(ow-iw)/2:(oh-ih)/2[siz]` + videoComplex += `;${currentVideo}scale=${this.wantedW}:${this.wantedH}:flags=bicubic:force_original_aspect_ratio=decrease,pad=${this.wantedW}:${this.wantedH}:(ow-iw)/2:(oh-ih)/2[siz]` currentVideo = "[siz]"; + iW = this.wantedW; + iH = this.wantedH; } // Channel overlay: @@ -110,6 +186,11 @@ class FFMPEG extends events.EventEmitter { currentVideo = '[comb]'; } + if (this.volumePercent != 100) { + var f = this.volumePercent / 100.0; + audioComplex += `;${currentAudio}volume=${f}[boosted]`; + currentAudio = '[boosted]'; + } // Align audio is just the apad filter applied to audio stream if (this.alignAudio) { audioComplex += `;${currentAudio}apad=whole_dur=${streamStats.duration}ms[padded]`; @@ -119,18 +200,18 @@ class FFMPEG extends events.EventEmitter { // 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 = FIX_CODECS; - var changeAudioCodec = FIX_CODECS; + var changeVideoCodec = (this.opts.normalizeVideoCodec && isDifferentVideoCodec( streamStats.videoCodec, this.opts.videoEncoder) ); + var changeAudioCodec = (this.opts.normalizeAudioCodec && isDifferentAudioCodec( streamStats.audioCodec, this.opts.audioEncoder) ); var filterComplex = ''; if (currentVideo != '[video]') { - changeVideoCodec = true; + changeVideoCodec = true; //this is useful so that it adds some lines below filterComplex += videoComplex; } else { - currentVideo = '0:v'; + currentVideo = `0:${videoIndex}`; } - // same with audi: + // same with audio: if (currentAudio != '[audio]') { - changeAudioCodec = true; //this is useful for some more flags later + changeAudioCodec = true; filterComplex += audioComplex; } else { currentAudio = `0:${audioIndex}`; @@ -161,13 +242,14 @@ class FFMPEG extends events.EventEmitter { ); } ffmpegArgs.push( - `-c:a`, (changeAudioCodec ? AUDIO_ENCODER : 'copy'), + `-c:a`, (changeAudioCodec ? this.opts.audioEncoder : 'copy'), `-muxdelay`, `0`, `-muxpreload`, `0` ); } else { //Concat stream is simpler and should always copy the codec ffmpegArgs.push( + `-probesize`, `25000000`, `-i`, streamUrl, `-map`, `0:v`, `-map`, `0:${audioIndex}`, @@ -191,6 +273,7 @@ class FFMPEG extends events.EventEmitter { this.ffmpeg = spawn(this.ffmpegPath, ffmpegArgs) this.ffmpeg.stdout.on('data', (chunk) => { + this.sentData = true; this.emit('data', chunk) }) if (this.opts.logFfmpeg) { @@ -199,14 +282,18 @@ class FFMPEG extends events.EventEmitter { }) } this.ffmpeg.on('close', (code) => { - if (code === null) + if (code === null) { this.emit('close', code) - else if (code === 0) + } else if (code === 0) { this.emit('end') - else if (code === 255) + } else if (code === 255) { + if (! this.sentData) { + this.emit('error', { code: code, cmd: `${this.opts.ffmpegPath} ${ffmpegArgs.join(' ')}` }) + } this.emit('close', code) - else + } else { this.emit('error', { code: code, cmd: `${this.opts.ffmpegPath} ${ffmpegArgs.join(' ')}` }) + } }) } kill() { @@ -216,4 +303,42 @@ class FFMPEG extends events.EventEmitter { } } +function isDifferentVideoCodec(codec, encoder) { + if (codec == 'mpeg2video') { + return ! encoder.includes("mpeg2"); + } else if (codec == 'h264') { + return ! encoder.includes("264"); + } else if (codec == 'hevc') { + return !( encoder.includes("265") || encoder.includes("hevc") ); + } + // if the encoder/codec combinations are unknown, always encode, just in case + return true; +} + +function isDifferentAudioCodec(codec, encoder) { + + if (codec == 'mp3') { + return !( encoder.includes("mp3") || encoder.includes("lame") ); + } else if (codec == 'aac') { + return !encoder.includes("aac"); + } else if (codec == 'ac3') { + return !encoder.includes("ac3"); + } else if (codec == 'flac') { + return !encoder.includes("flac"); + } + // if the encoder/codec combinations are unknown, always encode, just in case + return true; +} + +function parseResolutionString(s) { + var i = s.indexOf('x'); + if (i == -1) { + return {w:1920, h:1080} + } + return { + w: parseInt( s.substring(0,i) , 10 ), + h: parseInt( s.substring(i+1) , 10 ), + } +} + module.exports = FFMPEG diff --git a/src/helperFuncs.js b/src/helperFuncs.js index cc5a665..c772a06 100644 --- a/src/helperFuncs.js +++ b/src/helperFuncs.js @@ -113,8 +113,15 @@ function createLineup(obj) { return lineup } -function isChannelIconEnabled(enableChannelOverlay, icon, overlayIcon, type) { - if (typeof type === `undefined`) - return enableChannelOverlay == true && icon !== '' && overlayIcon - return enableChannelOverlay == true && icon !== '' && overlayIcon +function isChannelIconEnabled( ffmpegSettings, channel, type) { + if (! ffmpegSettings.enableFFMPEGTranscoding || ffmpegSettings.disableChannelOverlay ) { + return false; + } + if ( (typeof type !== `undefined`) && (type == 'commercial') ) { + return false; + } + if (channel.icon === '' || !channel.overlayIcon) { + return false; + } + return true; } diff --git a/src/svg/generic-error-screen.svg b/src/svg/generic-error-screen.svg new file mode 100644 index 0000000..192ef64 --- /dev/null +++ b/src/svg/generic-error-screen.svg @@ -0,0 +1,731 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + This stream is facing technical issues.Try again later. + + diff --git a/src/video.js b/src/video.js index 57b8cd3..f09bc60 100644 --- a/src/video.js +++ b/src/video.js @@ -31,7 +31,7 @@ function video(db) { return }) ffmpeg.on('close', () => { - res.send() + res.end() }) res.on('close', () => { // on HTTP close, kill ffmpeg @@ -80,7 +80,7 @@ function video(db) { }) ffmpeg.on('close', () => { - res.send(); + res.end(); }) res.on('close', () => { // on HTTP close, kill ffmpeg @@ -129,25 +129,38 @@ function video(db) { let lineup = helperFuncs.createLineup(prog) let lineupItem = lineup.shift() + let streamDuration = lineupItem.streamDuration / 1000; // Only episode in this lineup, or item is a commercial, let stream end naturally if (lineup.length === 0 || lineupItem.type === 'commercial' || lineup.length === 1 && lineup[0].type === 'commercial') streamDuration = undefined - let deinterlace = enableChannelIcon = helperFuncs.isChannelIconEnabled(ffmpegSettings.enableChannelOverlay, channel.icon, channel.overlayIcon) + let enableChannelIcon = helperFuncs.isChannelIconEnabled( ffmpegSettings, channel, lineupItem.type); + let deinterlace = ffmpegSettings.enableFFMPEGTranscoding; //for now it will always deinterlace when transcoding is enabled but this is sub-optimal let plexTranscoder = new PlexTranscoder(plexSettings, lineupItem); let ffmpeg = new FFMPEG(ffmpegSettings, channel); // Set the transcoder options + var ffmpeg1Ended = false; ffmpeg.on('data', (data) => { res.write(data) }) ffmpeg.on('error', (err) => { + if (ffmpeg1Ended) { + return; + } + ffmpeg1Ended = true; plexTranscoder.stopUpdatingPlex(); if (typeof(this.backup) !== 'undefined') { let ffmpeg2 = new FFMPEG(ffmpegSettings, channel); // Set the transcoder options ffmpeg2.spawnError('Source error', `ffmpeg returned code ${err.code}`, this.backup.stream.streamStats, this.backup.enableChannelIcon, this.backup.type); // Spawn the ffmpeg process, fire this bitch up - ffmpeg2.on('data', (data) => { res.write(data) } ); + ffmpeg2.on('data', (data) => { + try { + res.write(data) + } catch (err) { + console.log("err="+err); + } + } ); ffmpeg2.on('error', (err) => { res.end() } ); ffmpeg2.on('close', () => { res.send() } ); ffmpeg2.on('end', () => { res.end() } ); @@ -160,11 +173,17 @@ function video(db) { }) ffmpeg.on('close', () => { + if (ffmpeg1Ended) { + return; + } plexTranscoder.stopUpdatingPlex(); - res.send(); + res.end(); }) ffmpeg.on('end', () => { // On finish transcode - END of program or commercial... + if (ffmpeg1Ended) { + return; + } plexTranscoder.stopUpdatingPlex(); res.end() }) @@ -178,14 +197,13 @@ function video(db) { let streamStart = (stream.directPlay) ? plexTranscoder.currTimeS : undefined; - let streamStats = stream.streamStats + let streamStats = stream.streamStats; + streamStats.duration = lineupItem.streamDuration; console.log("timeElapsed=" + prog.timeElapsed ); - streamStats.duration = streamStats.duration - prog.timeElapsed; this.backup = { stream: stream, streamStart: streamStart, - streamDuration: streamDuration, enableChannelIcon: enableChannelIcon, type: lineupItem.type }; @@ -214,7 +232,7 @@ function video(db) { // If someone passes this number then they probably watch too much television let maxStreamsToPlayInARow = 100; - var data = "#ffconcat version 1.0\n" + var data = "ffconcat version 1.0\n" for (var i = 0; i < maxStreamsToPlayInARow; i++) data += `file 'http://localhost:${process.env.PORT}/stream?channel=${channelNum}'\n` diff --git a/web/directives/ffmpeg-settings.js b/web/directives/ffmpeg-settings.js index f8e2fcd..1ceb042 100644 --- a/web/directives/ffmpeg-settings.js +++ b/web/directives/ffmpeg-settings.js @@ -19,21 +19,22 @@ scope.settings = _settings }) } - scope.hideIfNotEnableChannelOverlay = () => { - return scope.settings.enableChannelOverlay != true + scope.isTranscodingNotNeeded = () => { + return ! (scope.settings.enableFFMPEGTranscoding) }; scope.hideIfNotAutoPlay = () => { return scope.settings.enableAutoPlay != true }; scope.resolutionOptions=[ - {id:"420",description:"420x420"}, - {id:"320",description:"576x320"}, - {id:"480",description:"720x480"}, - {id:"768",description:"1024x768"}, - {id:"720",description:"1280x720"}, - {id:"1080",description:"1920x1080"}, - {id:"2160",description:"3840x2160"}, - {id:"unchanged",description:"Same as source"} + {id:"420x420",description:"420x420 (1:1)"}, + {id:"576x320",description:"576x320 (18:10)"}, + {id:"640×360",description:"640×360 (nHD 16:9)"}, + {id:"720x480",description:"720x480 (WVGA 3:2)"}, + {id:"800x600",description:"800x600 (SVGA 4:3)"}, + {id:"1024x768",description:"1024x768 (WXGA 4:3)"}, + {id:"1280x720",description:"1280x720 (HD 16:9)"}, + {id:"1920x1080",description:"1920x1080 (FHD 16:9)"}, + {id:"3840x2160",description:"3840x2160 (4K 16:9)"}, ]; scope.muxDelayOptions=[ {id:"0",description:"0 Seconds"}, @@ -41,8 +42,22 @@ {id:"2",description:"2 Seconds"}, {id:"3",description:"3 Seconds"}, {id:"4",description:"4 Seconds"}, - {id:"5",description:"5 Seconds"} + {id:"5",description:"5 Seconds"}, + {id:"10",description:"10 Seconds"}, ]; + scope.errorScreens = [ + {value:"pic", description:"images/generic-error-screen.png"}, + {value:"blank", description:"Blank Screen"}, + {value:"static", description:"Static"}, + {value:"testsrc", description:"Test Pattern (color bars + timer)"}, + {value:"text", description:"Detailed error (requires ffmpeg with drawtext)"}, + {value:"kill", description:"Stop stream, show errors in logs"}, + ] + scope.errorAudios = [ + {value:"whitenoise", description:"White Noise"}, + {value:"sine", description:"Beep"}, + {value:"silent", description:"No Audio"}, + ] } } } \ No newline at end of file diff --git a/web/public/templates/ffmpeg-settings.html b/web/public/templates/ffmpeg-settings.html index 48b8dbd..e400170 100644 --- a/web/public/templates/ffmpeg-settings.html +++ b/web/public/templates/ffmpeg-settings.html @@ -31,14 +31,8 @@ -
-
- - - Note: This transcoding is done by PseudoTV, not Plex. -
+ + Transcoding is required for some features like channel overlay and measures to prevent issues when switching episodes. The trade-off is quality loss and additional computing resource requirements. + +
+
+
+ +
+ +
+
+ + + Some possible values are: + Intel Quick Sync: h264_qsv, mpeg2_qsv + NVIDIA: GPU: h264_nvenc + MPEG2: mpeg2video (default) + H264: libx264 + MacOS: h264_videotoolbox +
+
+
+ + + Some possible values are: + aac + ac3 (default), ac3_fixed + flac + libmp3lame +
+
+
+
+ + +
+ + +
+ +
+ + + Values higher than 100 will boost the audio. +
+ + +
+
+ + +
+ + If there are issues playing a video, pseudoTV will try to use an error screen as a placeholder while retrying loading the video every 60 seconds. +
+ + +
+
- - - Some possible values are: - Intel Quick Sync: h264_qsv, mpeg2_qsv - NVIDIA: GPU: h264_nvenc - MPEG2: mpeg2video (default) - H264: libx264 - MacOS: h264_videotoolbox -
-
- - -
-
- - + + + Some clients experience issues when the video stream changes resolution. This option will make pseudoTV convert all videos to the preferred resolution selected above. +
+ +
+
+
+
+
+ + +
+
+ + +
+
+ Some clients experience issues when the stream's codecs change. Enable these so that any videos with different codecs than the ones specified above are forcefully transcoded. + +
+
+ +
+
+
+
+ + + 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. + +
+
+
+ +
+
+
+
+ + + Toggling this option will disable channel overlays regardless of channel settings. + +
+
+
+
+ + +