* Fix Plex mobile apps spamming a new notification every time a video plays.

* Loading Screen.
* Minor log improvements.
* Minor defaults improvements.
* FFMPEG concat process to use 1 thread.
* Fix av1 bug when plex has to transcode the audio.
* /m3u8 endpoint
This commit is contained in:
vexorian 2020-08-14 23:31:28 -04:00
parent db70e56129
commit b54b5d9112
10 changed files with 254 additions and 24 deletions

View File

@ -15,7 +15,16 @@ const Plex = require('./src/plex');
const channelCache = require('./src/channel-cache');
const constants = require('./src/constants')
console.log("dizqueTV Version: " + constants.VERSION_NAME)
console.log(
` \\
dizqueTV ${constants.VERSION_NAME}
.------------.
|###:::||| o |
|###:::||| |
'###:::||| o |
'------------'
`);
for (let i = 0, l = process.argv.length; i < l; i++) {
if ((process.argv[i] === "-p" || process.argv[i] === "--port") && i + 1 !== l)
@ -37,7 +46,7 @@ if (!fs.existsSync(process.env.DATABASE)) {
if(!fs.existsSync(path.join(process.env.DATABASE, 'images')))
fs.mkdirSync(path.join(process.env.DATABASE, 'images'))
db.connect(process.env.DATABASE, ['channels', 'plex-servers', 'ffmpeg-settings', 'plex-settings', 'xmltv-settings', 'hdhr-settings', 'db-version'])
db.connect(process.env.DATABASE, ['channels', 'plex-servers', 'ffmpeg-settings', 'plex-settings', 'xmltv-settings', 'hdhr-settings', 'db-version', 'client-id'])
initDB(db)
@ -65,7 +74,7 @@ let xmltvInterval = {
if (plexServers[i].arChannels && channels.length !== 0)
plex.RefreshChannels(channels, dvrs).then(() => { }, (err) => { console.error(err, i) })
}).catch( (err) => {
console.error("There was an error when fetching Plex DVRs. This means dizqueTV couldn't trigger Plex to update its TV guide." + err);
console.log("Couldn't tell Plex to refresh channels for some reason.");
});
}
}, (err) => {
@ -123,6 +132,9 @@ function initDB(db) {
let data = fs.readFileSync(path.resolve(path.join(__dirname, 'resources/generic-offline-screen.png')))
fs.writeFileSync(process.env.DATABASE + '/images/generic-offline-screen.png', data)
}
if (!fs.existsSync(process.env.DATABASE + '/images/loading-screen.png')) {
let data = fs.readFileSync(path.resolve(path.join(__dirname, 'resources/loading-screen.png')))
fs.writeFileSync(process.env.DATABASE + '/images/loading-screen.png', data)
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -17,15 +17,29 @@
* but with time it will be worth it, really.
*
***/
const TARGET_VERSION = 300;
const TARGET_VERSION = 400;
const STEPS = [
// [v, v2, x] : if the current version is v, call x(db), and version becomes v2
[ 0, 100, (db) => basicDB(db) ],
[ 100, 200, (db) => commercialsRemover(db) ],
[ 200, 300, (db) => appNameChange(db) ],
[ 300, 400, (db) => createDeviceId(db) ],
]
const { v4: uuidv4 } = require('uuid');
function createDeviceId(db) {
let deviceId = db['client-id'].find();
if (deviceId.length == 0) {
let clientId = uuidv4().replace(/-/g,"").slice(0,16) + "-org-dizquetv-" + process.platform
let dev = {
clientId: clientId,
}
db['client-id'].save( dev );
}
}
function appNameChange(db) {
let xmltv = db['xmltv-settings'].find()
@ -66,7 +80,7 @@ function basicDB(db) {
maxPlayableResolution: "1920x1080",
maxTranscodeResolution: "1920x1080",
videoCodecs: 'h264,hevc,mpeg2video',
audioCodecs: 'ac3',
audioCodecs: 'ac3,aac',
maxAudioChannels: '2',
audioBoost: '100',
enableSubtitles: false,
@ -105,7 +119,7 @@ function basicDB(db) {
let hdhrSettings = db['hdhr-settings'].find()
if (hdhrSettings.length === 0) {
db['hdhr-settings'].save({
tunerCount: 1,
tunerCount: 2,
autoDiscovery: true
})
}

View File

@ -73,8 +73,9 @@ class FFMPEG extends events.EventEmitter {
this.spawn( {errorTitle: 'offline'}, streamStats, undefined, `${duration}ms`, true, false, 'offline', false);
}
async spawn(streamUrl, streamStats, startTime, duration, limitRead, enableIcon, type, isConcatPlaylist) {
let ffmpegArgs = [
`-threads`, this.opts.threads,
`-threads`, isConcatPlaylist? 1 : this.opts.threads,
`-fflags`, `+genpts+discardcorrupt+igndts`];
if (limitRead === true)
@ -94,6 +95,17 @@ class FFMPEG extends events.EventEmitter {
//TODO: Do something about missing audio stream
if (!isConcatPlaylist) {
let inputFiles = 0;
let audioFile = -1;
let videoFile = -1;
let overlayFile = -1;
if ( typeof(streamUrl.errorTitle) === 'undefined') {
ffmpegArgs.push(`-i`, streamUrl);
videoFile = inputFiles++;
audioFile = videoFile;
}
// When we have an individual stream, there is a pipeline of possible
// filters to apply.
//
@ -108,8 +120,8 @@ class FFMPEG extends events.EventEmitter {
// Initially, videoComplex does nothing besides assigning the label
// to the input stream
var videoIndex = 'v';
var audioComplex = `;[0:${audioIndex}]anull[audio]`;
var videoComplex = `;[0:${videoIndex}]null[video]`;
var audioComplex = `;[${audioFile}:${audioIndex}]anull[audio]`;
var videoComplex = `;[${videoFile}:${videoIndex}]null[video]`;
// Depending on the options we will apply multiple filters
// each filter modifies the current video stream. Adds a filter to
// the videoComplex variable. The result of the filter becomes the
@ -197,11 +209,10 @@ class FFMPEG extends events.EventEmitter {
audioComplex += ';[audioy]arealtime[audiox]';
currentVideo = "[videox]";
currentAudio = "[audiox]";
} else {
ffmpegArgs.push(`-i`, streamUrl);
}
if (doOverlay) {
ffmpegArgs.push(`-i`, `${this.channel.icon}` );
overlayFile = inputFiles++;
}
// Resolution fix: Add scale filter, current stream becomes [siz]
@ -223,7 +234,7 @@ class FFMPEG extends events.EventEmitter {
if (this.channel.iconDuration > 0)
icnDur = `:enable='between(t,0,${this.channel.iconDuration})'`
videoComplex += `;[1:v]scale=${this.channel.iconWidth}:-1[icn];${currentVideo}[icn]overlay=${posAry[this.channel.iconPosition]}${icnDur}[comb]`
videoComplex += `;[${overlayFile}:v]scale=${this.channel.iconWidth}:-1[icn];${currentVideo}[icn]overlay=${posAry[this.channel.iconPosition]}${icnDur}[comb]`
currentVideo = '[comb]';
}
if (this.volumePercent != 100) {
@ -250,14 +261,14 @@ class FFMPEG extends events.EventEmitter {
transcodeVideo = true; //this is useful so that it adds some lines below
filterComplex += videoComplex;
} else {
currentVideo = `0:${videoIndex}`;
currentVideo = `${videoFile}:${videoIndex}`;
}
// same with audio:
if (currentAudio != '[audio]') {
transcodeAudio = true;
filterComplex += audioComplex;
} else {
currentAudio = `0:${audioIndex}`;
currentAudio = `${audioFile}:${audioIndex}`;
}
//If there is a filter complex, add it.
@ -309,7 +320,7 @@ class FFMPEG extends events.EventEmitter {
} else {
//Concat stream is simpler and should always copy the codec
ffmpegArgs.push(
`-probesize`, `100000000`,
`-probesize`, 32 /*`100000000`*/,
`-i`, streamUrl,
`-map`, `0:v`,
`-map`, `0:${audioIndex}`,

View File

@ -13,6 +13,11 @@ class OfflinePlayer {
constructor(error, context) {
this.context = context;
this.error = error;
if (context.isLoading === true) {
context.channel = JSON.parse( JSON.stringify(context.channel) );
context.channel.offlinePicture = `http://localhost:${process.env.PORT}/images/loading-screen.png`;
context.channel.offlineSoundtrack = undefined;
}
this.ffmpeg = new FFMPEG(context.ffmpegSettings, context.channel);
}

View File

@ -10,6 +10,8 @@ const EventEmitter = require('events');
const helperFuncs = require('./helperFuncs')
const FFMPEG = require('./ffmpeg')
let USED_CLIENTS = {};
class PlexPlayer {
constructor(context) {
@ -17,9 +19,17 @@ class PlexPlayer {
this.ffmpeg = null;
this.plexTranscoder = null;
this.killed = false;
let coreClientId = this.context.db['client-id'].find()[0].clientId;
let i = 0;
while ( USED_CLIENTS[coreClientId+"-"+i]===true) {
i++;
}
this.clientId = coreClientId+"-"+i;
USED_CLIENTS[this.clientId] = true;
}
cleanUp() {
USED_CLIENTS[this.clientId] = false;
this.killed = true;
if (this.plexTranscoder != null) {
this.plexTranscoder.stopUpdatingPlex();
@ -39,7 +49,7 @@ class PlexPlayer {
try {
let plexSettings = db['plex-settings'].find()[0];
let plexTranscoder = new PlexTranscoder(plexSettings, channel, lineupItem);
let plexTranscoder = new PlexTranscoder(this.clientId, plexSettings, channel, lineupItem);
this.plexTranscoder = plexTranscoder;
let enableChannelIcon = this.context.enableChannelIcon;
let ffmpeg = new FFMPEG(ffmpegSettings, channel); // Set the transcoder options

View File

@ -2,12 +2,12 @@ const { v4: uuidv4 } = require('uuid');
const axios = require('axios');
class PlexTranscoder {
constructor(settings, channel, lineupItem) {
constructor(clientId, settings, channel, lineupItem) {
this.session = uuidv4()
this.device = "channel-" + channel.number;
this.deviceName = this.device;
this.clientIdentifier = this.session.replace(/-/g,"").slice(0,16) + "-org-dizquetv-" + process.platform;
this.clientIdentifier = clientId;
this.product = "dizqueTV";
this.settings = settings
@ -60,7 +60,10 @@ class PlexTranscoder {
stream.directPlay = true;
}
}
if (stream.directPlay) {
if (stream.directPlay || this.isAV1() ) {
if (! stream.directPlay) {
this.log("Plex doesn't support av1, so we are forcing direct play, including for audio because otherwise plex breaks the stream.")
}
this.log("Direct play forced or native paths enabled")
stream.directPlay = true
this.setTranscodingArgs(stream.directPlay, true, false)
@ -78,14 +81,15 @@ class PlexTranscoder {
await this.getDecision(stream.directPlay);
stream.streamUrl = `${this.transcodeUrlBase}${this.transcodingArgs}`
} else {
//This case sounds complex. Apparently plex is sending us just the audio, so we would need to get the video in a separate stream.
this.log("Decision: Direct stream. Audio is being transcoded")
stream.separateVideoStream = (this.settings.streamPath === 'direct') ? this.file : this.plexFile;
stream.streamUrl = `${this.transcodeUrlBase}${this.transcodingArgs}`
}
stream.streamStats = this.getVideoStats();
// use correct audio stream if direct play
let audioIndex = await this.getAudioIndex();
stream.streamStats.audioIndex = (stream.directPlay) ? audioIndex : 'a'
stream.streamStats.audioIndex = (stream.directPlay) ? ( await this.getAudioIndex() ) : 'a'
this.log(stream)
@ -178,6 +182,14 @@ lang=en`
}
}
isAV1() {
try {
return this.getVideoStats().videoCodec === 'av1';
} catch (e) {
return false;
}
}
isDirectPlay() {
try {
return this.getVideoStats().videoDecision === "copy" && this.getVideoStats().audioDecision === "copy";

View File

@ -32,6 +32,11 @@ class ProgramPlayer {
if (program.err instanceof Error) {
console.log("About to play error stream");
this.delegate = new OfflinePlayer(true, context);
} else if (program.type === 'loading') {
console.log("About to play loading stream");
/* loading */
context.isLoading = true;
this.delegate = new OfflinePlayer(false, context);
} else if (program.type === 'offline') {
console.log("About to play offline stream");
/* offline */

107
src/svg/loading-screen.svg Normal file
View File

@ -0,0 +1,107 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="1920"
height="1080"
viewBox="0 0 507.99999 285.75001"
version="1.1"
id="svg8"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
sodipodi:docname="loading-screen.svg"
inkscape:export-filename="/home/vx/dev/pseudotv/resources/loading-screen.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.37433674"
inkscape:cx="1004.7641"
inkscape:cy="545.11626"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1056"
inkscape:window-x="0"
inkscape:window-y="24"
inkscape:window-maximized="1"
units="px" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Capa 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-11.249983)">
<rect
style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:3.20000029;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect836"
width="508"
height="285.75"
x="0"
y="11.249983" />
<g
id="g6050"
transform="translate(-8.4960767,30.053154)">
<rect
transform="rotate(0.52601418)"
y="85.000603"
x="214.56714"
height="73.832573"
width="32.814484"
id="rect4518"
style="opacity:1;fill:#9cbc28;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
<rect
transform="rotate(1.4727575)"
style="opacity:1;fill:#289bbc;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect4520"
width="32.81448"
height="73.832573"
x="248.74632"
y="80.901688" />
<rect
transform="rotate(-3.2986121)"
y="103.78287"
x="269.35843"
height="73.832588"
width="32.814476"
id="rect4522"
style="opacity:1;fill:#bc289b;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
</g>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:10.58333302px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="228.70648"
y="210.99644"
id="text939"><tspan
sodipodi:role="line"
id="tspan937"
x="228.70648"
y="210.99644"
style="fill:#f9f9f9;stroke-width:0.26458332px">Loading...</tspan></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -143,6 +143,11 @@ function video(db) {
res.status(404).send("Channel doesn't exist")
return
}
let isLoading = false;
if ( (typeof req.query.first !== 'undefined') && (req.query.first=='0') ) {
isLoading = true;
}
let isFirst = false;
if ( (typeof req.query.first !== 'undefined') && (req.query.first=='1') ) {
isFirst = true;
@ -164,7 +169,14 @@ function video(db) {
// Get video lineup (array of video urls with calculated start times and durations.)
let t0 = (new Date()).getTime();
let lineupItem = channelCache.getCurrentLineupItem( channel.number, t0);
if (lineupItem == null) {
if (isLoading) {
lineupItem = {
type: 'loading',
streamDuration: 1000,
duration: 1000,
start: 0,
};
} else if (lineupItem == null) {
let prog = helperFuncs.getCurrentProgramAndTimeElapsed(t0, channel)
if (prog.program.isOffline && channel.programs.length == 1) {
@ -207,7 +219,9 @@ function video(db) {
}
console.log("=========================================================");
channelCache.recordPlayback(channel.number, t0, lineupItem);
if (! isLoading) {
channelCache.recordPlayback(channel.number, t0, lineupItem);
}
let playerContext = {
lineupItem : lineupItem,
@ -280,6 +294,41 @@ function video(db) {
});
});
router.get('/m3u8', (req, res) => {
res.type('text')
// Check if channel queried is valid
if (typeof req.query.channel === 'undefined') {
res.status(500).send("No Channel Specified")
return
}
let channelNum = parseInt(req.query.channel, 10)
let channel = channelCache.getChannelConfig(db, channelNum );
if (channel.length === 0) {
res.status(500).send("Channel doesn't exist")
return
}
// Maximum number of streams to concatinate beyond channel starting
// If someone passes this number then they probably watch too much television
let maxStreamsToPlayInARow = 100;
var data = "#EXTM3U\n"
let ffmpegSettings = db['ffmpeg-settings'].find()[0]
if ( ffmpegSettings.enableFFMPEGTranscoding === true) {
data += `${req.protocol}://${req.get('host')}/stream?channel=${channelNum}&first=0\n`;
}
data += `${req.protocol}://${req.get('host')}/stream?channel=${channelNum}&first=1\n`
for (var i = 0; i < maxStreamsToPlayInARow - 1; i++) {
data += `${req.protocol}://${req.get('host')}/stream?channel=${channelNum}\n`
}
res.send(data)
})
router.get('/playlist', (req, res) => {
res.type('text')
@ -302,6 +351,11 @@ function video(db) {
var data = "ffconcat version 1.0\n"
let ffmpegSettings = db['ffmpeg-settings'].find()[0]
if ( ffmpegSettings.enableFFMPEGTranscoding === 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=1'\n`
for (var i = 0; i < maxStreamsToPlayInARow - 1; i++) {
data += `file 'http://localhost:${process.env.PORT}/stream?channel=${channelNum}'\n`