From ba8673e3b4cf04cc114502b098bac5ef076d779b Mon Sep 17 00:00:00 2001 From: Jordan Koehn Date: Mon, 1 Jun 2020 22:00:08 +0000 Subject: [PATCH] Improvements. Sign in/Urls: Sign in now adds all servers; however refresh guide/refresh channels must be edited through the json. Now uses local/remote server https url instead of local 34440 port url with no cert. Video Playback: Remove code not enforcing time limit for streams. Make default stream protocol http instread of hls which was used previously. Add option to choose. Add option to specify video codecs (which is prone to user error). Added mpeg2video to default video codecs. Add option to specify direct stream/transcode media buffer size. Not sure how much of a difference this makes. Add in safeguard to ffmpeg's kill so failed streams don't crash the application M3Us: Add group-title="PseudoTV" for easier management in xteve --- index.js | 7 ++- src/api.js | 11 ++-- src/ffmpeg.js | 15 ++--- src/plex.js | 23 ++++---- src/plexTranscoder.js | 34 +++++------ src/video.js | 9 +-- web/directives/plex-settings.js | 40 +++++-------- web/public/templates/plex-settings.html | 75 ++++++++---------------- web/services/plex.js | 77 +++++++++++++++---------- 9 files changed, 135 insertions(+), 156 deletions(-) diff --git a/index.js b/index.js index fd9d6e4..a0b8d35 100644 --- a/index.js +++ b/index.js @@ -123,14 +123,17 @@ function initDB(db) { db['plex-settings'].save({ directStreamBitrate: '40000', transcodeBitrate: '3000', + mediaBufferSize: 1000, + transcodeMediaBufferSize: 20000, maxPlayableResolution: "1920x1080", maxTranscodeResolution: "1920x1080", - enableHEVC: true, + videoCodecs: 'h264,hevc,mpeg2video', audioCodecs: 'ac3,aac,mp3', maxAudioChannels: '6', enableSubtitles: false, subtitleSize: '100', - updatePlayStatus: false + updatePlayStatus: false, + streamProtocol: 'http' }) } diff --git a/src/api.js b/src/api.js index 230318b..efba488 100644 --- a/src/api.js +++ b/src/api.js @@ -92,14 +92,17 @@ function api(db, xmltvInterval) { db['plex-settings'].update({ _id: req.body._id }, { directStreamBitrate: '40000', transcodeBitrate: '3000', + mediaBufferSize: 1000, + transcodeMediaBufferSize: 20000, maxPlayableResolution: "1920x1080", maxTranscodeResolution: "1920x1080", - enableHEVC: true, + videoCodecs: 'h264,hevc,mpeg2video', audioCodecs: 'ac3,aac,mp3', maxAudioChannels: '6', enableSubtitles: false, subtitleSize: '100', - updatePlayStatus: false + updatePlayStatus: false, + streamProtocol: 'http' }) let plex = db['plex-settings'].find()[0] res.send(plex) @@ -167,11 +170,11 @@ function api(db, xmltvInterval) { let channels = db['channels'].find() var data = "#EXTM3U\n" for (var i = 0; i < channels.length; i++) { - data += `#EXTINF:0 tvg-id="${channels[i].number}" tvg-name="${channels[i].name}" tvg-logo="${channels[i].icon}",${channels[i].name}\n` + data += `#EXTINF:0 tvg-id="${channels[i].number}" tvg-name="${channels[i].name}" tvg-logo="${channels[i].icon}" group-title="PseudoTV",${channels[i].name}\n` data += `${req.protocol}://${req.get('host')}/video?channel=${channels[i].number}\n` } if (channels.length === 0) { - data += `#EXTINF:0 tvg-id="1" tvg-name="PseudoTV" tvg-logo="https://raw.githubusercontent.com/DEFENDORe/pseudotv/master/resources/pseudotv.png",PseudoTV\n` + data += `#EXTINF:0 tvg-id="1" tvg-name="PseudoTV" tvg-logo="https://raw.githubusercontent.com/DEFENDORe/pseudotv/master/resources/pseudotv.png" group-title="PseudoTV",PseudoTV\n` data += `${req.protocol}://${req.get('host')}/setup\n` } res.send(data) diff --git a/src/ffmpeg.js b/src/ffmpeg.js index 12fc48f..e99416f 100644 --- a/src/ffmpeg.js +++ b/src/ffmpeg.js @@ -10,12 +10,11 @@ class FFMPEG extends events.EventEmitter { this.ffmpegPath = opts.ffmpegPath } async spawn(streamUrl, duration, enableIcon, videoResolution) { - let ffmpegArgs = [`-threads`, this.opts.threads]; - - if (duration > 0) - ffmpegArgs.push(`-t`, duration); - - ffmpegArgs.push(`-re`, `-i`, streamUrl); + let ffmpegArgs = [`-threads`, this.opts.threads, + `-t`, duration, + `-re`, + `-fflags`, `+genpts`, + `-i`, streamUrl]; if (enableIcon == true) { if (process.env.DEBUG) console.log('Channel Icon Overlay Enabled') @@ -83,7 +82,9 @@ class FFMPEG extends events.EventEmitter { }) } kill() { - this.ffmpeg.kill() + if (typeof this.ffmpeg != "undefined") { + this.ffmpeg.kill('SIGQUIT') + } } } diff --git a/src/plex.js b/src/plex.js index e9f0fdd..de00333 100644 --- a/src/plex.js +++ b/src/plex.js @@ -1,8 +1,9 @@ const request = require('request') class Plex { constructor(opts) { - this._token = typeof opts.token !== 'undefined' ? opts.token : '' + this._accessToken = typeof opts.accessToken !== 'undefined' ? opts.accessToken : '' this._server = { + uri: typeof opts.uri !== 'undefined' ? opts.uri : 'http://127.0.0.1:32400', host: typeof opts.host !== 'undefined' ? opts.host : '127.0.0.1', port: typeof opts.port !== 'undefined' ? opts.port : '32400', protocol: typeof opts.protocol !== 'undefined' ? opts.protocol : 'http' @@ -19,7 +20,7 @@ class Plex { } } - get URL() { return `${this._server.protocol}://${this._server.host}:${this._server.port}` } + get URL() { return `${this._server.uri}` } SignIn(username, password) { return new Promise((resolve, reject) => { @@ -41,8 +42,8 @@ class Plex { if (err || res.statusCode !== 201) reject("Plex 'SignIn' Error - Username/Email and Password is incorrect!.") else { - this._token = JSON.parse(body).user.authToken - resolve({ token: this._token }) + this._accessToken = JSON.parse(body).user.authToken + resolve({ accessToken: this._accessToken }) } }) }) @@ -55,9 +56,9 @@ class Plex { jar: false } Object.assign(req, optionalHeaders) - req.headers['X-Plex-Token'] = this._token + req.headers['X-Plex-Token'] = this._accessToken return new Promise((resolve, reject) => { - if (this._token === '') + if (this._accessToken === '') reject("No Plex token provided. Please use the SignIn method or provide a X-Plex-Token in the Plex constructor.") else request(req, (err, res) => { @@ -77,9 +78,9 @@ class Plex { jar: false } Object.assign(req, optionalHeaders) - req.headers['X-Plex-Token'] = this._token + req.headers['X-Plex-Token'] = this._accessToken return new Promise((resolve, reject) => { - if (this._token === '') + if (this._accessToken === '') reject("No Plex token provided. Please use the SignIn method or provide a X-Plex-Token in the Plex constructor.") else request(req, (err, res) => { @@ -99,9 +100,9 @@ class Plex { jar: false } Object.assign(req, optionalHeaders) - req.headers['X-Plex-Token'] = this._token + req.headers['X-Plex-Token'] = this._accessToken return new Promise((resolve, reject) => { - if (this._token === '') + if (this._accessToken === '') reject("No Plex token provided. Please use the SignIn method or provide a X-Plex-Token in the Plex constructor.") else request(req, (err, res) => { @@ -140,4 +141,4 @@ class Plex { } } -module.exports = Plex \ No newline at end of file +module.exports = Plex diff --git a/src/plexTranscoder.js b/src/plexTranscoder.js index 83615cf..21b00f1 100644 --- a/src/plexTranscoder.js +++ b/src/plexTranscoder.js @@ -36,50 +36,50 @@ class PlexTranscoder { await this.getDecision(); } - return `${this.server.protocol}://${this.server.host}:${this.server.port}/video/:/transcode/universal/start.m3u8?${this.transcodingArgs}` + return `${this.server.uri}/video/:/transcode/universal/start.m3u8?${this.transcodingArgs}` } setTranscodingArgs(directStream, deinterlace) { let resolution = (directStream == true) ? this.settings.maxPlayableResolution : this.settings.maxTranscodeResolution let bitrate = (directStream == true) ? this.settings.directStreamBitrate : this.settings.transcodeBitrate - let videoCodecs = (this.settings.enableHEVC == true) ? "h264,hevc" : "h264" + let mediaBufferSize = (directStream == true) ? this.settings.mediaBufferSize : this.settings.transcodeMediaBufferSize let subtitles = (this.settings.enableSubtitles == true) ? "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 videoQuality=`100` // Not sure how this applies, maybe this works if maxVideoBitrate is not set let audioBoost=`100` // only applies when downmixing to stereo I believe, add option later? - let mediaBufferSize=`30720` // Not sure what this should be set to let profileName=`Generic` // Blank profile, everything is specified through X-Plex-Client-Profile-Extra let resolutionArr = resolution.split("x") - let clientProfileHLS=`add-transcode-target(type=videoProfile&protocol=hls&container=mpegts&videoCodec=${videoCodecs}&audioCodec=${this.settings.audioCodecs}&subtitleCodec=&context=streaming&replace=true)+\ -add-transcode-target-settings(type=videoProfile&context=streaming&protocol=hls&CopyMatroskaAttachments=true)+\ + let clientProfile=`add-transcode-target(type=videoProfile&protocol=${this.settings.streamProtocol}&container=${streamContainer}&videoCodec=${this.settings.videoCodecs}&audioCodec=${this.settings.audioCodecs}&subtitleCodec=&context=streaming&replace=true)+\ +add-transcode-target-settings(type=videoProfile&context=streaming&protocol=${this.settings.streamProtocol}&CopyMatroskaAttachments=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]})` // Set transcode settings per audio codec this.settings.audioCodecs.split(",").forEach(function (codec) { - clientProfileHLS+=`+add-transcode-target-audio-codec(type=videoProfile&context=streaming&protocol=hls&audioCodec=${codec})` + clientProfile+=`+add-transcode-target-audio-codec(type=videoProfile&context=streaming&protocol=${this.settings.streamProtocol}&audioCodec=${codec})` if (codec == "mp3") { - clientProfileHLS+=`+add-limitation(scope=videoAudioCodec&scopeName=${codec}type=upperBound&name=audio.channels&value=2)` + clientProfile+=`+add-limitation(scope=videoAudioCodec&scopeName=${codec}type=upperBound&name=audio.channels&value=2)` } else { - clientProfileHLS+=`+add-limitation(scope=videoAudioCodec&scopeName=${codec}type=upperBound&name=audio.channels&value=${this.settings.maxAudioChannels})` + clientProfile+=`+add-limitation(scope=videoAudioCodec&scopeName=${codec}type=upperBound&name=audio.channels&value=${this.settings.maxAudioChannels})` } }.bind(this)); // deinterlace video if specified, only useful if overlaying channel logo later if (deinterlace == true) { - clientProfileHLS+=`+add-limitation(scope=videoCodec&scopeName=*&type=notMatch&name=video.scanType&value=interlaced)` + clientProfile+=`+add-limitation(scope=videoCodec&scopeName=*&type=notMatch&name=video.scanType&value=interlaced)` } - let clientProfileHLS_enc=encodeURIComponent(clientProfileHLS) + let clientProfile_enc=encodeURIComponent(clientProfile) this.transcodingArgs=`X-Plex-Platform=${profileName}&\ X-Plex-Client-Platform=${profileName}&\ X-Plex-Client-Profile-Name=${profileName}&\ X-Plex-Platform=${profileName}&\ -X-Plex-Token=${this.server.token}&\ -X-Plex-Client-Profile-Extra=${clientProfileHLS_enc}&\ -protocol=hls&\ +X-Plex-Token=${this.server.accessToken}&\ +X-Plex-Client-Profile-Extra=${clientProfile_enc}&\ +protocol=${this.settings.streamProtocol}&\ Connection=keep-alive&\ hasMDE=1&\ path=${this.key}&\ @@ -146,7 +146,7 @@ lang=en` } async getDecision() { - const response = await fetch(`${this.server.protocol}://${this.server.host}:${this.server.port}/video/:/transcode/universal/decision?${this.transcodingArgs}`, { + const response = await fetch(`${this.server.uri}/video/:/transcode/universal/decision?${this.transcodingArgs}`, { method: 'GET', headers: { Accept: 'application/json' } @@ -160,7 +160,7 @@ lang=en` let containerKey=`/video/:/transcode/universal/decision?${this.transcodingArgs}`; let containerKey_enc=encodeURIComponent(containerKey); - let statusUrl=`${this.server.protocol}://${this.server.host}:${this.server.port}/:/timeline?\ + let statusUrl=`${this.server.uri}/:/timeline?\ containerKey=${containerKey_enc}&\ ratingKey=${this.ratingKey}&\ state=${this.playState}&\ @@ -174,7 +174,7 @@ X-Plex-Device-Name=PseudoTV-Plex&\ X-Plex-Device=PseudoTV-Plex&\ X-Plex-Client-Identifier=${this.session}&\ X-Plex-Platform=${profileName}&\ -X-Plex-Token=${this.server.token}`; +X-Plex-Token=${this.server.accessToken}`; return statusUrl; } diff --git a/src/video.js b/src/video.js index d0a6bd0..ff36ae6 100644 --- a/src/video.js +++ b/src/video.js @@ -103,9 +103,6 @@ function video(db) { deinterlace = enableChannelIcon 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 = -1 plexTranscoder = new PlexTranscoder(plexSettings, lineupItem); @@ -140,11 +137,7 @@ function video(db) { }) 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 = -1 - + plexTranscoder.getStreamUrl(deinterlace).then(streamUrl => ffmpeg.spawn(streamUrl, streamDuration, enableChannelIcon, plexTranscoder.getResolutionHeight())); // Spawn the ffmpeg process, fire this bitch up plexTranscoder.startUpdatingPlex(); }) diff --git a/web/directives/plex-settings.js b/web/directives/plex-settings.js index f4c8fcf..17e9f01 100644 --- a/web/directives/plex-settings.js +++ b/web/directives/plex-settings.js @@ -8,34 +8,21 @@ module.exports = function (plex, pseudotv, $timeout) { pseudotv.getPlexServers().then((servers) => { scope.servers = servers }) - scope.plex = { protocol: 'http', host: '', port: '32400', arGuide: false, arChannels: false } - scope.addPlexServer = function (p) { + scope.addPlexServer = function () { scope.isProcessing = true - if (scope.plex.host === '') { - scope.isProcessing = false - scope.error = 'Invalid HOST set' - $timeout(() => { - scope.error = null - }, 3500) - return - } else if (scope.plex.port <= 0) { - scope.isProcessing = false - scope.error = 'Invalid PORT set' - $timeout(() => { - scope.error = null - }, 3500) - return - } - plex.login(p) + plex.login() .then((result) => { - p.token = result.token - p.name = result.name - return pseudotv.addPlexServer(p) + result.servers.forEach((server) => { + // add in additional settings + server.arGuide = true + server.arChannels = false // should not be enabled unless PseudoTV tuner already added to plex + pseudotv.addPlexServer(server) + }); + return pseudotv.getPlexServers() }).then((servers) => { scope.$apply(() => { scope.servers = servers scope.isProcessing = false - scope.visible = false }) }, (err) => { scope.$apply(() => { @@ -53,9 +40,6 @@ module.exports = function (plex, pseudotv, $timeout) { scope.servers = servers }) } - scope.toggleVisiblity = function () { - scope.visible = !scope.visible - } pseudotv.getPlexSettings().then((settings) => { scope.settings = settings }) @@ -88,6 +72,10 @@ module.exports = function (plex, pseudotv, $timeout) { {id:"1920x1080",description:"1920x1080"}, {id:"3840x2160",description:"3840x2160"} ]; + scope.streamProtocols=[ + {id:"http",description:"HTTP"}, + {id:"hls",description:"HLS"} + ]; } }; -} \ No newline at end of file +} diff --git a/web/public/templates/plex-settings.html b/web/public/templates/plex-settings.html index 36e4b4d..879b119 100644 --- a/web/public/templates/plex-settings.html +++ b/web/public/templates/plex-settings.html @@ -1,54 +1,16 @@
Plex Settings
-
Plex Servers -
-
-
-
Add a Plex Server - {{error}} +
+
+
{{ isProcessing ? 'You have 2 minutes to sign into your Plex Account.' : ''}}
-
-
- -
-
- -
-
- -
-
-
-
-
- - -
-
-
-
- - -
-
-
- - - - -
-
- -

- WARNING - Do not check "Auto Map Channels" unless the PseudoTV tuner is added to this specific Plex server. -

+
@@ -65,7 +27,7 @@ - +
{{ x.name }}{{ x.protocol }}://{{ x.host }}:{{ x.port }}{{ x.uri }} {{ x.arGuide }} {{ x.arChannels }} @@ -90,6 +52,10 @@
Video Options
+
+ + +
- -
Audio Options
@@ -130,11 +92,24 @@
+
+ + +
+
+ + +
Note: This affects the "on deck" for your plex account.
+
+ +