Implemented back-end for basic time-based queueing system.
This commit is contained in:
parent
f38eae170d
commit
4f6b3318a0
11 changed files with 329 additions and 140 deletions
|
|
@ -16,6 +16,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
|
|||
|
||||
//Local imports
|
||||
const queuedMedia = require('./queuedMedia');
|
||||
const yanker = require('../../../utils/media/yanker');
|
||||
const loggerUtils = require('../../../utils/loggerUtils');
|
||||
|
||||
module.exports = class{
|
||||
constructor(server, channel){
|
||||
|
|
@ -23,23 +25,137 @@ module.exports = class{
|
|||
this.server = server
|
||||
//Set channel
|
||||
this.channel = channel;
|
||||
|
||||
//Create map to hold currently queued media
|
||||
this.schedule = new Map();
|
||||
|
||||
//Create variable to hold sync delta in ms
|
||||
this.syncDelta = 1000;
|
||||
//Create variable to hold current timestamp within the video
|
||||
this.timestamp = 0;
|
||||
|
||||
//Create variable to hold sync timer
|
||||
this.syncTimer = null;
|
||||
//Create variable to hold next playing item timer
|
||||
this.nextTimer = null;
|
||||
//Create variable to hold currently playing media object
|
||||
this.nowPlaying = null;
|
||||
|
||||
}
|
||||
|
||||
queueMedia(inputMedia){
|
||||
//Create new media object, set start time to now
|
||||
const mediaObj = queuedMedia.fromMedia(inputMedia, new Date().getTime());
|
||||
defineListeners(socket){
|
||||
socket.on("queue", (data) => {this.queueURL(socket, data)});
|
||||
}
|
||||
|
||||
//Start playback
|
||||
this.start(mediaObj);
|
||||
|
||||
async queueURL(socket, data){
|
||||
try{
|
||||
//pull URL and start time from data
|
||||
let {url, start} = data;
|
||||
|
||||
//Pull media list
|
||||
const mediaList = await yanker.yankMedia(url);
|
||||
|
||||
//Queue the first media object given
|
||||
this.queueMedia(mediaList[0], start, socket);
|
||||
}catch(err){
|
||||
return loggerUtils.socketExceptionHandler(socket, err);
|
||||
}
|
||||
}
|
||||
|
||||
//Default start time to now + half a second to give everyone time to process shit
|
||||
queueMedia(inputMedia, start = new Date().getTime() + 50, socket){
|
||||
//Create a new media queued object, set start time to now
|
||||
const mediaObj = queuedMedia.fromMedia(inputMedia, start);
|
||||
|
||||
//schedule the media
|
||||
this.scheduleMedia(mediaObj, socket);
|
||||
|
||||
//Refresh the next timer to ensure whatever comes on next is right
|
||||
this.refreshNextTimer();
|
||||
}
|
||||
|
||||
refreshNextTimer(){
|
||||
//Grab the next item
|
||||
const nextItem = this.getNextItem();
|
||||
|
||||
//If we have no next item
|
||||
if(nextItem == null){
|
||||
//Fuck off and die
|
||||
return;
|
||||
}
|
||||
|
||||
//Calculate the amount of time in ms that the next item will start in
|
||||
const startsIn = nextItem.startTime - new Date().getTime();
|
||||
|
||||
//Clear out any item that might be up next
|
||||
clearTimeout(this.nextTimer);
|
||||
//Set the next timer
|
||||
this.nextTimer = setTimeout(()=>{this.start(nextItem)}, startsIn);
|
||||
}
|
||||
|
||||
scheduleMedia(mediaObj, socket){
|
||||
/* This is a fun method and I think it deserves it's own little explination...
|
||||
Since we're working with a time based schedule, using start epochs as keys for our iterable seemed the best option
|
||||
I don't want to store everything in a sparse array because that *feels* icky, and would probably be a pain in the ass.
|
||||
Maps seem like a good choice, if it wheren't for the issue of keeping them ordered...
|
||||
|
||||
That's where this comes in. You see if we temporarily store it in a sparse array and convert into a map,
|
||||
we can quickly and easily create a properly sorted schedule map that, out side of adding items, behaves normally.
|
||||
|
||||
Also a note on preformance:
|
||||
While .forEach ONLY runs through populated items in sparse arrays, many JS implementations run through them in the background,
|
||||
simply skipping them before executing the provided function. Looping through object.keys(arr), however, avoids this entirely,
|
||||
since it ONLY loops through defiened items within the array. No skipped empties for your runtime to worry about.
|
||||
Even more preformance benefits can be had by using a real for loop on the arrays keys, skipping the overhead of forEach entirely.
|
||||
This might seem gross but it completely avoids the computational workload of a sorting algo, especially when you consider
|
||||
that, no matter what, re-ordering the schedule map would've required us to iterate through and convert it to an array and back anyways...
|
||||
|
||||
|
||||
Also it looks like due to implementation limitations, epochs stored as MS are too large for array elements, so we store them as seconds.
|
||||
This also means that our current implementation will break exactly on unix epoch 4294967295 (Feb 7, 2106 6:28:15 AM UTC)
|
||||
Hopefully javascript arrays will allow for larger lengths by then. If not blame the W3C :P
|
||||
|
||||
If for some reason they haven't we could probably implement an object that wraps a 2d array and set/gets it using modulo/devision/multiplication
|
||||
|
||||
Further Reading:
|
||||
https://stackoverflow.com/questions/59480871/foreach-vs-object-keys-foreach-performance-on-sparse-arrays
|
||||
https://community.appsmith.com/content/blog/dark-side-foreach-why-you-should-think-twice-using-it
|
||||
*/
|
||||
|
||||
//If there's already something queued right now
|
||||
if(this.getItemAtEpoch(mediaObj.startTime) != null){
|
||||
//If an originating socket was provided for this request
|
||||
if(socket != null){
|
||||
//Yell at the user for being an asshole
|
||||
loggerUtils.socketErrorHandler(socket, "This time slot has already been taken in the queue!", "queue");
|
||||
}
|
||||
//Ignore it
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
//Create an empty temp array to sparsley populate with our schedule
|
||||
const tempSchedule = [];
|
||||
//Create new map to replace our current schedule map
|
||||
const newSchedule = new Map();
|
||||
|
||||
//For every item that's already been scheduled
|
||||
for(let item of this.schedule){
|
||||
//add it to the slot corresponding to it's start epoch in seconds
|
||||
tempSchedule[Math.round(item[0] / 1000)] = item[1];
|
||||
}
|
||||
|
||||
//Inject the media object into the slot corresponding to it's epoch in the temp schedule array
|
||||
tempSchedule[Math.round(mediaObj.startTime / 1000)] = mediaObj;
|
||||
|
||||
//For every populated key in our array
|
||||
for(let startTime of Object.keys(tempSchedule)){
|
||||
//Add item to replacement schedule map
|
||||
newSchedule.set(tempSchedule[startTime].startTime, tempSchedule[startTime]);
|
||||
}
|
||||
|
||||
//Replace the existing schedule map with our new one
|
||||
this.schedule = newSchedule;
|
||||
}
|
||||
|
||||
start(mediaObj){
|
||||
|
|
@ -53,10 +169,13 @@ module.exports = class{
|
|||
this.nowPlaying = mediaObj;
|
||||
|
||||
//Send play signal out to the channel
|
||||
this.sendQueue();
|
||||
this.sendMedia();
|
||||
|
||||
//Kick off the sync timer
|
||||
this.syncTimer = setTimeout(this.sync.bind(this), this.syncDelta);
|
||||
|
||||
//Setup the next video
|
||||
this.refreshNextTimer();
|
||||
}
|
||||
|
||||
sync(){
|
||||
|
|
@ -99,7 +218,56 @@ module.exports = class{
|
|||
}
|
||||
}
|
||||
|
||||
sendQueue(socket){
|
||||
getItemAtEpoch(epoch = new Date().getTime()){
|
||||
//Loop through scheduled items
|
||||
for(let item of this.schedule){
|
||||
//If we're past or at the start time and at or before the end time
|
||||
if(item[0] <= epoch && item[1].getEndTime() >= epoch){
|
||||
//return the current item
|
||||
return item[1]
|
||||
}
|
||||
}
|
||||
|
||||
//If we fell through the loop return null
|
||||
return null;
|
||||
}
|
||||
|
||||
getLastItem(epoch = new Date().getTime()){
|
||||
//Create variable to hold the last item
|
||||
let last;
|
||||
|
||||
//Loop through scheduled items
|
||||
for(let item of this.schedule){
|
||||
//If we've stumbled on to the next item
|
||||
if(item[0] >= epoch){
|
||||
//Break the loop
|
||||
break;
|
||||
//If we've stumbled upon an item that is currently playing
|
||||
}else if(item[1].getEndTime() >= epoch){
|
||||
//Break the loop
|
||||
break;
|
||||
//If we made it through this iteration without breaking the loop
|
||||
}
|
||||
|
||||
//Set current item to last item
|
||||
last = item[1];
|
||||
}
|
||||
|
||||
//If the loop has been broken or fallen through, return last.
|
||||
return last;
|
||||
}
|
||||
|
||||
getNextItem(epoch = new Date().getTime()){
|
||||
//Iterate through the schedule
|
||||
for(let item of this.schedule){
|
||||
if(item[0] >= epoch){
|
||||
//Pull the scheduled media object from the map entry array
|
||||
return item[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sendMedia(socket){
|
||||
//Create data object
|
||||
const data = {
|
||||
media: this.nowPlaying,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue