From ad6dcb4a33a4f7790f247027da5c82e7aa28c385 Mon Sep 17 00:00:00 2001 From: vexorian Date: Sat, 29 May 2021 16:13:01 -0400 Subject: [PATCH 1/3] #299 Hopefully fix the playback issues introduced when adding music library-support --- src/plexTranscoder.js | 90 ++++++++++++++++++++++++++++--------------- 1 file changed, 60 insertions(+), 30 deletions(-) diff --git a/src/plexTranscoder.js b/src/plexTranscoder.js index 22b0017..b4f8524 100644 --- a/src/plexTranscoder.js +++ b/src/plexTranscoder.js @@ -35,6 +35,7 @@ class PlexTranscoder { this.updateInterval = 30000 this.updatingPlex = undefined this.playState = "stopped" + this.mediaHasNoVideo = false; this.albumArt = { attempted : false, path: null, @@ -48,23 +49,26 @@ class PlexTranscoder { this.log(` deinterlace: ${deinterlace}`) this.log(` streamPath: ${this.settings.streamPath}`) + this.setTranscodingArgs(stream.directPlay, true, false, false); + await this.tryToDetectAudioOnly(); + if (this.settings.streamPath === 'direct' || this.settings.forceDirectPlay) { if (this.settings.enableSubtitles) { - console.log("Direct play is forced, so subtitles are forcibly disabled."); + this.log("Direct play is forced, so subtitles are forcibly disabled."); this.settings.enableSubtitles = false; } stream = {directPlay: true} } else { try { this.log("Setting transcoding parameters") - this.setTranscodingArgs(stream.directPlay, true, deinterlace, true) + this.setTranscodingArgs(stream.directPlay, true, deinterlace, this.mediaHasNoVideo) await this.getDecision(stream.directPlay); if (this.isDirectPlay()) { stream.directPlay = true; stream.streamUrl = this.plexFile; } } catch (err) { - this.log("Error when getting decision. 1. Check Plex connection. 2. This might also be a sign that plex direct play and transcode settings are too strict and it can't find any allowed action for the selected video.") + console.error("Error when getting decision. 1. Check Plex connection. 2. This might also be a sign that plex direct play and transcode settings are too strict and it can't find any allowed action for the selected video.", err) stream.directPlay = true; } } @@ -74,7 +78,7 @@ class PlexTranscoder { } this.log("Direct play forced or native paths enabled") stream.directPlay = true - this.setTranscodingArgs(stream.directPlay, true, false) + this.setTranscodingArgs(stream.directPlay, true, false, this.mediaHasNoVideo ) // Update transcode decision for session await this.getDecision(stream.directPlay); stream.streamUrl = (this.settings.streamPath === 'direct') ? this.file : this.plexFile; @@ -92,7 +96,7 @@ class PlexTranscoder { } else if (this.isVideoDirectStream() === false) { this.log("Decision: Should transcode") // Change transcoding arguments to be the user chosen transcode parameters - this.setTranscodingArgs(stream.directPlay, false, deinterlace) + this.setTranscodingArgs(stream.directPlay, false, deinterlace, this.mediaHasNoVideo) // Update transcode decision for session await this.getDecision(stream.directPlay); stream.streamUrl = `${this.transcodeUrlBase}${this.transcodingArgs}` @@ -114,13 +118,13 @@ class PlexTranscoder { return stream } - setTranscodingArgs(directPlay, directStream, deinterlace, firstTry) { + setTranscodingArgs(directPlay, directStream, deinterlace, audioOnly) { 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' : (firstTry? '': '0'); + let isDirectPlay = (directPlay) ? '1' : '0'; let hasMDE = '1'; let videoQuality=`100` // Not sure how this applies, maybe this works if maxVideoBitrate is not set @@ -137,12 +141,17 @@ class PlexTranscoder { vc = "av1"; } - let clientProfile=`add-transcode-target(type=videoProfile&protocol=${this.settings.streamProtocol}&container=${streamContainer}&videoCodec=${vc}&audioCodec=${this.settings.audioCodecs}&subtitleCodec=&context=streaming&replace=true)+\ + let clientProfile =""; + if (! audioOnly ) { + clientProfile=`add-transcode-target(type=videoProfile&protocol=${this.settings.streamProtocol}&container=${streamContainer}&videoCodec=${vc}&audioCodec=${this.settings.audioCodecs}&subtitleCodec=&context=streaming&replace=true)+\ add-transcode-target-settings(type=videoProfile&context=streaming&protocol=${this.settings.streamProtocol}&CopyMatroskaAttachments=true)+\ add-transcode-target-settings(type=videoProfile&context=streaming&protocol=${this.settings.streamProtocol}&BreakNonKeyframes=true)+\ add-limitation(scope=videoCodec&scopeName=*&type=upperBound&name=video.width&value=${resolutionArr[0]})+\ add-limitation(scope=videoCodec&scopeName=*&type=upperBound&name=video.height&value=${resolutionArr[1]})` - + } else { + clientProfile=`add-transcode-target(type=musicProfile&protocol=${this.settings.streamProtocol}&container=${streamContainer}&audioCodec=${this.settings.audioCodecs}&subtitleCodec=&context=streaming&replace=true)` + + } // Set transcode settings per audio codec this.settings.audioCodecs.split(",").forEach(function (codec) { clientProfile+=`+add-transcode-target-audio-codec(type=videoProfile&context=streaming&protocol=${this.settings.streamProtocol}&audioCodec=${codec})` @@ -196,7 +205,7 @@ lang=en` try { return this.getVideoStats().videoDecision === "copy"; } catch (e) { - console.log("Error at decision:", e); + console.error("Error at decision:", e); return false; } } @@ -216,7 +225,7 @@ lang=en` } return this.getVideoStats().videoDecision === "copy" && this.getVideoStats().audioDecision === "copy"; } catch (e) { - console.log("Error at decision:" , e); + console.error("Error at decision:" , e); return false; } } @@ -262,7 +271,7 @@ lang=en` } }.bind(this) ) } catch (e) { - console.log("Error at decision:" , e); + console.error("Error at decision:" , e); } if (typeof(ret.videoCodec) === 'undefined') { ret.audioOnly = true; @@ -297,11 +306,11 @@ lang=en` } }) } catch (e) { - console.log("Error at get media info:" + e); + console.error("Error at get media info:" + e); } }) .catch((err) => { - console.log(err); + console.error("Error getting audio index",err); }); this.log(`Found audio index: ${index}`) @@ -326,36 +335,42 @@ lang=en` // Print error message if transcode not possible // TODO: handle failure better + if (res.data.MediaContainer.mdeDecisionCode === 1000) { + this.log("mde decision code 1000, so it's all right?"); + return; + } + let transcodeDecisionCode = res.data.MediaContainer.transcodeDecisionCode; if ( ( typeof(transcodeDecisionCode) === 'undefined' ) ) { this.decisionJson.MediaContainer.transcodeDecisionCode = 'novideo'; - console.log("Audio-only file detected"); - await this.tryToGetAlbumArt(); + this.log("Strange case, attempt direct play"); } else if (!(directPlay || transcodeDecisionCode == "1001")) { - console.log(`IMPORTANT: Recieved transcode decision code ${transcodeDecisionCode}! Expected code 1001.`) - console.log(`Error message: '${res.data.MediaContainer.transcodeDecisionText}'`) + this.log(`IMPORTANT: Recieved transcode decision code ${transcodeDecisionCode}! Expected code 1001.`) + this.log(`Error message: '${res.data.MediaContainer.transcodeDecisionText}'`) } } - - async tryToGetAlbumArt() { + + async tryToDetectAudioOnly() { try { - if(this.albumArt.attempted ) { - return; - } - this.albumArt.attempted = true; - - this.log("Try to get album art:"); + this.log("Try to detect audio only:"); 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}?X-Plex-Token=${this.server.accessToken}`; + let metadata = getOneOrUndefined( mediaContainer, "Metadata"); + if (typeof(metadata) !== 'undefined') { + this.albumArt.path = `${this.server.uri}${metadata.thumb}?X-Plex-Token=${this.server.accessToken}`; + + let media = getOneOrUndefined( metadata, "Media"); + if (typeof(media) !== 'undefined') { + if (typeof(media.videoCodec)==='undefined') { + this.log("Audio-only file detected"); + this.mediaHasNoVideo = true; + } } } } catch (err) { @@ -447,4 +462,19 @@ function parsePixelAspectRatio(s) { q: parseInt(x[1], 10), } } + +function getOneOrUndefined(object, field) { + if (typeof(object) === 'undefined') { + return undefined; + } + if ( typeof(object[field]) === "undefined") { + return undefined; + } + let x = object[field]; + if (x.length < 1) { + return undefined; + } + return x[0]; +} + module.exports = PlexTranscoder From df382f26f728465af710672f012b7e361daf755e Mon Sep 17 00:00:00 2001 From: vexorian Date: Sat, 29 May 2021 16:53:25 -0400 Subject: [PATCH 2/3] TV Guide will be updater much quicker than before. Thanks to using setIPmmediate instead of 0-seconds timer. --- src/tv-guide-service.js | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/tv-guide-service.js b/src/tv-guide-service.js index d64da30..84027be 100644 --- a/src/tv-guide-service.js +++ b/src/tv-guide-service.js @@ -14,8 +14,6 @@ class TVGuideService this.currentUpdate = -1; this.currentLimit = -1; this.currentChannels = null; - this.throttleX = 0; - this.doThrottle = false; this.xmltv = xmltv; this.db = db; this.cacheImageService = cacheImageService; @@ -26,7 +24,7 @@ class TVGuideService while (this.cached == null) { await _wait(100); } - this.doThrottle = true; + return this.cached; } @@ -357,11 +355,10 @@ class TVGuideService } } - async _throttle() { - //this.doThrottle = true; - if ( this.doThrottle && (this.throttleX++)%10 == 0) { - await _wait(0); - } + _throttle() { + return new Promise((resolve) => { + setImmediate(() => resolve()); + }); } async refreshXML() { From f134a75e9822acf64b16c97aafd339b7a0b38aef Mon Sep 17 00:00:00 2001 From: vexorian Date: Sun, 30 May 2021 07:25:24 -0400 Subject: [PATCH 3/3] Fix #316 . Opening a large channel in the UI causes playback to freeze for a second. --- package.json | 1 + src/api.js | 68 +++++++++++++++++++++++++++++++++++-- web/controllers/channels.js | 7 +++- web/services/dizquetv.js | 7 ++++ 4 files changed, 80 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index d6f96d5..5990b96 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "author": "Dan Ferguson", "license": "ISC", "dependencies": { + "JSONStream": "1.0.5", "angular": "^1.7.9", "angular-router-browserify": "0.0.2", "angular-vs-repeat": "2.0.13", diff --git a/src/api.js b/src/api.js index ef2dd40..a00501a 100644 --- a/src/api.js +++ b/src/api.js @@ -5,6 +5,7 @@ const fs = require('fs') const databaseMigration = require('./database-migration'); const channelCache = require('./channel-cache') const constants = require('./constants'); +const JSONStream = require('JSONStream'); const FFMPEGInfo = require('./ffmpeg-info'); const PlexServerDB = require('./dao/plex-server-db'); const Plex = require("./plex.js"); @@ -232,9 +233,10 @@ function api(db, channelDB, fillerDB, customShowDB, xmltvInterval, guideService try { let number = parseInt(req.params.number, 10); let channel = await channelCache.getChannelConfig(channelDB, number); + if (channel.length == 1) { - channel = channel[0]; - res.send( channel ); + channel = channel[0]; + res.send(channel); } else { return res.status(404).send("Channel not found"); } @@ -243,6 +245,61 @@ function api(db, channelDB, fillerDB, customShowDB, xmltvInterval, guideService res.status(500).send("error"); } }) + router.get('/api/channel/programless/:number', async (req, res) => { + try { + let number = parseInt(req.params.number, 10); + let channel = await channelCache.getChannelConfig(channelDB, number); + + if (channel.length == 1) { + channel = channel[0]; + let copy = {}; + Object.keys(channel).forEach( (key) => { + if (key != 'programs') { + copy[key] = channel[key]; + } + } ); + res.send(copy); + } else { + return res.status(404).send("Channel not found"); + } + } catch(err) { + console.error(err); + res.status(500).send("error"); + } + }) + + router.get('/api/channel/programs/:number', async (req, res) => { + try { + let number = parseInt(req.params.number, 10); + let channel = await channelCache.getChannelConfig(channelDB, number); + + if (channel.length == 1) { + channel = channel[0]; + let programs = channel.programs; + if (typeof(programs) === 'undefined') { + return res.status(404).send("Channel doesn't have programs?"); + } + res.writeHead(200, { + 'Content-Type': 'application.json' + }); + + let transformStream = JSONStream.stringify(); //false makes it not add 'separators' + transformStream.pipe(res); + + for (let i = 0; i < programs.length; i++) { + transformStream.write( programs[i] ); + await throttle(); + } + transformStream.end(); + + } else { + return res.status(404).send("Channel not found"); + } + } catch(err) { + console.error(err); + res.status(500).send("error"); + } + }) router.get('/api/channel/description/:number', async (req, res) => { try { let number = parseInt(req.params.number, 10); @@ -1014,3 +1071,10 @@ function api(db, channelDB, fillerDB, customShowDB, xmltvInterval, guideService return router } + + +async function throttle() { + return new Promise((resolve) => { + setImmediate(() => resolve()); + }); +} diff --git a/web/controllers/channels.js b/web/controllers/channels.js index c9fa27f..1cc861d 100644 --- a/web/controllers/channels.js +++ b/web/controllers/channels.js @@ -80,7 +80,12 @@ module.exports = function ($scope, dizquetv) { $scope.showChannelConfig = true } else { $scope.channels[index].pending = true; - let ch = await dizquetv.getChannel($scope.channels[index].number); + let p = await Promise.all([ + dizquetv.getChannelProgramless($scope.channels[index].number), + dizquetv.getChannelPrograms($scope.channels[index].number), + ]); + let ch = p[0]; + ch.programs = p[1]; let newObj = ch; newObj.startTime = new Date(newObj.startTime) $scope.originalChannelNumber = newObj.number; diff --git a/web/services/dizquetv.js b/web/services/dizquetv.js index 2ad247e..4e8b8f6 100644 --- a/web/services/dizquetv.js +++ b/web/services/dizquetv.js @@ -137,6 +137,13 @@ module.exports = function ($http, $q) { return $http.get(`/api/channel/description/${number}`).then( (d) => { return d.data } ) }, + getChannelProgramless: (number) => { + return $http.get(`/api/channel/programless/${number}`).then( (d) => { return d.data }) + }, + getChannelPrograms: (number) => { + return $http.get(`/api/channel/programs/${number}`).then( (d) => { return d.data } ) + }, + getChannelNumbers: () => { return $http.get('/api/channelNumbers').then( (d) => { return d.data } )