add plex options to pick between plex paths and direct paths. Change ffmpeg concat to always read in at fastest rate possible. /stream endpoint will now always spawn a seperate ffmpeg process now, no longer redirects sometimes. Plex transcoder has ability to update play status regardless of direct/plex paths. Add debug logging.
This commit is contained in:
parent
4019feba01
commit
52052bf91d
11
index.js
11
index.js
@ -120,6 +120,8 @@ function initDB(db) {
|
||||
|
||||
if (plexSettings.length === 0) {
|
||||
db['plex-settings'].save({
|
||||
streamPath: 'plex',
|
||||
debugLogging: true,
|
||||
directStreamBitrate: '40000',
|
||||
transcodeBitrate: '3000',
|
||||
mediaBufferSize: 1000,
|
||||
@ -127,13 +129,16 @@ function initDB(db) {
|
||||
maxPlayableResolution: "1920x1080",
|
||||
maxTranscodeResolution: "1920x1080",
|
||||
videoCodecs: 'h264,hevc,mpeg2video',
|
||||
audioCodecs: 'ac3,aac,mp3',
|
||||
maxAudioChannels: '6',
|
||||
audioCodecs: 'ac3',
|
||||
maxAudioChannels: '2',
|
||||
audioBoost: '100',
|
||||
enableSubtitles: false,
|
||||
subtitleSize: '100',
|
||||
updatePlayStatus: false,
|
||||
streamProtocol: 'http'
|
||||
streamProtocol: 'http',
|
||||
forceDirectPlay: false,
|
||||
pathReplace: '',
|
||||
pathReplaceWith: ''
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
11
src/api.js
11
src/api.js
@ -89,6 +89,8 @@ function api(db, xmltvInterval) {
|
||||
})
|
||||
router.post('/api/plex-settings', (req, res) => { // RESET
|
||||
db['plex-settings'].update({ _id: req.body._id }, {
|
||||
streamPath: 'plex',
|
||||
debugLogging: true,
|
||||
directStreamBitrate: '40000',
|
||||
transcodeBitrate: '3000',
|
||||
mediaBufferSize: 1000,
|
||||
@ -96,13 +98,16 @@ function api(db, xmltvInterval) {
|
||||
maxPlayableResolution: "1920x1080",
|
||||
maxTranscodeResolution: "1920x1080",
|
||||
videoCodecs: 'h264,hevc,mpeg2video',
|
||||
audioCodecs: 'ac3,aac,mp3',
|
||||
maxAudioChannels: '6',
|
||||
audioCodecs: 'ac3',
|
||||
maxAudioChannels: '2',
|
||||
audioBoost: '100',
|
||||
enableSubtitles: false,
|
||||
subtitleSize: '100',
|
||||
updatePlayStatus: false,
|
||||
streamProtocol: 'http'
|
||||
streamProtocol: 'http',
|
||||
forceDirectPlay: false,
|
||||
pathReplace: '',
|
||||
pathReplaceWith: ''
|
||||
})
|
||||
let plex = db['plex-settings'].find()[0]
|
||||
res.send(plex)
|
||||
|
||||
@ -9,13 +9,24 @@ class FFMPEG extends events.EventEmitter {
|
||||
this.channel = channel
|
||||
this.ffmpegPath = opts.ffmpegPath
|
||||
}
|
||||
async spawn(streamUrl, streamStats, duration, enableIcon, type, isConcatPlaylist) {
|
||||
async spawnConcat(streamUrl) {
|
||||
this.spawn(streamUrl, undefined, undefined, undefined, false, false, undefined, true)
|
||||
}
|
||||
async spawnStream(streamUrl, streamStats, startTime, duration, enableIcon, type) {
|
||||
this.spawn(streamUrl, streamStats, startTime, duration, true, enableIcon, type, false)
|
||||
}
|
||||
async spawn(streamUrl, streamStats, startTime, duration, limitRead, enableIcon, type, isConcatPlaylist) {
|
||||
let ffmpegArgs = [`-threads`, this.opts.threads,
|
||||
`-re`,
|
||||
`-fflags`, `+genpts+discardcorrupt+igndts`];
|
||||
|
||||
if (duration > 0)
|
||||
if (limitRead === true)
|
||||
ffmpegArgs.push(`-re`)
|
||||
|
||||
if (typeof duration !== 'undefined')
|
||||
ffmpegArgs.push(`-t`, duration)
|
||||
|
||||
if (typeof startTime !== 'undefined')
|
||||
ffmpegArgs.push(`-ss`, startTime)
|
||||
|
||||
if (isConcatPlaylist == true)
|
||||
ffmpegArgs.push(`-f`, `concat`,
|
||||
|
||||
@ -50,6 +50,8 @@ function createLineup(obj) {
|
||||
lineup.push({
|
||||
type: 'commercial',
|
||||
key: commercials[i][y].key,
|
||||
plexFile: commercials[i][y].plexFile,
|
||||
file: commercials[i][y].file,
|
||||
ratingKey: commercials[i][y].ratingKey,
|
||||
start: timeElapsed, // start time will be the time elapsed, cause this is the first video
|
||||
streamDuration: commercials[i][y].duration - timeElapsed, // stream duration set accordingly
|
||||
@ -60,6 +62,8 @@ function createLineup(obj) {
|
||||
lineup.push({ // just add the video, starting at 0, playing the entire duration
|
||||
type: 'commercial',
|
||||
key: commercials[i][y].key,
|
||||
plexFile: commercials[i][y].plexFile,
|
||||
file: commercials[i][y].file,
|
||||
ratingKey: commercials[i][y].ratingKey,
|
||||
start: 0,
|
||||
streamDuration: commercials[i][y].actualDuration,
|
||||
@ -76,6 +80,8 @@ function createLineup(obj) {
|
||||
lineup.push({
|
||||
type: 'program',
|
||||
key: activeProgram.key,
|
||||
plexFile: activeProgram.plexFile,
|
||||
file: activeProgram.file,
|
||||
ratingKey: activeProgram.ratingKey,
|
||||
start: progTimeElapsed + timeElapsed, // add the duration of already played program chunks to the timeElapsed
|
||||
streamDuration: (programStartTimes[i + 1] - programStartTimes[i]) - timeElapsed,
|
||||
@ -89,6 +95,8 @@ function createLineup(obj) {
|
||||
lineup.push({
|
||||
type: 'program',
|
||||
key: activeProgram.key,
|
||||
plexFile: activeProgram.plexFile,
|
||||
file: activeProgram.file,
|
||||
ratingKey: activeProgram.ratingKey,
|
||||
start: programStartTimes[i],
|
||||
streamDuration: (programStartTimes[i + 1] - programStartTimes[i]),
|
||||
@ -109,4 +117,4 @@ function isChannelIconEnabled(enableChannelOverlay, icon, overlayIcon, type) {
|
||||
if (typeof type === `undefined`)
|
||||
return enableChannelOverlay == true && icon !== '' && overlayIcon
|
||||
return enableChannelOverlay == true && icon !== '' && overlayIcon
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,7 +7,13 @@ class PlexTranscoder {
|
||||
|
||||
this.settings = settings
|
||||
|
||||
this.log("Plex transcoder initiated")
|
||||
this.log("Debug logging enabled")
|
||||
|
||||
this.key = lineupItem.key
|
||||
this.plexFile = `${lineupItem.server.uri}${lineupItem.plexFile}?X-Plex-Token=${lineupItem.server.accessToken}`
|
||||
this.file = lineupItem.file.replace(settings.pathReplace, settings.pathReplaceWith)
|
||||
this.transcodeUrlBase = `${lineupItem.server.uri}/video/:/transcode/universal/start.m3u8?`
|
||||
this.ratingKey = lineupItem.ratingKey
|
||||
this.currTimeMs = lineupItem.start
|
||||
this.currTimeS = this.currTimeMs / 1000
|
||||
@ -23,35 +29,61 @@ class PlexTranscoder {
|
||||
}
|
||||
|
||||
async getStream(deinterlace) {
|
||||
let stream = {}
|
||||
stream.streamUrl = await this.getStreamUrl(deinterlace);
|
||||
stream.streamStats = this.getVideoStats();
|
||||
return stream;
|
||||
}
|
||||
let stream = {directPlay: false}
|
||||
|
||||
async getStreamUrl(deinterlace) {
|
||||
// Set transcoding parameters based off direct stream params
|
||||
this.setTranscodingArgs(true, deinterlace)
|
||||
this.log("Getting stream")
|
||||
this.log(` deinterlace: ${deinterlace}`)
|
||||
this.log(` streamPath: ${this.settings.streamPath}`)
|
||||
this.log(` forceDirectPlay: ${this.settings.forceDirectPlay}`)
|
||||
|
||||
await this.getDecision();
|
||||
const videoIsDirectStream = this.isVideoDirectStream();
|
||||
// direct play forced
|
||||
if (this.settings.streamPath === 'direct' || this.settings.forceDirectPlay) {
|
||||
this.log("Direct play forced or native paths enabled")
|
||||
stream.directPlay = true
|
||||
this.setTranscodingArgs(stream.directPlay, true, false)
|
||||
// Update transcode decision for session
|
||||
await this.getDecision(stream.directPlay);
|
||||
stream.streamUrl = (this.settings.streamPath === 'direct') ? this.file : this.plexFile;
|
||||
} else { // Set transcoding parameters based off direct stream params
|
||||
this.log("Setting transcoding parameters")
|
||||
this.setTranscodingArgs(stream.directPlay, true, deinterlace)
|
||||
|
||||
// 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();
|
||||
await this.getDecision(stream.directPlay);
|
||||
|
||||
if (this.isDirectPlay()) {
|
||||
this.log("Decision: File can direct play")
|
||||
stream.directPlay = true
|
||||
this.setTranscodingArgs(stream.directPlay, true, false)
|
||||
// Update transcode decision for session
|
||||
await this.getDecision(stream.directPlay);
|
||||
stream.streamUrl = this.plexFile;
|
||||
} else if (this.isVideoDirectStream() === false) {
|
||||
this.log("Decision: File can direct play")
|
||||
// Change transcoding arguments to be the user chosen transcode parameters
|
||||
this.setTranscodingArgs(stream.directPlay, false, deinterlace)
|
||||
// Update transcode decision for session
|
||||
await this.getDecision(stream.directPlay);
|
||||
stream.streamUrl = `${this.transcodeUrlBase}${this.transcodingArgs}`
|
||||
} else {
|
||||
this.log("Decision: Direct stream. Audio is being transcoded")
|
||||
stream.streamUrl = `${this.transcodeUrlBase}${this.transcodingArgs}`
|
||||
}
|
||||
}
|
||||
|
||||
return `${this.server.uri}/video/:/transcode/universal/start.m3u8?${this.transcodingArgs}`
|
||||
stream.streamStats = this.getVideoStats();
|
||||
|
||||
this.log(stream)
|
||||
|
||||
return stream
|
||||
}
|
||||
|
||||
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
|
||||
setTranscodingArgs(directPlay, directStream, deinterlace) {
|
||||
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 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
|
||||
@ -93,7 +125,7 @@ path=${this.key}&\
|
||||
mediaIndex=0&\
|
||||
partIndex=0&\
|
||||
fastSeek=1&\
|
||||
directPlay=0&\
|
||||
directPlay=${isDirectPlay}&\
|
||||
directStream=1&\
|
||||
directStreamAudio=1&\
|
||||
copyts=1&\
|
||||
@ -111,52 +143,69 @@ lang=en`
|
||||
|
||||
isVideoDirectStream() {
|
||||
try {
|
||||
return this.decisionJson["MediaContainer"]["Metadata"][0]["Media"][0]["Part"][0]["Stream"][0]["decision"] == "copy";
|
||||
return this.getVideoStats().videoDecision === "copy";
|
||||
} catch (e) {
|
||||
console.log("Error at decision:" + e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
getResolutionHeight() {
|
||||
return this.decisionJson["MediaContainer"]["Metadata"][0]["Media"][0]["Part"][0]["Stream"][0]["height"];
|
||||
isDirectPlay() {
|
||||
try {
|
||||
return this.getVideoStats().videoDecision === "copy" && this.getVideoStats().audioDecision === "copy";
|
||||
} catch (e) {
|
||||
console.log("Error at decision:" + e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
getVideoStats() {
|
||||
let ret = {}
|
||||
let streams = this.decisionJson["MediaContainer"]["Metadata"][0]["Media"][0]["Part"][0]["Stream"]
|
||||
try {
|
||||
let streams = this.decisionJson.MediaContainer.Metadata[0].Media[0].Part[0].Stream
|
||||
|
||||
streams.forEach(function (stream) {
|
||||
// Video
|
||||
if (stream["streamType"] == "1") {
|
||||
ret.videoCodec = stream["codec"];
|
||||
ret.videoWidth = stream["width"];
|
||||
ret.videoHeight = stream["height"];
|
||||
ret.videoFramerate = Math.round(stream["frameRate"]);
|
||||
// Rounding framerate avoids scenarios where
|
||||
// 29.9999999 & 30 don't match.
|
||||
}
|
||||
// Audio. Only look at stream being used
|
||||
if (stream["streamType"] == "2" && stream["selected"] == "1") {
|
||||
ret.audioChannels = stream["channels"];
|
||||
ret.audioCodec = stream["codec"];
|
||||
}
|
||||
})
|
||||
streams.forEach(function (stream) {
|
||||
// Video
|
||||
if (stream["streamType"] == "1") {
|
||||
ret.videoCodec = stream.codec;
|
||||
ret.videoWidth = stream.width;
|
||||
ret.videoHeight = stream.height;
|
||||
ret.videoFramerate = Math.round(stream["frameRate"]);
|
||||
// Rounding framerate avoids scenarios where
|
||||
// 29.9999999 & 30 don't match.
|
||||
ret.videoDecision = (typeof stream.decision === 'undefined') ? 'copy' : stream.decision;
|
||||
}
|
||||
// Audio. Only look at stream being used
|
||||
if (stream["streamType"] == "2" && stream["selected"] == "1") {
|
||||
ret.audioChannels = stream["channels"];
|
||||
ret.audioCodec = stream["codec"];
|
||||
ret.audioDecision = (typeof stream.decision === 'undefined') ? 'copy' : stream.decision;
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
console.log("Error at decision:" + e);
|
||||
}
|
||||
|
||||
this.log("Current video stats:")
|
||||
this.log(ret)
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
async getDecision() {
|
||||
async getDecision(directPlay) {
|
||||
await axios.get(`${this.server.uri}/video/:/transcode/universal/decision?${this.transcodingArgs}`, {
|
||||
headers: { Accept: 'application/json' }
|
||||
})
|
||||
.then((res) => {
|
||||
this.decisionJson = res.data;
|
||||
|
||||
this.log("Recieved transcode decision:")
|
||||
this.log(res.data)
|
||||
|
||||
// Print error message if transcode not possible
|
||||
// TODO: handle failure better
|
||||
let transcodeDecisionCode = res.data.MediaContainer.transcodeDecisionCode
|
||||
if (transcodeDecisionCode != "1001") {
|
||||
if (!(directPlay || transcodeDecisionCode == "1001")) {
|
||||
console.log(`IMPORTANT: Recieved transcode decision code ${transcodeDecisionCode}! Expected code 1001.`)
|
||||
console.log(`Error message: '${res.data.MediaContainer.transcodeDecisionText}'`)
|
||||
}
|
||||
@ -208,6 +257,7 @@ X-Plex-Token=${this.server.accessToken}`;
|
||||
}
|
||||
|
||||
updatePlex() {
|
||||
this.log("Updating plex status")
|
||||
axios.post(this.getStatusUrl());
|
||||
this.currTimeMs += this.updateInterval;
|
||||
if (this.currTimeMs > this.duration) {
|
||||
@ -215,6 +265,12 @@ X-Plex-Token=${this.server.accessToken}`;
|
||||
}
|
||||
this.currTimeS = this.duration / 1000;
|
||||
}
|
||||
|
||||
log(message) {
|
||||
if (this.settings.debugLogging) {
|
||||
console.log(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PlexTranscoder
|
||||
|
||||
76
src/video.js
76
src/video.js
@ -94,7 +94,7 @@ function video(db) {
|
||||
})
|
||||
|
||||
let channelNum = parseInt(req.query.channel, 10)
|
||||
ffmpeg.spawn(`http://localhost:${process.env.PORT}/playlist?channel=${channelNum}`, `undefined`, -1, false, `undefined`, true);
|
||||
ffmpeg.spawnConcat(`http://localhost:${process.env.PORT}/playlist?channel=${channelNum}`);
|
||||
})
|
||||
// Stream individual video to ffmpeg concat above. This is used by the server, NOT the client
|
||||
router.get('/stream', (req, res) => {
|
||||
@ -120,6 +120,10 @@ function video(db) {
|
||||
return
|
||||
}
|
||||
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'video/mp2t'
|
||||
})
|
||||
|
||||
// Get video lineup (array of video urls with calculated start times and durations.)
|
||||
let prog = helperFuncs.getCurrentProgramAndTimeElapsed(Date.now(), channel)
|
||||
let lineup = helperFuncs.createLineup(prog)
|
||||
@ -129,50 +133,40 @@ function video(db) {
|
||||
|
||||
// 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
|
||||
|
||||
let plexTranscoder = new PlexTranscoder(plexSettings, lineupItem);
|
||||
streamDuration = undefined
|
||||
|
||||
let deinterlace = enableChannelIcon = helperFuncs.isChannelIconEnabled(ffmpegSettings.enableChannelOverlay, channel.icon, channel.overlayIcon)
|
||||
|
||||
let plexTranscoder = new PlexTranscoder(plexSettings, lineupItem);
|
||||
|
||||
let ffmpeg = new FFMPEG(ffmpegSettings, channel); // Set the transcoder options
|
||||
|
||||
ffmpeg.on('data', (data) => { res.write(data) })
|
||||
|
||||
ffmpeg.on('error', (err) => {
|
||||
console.error("FFMPEG ERROR", err);
|
||||
res.status(500).send("FFMPEG ERROR");
|
||||
return;
|
||||
})
|
||||
|
||||
ffmpeg.on('close', () => {
|
||||
res.send();
|
||||
})
|
||||
|
||||
ffmpeg.on('end', () => { // On finish transcode - END of program or commercial...
|
||||
plexTranscoder.stopUpdatingPlex();
|
||||
res.end()
|
||||
})
|
||||
|
||||
res.on('close', () => { // on HTTP close, kill ffmpeg
|
||||
ffmpeg.kill();
|
||||
})
|
||||
|
||||
plexTranscoder.getStream(deinterlace).then(stream => {
|
||||
let streamUrl = stream.streamUrl
|
||||
let streamStats = stream.streamStats
|
||||
// Not time limited & no transcoding required. Pass plex transcode url directly
|
||||
if (!enableChannelIcon && streamDuration === -1) {
|
||||
res.redirect(streamUrl);
|
||||
} else { // ffmpeg needed limit time or insert channel icon
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'video/mp2t'
|
||||
})
|
||||
|
||||
let ffmpeg = new FFMPEG(ffmpegSettings, channel); // Set the transcoder options
|
||||
|
||||
ffmpeg.on('data', (data) => { res.write(data) })
|
||||
|
||||
ffmpeg.on('error', (err) => {
|
||||
console.error("FFMPEG ERROR", err);
|
||||
res.status(500).send("FFMPEG ERROR");
|
||||
return;
|
||||
})
|
||||
|
||||
ffmpeg.on('close', () => {
|
||||
res.send();
|
||||
})
|
||||
|
||||
ffmpeg.on('end', () => { // On finish transcode - END of program or commercial...
|
||||
plexTranscoder.stopUpdatingPlex();
|
||||
res.end()
|
||||
})
|
||||
|
||||
res.on('close', () => { // on HTTP close, kill ffmpeg
|
||||
ffmpeg.kill();
|
||||
})
|
||||
|
||||
ffmpeg.spawn(streamUrl, streamStats, streamDuration, enableChannelIcon, lineupItem.type, false); // Spawn the ffmpeg process, fire this bitch up
|
||||
plexTranscoder.startUpdatingPlex();
|
||||
}
|
||||
});
|
||||
let streamStart = (stream.directPlay) ? plexTranscoder.currTimeS : undefined;
|
||||
ffmpeg.spawnStream(stream.streamUrl, stream.streamStats, streamStart, streamDuration, enableChannelIcon, lineupItem.type); // Spawn the ffmpeg process, fire this bitch up
|
||||
plexTranscoder.startUpdatingPlex();
|
||||
});
|
||||
})
|
||||
router.get('/playlist', (req, res) => {
|
||||
res.type('text')
|
||||
|
||||
@ -53,6 +53,16 @@ module.exports = function (plex, pseudotv, $timeout) {
|
||||
scope.settings = _settings
|
||||
})
|
||||
}
|
||||
scope.pathOptions=[
|
||||
{id:"plex",description:"Plex"},
|
||||
{id:"direct",description:"Direct"}
|
||||
];
|
||||
scope.hideIfNotPlexPath = () => {
|
||||
return scope.settings.streamPath != 'plex'
|
||||
};
|
||||
scope.hideIfNotDirectPath = () => {
|
||||
return scope.settings.streamPath != 'direct'
|
||||
};
|
||||
scope.maxAudioChannelsOptions=[
|
||||
{id:"1",description:"1.0"},
|
||||
{id:"2",description:"2.0"},
|
||||
|
||||
@ -55,6 +55,29 @@
|
||||
</h6>
|
||||
<hr>
|
||||
<div class="row" >
|
||||
<div class="col-sm-3">
|
||||
<div class="form-group">
|
||||
<input id="debugLogging" type="checkbox" ng-model="settings.debugLogging"/>
|
||||
<label for="debugLogging">Debug logging</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Paths</label>
|
||||
<select ng-model="settings.streamPath"
|
||||
ng-options="o.id as o.description for o in pathOptions" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<div class="form-group">
|
||||
<input id="updatePlayStatus" type="checkbox" ng-model="settings.updatePlayStatus" ria-describedby="updatePlayStatusHelp"/>
|
||||
<label for="updatePlayStatus">Send play status to Plex</label>
|
||||
<small id="updatePlayStatusHelp" class="form-text text-muted">Note: This affects the "on deck" for your plex account.</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<p class="text-center text-danger">If stream changes video codec, audio codec, or audio channels upon episode change, you will experience playback issues.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" ng-hide="hideIfNotPlexPath()">
|
||||
<div class="col-sm-6">
|
||||
<h6 style="font-weight: bold">Video Options</h6>
|
||||
<div class="form-group">
|
||||
@ -76,7 +99,8 @@
|
||||
<h6 style="font-weight: bold">Audio Options</h6>
|
||||
<div class="form-group">
|
||||
<label>Supported Audio Formats</label>
|
||||
<input type="text" class="form-control form-control-sm" ng-model="settings.audioCodecs" />
|
||||
<input type="text" class="form-control form-control-sm" ng-model="settings.audioCodecs" ria-describedby="audioCodecsHelp" />
|
||||
<small id="audioCodecsHelp" class="form-text text-muted">Comma separated list. Some possible values are 'ac3,aac,mp3'.</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Maximum Audio Channels</label>
|
||||
@ -92,7 +116,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="row" ng-hide="hideIfNotPlexPath()">
|
||||
<div class="col-sm-6">
|
||||
<h6 style="font-weight: bold">Miscellaneous Options</h6>
|
||||
<div class="form-group">
|
||||
@ -111,16 +135,15 @@
|
||||
<label>Transcode Media Buffer Size</label>
|
||||
<input type="text" class="form-control form-control-sm" ng-model="settings.transcodeMediaBufferSize" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input id="updatePlayStatus" type="checkbox" ng-model="settings.updatePlayStatus" ria-describedby="updatePlayStatusHelp"/>
|
||||
<label for="updatePlayStatus">Send play status to Plex when possible</label>
|
||||
<small id="updatePlayStatusHelp" class="form-text text-muted">Note: This affects the "on deck" for your plex account.</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Stream Protocol</label>
|
||||
<select ng-model="settings.streamProtocol"
|
||||
ng-options="o.id as o.description for o in streamProtocols" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input id="updatePlayStatus" type="checkbox" ng-model="settings.forceDirectPlay" />
|
||||
<label for="updatePlayStatus">Force Direct Play</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<h6 style="font-weight: bold">Subtitle Options</h6>
|
||||
@ -134,5 +157,16 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-hide="hideIfNotDirectPath()">
|
||||
<div class="col-sm-6">
|
||||
<h6 style="font-weight: bold">Path Replacements</h6>
|
||||
<div class="form-group">
|
||||
<label>Original Plex path to replace:</label>
|
||||
<input type="text" class="form-control form-control-sm" ng-model="settings.pathReplace" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Replace Plex path with:</label>
|
||||
<input type="text" class="form-control form-control-sm" ng-model="settings.pathReplaceWith" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -146,6 +146,10 @@ module.exports = function ($http, $window, $interval) {
|
||||
date: res.Metadata[i].originallyAvailableAt,
|
||||
year: res.Metadata[i].year,
|
||||
}
|
||||
if (program.type === 'episode' || program.type === 'movie') {
|
||||
program.plexFile = `${res.Metadata[i].Media[0].Part[0].key}`
|
||||
program.file = `${res.Metadata[i].Media[0].Part[0].file}`
|
||||
}
|
||||
if (program.type === 'episode') {
|
||||
//Make sure that video files that contain multiple episodes are only listed once:
|
||||
var anyNewFile = false;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user