diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..90a253c --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +zlib License + +Copyright (c) 2020 Dan Ferguson, Victor Hugo Soliz Kuncar + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. diff --git a/README.md b/README.md index 1006a4a..555a8fe 100644 --- a/README.md +++ b/README.md @@ -69,3 +69,8 @@ npm run dev-server * Pull requests welcome but please read the [Code of Conduct](CODE_OF_CONDUCT.md) and the [Pull Request Template](pull_request_template.md) first. * Tip Jar: https://buymeacoffee.com/vexorian + +## License + + * Original pseudotv-Plex code was released under [MIT license (c) 2020 Dan Ferguson](https://github.com/DEFENDORe/pseudotv/blob/665e71e24ee5e93d9c9c90545addb53fdc235ff6/LICENSE) + * dizqueTV's improvements are released under zlib license (c) 2020 Victor Hugo Soliz Kuncar diff --git a/src/channel-cache.js b/src/channel-cache.js index 5889dcc..2a9fbdd 100644 --- a/src/channel-cache.js +++ b/src/channel-cache.js @@ -32,10 +32,21 @@ function getCurrentLineupItem(channelId, t1) { let recorded = cache[channelId]; let lineupItem = JSON.parse( JSON.stringify(recorded.lineupItem) ); let diff = t1 - recorded.t0; - if ( (diff <= SLACK) && (lineupItem.duration >= 2*SLACK) ) { + let rem = lineupItem.duration - lineupItem.start; + if (typeof(lineupItem.streamDuration) !== 'undefined') { + rem = Math.min(rem, lineupItem.streamDuration); + } + if ( (diff <= SLACK) && (diff + SLACK < rem) ) { //closed the stream and opened it again let's not lose seconds for //no reason - return lineupItem; + let originalT0 = recorded.lineupItem.originalT0; + if (typeof(originalT0) === 'undefined') { + originalT0 = recorded.t0; + } + if (t1 - originalT0 <= SLACK) { + lineupItem.originalT0 = originalT0; + return lineupItem; + } } lineupItem.start += diff; diff --git a/src/constants.js b/src/constants.js index fd2fcd3..731753d 100644 --- a/src/constants.js +++ b/src/constants.js @@ -3,6 +3,7 @@ module.exports = { TVGUIDE_MAXIMUM_PADDING_LENGTH_MS: 30*60*1000, STEALTH_DURATION: 5 * 60* 1000, TVGUIDE_MAXIMUM_FLEX_DURATION : 6 * 60 * 60 * 1000, + TOO_FREQUENT: 100, VERSION_NAME: "1.1.1-prerelease" } diff --git a/src/plex-player.js b/src/plex-player.js index baf5c9c..73ba96f 100644 --- a/src/plex-player.js +++ b/src/plex-player.js @@ -82,18 +82,38 @@ class PlexPlayer { let emitter = new EventEmitter(); //setTimeout( () => { let ff = await ffmpeg.spawnStream(stream.streamUrl, stream.streamStats, streamStart, streamDuration, enableChannelIcon, lineupItem.type); // Spawn the ffmpeg process - ff.pipe(outStream); + ff.pipe(outStream, {'end':false} ); //}, 100); plexTranscoder.startUpdatingPlex(); - + ffmpeg.on('end', () => { emitter.emit('end'); }); ffmpeg.on('close', () => { emitter.emit('close'); }); - ffmpeg.on('error', (err) => { + ffmpeg.on('error', async (err) => { + console.log("Replacing failed stream with error streram"); + ff.unpipe(outStream); + ffmpeg.removeAllListeners('data'); + ffmpeg.removeAllListeners('end'); + ffmpeg.removeAllListeners('error'); + ffmpeg.removeAllListeners('close'); + ffmpeg = new FFMPEG(ffmpegSettings, channel); // Set the transcoder options + ffmpeg.on('close', () => { + emitter.emit('close'); + }); + ffmpeg.on('end', () => { + emitter.emit('end'); + }); + ffmpeg.on('error', (err) => { + emitter.emit('error', err ); + }); + + ff = await ffmpeg.spawnError('oops', 'oops', Math.min(streamStats.duration, 60000) ); + ff.pipe(outStream); + emitter.emit('error', err); }); return emitter; diff --git a/src/throttler.js b/src/throttler.js new file mode 100644 index 0000000..543cbe9 --- /dev/null +++ b/src/throttler.js @@ -0,0 +1,45 @@ +let constants = require('./constants'); + +let cache = {} + + +function equalItems(a, b) { + if ( (typeof(a) === 'undefined') || a.isOffline || b.isOffline ) { + return false; + } + console.log("no idea how to compare this: " + JSON.stringify(a) ); + console.log(" with this: " + JSON.stringify(b) ); + return true; + +} + + +function wereThereTooManyAttempts(sessionId, lineupItem) { + let obj = cache[sessionId]; + let t1 = (new Date()).getTime(); + if (typeof(obj) === 'undefined') { + previous = cache[sessionId] = { + t0: t1 - constants.TOO_FREQUENT * 5 + }; + + } else { + clearTimeout(obj.timer); + } + previous.timer = setTimeout( () => { + cache[sessionId].timer = null; + delete cache[sessionId]; + }, constants.TOO_FREQUENT*5 ); + + let result = false; + + if (previous.t0 + constants.TOO_FREQUENT >= t1) { + //certainly too frequent + result = equalItems( previous.lineupItem, lineupItem ); + } + cache[sessionId].t0 = t1; + cache[sessionId].lineupItem = lineupItem; + return result; + +} + +module.exports = wereThereTooManyAttempts; \ No newline at end of file diff --git a/src/video.js b/src/video.js index 0219cd4..4ebab8f 100644 --- a/src/video.js +++ b/src/video.js @@ -6,9 +6,12 @@ const PlexTranscoder = require('./plexTranscoder') const fs = require('fs') const ProgramPlayer = require('./program-player'); const channelCache = require('./channel-cache') +const wereThereTooManyAttempts = require('./throttler'); module.exports = { router: video } +let StreamCount = 0; + function video( channelDB , fillerDB, db) { var router = express.Router() @@ -107,7 +110,7 @@ function video( channelDB , fillerDB, db) { let channelNum = parseInt(req.query.channel, 10) let ff = await ffmpeg.spawnConcat(`http://localhost:${process.env.PORT}/playlist?channel=${channelNum}`); - ff.pipe(res); + ff.pipe(res ); }) // Stream individual video to ffmpeg concat above. This is used by the server, NOT the client router.get('/stream', async (req, res) => { @@ -116,6 +119,7 @@ function video( channelDB , fillerDB, db) { res.status(400).send("No Channel Specified") return } + let session = parseInt(req.query.session); let m3u8 = (req.query.m3u8 === '1'); let number = parseInt(req.query.channel); let channel = await channelCache.getChannelConfig(channelDB, number); @@ -275,6 +279,14 @@ function video( channelDB , fillerDB, db) { if (! isLoading) { channelCache.recordPlayback(channel.number, t0, lineupItem); } + if (wereThereTooManyAttempts(session, lineupItem)) { + lineupItem = { + isOffline: true, + err: Error("Too many attempts, throttling.."), + duration : 60000, + }; + } + let playerContext = { lineupItem : lineupItem, @@ -330,6 +342,8 @@ function video( channelDB , fillerDB, db) { router.get('/m3u8', async (req, res) => { + let sessionId = StreamCount++; + //res.type('application/vnd.apple.mpegurl') res.type("application/x-mpegURL"); @@ -364,13 +378,13 @@ function video( channelDB , fillerDB, db) { if ( ffmpegSettings.enableFFMPEGTranscoding === true) { //data += `#EXTINF:${cur},\n`; - data += `${req.protocol}://${req.get('host')}/stream?channel=${channelNum}&first=0&m3u8=1\n`; + data += `${req.protocol}://${req.get('host')}/stream?channel=${channelNum}&first=0&m3u8=1&session=${sessionId}\n`; } //data += `#EXTINF:${cur},\n`; - data += `${req.protocol}://${req.get('host')}/stream?channel=${channelNum}&first=1&m3u8=1\n` + data += `${req.protocol}://${req.get('host')}/stream?channel=${channelNum}&first=1&m3u8=1&session=${sessionId}\n` for (var i = 0; i < maxStreamsToPlayInARow - 1; i++) { //data += `#EXTINF:${cur},\n`; - data += `${req.protocol}://${req.get('host')}/stream?channel=${channelNum}&m3u8=1\n` + data += `${req.protocol}://${req.get('host')}/stream?channel=${channelNum}&m3u8=1&session=${sessionId}\n` } res.send(data) @@ -399,6 +413,8 @@ function video( channelDB , fillerDB, db) { let ffmpegSettings = db['ffmpeg-settings'].find()[0] + let sessionId = StreamCount++; + if ( (ffmpegSettings.enableFFMPEGTranscoding === true) && (ffmpegSettings.normalizeVideoCodec === true) @@ -406,11 +422,11 @@ function video( channelDB , fillerDB, db) { && (ffmpegSettings.normalizeResolution === true) && (ffmpegSettings.normalizeAudio === true) ) { - data += `file 'http://localhost:${process.env.PORT}/stream?channel=${channelNum}&first=0'\n`; + data += `file 'http://localhost:${process.env.PORT}/stream?channel=${channelNum}&first=0&session=${sessionId}'\n`; } - data += `file 'http://localhost:${process.env.PORT}/stream?channel=${channelNum}&first=1'\n` + data += `file 'http://localhost:${process.env.PORT}/stream?channel=${channelNum}&first=1&session=${sessionId}'\n` for (var i = 0; i < maxStreamsToPlayInARow - 1; i++) { - data += `file 'http://localhost:${process.env.PORT}/stream?channel=${channelNum}'\n` + data += `file 'http://localhost:${process.env.PORT}/stream?channel=${channelNum}&session=${sessionId}'\n` } res.send(data) diff --git a/web/directives/channel-config.js b/web/directives/channel-config.js index 84109c5..40af884 100644 --- a/web/directives/channel-config.js +++ b/web/directives/channel-config.js @@ -1096,8 +1096,9 @@ module.exports = function ($timeout, $location, dizquetv) { } scope.importPrograms = (selectedPrograms) => { - for (let i = 0, l = selectedPrograms.length; i < l; i++) - selectedPrograms[i].commercials = [] + for (let i = 0, l = selectedPrograms.length; i < l; i++) { + delete selectedPrograms[i].commercials; + } scope.channel.programs = scope.channel.programs.concat(selectedPrograms) updateChannelDuration() setTimeout( diff --git a/web/directives/program-config.js b/web/directives/program-config.js index ce50987..f274eb5 100644 --- a/web/directives/program-config.js +++ b/web/directives/program-config.js @@ -9,13 +9,6 @@ module.exports = function ($timeout) { onDone: "=onDone" }, link: function (scope, element, attrs) { - scope.selectedCommercials = (items) => { - scope.program.commercials = scope.program.commercials.concat(items) - for (let i = 0, l = scope.program.commercials.length; i < l; i++) { - if (typeof scope.program.commercials[i].commercialPosition === 'undefined') - scope.program.commercials[i].commercialPosition = 0 - } - } scope.finished = (prog) => { if (prog.title === "") scope.error = { title: 'You must set a program title.' } @@ -37,9 +30,6 @@ module.exports = function ($timeout) { return } - prog.duration = prog.duration - for (let i = 0, l = prog.commercials.length; i < l; i++) - prog.duration += prog.commercials[i].duration scope.onDone(JSON.parse(angular.toJson(prog))) scope.program = null } diff --git a/web/public/templates/program-config.html b/web/public/templates/program-config.html index 3577ea7..ce48dab 100644 --- a/web/public/templates/program-config.html +++ b/web/public/templates/program-config.html @@ -84,5 +84,5 @@ - + \ No newline at end of file