diff --git a/src/ffmpeg.js b/src/ffmpeg.js index a218943..801b9ba 100644 --- a/src/ffmpeg.js +++ b/src/ffmpeg.js @@ -30,6 +30,7 @@ class FFMPEG extends events.EventEmitter { this.audioChannelsSampleRate = this.opts.normalizeAudio; this.ensureResolution = this.opts.normalizeResolution; this.volumePercent = this.opts.audioVolumePercent; + this.hasBeenKilled = false; } async spawnConcat(streamUrl) { return await this.spawn(streamUrl, undefined, undefined, undefined, true, false, undefined, true) @@ -404,24 +405,37 @@ class FFMPEG extends events.EventEmitter { let doLogs = this.opts.logFfmpeg && !isConcatPlaylist; this.ffmpeg = spawn(this.ffmpegPath, ffmpegArgs, { stdio: ['ignore', 'pipe', (doLogs?process.stderr:"ignore") ] } ); - this.ffmpeg.on('close', (code) => { + let ffmpegName = (isConcatPlaylist ? "Concat FFMPEG": "Stream FFMPEG"); + + this.ffmpeg.on('exit', (code, signal) => { if (code === null) { + console.log( `${ffmpegName} exited due to signal: ${signal}` ); this.emit('close', code) } else if (code === 0) { + console.log( `${ffmpegName} exited normally.` ); this.emit('end') } else if (code === 255) { + if (this.hasBeenKilled) { + console.log( `${ffmpegName} finished with code 255.` ); + this.emit('close', code) + return; + } if (! this.sentData) { this.emit('error', { code: code, cmd: `${this.opts.ffmpegPath} ${ffmpegArgs.join(' ')}` }) } + console.log( `${ffmpegName} exited with code 255.` ); this.emit('close', code) } else { + console.log( `${ffmpegName} exited with code ${code}.` ); this.emit('error', { code: code, cmd: `${this.opts.ffmpegPath} ${ffmpegArgs.join(' ')}` }) } - }) + }); + return this.ffmpeg.stdout; } kill() { if (typeof this.ffmpeg != "undefined") { + this.hasBeenKilled = true; this.ffmpeg.kill() } } diff --git a/src/plexTranscoder.js b/src/plexTranscoder.js index a88f6d7..47caba5 100644 --- a/src/plexTranscoder.js +++ b/src/plexTranscoder.js @@ -42,9 +42,11 @@ class PlexTranscoder { this.log(` deinterlace: ${deinterlace}`) this.log(` streamPath: ${this.settings.streamPath}`) - - if (this.settings.streamPath === 'direct' || this.settings.forceDirectPlay) { + if (this.settings.enableSubtitles) { + console.log("Direct play is forced, so subtitles are forcibly disabled."); + this.settings.enableSubtitles = false; + } stream = {directPlay: true} } else { try { diff --git a/web/directives/channel-config.js b/web/directives/channel-config.js index 63266eb..7a1988e 100644 --- a/web/directives/channel-config.js +++ b/web/directives/channel-config.js @@ -16,6 +16,9 @@ module.exports = function ($timeout, $location, dizquetv) { scope._frequencyMessage = ""; scope.millisecondsOffset = 0; scope.minProgramIndex = 0; + scope.episodeMemory = { + saved : false, + }; if (typeof scope.channel === 'undefined' || scope.channel == null) { scope.channel = {} scope.channel.programs = [] @@ -329,6 +332,22 @@ module.exports = function ($timeout, $location, dizquetv) { } } + let interpolate = ( () => { + let h = 60*60*1000; + let ix = [0, 1*h, 2*h, 4*h, 8*h, 24*h]; + let iy = [0, 1.0, 1.25, 1.5, 1.75, 2.0]; + let n = ix.length; + + return (x) => { + for (let i = 0; i < n-1; i++) { + if( (ix[i] <= x) && ( (x < ix[i+1]) || i==n-2 ) ) { + return iy[i] + (iy[i+1] - iy[i]) * ( (x - ix[i]) / (ix[i+1] - ix[i]) ); + } + } + } + + } )(); + scope.programSquareStyle = (program) => { let background =""; if ( (program.isOffline) && (program.type !== 'redirect') ) { @@ -372,27 +391,26 @@ module.exports = function ($timeout, $location, dizquetv) { } let rgb1 = "rgb("+ r + "," + g + "," + b +")"; let rgb2 = "rgb("+ r2 + "," + g2 + "," + b2 +")" + angle += 90; background = "repeating-linear-gradient( " + angle + "deg, " + rgb1 + ", " + rgb1 + " " + w + "px, " + rgb2 + " " + w + "px, " + rgb2 + " " + (w*2) + "px)"; } - let ems = Math.pow( Math.min(24*60*60*1000, program.duration), 0.7 ); - ems = 1.3; - let top = 0.01; - if (top == 0.0) { - top = "1px"; - } else { - top = top + "em"; - } - - + let f = interpolate; + let w = 5.0; + let t = 4*60*60*1000; + //let d = Math.log( Math.min(t, program.duration) ) / Math.log(2); + //let a = (d * Math.log(2) ) / Math.log(t); + let a = ( f(program.duration) *w) / f(t); + a = Math.min( w, Math.max(0.3, a) ); + b = w - a + 0.01; return { - 'width': '0.5em', - 'height': ems + 'em', - 'margin-right': '0.50em', + 'width': `${a}%`, + 'height': '1.3em', + 'margin-right': `${b}%`, 'background': background, 'border': '1px solid black', - 'margin-top': top, + 'margin-top': "0.01em", 'margin-bottom': '1px', }; } @@ -544,6 +562,103 @@ module.exports = function ($timeout, $location, dizquetv) { scope.channel.programs = progs; updateChannelDuration(); } + scope.savePositions = () => { + scope.episodeMemory = { + saved : false, + }; + let array = scope.channel.programs; + for (let i = 0; i < array.length; i++) { + if (array[i].type === 'episode' && array[i].season != 0) { + let key = array[i].showTitle; + if (typeof(scope.episodeMemory[key]) === 'undefined') { + scope.episodeMemory[key] = { + season: array[i].season, + episode: array[i].episode, + } + } + } + } + scope.episodeMemory.saved = true; + } + scope.recoverPositions = () => { + //this is basically the code for cyclic shuffle + let array = scope.channel.programs; + let shows = {}; + let next = {}; + let counts = {}; + // some precalculation, useful to stop the shuffle from being quadratic... + for (let i = 0; i < array.length; i++) { + let vid = array[i]; + if (vid.type === 'episode' && vid.season != 0) { + let countKey = { + title: vid.showTitle, + s: vid.season, + e: vid.episode, + } + let key = JSON.stringify(countKey); + let c = ( (typeof(counts[key]) === 'undefined') ? 0 : counts[key] ); + counts[key] = c + 1; + let showEntry = { + c: c, + it: vid + } + if ( typeof(shows[vid.showTitle]) === 'undefined') { + shows[vid.showTitle] = []; + } + shows[vid.showTitle].push(showEntry); + } + } + //this is O(|N| log|M|) where |N| is the total number of TV + // episodes and |M| is the maximum number of episodes + // in a single show. I am pretty sure this is a lower bound + // on the time complexity that's possible here. + Object.keys(shows).forEach(function(key,index) { + shows[key].sort( (a,b) => { + if (a.c == b.c) { + if (a.it.season == b.it.season) { + if (a.it.episode == b.it.episode) { + return 0; + } else { + return (a.it.episode < b.it.episode)?-1: 1; + } + } else { + return (a.it.season < b.it.season)?-1: 1; + } + } else { + return (a.c < b.c)? -1: 1; + } + }); + next[key] = 0; + if (typeof(scope.episodeMemory[key]) !== 'undefined') { + for (let i = 0; i < shows[key].length; i++) { + if ( + (shows[key][i].it.season === scope.episodeMemory[key].season) + &&(shows[key][i].it.episode === scope.episodeMemory[key].episode) + ) { + next[key] = i; + break; + } + } + } + }); + for (let i = 0; i < array.length; i++) { + if (array[i].type === 'episode' && array[i].season != 0) { + let title = array[i].showTitle; + var sequence = shows[title]; + let j = next[title]; + array[i] = sequence[j].it; + + next[title] = (j + 1) % sequence.length; + } + } + scope.channel.programs = array; + updateChannelDuration(); + + } + scope.cannotRecoverPositions = () => { + return scope.episodeMemory.saved !== true; + } + scope.addBreaks = (afterMinutes, minDurationSeconds, maxDurationSeconds) => { let after = afterMinutes * 60 * 1000 + 5000; //allow some seconds of excess let minDur = minDurationSeconds; diff --git a/web/directives/plex-settings.js b/web/directives/plex-settings.js index 3b7abca..353962c 100644 --- a/web/directives/plex-settings.js +++ b/web/directives/plex-settings.js @@ -145,6 +145,9 @@ module.exports = function (plex, dizquetv, $timeout) { return r; } + scope.shouldDisableSubtitles = () => { + return scope.settings.forceDirectPlay || (scope.settings.streamPath === "direct" ); + } scope.addPlexServer = async () => { scope.isProcessing = true; diff --git a/web/public/templates/channel-config.html b/web/public/templates/channel-config.html index fa69d94..5dd4798 100644 --- a/web/public/templates/channel-config.html +++ b/web/public/templates/channel-config.html @@ -139,6 +139,9 @@
Sort Release Dates

Sorts everything by its release date. This will only work correctly if the release dates in Plex are correct. In case any item does not have a release date specified, it will be moved to the bottom.

+
Sort Release Dates
+

Sorts everything by its release date. This will only work correctly if the release dates in Plex are correct. In case any item does not have a release date specified, it will be moved to the bottom.

+
Balance Shows

Attempts to make the total amount of time each TV show appears in the programming as balanced as possible. This works by adding multiple copies of TV shows that have too little total time and by possibly removing duplicated episodes from TV shows that have too much total time. Note that in many situations it would be impossible to achieve perfect balance because channel duration is not infinite. Movies/Clips are treated as a single TV show. Note that this will most likely result in a larger channel and that having large channels makes some UI operations slower.

@@ -160,6 +163,10 @@
Reruns

Divides the programming in blocks of 6, 8 or 12 hours then repeats each of the blocks the specified number of times. For example, you can make a channel that plays exactly the same channels in the morning and in the afternoon.

+
Save|Recover Episode Positions
+

The "Save" button saves the current episodes that are next to be played for each tv show. Then whenever you click the "Recover Episode Popsitions" button, episodes will be rearranged cyclically and they will start with the saved positions. So you can maintain episode sequences even after modifying the channel. If there are any new TV shows, they will start at their current positions. Movies and specials won't change positions. +

+
Add Redirect

Adds a channel redirect. During this period of time, the channel will redirect to another channel.

@@ -189,7 +196,7 @@
- +
@@ -225,6 +232,7 @@
+