From 760f13ceccc3cb4812fd9efb6712b7260dc6c6f9 Mon Sep 17 00:00:00 2001 From: vexorian Date: Sun, 20 Sep 2020 22:52:39 -0400 Subject: [PATCH 01/35] 1.1.0 --- README.md | 2 +- src/constants.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9afff67..93b8e1f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# dizqueTV 1.1.0-prerelease +# dizqueTV 1.1.0 ![Discord](https://img.shields.io/discord/711313431457693727?logo=discord&logoColor=fff&style=flat-square) ![GitHub top language](https://img.shields.io/github/languages/top/vexorian/dizquetv?logo=github&style=flat-square) ![Docker Pulls](https://img.shields.io/docker/pulls/vexorian/dizquetv?logo=docker&logoColor=fff&style=flat-square) Create live TV channel streams from media on your Plex servers. diff --git a/src/constants.js b/src/constants.js index a0c44da..456291d 100644 --- a/src/constants.js +++ b/src/constants.js @@ -4,5 +4,5 @@ module.exports = { STEALTH_DURATION: 5 * 60* 1000, TVGUIDE_MAXIMUM_FLEX_DURATION : 6 * 60 * 60 * 1000, - VERSION_NAME: "1.1.0-prerelease" + VERSION_NAME: "1.1.0" } From 7250248345126de12aa78d0aeddd58af6b9c4e63 Mon Sep 17 00:00:00 2001 From: vexorian Date: Sat, 26 Sep 2020 15:59:58 -0400 Subject: [PATCH 02/35] 1.1.1 --- README.md | 2 +- src/constants.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0c06485..746baf6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# dizqueTV 1.1.1-prerelease +# dizqueTV 1.1.1 ![Discord](https://img.shields.io/discord/711313431457693727?logo=discord&logoColor=fff&style=flat-square) ![GitHub top language](https://img.shields.io/github/languages/top/vexorian/dizquetv?logo=github&style=flat-square) ![Docker Pulls](https://img.shields.io/docker/pulls/vexorian/dizquetv?logo=docker&logoColor=fff&style=flat-square) Create live TV channel streams from media on your Plex servers. diff --git a/src/constants.js b/src/constants.js index 731753d..c53a056 100644 --- a/src/constants.js +++ b/src/constants.js @@ -5,5 +5,5 @@ module.exports = { TVGUIDE_MAXIMUM_FLEX_DURATION : 6 * 60 * 60 * 1000, TOO_FREQUENT: 100, - VERSION_NAME: "1.1.1-prerelease" + VERSION_NAME: "1.1.1" } From 5a1db9683705e6cfa7084ca26205180a6e7e0548 Mon Sep 17 00:00:00 2001 From: vexorian Date: Mon, 28 Sep 2020 20:07:58 -0400 Subject: [PATCH 03/35] Prepare 1.2.0 development --- README.md | 2 +- src/constants.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0c06485..4e6ea66 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# dizqueTV 1.1.1-prerelease +# dizqueTV 1.2.0-prerelease ![Discord](https://img.shields.io/discord/711313431457693727?logo=discord&logoColor=fff&style=flat-square) ![GitHub top language](https://img.shields.io/github/languages/top/vexorian/dizquetv?logo=github&style=flat-square) ![Docker Pulls](https://img.shields.io/docker/pulls/vexorian/dizquetv?logo=docker&logoColor=fff&style=flat-square) Create live TV channel streams from media on your Plex servers. diff --git a/src/constants.js b/src/constants.js index 731753d..0e5dc5f 100644 --- a/src/constants.js +++ b/src/constants.js @@ -5,5 +5,5 @@ module.exports = { TVGUIDE_MAXIMUM_FLEX_DURATION : 6 * 60 * 60 * 1000, TOO_FREQUENT: 100, - VERSION_NAME: "1.1.1-prerelease" + VERSION_NAME: "1.2.0-prerelease" } From bd0ca01281fb4e656481b94f610146002b498740 Mon Sep 17 00:00:00 2001 From: vexorian Date: Tue, 29 Sep 2020 11:39:52 -0400 Subject: [PATCH 04/35] Option to limit the framerate of dizqueTV's output --- src/api.js | 12 ++++++++++++ src/database-migration.js | 11 ++++++++++- src/ffmpeg.js | 11 +++++++++-- web/directives/ffmpeg-settings.js | 11 +++++++++++ web/public/templates/ffmpeg-settings.html | 4 ++++ 5 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/api.js b/src/api.js index 006c8ae..cc75df8 100644 --- a/src/api.js +++ b/src/api.js @@ -303,6 +303,10 @@ function api(db, channelDB, fillerDB, xmltvInterval, guideService ) { try { db['ffmpeg-settings'].update({ _id: req.body._id }, req.body) let ffmpeg = db['ffmpeg-settings'].find()[0] + let err = fixupFFMPEGSettings(ffmpeg); + if (typeof(err) !== 'undefined') { + return res.status(400).send(err); + } res.send(ffmpeg) } catch(err) { console.error(err); @@ -323,6 +327,14 @@ function api(db, channelDB, fillerDB, xmltvInterval, guideService ) { }) + function fixupFFMPEGSettings(ffmpeg) { + if (typeof(ffmpeg.maxFPS) === 'undefined') { + ffmpeg.maxFPS = 60; + } else if ( isNaN(ffmpeg.maxFPS) ) { + return "maxFPS should be a number"; + } + } + // PLEX SETTINGS router.get('/api/plex-settings', (req, res) => { try { diff --git a/src/database-migration.js b/src/database-migration.js index 90f9995..ded452d 100644 --- a/src/database-migration.js +++ b/src/database-migration.js @@ -20,7 +20,7 @@ const path = require('path'); var fs = require('fs'); -const TARGET_VERSION = 600; +const TARGET_VERSION = 601; const STEPS = [ // [v, v2, x] : if the current version is v, call x(db), and version becomes v2 @@ -31,6 +31,7 @@ const STEPS = [ [ 400, 500, (db,channels) => splitServersSingleChannels(db, channels) ], [ 500, 501, (db) => fixCorruptedServer(db) ], [ 501, 600, () => extractFillersFromChannels() ], + [ 600, 601, (db) => addFPS(db) ], ] const { v4: uuidv4 } = require('uuid'); @@ -392,6 +393,7 @@ function ffmpeg() { normalizeAudioCodec: true, normalizeResolution: true, normalizeAudio: true, + maxFPS: 60, } } @@ -662,6 +664,13 @@ function extractFillersFromChannels() { } +function addFPS(db) { + let ffmpegSettings = db['ffmpeg-settings'].find()[0]; + let f = path.join(process.env.DATABASE, 'ffmpeg-settings.json'); + ffmpegSettings.maxFPS = 60; + fs.writeFileSync( f, JSON.stringify( [ffmpegSettings] ) ); +} + module.exports = { initDB: initDB, defaultFFMPEG: ffmpeg, diff --git a/src/ffmpeg.js b/src/ffmpeg.js index 801b9ba..59b9485 100644 --- a/src/ffmpeg.js +++ b/src/ffmpeg.js @@ -2,6 +2,7 @@ const spawn = require('child_process').spawn const events = require('events') const MAXIMUM_ERROR_DURATION_MS = 60000; +const REALLY_RIDICULOUSLY_HIGH_FPS_FOR_DIZQUETVS_USECASE = 120; class FFMPEG extends events.EventEmitter { constructor(opts, channel) { @@ -9,7 +10,7 @@ class FFMPEG extends events.EventEmitter { 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 + //this ensures transcoding is completely disabled even if // some settings are true this.opts.normalizeAudio = false; this.opts.normalizeAudioCodec = false; @@ -17,6 +18,7 @@ class FFMPEG extends events.EventEmitter { this.opts.errorScreen = 'kill'; this.opts.normalizeResolution = false; this.opts.audioVolumePercent = 100; + this.opts.maxFPS = REALLY_RIDICULOUSLY_HIGH_FPS_FOR_DIZQUETVS_USECASE; } this.channel = channel this.ffmpegPath = opts.ffmpegPath @@ -129,6 +131,11 @@ class FFMPEG extends events.EventEmitter { // When adding filters, make sure that // videoComplex always begins wiht ; and doesn't end with ; + if ( streamStats.videoFramerate >= this.opts.maxFPS + 0.000001 ) { + videoComplex += `;${currentVideo}fps=${this.opts.maxFPS}[fpchange]`; + currentVideo ="[fpchange]"; + } + // prepare input streams if ( typeof(streamUrl.errorTitle) !== 'undefined') { doOverlay = false; //never show icon in the error screen @@ -263,7 +270,7 @@ class FFMPEG extends events.EventEmitter { currentVideo = "blackpadded"; } let name = "siz"; - if (! this.ensureResolution) { + if (! this.ensureResolution && (beforeSizeChange != '[fpchange]') ) { name = "minsiz"; } videoComplex += `;[${currentVideo}]setsar=1[${name}]`; diff --git a/web/directives/ffmpeg-settings.js b/web/directives/ffmpeg-settings.js index 23dfdc4..cebdf4f 100644 --- a/web/directives/ffmpeg-settings.js +++ b/web/directives/ffmpeg-settings.js @@ -59,6 +59,17 @@ {value:"sine", description:"Beep"}, {value:"silent", description:"No Audio"}, ] + scope.fpsOptions = [ + {id: 23.976, description: "23.976 frames per second"}, + {id: 24, description: "24 frames per second"}, + {id: 25, description: "25 frames per second"}, + {id: 29.97, description: "29.97 frames per second"}, + {id: 30, description: "30 frames per second"}, + {id: 50, description: "50 frames per second"}, + {id: 59.94, description: "59.94 frames per second"}, + {id: 60, description: "60 frames per second"}, + {id: 120, description: "120 frames per second"}, + ]; } } } \ No newline at end of file diff --git a/web/public/templates/ffmpeg-settings.html b/web/public/templates/ffmpeg-settings.html index 2dec9a5..61d8735 100644 --- a/web/public/templates/ffmpeg-settings.html +++ b/web/public/templates/ffmpeg-settings.html @@ -91,6 +91,10 @@
+ + + Will transcode videos that have FPS higher than this.
From f719fc2a91092c165181b8e88536a16a9f444ff5 Mon Sep 17 00:00:00 2001 From: vexorian Date: Fri, 2 Oct 2020 23:16:55 -0400 Subject: [PATCH 05/35] Channel Editor Rework --- web/directives/channel-config.js | 76 ++- web/public/style.css | 45 ++ web/public/templates/channel-config.html | 680 +++++++++++------------ web/public/templates/filler-config.html | 2 +- web/public/views/channels.html | 2 +- web/public/views/filler.html | 2 +- 6 files changed, 448 insertions(+), 359 deletions(-) diff --git a/web/directives/channel-config.js b/web/directives/channel-config.js index 3c9fca1..a0a51d1 100644 --- a/web/directives/channel-config.js +++ b/web/directives/channel-config.js @@ -11,6 +11,9 @@ module.exports = function ($timeout, $location, dizquetv) { }, link: function (scope, element, attrs) { scope.maxSize = 50000; + + scope.blockCount = 1; + scope.showShuffleOptions = false; scope.hasFlex = false; scope.showHelp = false; @@ -18,6 +21,7 @@ module.exports = function ($timeout, $location, dizquetv) { scope._frequencyMessage = ""; scope.minProgramIndex = 0; scope.libraryLimit = 50000; + scope.displayPlexLibrary = false; scope.episodeMemory = { saved : false, }; @@ -98,6 +102,27 @@ module.exports = function ($timeout, $location, dizquetv) { updateChannelDuration(); setTimeout( () => { scope.showRotatedNote = true }, 1, 'funky'); } + if (scope.isNewChannel) { + scope.tab = "basic"; + } else { + scope.tab = "programming"; + } + + scope.getTitle = () => { + if (scope.isNewChannel) { + return "Create Channel"; + } else { + let x = "?"; + if ( (scope.channel.number != null) && ( typeof(scope.channel.number) !== 'undefined') && (! isNaN(scope.channel.number) ) ) { + x = "" + scope.channel.number; + } + let y = "Unnamed"; + if (typeof(scope.channel.name) !== 'undefined') { + y = scope.channel.name; + } + return `${x} - ${y}`; + } + } scope._selectedRedirect = { isOffline : true, @@ -1073,25 +1098,34 @@ module.exports = function ($timeout, $location, dizquetv) { // validate var now = new Date() scope.error.any = true; - if (typeof channel.number === "undefined" || channel.number === null || channel.number === "") + if (typeof channel.number === "undefined" || channel.number === null || channel.number === "") { scope.error.number = "Select a channel number" - else if (channelNumbers.indexOf(parseInt(channel.number, 10)) !== -1 && scope.isNewChannel) // we need the parseInt for indexOf to work properly + scope.error.tab = "basic"; + } else if (channelNumbers.indexOf(parseInt(channel.number, 10)) !== -1 && scope.isNewChannel) { // we need the parseInt for indexOf to work properly scope.error.number = "Channel number already in use." - else if (!scope.isNewChannel && channel.number !== scope.beforeEditChannelNumber && channelNumbers.indexOf(parseInt(channel.number, 10)) !== -1) + scope.error.tab = "basic"; + } else if (!scope.isNewChannel && channel.number !== scope.beforeEditChannelNumber && channelNumbers.indexOf(parseInt(channel.number, 10)) !== -1) { scope.error.number = "Channel number already in use." - else if (channel.number < 0 || channel.number > 9999) + scope.error.tab = "basic"; + } else if (channel.number < 0 || channel.number > 9999) { scope.error.name = "Enter a valid number (0-9999)" - else if (typeof channel.name === "undefined" || channel.name === null || channel.name === "") + scope.error.tab = "basic"; + } else if (typeof channel.name === "undefined" || channel.name === null || channel.name === "") { scope.error.name = "Enter a channel name." - else if (channel.icon !== "" && !validURL(channel.icon)) + scope.error.tab = "basic"; + } else if (channel.icon !== "" && !validURL(channel.icon)) { scope.error.icon = "Please enter a valid image URL. Or leave blank." - else if (channel.overlayIcon && !validURL(channel.icon)) + scope.error.tab = "basic"; + } else if (channel.overlayIcon && !validURL(channel.icon)) { scope.error.icon = "Please enter a valid image URL. Cant overlay an invalid image." - else if (now < channel.startTime) + scope.error.tab = "basic"; + } else if (now < channel.startTime) { scope.error.startTime = "Start time must not be set in the future." - else if (channel.programs.length === 0) + scope.error.tab = "programming"; + } else if (channel.programs.length === 0) { scope.error.programs = "No programs have been selected. Select at least one program." - else { + scope.error.tab = "programming"; + } else { scope.error.any = false; for (let i = 0; i < scope.channel.programs.length; i++) { delete scope.channel.programs[i].$index; @@ -1101,6 +1135,7 @@ module.exports = function ($timeout, $location, dizquetv) { if (s.length > 50*1000*1000) { scope.error.any = true; scope.error.programs = "Channel is too large, can't save."; + scope.error.tab = "programming"; } else { await scope.onDone(JSON.parse(s)) s = null; @@ -1110,6 +1145,7 @@ module.exports = function ($timeout, $location, dizquetv) { console.error(err); scope.error.any = true; scope.error.programs = "Unable to save channel." + scope.error.tab = "programming"; } } $timeout(() => { scope.error = {} }, 60000) @@ -1202,6 +1238,26 @@ module.exports = function ($timeout, $location, dizquetv) { }; scope.loadChannels(); + scope.setTool = (toolName) => { + scope.tool = toolName; + } + + scope.hasPrograms = () => { + return scope.channel.programs.length > 0; + } + + scope.showPlexLibrary = () => { + scope.displayPlexLibrary = true; + } + + scope.toggleTools = () => { + scope.showShuffleOptions = !scope.showShuffleOptions + } + + scope.toggleToolHelp = () => { + scope.showHelp= !scope.showHelp + } + scope.disablePadding = () => { return (scope.paddingOption.id==-1) || (2*scope.channel.programs.length > scope.maxSize); } diff --git a/web/public/style.css b/web/public/style.css index 266cd05..ae4e554 100644 --- a/web/public/style.css +++ b/web/public/style.css @@ -1,5 +1,10 @@ .pull-right { float: right; } +.modal-semi-body { + padding: 1rem; + flex: 1 1 auto; +} + .commercials-panel { background-color: rgb(70, 70, 70); border-top: 1px solid #daa104; @@ -249,6 +254,42 @@ table.tvguide { text-align: right; } +div.channel-tools { + max-height: 20em; + overflow-y: scroll; + overflow-x: hidden; + margin-bottom: 1.5rem; + padding-left: 1.5rem; + padding-right: 1.5rem; + border-top: 1px solid #888; + border-bottom: 1px solid #888; +} +div.channel-tools p { + font-size: 0.5rem; + margin-top: 0.01rem; +} + +div.programming-panes { + padding-top: 0; + padding-bottom: 0; +} +div.programming-panes div.programming-pane { + max-height: 30rem; + overflow-y: auto; + padding-top: 0; + padding-bottom: 0; + padding-left: 1.5rem; + padding-right: 1.5rem; +} + +div.programming-programs div.list-group-item { + height: 1.5rem; +} +.channel-editor-modal { + width:1200px; + min-width: 98%; +} + /* Safari */ @-webkit-keyframes spin { 0% { -webkit-transform: rotate(0deg); } @@ -260,3 +301,7 @@ table.tvguide { 100% { transform: rotate(360deg); } } + +.program-row:nth-child(odd), .filler-row:nth-child(odd), .channel-row:nth-child(odd) { + background-color: #eeeeee; +} \ No newline at end of file diff --git a/web/public/templates/channel-config.html b/web/public/templates/channel-config.html index f6fdc75..402814e 100644 --- a/web/public/templates/channel-config.html +++ b/web/public/templates/channel-config.html @@ -1,13 +1,35 @@