diff --git a/package.json b/package.json
index ecc3f6a..c3c1bc6 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,7 @@
"express": "^4.18.2",
"express-session": "^1.18.0",
"express-validator": "^7.2.0",
+ "hls.js": "^1.6.2",
"mongoose": "^8.4.3",
"node-cron": "^3.0.3",
"nodemailer": "^6.9.16",
diff --git a/src/app/channel/media/queue.js b/src/app/channel/media/queue.js
index af7cc67..6638f57 100644
--- a/src/app/channel/media/queue.js
+++ b/src/app/channel/media/queue.js
@@ -65,6 +65,7 @@ module.exports = class{
socket.on("clear", (data) => {this.deleteRange(socket, data)});
socket.on("move", (data) => {this.moveMedia(socket, data)});
socket.on("lock", () => {this.toggleLock(socket)});
+ socket.on("goLive", (data) => {this.goLive(socket, data)});
}
//--- USER FACING QUEUEING FUNCTIONS ---
@@ -128,32 +129,16 @@ module.exports = class{
}
async stopMedia(socket){
- //Get the current channel from the database
- const chanDB = await channelModel.findOne({name: socket.chan});
+ try{
+ //Get the current channel from the database
+ const chanDB = await channelModel.findOne({name: socket.chan});
- //Permcheck to make sure the user can fuck w/ the queue
- if((!this.locked && await chanDB.permCheck(socket.user, 'scheduleMedia')) || await chanDB.permCheck(socket.user, 'scheduleAdmin')){
-
- //If we're not currently playing anything
- if(this.nowPlaying == null){
- //If an originating socket was provided for this request
- if(socket != null){
- //Yell at the user for being an asshole
- loggerUtils.socketErrorHandler(socket, "No media playing!", "queue");
- }
-
- //Ignore it
- return false;
+ //Permcheck to make sure the user can fuck w/ the queue
+ if((!this.locked && await chanDB.permCheck(socket.user, 'scheduleMedia')) || await chanDB.permCheck(socket.user, 'scheduleAdmin')){
+ await this.stop();
}
-
- //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;
-
- //End the media
- this.end();
+ }catch(err){
+ return loggerUtils.socketExceptionHandler(socket, err);
}
}
@@ -249,7 +234,90 @@ module.exports = class{
}
}
+ async goLive(socket, data){
+ //Grab the channel from DB
+ const chanDB = await channelModel.findOne({name:this.channel.name});
+
+ 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;
+ }
+
+ //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;
+
+ //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 ---
+ async stopScheduleTimers(){
+ //End any currently playing media media
+ await this.end();
+
+ //Clear sync timer
+ clearTimeout(this.syncTimer);
+ //Clear next timer
+ clearTimeout(this.nextTimer);
+ //Clear the pre-switch timer
+ clearTimeout(this.preSwitchTimer);
+
+ //Null out the sync timer
+ this.syncTimer = null;
+ //Null out the next playing item timer
+ this.nextTimer = null;
+ //Null out the pre-switch timer
+ this.preSwitchTimer = null;
+ }
+
getStart(start){
//Pull current time
const now = new Date().getTime();
@@ -421,8 +489,11 @@ module.exports = class{
//If we got a bad request
if(media == null){
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){
@@ -781,13 +852,18 @@ module.exports = class{
async end(quiet = false, noArchive = false, volatile = false, chanDB){
try{
+ //If we're not playing anything
+ if(this.nowPlaying == null){
+ //Silently ignore the request
+ return;
+ }
+
//Call off any existing sync timer
clearTimeout(this.syncTimer);
//Clear out the sync timer
this.syncTimer = null;
-
//Keep a copy of whats playing for later when we need to clear the DB
const wasPlaying = this.nowPlaying;
@@ -842,6 +918,40 @@ module.exports = class{
}
}
+ async stop(chanDB){
+ //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 wheren't handed a channel
+ if(chanDB == null){
+ //Complain about the lack of a channel
+ throw loggerUtils.exceptionSmith(`Channel not found!`, "queue");
+ }
+
+ //If we're not currently playing anything
+ if(this.nowPlaying == null){
+ //If an originating socket was provided for this request
+ if(socket != null){
+ //Yell at the user for being an asshole
+ throw loggerUtils.exceptionSmith(`No media playing`, "queue");
+ }
+ //Ignore it
+ return false;
+ }
+
+ //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;
+
+ //End the media
+ this.end();
+ }
+
getItemsBetweenEpochs(start, end){
//Create an empty array to hold found items
const foundItems = [];
diff --git a/src/server.js b/src/server.js
index fc32a3d..f6dcd98 100644
--- a/src/server.js
+++ b/src/server.js
@@ -168,9 +168,10 @@ app.use('/tooltip', tooltipRouter);
app.use('/api', apiRouter);
//Static File Server
-//Serve bootstrap icons
+//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')));
//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 2bd8cc9..d66fdc3 100644
--- a/src/views/channel.ejs
+++ b/src/views/channel.ejs
@@ -34,6 +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/js/channel/mediaHandler.js b/www/js/channel/mediaHandler.js
index 6b03c30..4f64926 100644
--- a/www/js/channel/mediaHandler.js
+++ b/www/js/channel/mediaHandler.js
@@ -158,8 +158,13 @@ class rawFileBase extends mediaHandler{
buildPlayer(){
//Create player
this.video = document.createElement('video');
+
+ //Enable controls
+ this.video.controls = true;
+
//Append it to page
this.player.videoContainer.appendChild(this.video);
+
//Run derived method
super.buildPlayer();
}
@@ -488,4 +493,43 @@ class youtubeEmbedHandler extends mediaHandler{
this.iframe.style.pointerEvents = (lock ? "none" : "");
}
}
+}
+
+class hlsBase extends rawFileBase{
+ constructor(client, player, media, type){
+ //Call derived constructor
+ super(client, player, media, type);
+
+ //Create property to hold HLS object
+ this.hls = null;
+ }
+
+ buildPlayer(){
+ //Call derived player
+ super.buildPlayer();
+
+ //Instantiate HLS object
+ this.hls = new Hls();
+
+ //Load HLS Stream
+ this.hls.loadSource(this.nowPlaying.url);
+
+ //Attatch hls object to video element
+ this.hls.attachMedia(this.video);
+
+ //Bind onMetadataLoad to MANIFEST_PARSED
+ this.hls.on(Hls.Events.MANIFEST_PARSED, this.onMetadataLoad.bind(this));
+ }
+
+ onMetadataLoad(){
+ //Start the video
+ this.video.play();
+ }
+}
+
+class hlsLiveStreamHandler extends hlsBase{
+ constructor(client, player, media){
+ //Call derived constructor
+ super(client, player, media, "livehls");
+ }
}
\ No newline at end of file
diff --git a/www/js/channel/player.js b/www/js/channel/player.js
index 55b8387..009013c 100644
--- a/www/js/channel/player.js
+++ b/www/js/channel/player.js
@@ -89,13 +89,20 @@ class player{
}else{
//If we have a youtube video and the official embedded iframe player is selected
if(data.media.type == 'yt' && localStorage.getItem("ytPlayerType") == 'embed'){
+ //Create a new yt handler for it
this.mediaHandler = new youtubeEmbedHandler(this.client, this, data.media);
+ //Sync to time stamp
+ this.mediaHandler.sync(data.timestamp);
+ //If we have an HLS Livestream
+ }else if(data.media.type == "livehls"){
+ //Create a new HLS Livestream Handler for it
+ this.mediaHandler = new hlsLiveStreamHandler(this.client, this, data.media);
//Otherwise, if we have a raw-file compatible source
}else if(data.media.type == 'ia' || data.media.type == 'raw' || data.media.type == 'yt' || data.media.type == 'dm'){
//Create a new raw file handler for it
this.mediaHandler = new rawFileHandler(client, this, data.media);
//Sync to time stamp
- this.mediaHandler.sync(data.timestamp);
+ this.mediaHandler.sync(data.timestamp);
}else{
this.mediaHandler = new nullHandler(client, this);
}