Livestream Database Handling for Overwrite mode complete. Schedule goes back to pre-stream state if server crashes/stops.

This commit is contained in:
rainbow napkin 2025-05-18 17:47:47 -04:00
parent 85c1258bb6
commit 8c8b2a6f0b
6 changed files with 194 additions and 55 deletions

View file

@ -49,6 +49,8 @@ module.exports = class{
this.preSwitchTimer = null;
//Create variable to hold currently playing media object
this.nowPlaying = null;
//Create variable to hold item that was playing during the last liveStream (can't check against full duration since it might've been stopped for other reasons)
this.liveRemainder = null;
//Create variable to lock standard queuing functions during livestreams
this.streamLock = false;
@ -267,8 +269,15 @@ module.exports = class{
throw loggerUtils.exceptionSmith(`Unable to find channel document ${this.channel.name} while queue item!`, "queue");
}
//Capture currently playing object
const wasPlaying = this.nowPlaying;
//If something is playing
if(this.nowPlaying != null){
//Capture currently playing object
this.liveRemainder = this.nowPlaying;
chanDB.media.liveRemainder = this.nowPlaying.uuid;
//Save the chanDB
await chanDB.save();
}
//Kill schedule timers to prevent items from starting during the stream
await this.stopScheduleTimers();
@ -309,13 +318,6 @@ module.exports = class{
//Throw stream lock
this.streamLock = true;
//If something was playing
if(wasPlaying != null){
//Force it back into the schedule w/ saveLate enabled from nowPlaying, since it was ended with noArchive, effectively deleting it
//This is also the easiest way to bring nowPlaying media back into the schedule, since end wants to archive it :P
await this.scheduleMedia([wasPlaying], socket, chanDB, true, false, false, true);
}
//Broadcast new media object to users
this.sendMedia();
}catch(err){
@ -326,7 +328,7 @@ module.exports = class{
//--- INTERNAL USE ONLY QUEUEING FUNCTIONS ---
async stopScheduleTimers(noArchive = true){
//End any currently playing media media w/o archiving
await this.end(false, noArchive);
await this.stop();
//Clear sync timer
clearTimeout(this.syncTimer);
@ -417,7 +419,7 @@ module.exports = class{
}
}
async removeRange(start = new Date().getTime() - 60 * 1000, end = new Date().getTime(), socket){
async removeRange(start = new Date().getTime() - 60 * 1000, end = new Date().getTime(), socket, noUnfinished = false){
//If we're streamlocked
if(this.streamLock){
//If an originating socket was provided for this request
@ -430,7 +432,7 @@ module.exports = class{
}
//Find items within given range
const foundItems = this.getItemsBetweenEpochs(start, end);
const foundItems = this.getItemsBetweenEpochs(start, end, noUnfinished);
try{
//DO everything ourselves since we don't have a fance end() function to do it
@ -969,7 +971,7 @@ module.exports = class{
//If we're ending an HLS Livestream
if(wasPlaying.type == "livehls"){
//Redirect to the endLivestream function
return this.endLivestream(chanDB);
return this.endLivestream(wasPlaying, chanDB)
}
//If we're not in volatile mode and we're not ending a livestream
@ -987,7 +989,7 @@ module.exports = class{
}
//If we haven't changed 'nowPlaying' in the play list
if(chanDB.media.nowPlaying.uuid == wasPlaying.uuid){
if(chanDB.media.nowPlaying != null && chanDB.media.nowPlaying.uuid == wasPlaying.uuid){
//Take it out
await chanDB.media.nowPlaying.deleteOne();
}
@ -1016,29 +1018,7 @@ module.exports = class{
}
}
async endLivestream(chanDB){
try{
//Disable stream lock
this.streamLock = false;
//Refresh next timer
this.refreshNextTimer();
//This is where I'd stick the IF statetement I'd add to switch between overwrite
await this.livestreamOverwriteSchedule(chanDB)
//Broadcast Queue
this.broadcastQueue();
//ACK
}catch(err){
//Broadcast queue
this.broadcastQueue();
//Handle the error
loggerUtils.localExceptionHandler(err);
}
}
async livestreamOverwriteSchedule(chanDB){
async endLivestream(wasPlaying, chanDB){
try{
//If we wheren't handed a channel
if(chanDB == null){
@ -1051,14 +1031,117 @@ module.exports = class{
//FUCK
throw loggerUtils.exceptionSmith(`Unable to find channel document ${this.channel.name} while ending queue item!`, "queue");
}
//Disable stream lock
this.streamLock = false;
//We don't have to here since someone else will do it for us :)
chanDB.media.liveRemainder = null;
//This is where I'd stick the IF statetement I'd add to switch between overwrite
await this.livestreamOverwriteSchedule(wasPlaying, chanDB)
//Refresh next timer
this.refreshNextTimer();
//Broadcast Queue
this.broadcastQueue();
//ACK
}catch(err){
//Broadcast queue
this.broadcastQueue();
//Handle the error
loggerUtils.localExceptionHandler(err);
}
}
async livestreamOverwriteSchedule(wasPlaying, chanDB){
try{
//Get current epoch
const now = new Date().getTime()
//If we wheren't handed a channel
if(chanDB == null){
//Now that everything is clean, we can take our time with the DB :P
chanDB = await channelModel.findOne({name:this.channel.name});
}
//If we couldn't find the channel
if(chanDB == null){
//FUCK
throw loggerUtils.exceptionSmith(`Unable to find channel document ${this.channel.name} while ending queue item!`, "queue");
}
//mark overwrite job as finished so we don't run two sets of logic, as one needs to do a null check before it can run it's conditional
//while the other needs to run regardless of this.liveRemainders definition
let finished = false;
//Set duration from start and end time
wasPlaying.duration = (now - wasPlaying.startTime) / 1000;
//Throw the livestream into the archive
chanDB.media.archived.push(wasPlaying);
//Save the DB
await chanDB.save();
//If we have a live remainder
if(this.liveRemainder != null){
//If the item hasn't ended
if(finished = (this.liveRemainder.getEndTime(true) > now)){
//Rip out early end
this.liveRemainder.earlyEnd = undefined;
//regenerate UUID to differentiate between this and the original item
this.liveRemainder.genUUID();
//Re-schedule the remainder
await this.scheduleMedia([this.liveRemainder], undefined, chanDB);
}
}
//if "THIS ISN'T OVER, PUNK!"
if(!finished){
//Pull item from end
const wasPlayingDuringEnd = this.getItemAtEpoch(now);
//If we ended in the middle of something
if(wasPlayingDuringEnd != null){
const difference = (now - wasPlayingDuringEnd.startTime);
//Take item out
await this.removeMedia(wasPlayingDuringEnd.uuid, null, chanDB);
//Push the item up to match the difference
wasPlayingDuringEnd.startTime += difference;
//re-set start time stamp based on media start and stream end
wasPlayingDuringEnd.startTimeStamp = Math.round(difference / 1000);
//Make unique, true
wasPlayingDuringEnd.genUUID();
//Re-schedule media now that it's been cut
await this.scheduleMedia([wasPlayingDuringEnd], null, chanDB);
}
//Remove all the in-betweeners
await this.removeRange(wasPlaying.startTime, now, null, true);
}
//Null out live remainder for the next stream
this.liveRemainder = null;
}catch(err){
//Null out live remainder for the next stream
this.liveRemainder = null;
//Handle the error
loggerUtils.localExceptionHandler(err);
}
}
stop(){
stop(socket){
//If we're not currently playing anything
if(this.nowPlaying == null){
//If an originating socket was provided for this request
@ -1073,14 +1156,17 @@ module.exports = class{
//Stop playing
const stoppedMedia = this.nowPlaying;
//Get difference between current time and start time and set as early end
stoppedMedia.earlyEnd = (new Date().getTime() - stoppedMedia.startTime) / 1000;
//Ignore early end for livestreams
if(this.nowPlaying.type != 'livehls'){
//Get difference between current time and start time and set as early end
stoppedMedia.earlyEnd = (new Date().getTime() - stoppedMedia.startTime) / 1000;
}
//End the media
this.end();
}
getItemsBetweenEpochs(start, end){
getItemsBetweenEpochs(start, end, noUnfinished = false){
//Create an empty array to hold found items
const foundItems = [];
@ -1088,8 +1174,11 @@ module.exports = class{
for(let item of this.schedule){
//If the item starts after our start date and before our end date
if(item[0] >= start && item[0] <= end ){
//Add the current item to the list
foundItems.push(item[1]);
//If we're allowed to add unifnished items, or the item has finished
if(!noUnfinished || item[1].getEndTime() <= end){
//Add the current item to the list
foundItems.push(item[1]);
}
}
}
@ -1207,6 +1296,12 @@ module.exports = class{
//If the media started within the last 24 hours
if(media.startTime > yesterday){
//If we're sending out the live remainder during a live stream
if(this.liveRemainder != null && media.uuid.toString() == this.liveRemainder.uuid.toString()){
//Throw out the early end before sending it off, so it looks like it hasn't been cut off yet (smoke n mirrors :P)
media.earlyEnd = null;
}
//Add it to the schedule array as if it where part of the actual schedule map
schedule.push([media.startTime, media]);
//Otherwise if it's older
@ -1277,7 +1372,7 @@ module.exports = class{
//Add record to new schedule
newSched.push(record);
//Re-Schedule it in RAM
//Re-Schedule it in RAM, with the start function running w/ DB transactions enabled, since it won't happen right away
await this.scheduleMedia([mediaObj], null, chanDB, true, true, false);
}else{
//If the media should be playing now
@ -1285,7 +1380,7 @@ module.exports = class{
//Save record to nowPlaying in the DB
chanDB.media.nowPlaying = record;
//Re-Schedule it in RAM
//Schedule the fucker in RAM, w/ the start function also running in RAM-Only mode
await this.scheduleMedia([mediaObj], null, chanDB, true, true, true);
//If it's been ended
}else{
@ -1295,6 +1390,39 @@ module.exports = class{
}
}
//If we have a remainder from a livestream
if(chanDB.media.liveRemainder){
//Iterate backwards through the archive to pull the newest first, since that's probably where this fucker is
for(let archiveIndex = (chanDB.media.archived.length - 1); archiveIndex > 0; archiveIndex--){
//Grab the current media object
const archivedMedia = chanDB.media.archived[archiveIndex];
//If the current object matches our remainder UUID
if((archivedMedia.uuid.toString() == chanDB.media.liveRemainder.toString())){
//Null out any early end
archivedMedia.earlyEnd = null;
//Re-hydrate the item
const archivedMediaObject = archivedMedia.rehydrate();
//if we still have a video to finish
if(archivedMediaObject.getEndTime() > now){
//Set the fucker as now playing
chanDB.media.nowPlaying = archivedMediaObject;
//Schedule the fucker in RAM, w/ the start function also running in RAM-Only mode
this.scheduleMedia([archivedMediaObject], null, chanDB, true, true, true);
//Splice the fucker out of the archive
chanDB.media.archived.splice(archiveIndex, 1);
}
//Break out of the loop
break;
}
}
}
//Update schedule to only contain what hasn't been played yet
chanDB.media.scheduled = newSched;

View file

@ -18,7 +18,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
const media = require('./media');
module.exports = class extends media{
constructor(title, fileName, url, id, type, duration, rawLink, startTime, startTimeStamp, earlyEnd, uuid){
constructor(title, fileName, url, id, type, duration, rawLink, startTime, startTimeStamp = 0, earlyEnd, uuid){
//Call derived constructor
super(title, fileName, url, id, type, duration, rawLink);
//Set media start time
@ -77,9 +77,9 @@ module.exports = class extends media{
this.uuid = crypto.randomUUID();
}
getEndTime(){
getEndTime(fullTime = false){
//If we have an early ending
if(this.earlyEnd == null){
if(this.earlyEnd == null || fullTime){
//Calculate our ending
return this.startTime + ((this.duration - this.startTimeStamp) * 1000);
}else{

View file

@ -111,7 +111,11 @@ const channelSchema = new mongoose.Schema({
scheduled: [queuedMediaSchema],
//We should consider moving archived media and channel playlists to their own collections/models for preformances sake
archived: [queuedMediaSchema],
playlists: [playlistSchema]
playlists: [playlistSchema],
liveRemainder: {
type: mongoose.SchemaTypes.UUID,
required: false
}
},
//Thankfully we don't have to keep track of alts, ips, or deleted users so this should be a lot easier than site-wide bans :P
banList: [channelBanSchema]

View file

@ -104,6 +104,11 @@ div.queue-entry{
user-select: none;
}
div.queue-entry.live{
z-index: 2;
}
div.queue-entry p{
z-index: 2;
pointer-events: none;

View file

@ -47,6 +47,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
--danger0-alt2: rgb(242, 189, 189);
--danger-glow0: 2px 2px 3px var(--danger0), -2px 2px 3px var(--danger0), 2px -2px 3px var(--danger0), -2px -2px 3px var(--danger0);
--danger-glow0-alt1: 2px 2px 3px var(--danger0-alt1), -2px 2px 3px var(--danger0-alt1), 2px -2px 3px var(--danger0-alt1), -2px -2px 3px var(--danger0-alt1);
--danger-glow0-smol: 2px 2px -1px var(--danger0), -2px 2px -1px var(--danger0), 2px -2px -1px var(--danger0), -2px -2px -1px var(--danger0);
--timer-glow: -2px 1px 3px var(--danger0-alt1), 2px -1px 3px var(--danger0-alt1);

View file

@ -616,8 +616,8 @@ class queuePanel extends panelObj{
menuMap.set("Play now", ()=>{this.client.socket.emit('move', {uuid: entry[1].uuid})});
//Add 'Move To...' option to context menu
menuMap.set("Move To...", (event)=>{new reschedulePopup(event, this.client, entry[1], null, this.ownerDoc)});
//Otherwise, if the item is currently playing
}else if(this.getMediaEnd(entry[1]) > now.getTime()){
//Otherwise, if the item is currently playing (confirm with UUID since time might not always be reliable, such as during livestreams)
}else if(entry[1].uuid == this.client.player.mediaHandler.nowPlaying.uuid){
//Add 'Stop' option to context menu
menuMap.set("Stop", ()=>{this.client.socket.emit('stop')});
//Add the Now Playing glow, not the prettiest place to add this, but why let a good conditional go to waste?
@ -1068,8 +1068,8 @@ class queuePanel extends panelObj{
const entryTitle = document.createElement('p');
entryTitle.textContent = utils.unescapeEntities(nowPlaying.title);
//Set entry div bottom-border location based on current time
entryDiv.style.bottom = `${this.offsetByDate(date, true)}px`
//Set entry div bottom-border location based on current time, round to match time marker
entryDiv.style.bottom = `${Math.round(this.offsetByDate(date, true))}px`
//Assembly entryDiv
entryDiv.appendChild(entryTitle);
@ -1100,8 +1100,8 @@ class queuePanel extends panelObj{
//Append entry div to queue container
this.queueContainer.appendChild(entryDiv);
}else{
//Update existing entry
staleEntry.style.bottom = `${this.offsetByDate(date, true)}px`
//Update existing entry, round offset to match time marker
staleEntry.style.bottom = `${Math.round(this.offsetByDate(date, true))}px`
}
//Keep tooltip date seperate so it re-calculates live duration properly
@ -1206,6 +1206,7 @@ class queuePanel extends panelObj{
}
getMediaEnd(media){
console.log(media);
//If we have an early end
if(media.earlyEnd != null){
return media.startTime + (media.earlyEnd * 1000);