dizquetv/src/plexTranscoder.js
Jordan Koehn ba8673e3b4 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
2020-06-20 13:26:35 -04:00

212 lines
8.3 KiB
JavaScript

const { v4: uuidv4 } = require('uuid');
const fetch = require('node-fetch');
class PlexTranscoder {
constructor(settings, lineupItem) {
this.session = uuidv4()
this.settings = settings
this.key = lineupItem.key
this.ratingKey = lineupItem.ratingKey
this.currTimeMs = lineupItem.start
this.currTimeS = this.currTimeMs / 1000
this.duration = lineupItem.duration
this.server = lineupItem.server
this.transcodingArgs = undefined
this.decisionJson = undefined
this.updateInterval = 30000
this.updatingPlex = undefined
this.playState = "stopped"
}
async getStreamUrl(deinterlace) {
// Set transcoding parameters based off direct stream params
this.setTranscodingArgs(true, deinterlace)
await this.getDecision();
const videoIsDirectStream = this.isVideoDirectStream();
// Change transcoding arguments to be the user chosen transcode parameters
if (videoIsDirectStream == false) {
this.setTranscodingArgs(false, deinterlace)
// Update transcode decision for session
await this.getDecision();
}
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 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 profileName=`Generic` // Blank profile, everything is specified through X-Plex-Client-Profile-Extra
let resolutionArr = resolution.split("x")
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) {
clientProfile+=`+add-transcode-target-audio-codec(type=videoProfile&context=streaming&protocol=${this.settings.streamProtocol}&audioCodec=${codec})`
if (codec == "mp3") {
clientProfile+=`+add-limitation(scope=videoAudioCodec&scopeName=${codec}type=upperBound&name=audio.channels&value=2)`
} else {
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) {
clientProfile+=`+add-limitation(scope=videoCodec&scopeName=*&type=notMatch&name=video.scanType&value=interlaced)`
}
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.accessToken}&\
X-Plex-Client-Profile-Extra=${clientProfile_enc}&\
protocol=${this.settings.streamProtocol}&\
Connection=keep-alive&\
hasMDE=1&\
path=${this.key}&\
mediaIndex=0&\
partIndex=0&\
fastSeek=1&\
directPlay=0&\
directStream=1&\
directStreamAudio=1&\
copyts=1&\
audioBoost=${audioBoost}&\
mediaBufferSize=${mediaBufferSize}&\
session=${this.session}&\
offset=${this.currTimeS}&\
subtitles=${subtitles}&\
subtitleSize=${this.settings.subtitleSize}&\
maxVideoBitrate=${bitrate}&\
videoQuality=${videoQuality}&\
videoResolution=${resolution}&\
lang=en`
}
isVideoDirectStream() {
return this.decisionJson["MediaContainer"]["Metadata"][0]["Media"][0]["Part"][0]["Stream"][0]["decision"] == "copy";
}
getResolutionHeight() {
return this.decisionJson["MediaContainer"]["Metadata"][0]["Media"][0]["Part"][0]["Stream"][0]["height"];
}
getVideoStats(channelIconEnabled, ffmpegEncoderName) {
let ret = []
let streams = this.decisionJson["MediaContainer"]["Metadata"][0]["Media"][0]["Part"][0]["Stream"]
streams.forEach(function (stream) {
// Video
if (stream["streamType"] == "1") {
ret.push(stream["width"],
stream["height"],
Math.round(stream["frameRate"]))
// Rounding framerate avoids scenarios where
// 29.9999999 & 30 don't match. Probably close enough
// to continue the stream as is.
// Implies future transcoding
if (channelIconEnabled == true)
if (ffmpegEncoderName.includes('mpeg2'))
ret.push("mpeg2video")
else if (ffmpegEncoderName.includes("264"))
ret.push("h264")
else if (ffmpegEncoderName.includes("hevc") || ffmpegEncoderName.includes("265"))
ret.push("hevc")
else
ret.push("unknown")
else
ret.push(stream["codec"])
}
// Audio. Only look at stream being used
if (stream["streamType"] == "2" && stream["selected"] == "1")
ret.push(stream["channels"], stream["codec"])
})
return ret
}
async getDecision() {
const response = await fetch(`${this.server.uri}/video/:/transcode/universal/decision?${this.transcodingArgs}`, {
method: 'GET', headers: {
Accept: 'application/json'
}
});
this.decisionJson = await response.json();
}
getStatusUrl() {
let profileName=`Generic`;
let containerKey=`/video/:/transcode/universal/decision?${this.transcodingArgs}`;
let containerKey_enc=encodeURIComponent(containerKey);
let statusUrl=`${this.server.uri}/:/timeline?\
containerKey=${containerKey_enc}&\
ratingKey=${this.ratingKey}&\
state=${this.playState}&\
key=${this.key}&\
time=${this.currTimeMs}&\
duration=${this.duration}&\
X-Plex-Platform=${profileName}&\
X-Plex-Client-Platform=${profileName}&\
X-Plex-Client-Profile-Name=${profileName}&\
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.accessToken}`;
return statusUrl;
}
startUpdatingPlex() {
if (this.settings.updatePlayStatus == true) {
this.playState = "playing";
this.updatePlex(); // do initial update
this.updatingPlex = setInterval(this.updatePlex.bind(this), this.updateInterval);
}
}
stopUpdatingPlex() {
if (this.settings.updatePlayStatus == true) {
clearInterval(this.updatingPlex);
this.playState = "stopped";
this.updatePlex();
}
}
updatePlex() {
let postUrl = this.getStatusUrl();
fetch(postUrl, {
method: 'POST'
});
this.currTimeMs += this.updateInterval;
if (this.currTimeMs > this.duration) {
this.currTimeMs = this.duration;
}
this.currTimeS = this.duration / 1000;
}
}
module.exports = PlexTranscoder