Started work on HLS livestreaming
This commit is contained in:
parent
93265b7890
commit
c6de68b474
|
|
@ -13,6 +13,7 @@
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-session": "^1.18.0",
|
"express-session": "^1.18.0",
|
||||||
"express-validator": "^7.2.0",
|
"express-validator": "^7.2.0",
|
||||||
|
"hls.js": "^1.6.2",
|
||||||
"mongoose": "^8.4.3",
|
"mongoose": "^8.4.3",
|
||||||
"node-cron": "^3.0.3",
|
"node-cron": "^3.0.3",
|
||||||
"nodemailer": "^6.9.16",
|
"nodemailer": "^6.9.16",
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,7 @@ module.exports = class{
|
||||||
socket.on("clear", (data) => {this.deleteRange(socket, data)});
|
socket.on("clear", (data) => {this.deleteRange(socket, data)});
|
||||||
socket.on("move", (data) => {this.moveMedia(socket, data)});
|
socket.on("move", (data) => {this.moveMedia(socket, data)});
|
||||||
socket.on("lock", () => {this.toggleLock(socket)});
|
socket.on("lock", () => {this.toggleLock(socket)});
|
||||||
|
socket.on("goLive", (data) => {this.goLive(socket, data)});
|
||||||
}
|
}
|
||||||
|
|
||||||
//--- USER FACING QUEUEING FUNCTIONS ---
|
//--- USER FACING QUEUEING FUNCTIONS ---
|
||||||
|
|
@ -128,32 +129,16 @@ module.exports = class{
|
||||||
}
|
}
|
||||||
|
|
||||||
async stopMedia(socket){
|
async stopMedia(socket){
|
||||||
//Get the current channel from the database
|
try{
|
||||||
const chanDB = await channelModel.findOne({name: socket.chan});
|
//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
|
//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((!this.locked && await chanDB.permCheck(socket.user, 'scheduleMedia')) || await chanDB.permCheck(socket.user, 'scheduleAdmin')){
|
||||||
|
await this.stop();
|
||||||
//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;
|
|
||||||
}
|
}
|
||||||
|
}catch(err){
|
||||||
//Stop playing
|
return loggerUtils.socketExceptionHandler(socket, err);
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -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 ---
|
//--- 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){
|
getStart(start){
|
||||||
//Pull current time
|
//Pull current time
|
||||||
const now = new Date().getTime();
|
const now = new Date().getTime();
|
||||||
|
|
@ -421,8 +489,11 @@ module.exports = class{
|
||||||
//If we got a bad request
|
//If we got a bad request
|
||||||
if(media == null){
|
if(media == null){
|
||||||
try{
|
try{
|
||||||
//DO everything ourselves since we don't have a fance end() function to do it
|
//If we wheren't handed a channel
|
||||||
chanDB = await channelModel.findOne({name:this.channel.name});
|
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 we couldn't find the channel
|
||||||
if(chanDB == null){
|
if(chanDB == null){
|
||||||
|
|
@ -781,13 +852,18 @@ module.exports = class{
|
||||||
|
|
||||||
async end(quiet = false, noArchive = false, volatile = false, chanDB){
|
async end(quiet = false, noArchive = false, volatile = false, chanDB){
|
||||||
try{
|
try{
|
||||||
|
//If we're not playing anything
|
||||||
|
if(this.nowPlaying == null){
|
||||||
|
//Silently ignore the request
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
//Call off any existing sync timer
|
//Call off any existing sync timer
|
||||||
clearTimeout(this.syncTimer);
|
clearTimeout(this.syncTimer);
|
||||||
|
|
||||||
//Clear out the sync timer
|
//Clear out the sync timer
|
||||||
this.syncTimer = null;
|
this.syncTimer = null;
|
||||||
|
|
||||||
|
|
||||||
//Keep a copy of whats playing for later when we need to clear the DB
|
//Keep a copy of whats playing for later when we need to clear the DB
|
||||||
const wasPlaying = this.nowPlaying;
|
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){
|
getItemsBetweenEpochs(start, end){
|
||||||
//Create an empty array to hold found items
|
//Create an empty array to hold found items
|
||||||
const foundItems = [];
|
const foundItems = [];
|
||||||
|
|
|
||||||
|
|
@ -168,9 +168,10 @@ app.use('/tooltip', tooltipRouter);
|
||||||
app.use('/api', apiRouter);
|
app.use('/api', apiRouter);
|
||||||
|
|
||||||
//Static File Server
|
//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/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/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
|
//Server public 'www' folder
|
||||||
app.use(express.static(path.join(__dirname, '../www')));
|
app.use(express.static(path.join(__dirname, '../www')));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. %>
|
||||||
<%- include('partial/scripts', {user}); %>
|
<%- include('partial/scripts', {user}); %>
|
||||||
<%# 3rd party code %>
|
<%# 3rd party code %>
|
||||||
<script src="/socket.io/socket.io.min.js"></script>
|
<script src="/socket.io/socket.io.min.js"></script>
|
||||||
|
<script src="/lib/hls.js/hls.js"></script>
|
||||||
<%# 1st party code %>
|
<%# 1st party code %>
|
||||||
<%# admin gunk %>
|
<%# admin gunk %>
|
||||||
<script src="/js/adminUtils.js"></script>
|
<script src="/js/adminUtils.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -158,8 +158,13 @@ class rawFileBase extends mediaHandler{
|
||||||
buildPlayer(){
|
buildPlayer(){
|
||||||
//Create player
|
//Create player
|
||||||
this.video = document.createElement('video');
|
this.video = document.createElement('video');
|
||||||
|
|
||||||
|
//Enable controls
|
||||||
|
this.video.controls = true;
|
||||||
|
|
||||||
//Append it to page
|
//Append it to page
|
||||||
this.player.videoContainer.appendChild(this.video);
|
this.player.videoContainer.appendChild(this.video);
|
||||||
|
|
||||||
//Run derived method
|
//Run derived method
|
||||||
super.buildPlayer();
|
super.buildPlayer();
|
||||||
}
|
}
|
||||||
|
|
@ -489,3 +494,42 @@ class youtubeEmbedHandler extends mediaHandler{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -89,7 +89,14 @@ class player{
|
||||||
}else{
|
}else{
|
||||||
//If we have a youtube video and the official embedded iframe player is selected
|
//If we have a youtube video and the official embedded iframe player is selected
|
||||||
if(data.media.type == 'yt' && localStorage.getItem("ytPlayerType") == 'embed'){
|
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);
|
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
|
//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'){
|
}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
|
//Create a new raw file handler for it
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue