From dd00a11b92a0541e8d8fdd30befe1e5da1d3db14 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Mon, 12 May 2025 17:54:47 -0400 Subject: [PATCH] Started work on HLS Livestreaming. Created basic HLS Livestream Media Handler. --- src/app/channel/media/queue.js | 162 +++++++++++++++++++++------------ src/server.js | 8 +- src/views/channel.ejs | 2 +- www/css/theme/movie-night.css | 5 + www/js/channel/mediaHandler.js | 101 ++++++++++++++++++-- www/js/channel/player.js | 2 + 6 files changed, 208 insertions(+), 72 deletions(-) diff --git a/src/app/channel/media/queue.js b/src/app/channel/media/queue.js index 6638f57..0887723 100644 --- a/src/app/channel/media/queue.js +++ b/src/app/channel/media/queue.js @@ -235,67 +235,76 @@ module.exports = class{ } async goLive(socket, data){ - //Grab the channel from DB - const chanDB = await channelModel.findOne({name:this.channel.name}); + try{ + //Grab the channel from DB + const chanDB = await channelModel.findOne({name:this.channel.name}); - let title = "Livestream"; + let title = "Livestream"; - if(data != null && data.title != null){ - //If the title is too long - if(!validator.isLength(data.title, {max:30})){ - //Bitch, moan, complain... - loggerUtils.socketErrorHandler(socket, "Title too long!", "validation"); - //and ignore it! - return; + if(data != null && data.title != null){ + //If the title is too long + if(!validator.isLength(data.title, {max:30})){ + //Bitch, moan, complain... + loggerUtils.socketErrorHandler(socket, "Title too long!", "validation"); + //and ignore it! + return; + } + + //Set title + title = validator.escape(validator.trim(data.title)); + + //If we've got no title + if(title == null || title == ''){ + title = "Livestream"; + } } - //Set title - title = validator.escape(validator.trim(data.title)); - - //If we've got no title - if(title == null || title == ''){ - title = "Livestream"; + //If we couldn't find the channel + if(chanDB == null){ + //FUCK + throw loggerUtils.exceptionSmith(`Unable to find channel document ${this.channel.name} while queue item!`, "queue"); } + + //Kill schedule timers to prevent items from starting during the stream + await this.stopScheduleTimers(); + + //Syntatic sugar because I'm lazy :P + const streamURL = chanDB.settings.streamURL; + + if(streamURL == ''){ + throw loggerUtils.exceptionSmith('This channel\'s HLS Livestream Source has not been set!', 'queue'); + } + + + //Pull filename from streamURL + let filename = streamURL.match(/^.+\..+\/(.+)$/); + + //If we're streaming from the root of the domain + if(filename == null){ + //Set filename to root + filename = '/'; + }else{ + //Otherwise, hand over the filename + filename = filename[1]; + } + + //Create queued media object from stream URL and set it to nowPlaying + this.nowPlaying = new queuedMedia( + title, + filename, + streamURL, + streamURL, + "livehls", + 0, + streamURL, + new Date().getTime() + ); + + //Broadcast new media object to users + this.sendMedia(); + }catch(err){ + return loggerUtils.socketExceptionHandler(socket, err); } - - //If we couldn't find the channel - if(chanDB == null){ - //FUCK - throw loggerUtils.exceptionSmith(`Unable to find channel document ${this.channel.name} while queue item!`, "queue"); - } - - //Kill schedule timers to prevent items from starting during the stream - await this.stopScheduleTimers(); - - //Syntatic sugar because I'm lazy :P - const streamURL = chanDB.settings.streamURL; - - //Pull filename from streamURL - let filename = streamURL.match(/^.+\..+\/(.+)$/); - - //If we're streaming from the root of the domain - if(filename == null){ - //Set filename to root - filename = '/'; - }else{ - //Otherwise, hand over the filename - filename = filename[1]; - } - - //Create queued media object from stream URL and set it to nowPlaying - this.nowPlaying = new queuedMedia( - title, - filename, - streamURL, - streamURL, - "livehls", - 0, - streamURL, - new Date().getTime() - ); - - //Broadcast new media object to users - this.sendMedia(); } //--- INTERNAL USE ONLY QUEUEING FUNCTIONS --- @@ -553,8 +562,11 @@ module.exports = class{ //otherwise }else{ try{ - //DO everything ourselves since we don't have a fance end() function to do it - chanDB = await channelModel.findOne({name:this.channel.name}); + //If we wheren't handed a channel + if(chanDB == null){ + //DO everything ourselves since we don't have a fance end() function to do it + chanDB = await channelModel.findOne({name:this.channel.name}); + } //If we couldn't find the channel if(chanDB == null){ @@ -814,6 +826,9 @@ module.exports = class{ //Send play signal out to the channel this.sendMedia(); + //Kill existing sync timers to prevent kicking-off ghost timer loops + clearTimeout(this.syncTimer); + //Kick off the sync timer this.syncTimer = setTimeout(this.sync.bind(this), this.syncDelta); @@ -879,9 +894,19 @@ module.exports = class{ this.server.io.in(this.channel.name).emit('end', {}); } + //If we're ending an HLS Livestream + if(wasPlaying.type == "livehls"){ + //Redirect to the endLivestream function + return this.endLivestream(chanDB); + } + + //If we're not in volatile mode and we're not ending a livestream if(!volatile){ - //Now that everything is clean, we can take our time with the DB :P - chanDB = await channelModel.findOne({name:this.channel.name}); + //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){ @@ -898,12 +923,13 @@ module.exports = class{ //Take it out of the active schedule this.schedule.delete(wasPlaying.startTime); + //If archiving is enabled if(!noArchive){ //Add the item to the channel archive chanDB.media.archived.push(wasPlaying); } - //broadcast queue using unsaved archive + //broadcast queue using unsaved archive, run this before chanDB.save() for better responsiveness this.broadcastQueue(chanDB); //Save our changes to the DB @@ -918,6 +944,22 @@ module.exports = class{ } } + async endLivestream(chanDB){ + try{ + //Refresh next timer + this.refreshNextTimer(); + + //Broadcast Queue + this.broadcastQueue(); + //ACK + }catch(err){ + //Broadcast queue + this.broadcastQueue(); + //Handle the error + loggerUtils.localExceptionHandler(err); + } + } + async stop(chanDB){ //If we wheren't handed a channel if(chanDB == null){ diff --git a/src/server.js b/src/server.js index f6dcd98..f584830 100644 --- a/src/server.js +++ b/src/server.js @@ -160,8 +160,6 @@ app.use('/passwordReset', passwordResetRouter); app.use('/emailChange', emailChangeRouter); //Panel app.use('/panel', panelRouter); -//Popup -//app.use('/popup', popupRouter); //tooltip app.use('/tooltip', tooltipRouter); //Bot-Ready @@ -169,9 +167,9 @@ app.use('/api', apiRouter); //Static File Server //Serve client-side libraries -app.use('/lib/bootstrap-icons',express.static(path.join(__dirname, '../node_modules/bootstrap-icons'))); -app.use('/lib/altcha',express.static(path.join(__dirname, '../node_modules/altcha/dist_external'))); -app.use('/lib/hls.js',express.static(path.join(__dirname, '../node_modules/hls.js/dist'))); +app.use('/lib/bootstrap-icons',express.static(path.join(__dirname, '../node_modules/bootstrap-icons'))); //Icon set +app.use('/lib/altcha',express.static(path.join(__dirname, '../node_modules/altcha/dist_external'))); //Self-Hosted PoW-based Captcha +app.use('/lib/hls.js',express.static(path.join(__dirname, '../node_modules/hls.js/dist'))); //HLS Media Handler //Server public 'www' folder app.use(express.static(path.join(__dirname, '../www'))); diff --git a/src/views/channel.ejs b/src/views/channel.ejs index d66fdc3..561702d 100644 --- a/src/views/channel.ejs +++ b/src/views/channel.ejs @@ -34,7 +34,7 @@ along with this program. If not, see . %> <%- include('partial/scripts', {user}); %> <%# 3rd party code %> - + <%# 1st party code %> <%# admin gunk %> diff --git a/www/css/theme/movie-night.css b/www/css/theme/movie-night.css index 1f28d4e..6df7a64 100644 --- a/www/css/theme/movie-night.css +++ b/www/css/theme/movie-night.css @@ -172,6 +172,11 @@ textarea{ box-shadow: var(--danger-glow0-alt1); } +.critical-danger-text{ + color: var(--danger0-alt1); + text-shadow: var(--danger-glow0); +} + .danger-link, .danger-text{ color: var(--danger0); } diff --git a/www/js/channel/mediaHandler.js b/www/js/channel/mediaHandler.js index 4f64926..aa272c5 100644 --- a/www/js/channel/mediaHandler.js +++ b/www/js/channel/mediaHandler.js @@ -141,6 +141,10 @@ class mediaHandler{ //reset self act flag this.selfAct = false; } + + onBuffer(){ + this.selfAct = true; + } } //Basic building blocks for anything that touches a