From 3391e9173f831b6a1aee22f34bc48e3a1b580a6a Mon Sep 17 00:00:00 2001 From: vexorian Date: Tue, 4 Aug 2020 22:38:44 -0400 Subject: [PATCH 1/6] Tweaked Flex random algorithm AGAIN --- src/helperFuncs.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helperFuncs.js b/src/helperFuncs.js index bbac0aa..39faf18 100644 --- a/src/helperFuncs.js +++ b/src/helperFuncs.js @@ -219,7 +219,7 @@ function pickRandomWithMaxDuration(channel, list, maxDuration) { //30 minutes is too little, don't repeat it at all } if (timeSince >= D) { - let w = Math.pow(clip.actualDuration, 1.0 / 8.0); + let w = Math.pow(clip.actualDuration, 1.0 / 4.0); n += w; if ( n*Math.random() < w) { pick1 = clip; From 3fef59c4da53a05b35925b1c907c9333571395c2 Mon Sep 17 00:00:00 2001 From: vexorian Date: Tue, 4 Aug 2020 22:43:15 -0400 Subject: [PATCH 2/6] Fix Pad Time bug. To improve on channel loading times in the UI, channel editor will only show 100 programs at once, but there's a slider to see more. --- web/directives/channel-config.js | 23 ++++++++--- web/public/templates/channel-config.html | 49 +++++++++++++++++++++--- 2 files changed, 61 insertions(+), 11 deletions(-) diff --git a/web/directives/channel-config.js b/web/directives/channel-config.js index 9f3b25e..4e84352 100644 --- a/web/directives/channel-config.js +++ b/web/directives/channel-config.js @@ -13,6 +13,7 @@ module.exports = function ($timeout, $location) { scope._frequencyModified = false; scope._frequencyMessage = ""; scope.millisecondsOffset = 0; + scope.minProgramIndex = 0; if (typeof scope.channel === 'undefined' || scope.channel == null) { scope.channel = {} scope.channel.programs = [] @@ -97,6 +98,17 @@ module.exports = function ($timeout, $location) { scope._selectedProgram = null updateChannelDuration() } + scope.dropFunction = (dropIndex, index, program) => { + if (scope.channel.programs[index].start == program.start) { + return false; + } + + setTimeout( () => { + scope.channel.programs.splice(dropIndex + index, 0, program); + updateChannelDuration() + }, 1); + return true; + } scope.updateChannelFromOfflineResult = (program) => { scope.channel.offlineMode = program.channelOfflineMode; scope.channel.offlinePicture = program.channelPicture; @@ -170,6 +182,9 @@ module.exports = function ($timeout, $location) { scope.channel.programs = newProgs.concat(movies) updateChannelDuration() } + scope.dateForGuide = (date) => { + return date.toLocaleString(); + } scope.sortByDate = () => { scope.removeOffline(); scope.channel.programs.sort( (a,b) => { @@ -474,7 +489,7 @@ module.exports = function ($timeout, $location) { scope.wipeSchedule = () => { - wipeSchedule(scope.channel.programs); + scope.channel.programs = []; updateChannelDuration(); } scope.makeOfflineFromChannel = (duration) => { @@ -520,10 +535,6 @@ module.exports = function ($timeout, $location) { } return array } - function wipeSchedule(array) { - array.splice(0, array.length) - return array; - } function equalizeShows(array, freqObject) { let shows = {}; let progs = []; @@ -702,6 +713,7 @@ module.exports = function ($timeout, $location) { updateChannelDuration() } scope.paddingOptions = [ + { id: -1, description: "Allowed start times", allow5: false }, { id: 30, description: ":00, :30", allow5: false }, { id: 15, description: ":00, :15, :30, :45", allow5: false }, { id: 60, description: ":00", allow5: false }, @@ -712,6 +724,7 @@ module.exports = function ($timeout, $location) { { id: 30, description: ":00, :05, :30, :35", allow5: true }, ] + scope.paddingOption = scope.paddingOptions[0]; scope.breakAfterOptions = [ { id: -1, description: "After" }, { id: 5, description: "5 minutes" }, diff --git a/web/public/templates/channel-config.html b/web/public/templates/channel-config.html index ea99578..fc66297 100644 --- a/web/public/templates/channel-config.html +++ b/web/public/templates/channel-config.html @@ -216,7 +216,7 @@ ng-options="o as o.description for o in paddingOptions" /> - + @@ -248,11 +248,27 @@ -
-
  • +
    +
    +
    Showing programs {{minProgramIndex+1}} to {{minProgramIndex+100}}
    +
    + +
    -
    {{x.start.toLocaleString()}}
    -
    {{x.stop.toLocaleString()}}
    +
    {{ dateForGuide(channel.startTime) }}
    +
    {{ dateForGuide(channel.programs[minProgramIndex-1].stop)}}
    +
    + +
    + ⋮ +
    +
    +
    +
    +
    +
    {{ dateForGuide(x.start) }}
    +
    {{ dateForGuide(x.stop) }}
    {{x.isOffline? channel.fillerContent.length: x.commercials.length}} @@ -264,7 +280,28 @@ Flex
    -
  • +
    + +
    +
    +
    {{ dateForGuide(channel.programs[minProgramIndex + 100 - 1].stop)}}
    +
    {{ dateForGuide(channel.programs[channel.programs.length-1].stop) }}
    +
    + +
    + ⋮ +
    +
    +
    +
    +
    {{ dateForGuide(channel.programs[channel.programs.length-1].stop)}}
    +
    + +
    + (Restart programming from beginning) +
    +
    + From 451b4ede143a94845b0e8318a4b450458769b281 Mon Sep 17 00:00:00 2001 From: vexorian Date: Sun, 9 Aug 2020 14:52:16 -0400 Subject: [PATCH 3/6] Fix unexpected when attempting to play error/offline streams. Fix streams ending abruptly after some minutes unless ffmpeg logs are enabled. --- src/ffmpeg.js | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/src/ffmpeg.js b/src/ffmpeg.js index f7ad6ff..6446ee3 100644 --- a/src/ffmpeg.js +++ b/src/ffmpeg.js @@ -2,14 +2,14 @@ const spawn = require('child_process').spawn const events = require('events') //they can customize this by modifying the picture in .pseudotv folder -const ERROR_PICTURE_PATH = 'http://localhost:8000/images/generic-error-screen.png' const MAXIMUM_ERROR_DURATION_MS = 60000; class FFMPEG extends events.EventEmitter { constructor(opts, channel) { super() - this.opts = opts + this.opts = opts; + this.errorPicturePath = `http://localhost:${process.env.PORT}/images/generic-error-screen.png`; if (! this.opts.enableFFMPEGTranscoding) { //this ensures transcoding is completely disabled even if // some settings are true @@ -34,15 +34,15 @@ class FFMPEG extends events.EventEmitter { this.volumePercent = this.opts.audioVolumePercent; } async spawnConcat(streamUrl) { - this.spawn(streamUrl, undefined, undefined, undefined, false, false, undefined, true) + this.spawn(streamUrl, undefined, undefined, undefined, true, false, undefined, true) } async spawnStream(streamUrl, streamStats, startTime, duration, enableIcon, type) { this.spawn(streamUrl, streamStats, startTime, duration, true, enableIcon, type, false); } async spawnError(title, subtitle, duration) { if (! this.opts.enableFFMPEGTranscoding || this.opts.errorScreen == 'kill') { - console.log("error: " + title + " ; " + subtitle); - this.emit('error', { code: -1, cmd: `error stream disabled. ${title} ${subtitles}`} ) + console.error("error: " + title + " ; " + subtitle); + this.emit('error', { code: -1, cmd: `error stream disabled. ${title} ${subtitle}`} ) return; } if (typeof(duration) === 'undefined') { @@ -61,7 +61,7 @@ class FFMPEG extends events.EventEmitter { async spawnOffline(duration) { if (! this.opts.enableFFMPEGTranscoding) { console.log("The channel has an offline period scheduled for this time slot. FFMPEG transcoding is disabled, so it is not possible to render an offline screen. Ending the stream instead"); - this.emit('end', { code: -1, cmd: `error stream disabled. ${title} ${subtitles}`} ) + this.emit('end', { code: -1, cmd: `offline stream disabled.`} ) return; } @@ -171,7 +171,7 @@ class FFMPEG extends events.EventEmitter { } else {//'pic' ffmpegArgs.push( '-loop', '1', - '-i', `${ERROR_PICTURE_PATH}`, + '-i', `${this.errorPicturePath}`, ); videoComplex = `;[0:0]scale=${iW}:${iH}[videoy];[videoy]realtime[videox]`; } @@ -226,7 +226,6 @@ class FFMPEG extends events.EventEmitter { videoComplex += `;[1:v]scale=${this.channel.iconWidth}:-1[icn];${currentVideo}[icn]overlay=${posAry[this.channel.iconPosition]}${icnDur}[comb]` currentVideo = '[comb]'; } - if (this.volumePercent != 100) { var f = this.volumePercent / 100.0; audioComplex += `;${currentAudio}volume=${f}[boosted]`; @@ -332,16 +331,12 @@ class FFMPEG extends events.EventEmitter { ffmpegArgs.push(`pipe:1`) - this.ffmpeg = spawn(this.ffmpegPath, ffmpegArgs) + let doLogs = this.opts.logFfmpeg && !isConcatPlaylist; + this.ffmpeg = spawn(this.ffmpegPath, ffmpegArgs, { stdio: ['ignore', 'pipe', (doLogs?process.stderr:"ignore") ] } ); this.ffmpeg.stdout.on('data', (chunk) => { this.sentData = true; this.emit('data', chunk) }) - if (this.opts.logFfmpeg) { - this.ffmpeg.stderr.on('data', (chunk) => { - process.stderr.write(chunk) - }) - } this.ffmpeg.on('close', (code) => { if (code === null) { this.emit('close', code) From 3184306e865639f0aba727225548e72db714f5ad Mon Sep 17 00:00:00 2001 From: vexorian Date: Sun, 9 Aug 2020 14:58:59 -0400 Subject: [PATCH 4/6] Major channel UI changes. Remove commercials. Tools for filler editor. Visible Color and Length signatures for programs and flex. --- web/directives/channel-config.js | 101 ++++++++++++++++++- web/directives/offline-config.js | 46 +++++++++ web/public/style.css | 33 ++++++- web/public/templates/channel-config.html | 117 +++++++++++++---------- web/public/templates/offline-config.html | 48 +++++++--- web/public/templates/program-config.html | 26 ----- 6 files changed, 282 insertions(+), 89 deletions(-) diff --git a/web/directives/channel-config.js b/web/directives/channel-config.js index 4e84352..3dc25fe 100644 --- a/web/directives/channel-config.js +++ b/web/directives/channel-config.js @@ -9,6 +9,7 @@ module.exports = function ($timeout, $location) { onDone: "=onDone" }, link: function (scope, element, attrs) { + scope.hasFlex = false; scope.showHelp = false; scope._frequencyModified = false; scope._frequencyMessage = ""; @@ -183,7 +184,19 @@ module.exports = function ($timeout, $location) { updateChannelDuration() } scope.dateForGuide = (date) => { - return date.toLocaleString(); + let t = date.toLocaleTimeString(undefined, { + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }); + if (t.charCodeAt(1) == 58) { + t = "0" + t; + } + return date.toLocaleDateString(undefined,{ + year: "numeric", + month: "2-digit", + day: "2-digit" + }) + " " + t; } scope.sortByDate = () => { scope.removeOffline(); @@ -254,6 +267,88 @@ module.exports = function ($timeout, $location) { scope.channel.programs = tmpProgs updateChannelDuration() } + + scope.describeFallback = () => { + if (scope.channel.offlineMode === 'pic') { + if ( + (typeof(scope.channel.offlineSoundtrack) !== 'undefined') + && (scope.channel.offlineSoundtrack.length > 0) + ) { + return "pic+sound"; + } else { + return "pic"; + } + } else { + return "clip"; + } + } + + scope.programSquareStyle = (program) => { + let background =""; + if (program.isOffline) { + background = "rgb(255, 255, 255)"; + } else { + let r = 0, g = 0, b = 0, r2=0, g2=0,b2=0; + let i = 0; + let angle = 45; + let w = 3; + if (program.type === 'episode') { + let h = Math.abs(scope.getHashCode(program.showTitle, false)); + let h2 = Math.abs(scope.getHashCode(program.showTitle, true)); + r = h % 256; + g = (h / 256) % 256; + b = (h / (256*256) ) % 256; + i = h % 360; + r2 = (h2 / (256*256) ) % 256; + g2 = (h2 / (256*256) ) % 256; + b2 = (h2 / (256*256) ) % 256; + angle = -90 + h % 180 + } else { + r = 10, g = 10, b = 10; + r2 = 245, g2 = 245, b2 = 245; + angle = 45; + w = 6; + } + + let rgb1 = "rgb("+ r + "," + g + "," + b +")"; + let rgb2 = "rgb("+ r2 + "," + g2 + "," + b2 +")" + 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.actualDuration), 0.7 ); + ems = ems / Math.pow(5*60*1000., 0.7); + ems = Math.max( 0.25 , ems); + let top = Math.max(0.0, (1.75 - ems) / 2.0) ; + if (top == 0.0) { + top = "1px"; + } else { + top = top + "em"; + } + + return { + 'width': '0.5em', + 'height': ems + 'em', + 'margin-right': '0.50em', + 'background': background, + 'border': '1px solid black', + 'margin-top': top, + 'margin-bottom': '1px', + }; + } + scope.getHashCode = (s, rev) => { + var hash = 0; + if (s.length == 0) return hash; + let inc = 1, st = 0, e = s.length; + if (rev) { + inc = -1, st = e - 1, e = -1; + } + for (var i = st; i != e; i+= inc) { + hash = s.charCodeAt(i) + ((hash << 5) - hash); + hash = hash & hash; // Convert to 32bit integer + } + return hash; + } + scope.nightChannel = (a, b) => { let o =(new Date()).getTimezoneOffset() * 60 * 1000; let m = 24*60*60*1000; @@ -650,10 +745,14 @@ module.exports = function ($timeout, $location) { function updateChannelDuration() { scope.showRotatedNote = false; scope.channel.duration = 0 + scope.hasFlex = false; for (let i = 0, l = scope.channel.programs.length; i < l; i++) { scope.channel.programs[i].start = new Date(scope.channel.startTime.valueOf() + scope.channel.duration) scope.channel.duration += scope.channel.programs[i].duration scope.channel.programs[i].stop = new Date(scope.channel.startTime.valueOf() + scope.channel.duration) + if (scope.channel.programs[i].isOffline) { + scope.hasFlex = true; + } } } scope.error = {} diff --git a/web/directives/offline-config.js b/web/directives/offline-config.js index df6b49f..7ba04eb 100644 --- a/web/directives/offline-config.js +++ b/web/directives/offline-config.js @@ -10,6 +10,7 @@ module.exports = function ($timeout) { onDone: "=onDone" }, link: function (scope, element, attrs) { + scope.showTools = false; scope.showPlexLibrary = false; scope.showFallbackPlexLibrary = false; scope.finished = (prog) => { @@ -32,6 +33,28 @@ module.exports = function ($timeout) { scope.onDone(JSON.parse(angular.toJson(prog))) scope.program = null } + scope.sortFillers = () => { + scope.program.filler.sort( (a,b) => { return a.actualDuration - b.actualDuration } ); + } + scope.fillerRemoveAllFiller = () => { + scope.program.filler = []; + } + scope.fillerRemoveDuplicates = () => { + function getKey(p) { + return p.server.uri + "|" + p.server.accessToken + "|" + p.plexFile; + } + let seen = {}; + let newFiller = []; + for (let i = 0; i < scope.program.filler.length; i++) { + let p = scope.program.filler[i]; + let k = getKey(p); + if ( typeof(seen[k]) === 'undefined') { + seen[k] = true; + newFiller.push(p); + } + } + scope.program.filler = newFiller; + } scope.importPrograms = (selectedPrograms) => { for (let i = 0, l = selectedPrograms.length; i < l; i++) { selectedPrograms[i].commercials = [] @@ -55,6 +78,29 @@ module.exports = function ($timeout) { date.setSeconds( Math.floor(duration / 1000) ); // specify value for SECONDS here return date.toISOString().substr(11, 8); } + + scope.programSquareStyle = (program, dash) => { + let background = "rgb(255, 255, 255)"; + let ems = Math.pow( Math.min(60*60*1000, program.actualDuration), 0.7 ); + ems = ems / Math.pow(1*60*1000., 0.7); + ems = Math.max( 0.25 , ems); + let top = Math.max(0.0, (1.75 - ems) / 2.0) ; + if (top == 0.0) { + top = "1px"; + } + let solidOrDash = (dash? 'dashed' : 'solid'); + + return { + 'width': '0.5em', + 'height': ems + 'em', + 'margin-right': '0.50em', + 'background': background, + 'border': `1px ${solidOrDash} black`, + 'margin-top': top, + 'margin-bottom': '1px', + }; + } + } }; } diff --git a/web/public/style.css b/web/public/style.css index deb9333..23d50e8 100644 --- a/web/public/style.css +++ b/web/public/style.css @@ -17,8 +17,9 @@ } .flex-pull-right { margin-left: auto; - padding-right: 20px + padding-right: 0.2em } + .list-group-item-video { background-color: rgb(70, 70, 70); border-top: 1px solid #daa104; @@ -61,4 +62,34 @@ display: inline-block; text-align: center; cursor: pointer; +} +.program-start { + margin-right: 2.5em; + display: inline-block; + vertical-align: top; + /*color: rgb(96,96,96);*/ + color: #0c5c68; + font-size: 80%; + font-weight: 400; + font-family: monospace; +} +.program-row { + align-items: start; +} +.programming-counter { + white-space: nowrap; + margin-right: 1em; + font-size: 80%; +} +.programming-counter > span { + font-weight: 300; +} +.programming-counter > b { + font-weight: 400; +} +.btn-programming-tools { + padding: .25rem .5rem; + font-size: .875rem; + line-height: 1.0; + margin-right: 0.5rem; } \ No newline at end of file diff --git a/web/public/templates/channel-config.html b/web/public/templates/channel-config.html index fc66297..916e533 100644 --- a/web/public/templates/channel-config.html +++ b/web/public/templates/channel-config.html @@ -76,24 +76,37 @@
    -
    Programs - Total: {{channel.programs.length}} - - Commercials - - - {{error.programs}} - - -
    +
    Programming
    + +
    + +
    + Programs: {{channel.programs.length}} +
    +
    + Filler: {{channel.fillerContent.length}} +
    +
    + Fallback: {{describeFallback()}} +
    +
    +
    + +
    + +
    + {{error.programs}} + +
    +
    +

    Tools to modify the schedule. @@ -118,8 +131,6 @@

    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.

    -
    Remove Duplicates
    -

    Removes repeated videos.

    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.

    @@ -130,9 +141,6 @@
    Add Flex

    Adds a "Flex" Time Slot. Can be configured to play a fallback screen and/or random "filler" content (e.g "commercials", trailers, prerolls, countdowns, music videos, channel bumpers, etc.). Short Flex periods are hidden from the TV guide and are displayed as extensions to the previous program. Long Flex periods appear as the channel name in the TV guide.

    -
    Remove Flex
    -

    Removes any Flex periods from the schedule.

    -
    Pad Times

    Adds Flex breaks after each TV episode or movie to ensure that the program starts at one of the allowed minute marks. For example, you can use this to ensure that all your programs start at either XX:00 times or XX:30 times. Removes any existing Flex periods before adding the new ones.

    @@ -141,6 +149,17 @@
    Add Breaks

    Adds Flex breaks between programs, attempting to avoid groups of consecutive programs that exceed the specified number of minutes.

    + +
    Remove Duplicates
    +

    Removes repeated videos.

    + +
    Remove Flex
    +

    Removes any Flex periods from the schedule.

    + +
    Remove All
    +

    Wipes out the schedule so that you can start over.

    + +
    @@ -173,14 +192,6 @@
    -
    -
    - -
    -
    - -
    -
    @@ -235,9 +246,17 @@
    - - - +
    +
    + +
    +
    + +
    +
    + +
    +
    Add programs to this channel by selecting media from your Plex library
    @@ -254,9 +273,8 @@
    -
    -
    {{ dateForGuide(channel.startTime) }}
    -
    {{ dateForGuide(channel.programs[minProgramIndex-1].stop)}}
    +
    + {{ dateForGuide(channel.startTime) }}
    @@ -265,14 +283,14 @@
    -
    -
    -
    {{ dateForGuide(x.start) }}
    -
    {{ dateForGuide(x.stop) }}
    -
    -
    - {{x.isOffline? channel.fillerContent.length: x.commercials.length}} +
    + +
    + {{ dateForGuide(x.start) }}
    +
    +
    {{ x.type === 'episode' ? x.showTitle + ' - S' + x.season.toString().padStart(2, '0') + 'E' + x.episode.toString().padStart(2, '0') : x.title}}
    @@ -283,18 +301,17 @@
    -
    -
    {{ dateForGuide(channel.programs[minProgramIndex + 100 - 1].stop)}}
    -
    {{ dateForGuide(channel.programs[channel.programs.length-1].stop) }}
    +
    + {{ dateForGuide(channel.programs[minProgramIndex + 100 - 1].stop)}}
    -
    -
    -
    {{ dateForGuide(channel.programs[channel.programs.length-1].stop)}}
    +
    +
    + {{ dateForGuide(channel.programs[channel.programs.length-1].stop)}}
    diff --git a/web/public/templates/offline-config.html b/web/public/templates/offline-config.html index 8b96a87..dce3945 100644 --- a/web/public/templates/offline-config.html +++ b/web/public/templates/offline-config.html @@ -60,9 +60,10 @@
    -
    +
    {{durationString(x.actualDuration)}}
    +
    Fallback: {{x.title}}
    @@ -74,7 +75,7 @@
    -
    +
    @@ -92,24 +93,49 @@

    -
    - - Filler Clips: {{program.filler.length}} - - +
    +
    + Filler Clips: {{program.filler.length}} +
    +
    +
    + +
    +
    + +
    - +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +

    Click the to import filler content from your Plex server(s).

    -
    +
    {{durationString(x.actualDuration)}}
    +
    {{x.title}}
    diff --git a/web/public/templates/program-config.html b/web/public/templates/program-config.html index bb5610a..3577ea7 100644 --- a/web/public/templates/program-config.html +++ b/web/public/templates/program-config.html @@ -76,32 +76,6 @@
    -
    -
    Commercials - -
    -
    -

    Click the to import "commercials" from your Plex server(s).

    -
    -
    -
    - {{x.title}} -
    - - Position
    - {{x.commercialPosition===0?'START':x.commercialPosition=== 1?'1/4':x.commercialPosition===2?'1/2':x.commercialPosition===3?'3/4':'END'}} -
    - - - - -
    - -
    -
    -