From 9f194e62c6aa5be0dabca3cfca34526d95d796cf Mon Sep 17 00:00:00 2001 From: vexorian Date: Sat, 23 Jan 2021 13:03:53 -0400 Subject: [PATCH] #18 Allowing to play audio files in channels. They actually play now, but requires editing the channel json manually because there is no UI to import them yet. --- index.js | 4 + resources/generic-music-screen.png | Bin 0 -> 22686 bytes src/ffmpeg.js | 85 ++++++++++++++------- src/plex-player.js | 6 +- src/plexTranscoder.js | 66 ++++++++++++++--- src/svg/generic-music-screen.svg | 115 +++++++++++++++++++++++++++++ 6 files changed, 236 insertions(+), 40 deletions(-) create mode 100644 resources/generic-music-screen.png create mode 100644 src/svg/generic-music-screen.svg diff --git a/index.js b/index.js index 30fc1e4..bbab982 100644 --- a/index.js +++ b/index.js @@ -212,6 +212,10 @@ function initDB(db, channelDB) { let data = fs.readFileSync(path.resolve(path.join(__dirname, 'resources/generic-offline-screen.png'))) fs.writeFileSync(process.env.DATABASE + '/images/generic-offline-screen.png', data) } + if (!fs.existsSync(process.env.DATABASE + '/images/generic-music-screen.png')) { + let data = fs.readFileSync(path.resolve(path.join(__dirname, 'resources/generic-music-screen.png'))) + fs.writeFileSync(process.env.DATABASE + '/images/generic-music-screen.png', data) + } if (!fs.existsSync(process.env.DATABASE + '/images/loading-screen.png')) { let data = fs.readFileSync(path.resolve(path.join(__dirname, 'resources/loading-screen.png'))) fs.writeFileSync(process.env.DATABASE + '/images/loading-screen.png', data) diff --git a/resources/generic-music-screen.png b/resources/generic-music-screen.png new file mode 100644 index 0000000000000000000000000000000000000000..4efa71f9115662577d182307e077e26049a251c4 GIT binary patch literal 22686 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 F{|95gT_FGf literal 0 HcmV?d00001 diff --git a/src/ffmpeg.js b/src/ffmpeg.js index 8fcd3cd..e264cf6 100644 --- a/src/ffmpeg.js +++ b/src/ffmpeg.js @@ -111,6 +111,7 @@ class FFMPEG extends events.EventEmitter { let ffmpegArgs = [ `-threads`, isConcatPlaylist? 1 : this.opts.threads, `-fflags`, `+genpts+discardcorrupt+igndts`]; + let stillImage = false; if ( (limitRead === true) @@ -185,28 +186,57 @@ class FFMPEG extends events.EventEmitter { } // prepare input streams - if ( typeof(streamUrl.errorTitle) !== 'undefined') { + if ( ( typeof(streamUrl.errorTitle) !== 'undefined') || (streamStats.audioOnly) ) { doOverlay = false; //never show icon in the error screen // for error stream, we have to generate the input as well 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 - //match iW x iH , so with this we save ourselves a second - // scale filter - iW = this.wantedW; - iH = this.wantedH; + //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; + + if (this.audioOnly !== true) { + ffmpegArgs.push("-r" , "24"); + let pic = null; + + //does an image to play exist? + if ( + (typeof(streamUrl.errorTitle) === 'undefined') + && + (streamStats.audioOnly) + ) { + pic = streamStats.placeholderImage; + } else if ( streamUrl.errorTitle == 'offline') { + pic = `${this.channel.offlinePicture}`; + } else if ( this.opts.errorScreen == 'pic' ) { + pic = `${this.errorPicturePath}`; } - if ( this.audioOnly !== true) { - ffmpegArgs.push("-r" , "24"); - if ( streamUrl.errorTitle == 'offline' ) { + if (pic != null) { ffmpegArgs.push( - '-loop', '1', - '-i', `${this.channel.offlinePicture}`, + '-i', pic, ); - videoComplex = `;[${inputFiles++}:0]loop=loop=-1:size=1:start=0[looped];[looped]scale=${iW}:${iH}[videoy];[videoy]realtime[videox]`; + if ( + (typeof duration === 'undefined') + && + (typeof(streamStats.duration) !== 'undefined' ) + ) { + //add 150 milliseconds just in case, exact duration seems to cut out the last bits of music some times. + duration = `${streamStats.duration + 150}ms`; + } + videoComplex = `;[${inputFiles++}:0]loop=loop=-1:size=1:start=0[looped]`; + videoComplex += `;[looped]format=yuv420p[formatted]`; + let stream = "scaled"; + videoComplex +=`;[formatted]scale=w=${iW}:h=${iH}:force_original_aspect_ratio=1[scaled]`; + if (this.ensureResolution) { + stream = "padded"; + videoComplex += `;[scaled]pad=${iW}:${iH}:(ow-iw)/2:(oh-ih)/2[padded]`; + } + videoComplex +=`;[${stream}]realtime[videox]`; + stillImage = true; } else if (this.opts.errorScreen == 'static') { ffmpegArgs.push( '-f', 'lavfi', @@ -232,23 +262,17 @@ class FFMPEG extends events.EventEmitter { inputFiles++; 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') { + } else { //blank ffmpegArgs.push( '-f', 'lavfi', '-i', `color=c=black:s=${iW}x${iH}` ); inputFiles++; videoComplex = `;realtime[videox]`; - } else {//'pic' - ffmpegArgs.push( - '-loop', '1', - '-i', `${this.errorPicturePath}`, - ); - inputFiles++; - videoComplex = `;[${videoFile+1}:0]scale=${iW}:${iH}[videoy];[videoy]realtime[videox]`; } } let durstr = `duration=${streamStats.duration}ms`; + if (typeof(streamUrl.errorTitle) !== 'undefined') { //silent audioComplex = `;aevalsrc=0:${durstr}[audioy]`; if ( streamUrl.errorTitle == 'offline' ) { @@ -280,8 +304,9 @@ class FFMPEG extends events.EventEmitter { ffmpegArgs.push('-pix_fmt' , 'yuv420p' ); } audioComplex += ';[audioy]arealtime[audiox]'; - currentVideo = "[videox]"; currentAudio = "[audiox]"; + } + currentVideo = "[videox]"; } if (doOverlay) { if (watermark.animated === true) { @@ -297,9 +322,13 @@ class FFMPEG extends events.EventEmitter { let algo = this.opts.scalingAlgorithm; let resizeMsg = ""; if ( + (!streamStats.audioOnly) + && + ( (this.ensureResolution && ( streamStats.anamorphic || (iW != this.wantedW || iH != this.wantedH) ) ) || isLargerResolution(iW, iH, this.wantedW, this.wantedH) + ) ) { //scaler stuff, need to change the size of the video and also add bars // calculate wanted aspect ratio @@ -444,6 +473,9 @@ class FFMPEG extends events.EventEmitter { `-c:v`, (transcodeVideo ? this.opts.videoEncoder : 'copy'), `-sc_threshold`, `1000000000`, ); + if (stillImage) { + ffmpegArgs.push('-tune', 'stillimage'); + } } ffmpegArgs.push( '-map', currentAudio, @@ -506,14 +538,14 @@ class FFMPEG extends events.EventEmitter { `service_provider="dizqueTV"`, `-metadata`, `service_name="${this.channel.name}"`, - `-f`, `mpegts`); + ); - //t should be before output + //t should be before -f if (typeof duration !== 'undefined') { - ffmpegArgs.push(`-t`, duration) + ffmpegArgs.push(`-t`, `${duration}`); } - ffmpegArgs.push(`pipe:1`) + ffmpegArgs.push(`-f`, `mpegts`, `pipe:1`) let doLogs = this.opts.logFfmpeg && !isConcatPlaylist; if (this.hasBeenKilled) { @@ -521,6 +553,7 @@ class FFMPEG extends events.EventEmitter { } this.ffmpeg = spawn(this.ffmpegPath, ffmpegArgs, { stdio: ['ignore', 'pipe', (doLogs?process.stderr:"ignore") ] } ); if (this.hasBeenKilled) { + console.log("Send SIGKILL to ffmpeg"); this.ffmpeg.kill("SIGKILL"); return; } diff --git a/src/plex-player.js b/src/plex-player.js index 4874bbd..d21bcdc 100644 --- a/src/plex-player.js +++ b/src/plex-player.js @@ -124,11 +124,7 @@ class PlexPlayer { return emitter; } catch(err) { - if (err instanceof Error) { - throw err; - } else { - return Error("Error when playing plex program: " + JSON.stringify(err) ); - } + return Error("Error when playing plex program: " + JSON.stringify(err) ); } } } diff --git a/src/plexTranscoder.js b/src/plexTranscoder.js index 8106572..2c3fbc6 100644 --- a/src/plexTranscoder.js +++ b/src/plexTranscoder.js @@ -35,6 +35,10 @@ class PlexTranscoder { this.updateInterval = 30000 this.updatingPlex = undefined this.playState = "stopped" + this.albumArt = { + attempted : false, + path: null, + } } async getStream(deinterlace) { @@ -53,7 +57,7 @@ class PlexTranscoder { } else { try { this.log("Setting transcoding parameters") - this.setTranscodingArgs(stream.directPlay, true, deinterlace) + this.setTranscodingArgs(stream.directPlay, true, deinterlace, true) await this.getDecision(stream.directPlay); if (this.isDirectPlay()) { stream.directPlay = true; @@ -110,13 +114,14 @@ class PlexTranscoder { return stream } - setTranscodingArgs(directPlay, directStream, deinterlace) { + setTranscodingArgs(directPlay, directStream, deinterlace, firstTry) { let resolution = (directStream) ? this.settings.maxPlayableResolution : this.settings.maxTranscodeResolution let bitrate = (directStream) ? this.settings.directStreamBitrate : this.settings.transcodeBitrate let mediaBufferSize = (directStream) ? this.settings.mediaBufferSize : this.settings.transcodeMediaBufferSize let subtitles = (this.settings.enableSubtitles) ? "burn" : "none" // subtitle options: burn, none, embedded, sidecar let streamContainer = "mpegts" // Other option is mkv, mkv has the option of copying it's subs for later processing - let isDirectPlay = (directPlay) ? '1' : '0' + let isDirectPlay = (directPlay) ? '1' : (firstTry? '': '0'); + let hasMDE = '1'; let videoQuality=`100` // Not sure how this applies, maybe this works if maxVideoBitrate is not set let profileName=`Generic` // Blank profile, everything is specified through X-Plex-Client-Profile-Extra @@ -166,7 +171,7 @@ X-Plex-Token=${this.server.accessToken}&\ X-Plex-Client-Profile-Extra=${clientProfile_enc}&\ protocol=${this.settings.streamProtocol}&\ Connection=keep-alive&\ -hasMDE=1&\ +hasMDE=${hasMDE}&\ path=${this.key}&\ mediaIndex=0&\ partIndex=0&\ @@ -206,6 +211,9 @@ lang=en` isDirectPlay() { try { + if (this.getVideoStats().audioOnly) { + return this.getVideoStats().audioDecision === "copy"; + } return this.getVideoStats().videoDecision === "copy" && this.getVideoStats().audioDecision === "copy"; } catch (e) { console.log("Error at decision:" , e); @@ -217,7 +225,6 @@ lang=en` let ret = {} try { let streams = this.decisionJson.MediaContainer.Metadata[0].Media[0].Part[0].Stream - ret.duration = parseFloat( this.decisionJson.MediaContainer.Metadata[0].Media[0].Part[0].duration ); streams.forEach(function (_stream, $index) { // Video @@ -257,6 +264,14 @@ lang=en` } catch (e) { console.log("Error at decision:" , e); } + if (typeof(ret.videoCodec) === 'undefined') { + ret.audioOnly = true; + ret.placeholderImage = (this.albumArt.path != null) ? + ret.placeholderImage = this.albumArt.path + : + ret.placeholderImage = `http://localhost:${process.env.PORT}/images/generic-music-screen.png` + ; + } this.log("Current video stats:") this.log(ret) @@ -300,23 +315,56 @@ lang=en` } async getDecisionUnmanaged(directPlay) { - let res = await axios.get(`${this.server.uri}/video/:/transcode/universal/decision?${this.transcodingArgs}`, { + let url = `${this.server.uri}/video/:/transcode/universal/decision?${this.transcodingArgs}`; + let res = await axios.get(url, { headers: { Accept: 'application/json' } }) this.decisionJson = res.data; - this.log("Recieved transcode decision:") + this.log("Received transcode decision:"); this.log(res.data) // Print error message if transcode not possible // TODO: handle failure better - let transcodeDecisionCode = res.data.MediaContainer.transcodeDecisionCode - if (!(directPlay || transcodeDecisionCode == "1001")) { + let transcodeDecisionCode = res.data.MediaContainer.transcodeDecisionCode; + if ( + ( typeof(transcodeDecisionCode) === 'undefined' ) + ) { + this.decisionJson.MediaContainer.transcodeDecisionCode = 'novideo'; + console.log("Audio-only file detected"); + await this.tryToGetAlbumArt(); + } else if (!(directPlay || transcodeDecisionCode == "1001")) { console.log(`IMPORTANT: Recieved transcode decision code ${transcodeDecisionCode}! Expected code 1001.`) console.log(`Error message: '${res.data.MediaContainer.transcodeDecisionText}'`) } } + async tryToGetAlbumArt() { + try { + if(this.albumArt.attempted ) { + return; + } + this.albumArt.attempted = true; + + this.log("Try to get album art:"); + let url = `${this.server.uri}${this.key}?${this.transcodingArgs}`; + let res = await axios.get(url, { + headers: { Accept: 'application/json' } + }); + let mediaContainer = res.data.MediaContainer; + if (typeof(mediaContainer) !== 'undefined') { + for( let i = 0; i < mediaContainer.Metadata.length; i++) { + console.log("got art: " + mediaContainer.Metadata[i].thumb ); + this.albumArt.path = `${this.server.uri}${mediaContainer.Metadata[i].thumb}?${this.transcodingArgs}`; + } + } + } catch (err) { + console.error("Error when getting album art", err); + } + + + } + async getDecision(directPlay) { try { await this.getDecisionUnmanaged(directPlay); diff --git a/src/svg/generic-music-screen.svg b/src/svg/generic-music-screen.svg new file mode 100644 index 0000000..8ba273b --- /dev/null +++ b/src/svg/generic-music-screen.svg @@ -0,0 +1,115 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + +