Started work on HLS Livestreaming. Created basic HLS Livestream Media Handler.
This commit is contained in:
parent
c6de68b474
commit
dd00a11b92
|
|
@ -235,6 +235,7 @@ module.exports = class{
|
||||||
}
|
}
|
||||||
|
|
||||||
async goLive(socket, data){
|
async goLive(socket, data){
|
||||||
|
try{
|
||||||
//Grab the channel from DB
|
//Grab the channel from DB
|
||||||
const chanDB = await channelModel.findOne({name:this.channel.name});
|
const chanDB = await channelModel.findOne({name:this.channel.name});
|
||||||
|
|
||||||
|
|
@ -270,6 +271,11 @@ module.exports = class{
|
||||||
//Syntatic sugar because I'm lazy :P
|
//Syntatic sugar because I'm lazy :P
|
||||||
const streamURL = chanDB.settings.streamURL;
|
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
|
//Pull filename from streamURL
|
||||||
let filename = streamURL.match(/^.+\..+\/(.+)$/);
|
let filename = streamURL.match(/^.+\..+\/(.+)$/);
|
||||||
|
|
||||||
|
|
@ -296,6 +302,9 @@ module.exports = class{
|
||||||
|
|
||||||
//Broadcast new media object to users
|
//Broadcast new media object to users
|
||||||
this.sendMedia();
|
this.sendMedia();
|
||||||
|
}catch(err){
|
||||||
|
return loggerUtils.socketExceptionHandler(socket, err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//--- INTERNAL USE ONLY QUEUEING FUNCTIONS ---
|
//--- INTERNAL USE ONLY QUEUEING FUNCTIONS ---
|
||||||
|
|
@ -553,8 +562,11 @@ module.exports = class{
|
||||||
//otherwise
|
//otherwise
|
||||||
}else{
|
}else{
|
||||||
try{
|
try{
|
||||||
|
//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
|
//DO everything ourselves since we don't have a fance end() function to do it
|
||||||
chanDB = await channelModel.findOne({name:this.channel.name});
|
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){
|
||||||
|
|
@ -814,6 +826,9 @@ module.exports = class{
|
||||||
//Send play signal out to the channel
|
//Send play signal out to the channel
|
||||||
this.sendMedia();
|
this.sendMedia();
|
||||||
|
|
||||||
|
//Kill existing sync timers to prevent kicking-off ghost timer loops
|
||||||
|
clearTimeout(this.syncTimer);
|
||||||
|
|
||||||
//Kick off the sync timer
|
//Kick off the sync timer
|
||||||
this.syncTimer = setTimeout(this.sync.bind(this), this.syncDelta);
|
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', {});
|
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){
|
if(!volatile){
|
||||||
|
//If we wheren't handed a channel
|
||||||
|
if(chanDB == null){
|
||||||
//Now that everything is clean, we can take our time with the DB :P
|
//Now that everything is clean, we can take our time with the DB :P
|
||||||
chanDB = await channelModel.findOne({name:this.channel.name});
|
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){
|
||||||
|
|
@ -898,12 +923,13 @@ module.exports = class{
|
||||||
//Take it out of the active schedule
|
//Take it out of the active schedule
|
||||||
this.schedule.delete(wasPlaying.startTime);
|
this.schedule.delete(wasPlaying.startTime);
|
||||||
|
|
||||||
|
//If archiving is enabled
|
||||||
if(!noArchive){
|
if(!noArchive){
|
||||||
//Add the item to the channel archive
|
//Add the item to the channel archive
|
||||||
chanDB.media.archived.push(wasPlaying);
|
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);
|
this.broadcastQueue(chanDB);
|
||||||
|
|
||||||
//Save our changes to the DB
|
//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){
|
async stop(chanDB){
|
||||||
//If we wheren't handed a channel
|
//If we wheren't handed a channel
|
||||||
if(chanDB == null){
|
if(chanDB == null){
|
||||||
|
|
|
||||||
|
|
@ -160,8 +160,6 @@ app.use('/passwordReset', passwordResetRouter);
|
||||||
app.use('/emailChange', emailChangeRouter);
|
app.use('/emailChange', emailChangeRouter);
|
||||||
//Panel
|
//Panel
|
||||||
app.use('/panel', panelRouter);
|
app.use('/panel', panelRouter);
|
||||||
//Popup
|
|
||||||
//app.use('/popup', popupRouter);
|
|
||||||
//tooltip
|
//tooltip
|
||||||
app.use('/tooltip', tooltipRouter);
|
app.use('/tooltip', tooltipRouter);
|
||||||
//Bot-Ready
|
//Bot-Ready
|
||||||
|
|
@ -169,9 +167,9 @@ app.use('/api', apiRouter);
|
||||||
|
|
||||||
//Static File Server
|
//Static File Server
|
||||||
//Serve client-side libraries
|
//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'))); //Icon set
|
||||||
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'))); //Self-Hosted PoW-based Captcha
|
||||||
app.use('/lib/hls.js',express.static(path.join(__dirname, '../node_modules/hls.js/dist')));
|
app.use('/lib/hls.js',express.static(path.join(__dirname, '../node_modules/hls.js/dist'))); //HLS Media Handler
|
||||||
//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,7 +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>
|
<script src="/lib/hls.js/hls.min.js"></script>
|
||||||
<%# 1st party code %>
|
<%# 1st party code %>
|
||||||
<%# admin gunk %>
|
<%# admin gunk %>
|
||||||
<script src="/js/adminUtils.js"></script>
|
<script src="/js/adminUtils.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -172,6 +172,11 @@ textarea{
|
||||||
box-shadow: var(--danger-glow0-alt1);
|
box-shadow: var(--danger-glow0-alt1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.critical-danger-text{
|
||||||
|
color: var(--danger0-alt1);
|
||||||
|
text-shadow: var(--danger-glow0);
|
||||||
|
}
|
||||||
|
|
||||||
.danger-link, .danger-text{
|
.danger-link, .danger-text{
|
||||||
color: var(--danger0);
|
color: var(--danger0);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -141,6 +141,10 @@ class mediaHandler{
|
||||||
//reset self act flag
|
//reset self act flag
|
||||||
this.selfAct = false;
|
this.selfAct = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onBuffer(){
|
||||||
|
this.selfAct = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//Basic building blocks for anything that touches a <video> tag
|
//Basic building blocks for anything that touches a <video> tag
|
||||||
|
|
@ -184,7 +188,7 @@ class rawFileBase extends mediaHandler{
|
||||||
this.video.load();
|
this.video.load();
|
||||||
|
|
||||||
//Set it back to the proper time
|
//Set it back to the proper time
|
||||||
this.video.currentTime = timestamp;
|
this.video.currentTime = this.lastTimestamp;
|
||||||
|
|
||||||
//Play the video
|
//Play the video
|
||||||
this.video.play();
|
this.video.play();
|
||||||
|
|
@ -234,18 +238,22 @@ class nullHandler extends rawFileBase{
|
||||||
}
|
}
|
||||||
|
|
||||||
start(){
|
start(){
|
||||||
|
//call derived start function
|
||||||
|
super.start();
|
||||||
|
|
||||||
//Lock the player
|
//Lock the player
|
||||||
this.setPlayerLock(true);
|
this.setPlayerLock(true);
|
||||||
|
|
||||||
//Set the static placeholder
|
//Set the static placeholder
|
||||||
this.video.src = '/video/static.webm';
|
this.video.src = '/video/static.webm';
|
||||||
|
|
||||||
//Set video title manually
|
|
||||||
this.player.title.textContent = 'Channel Off Air';
|
|
||||||
|
|
||||||
//play the placeholder video
|
//play the placeholder video
|
||||||
this.video.play();
|
this.video.play();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setVideoTitle(title){
|
||||||
|
this.player.title.textContent = `Channel Off Air`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//Basic building blocks needed for proper time-synchronized raw-file playback
|
//Basic building blocks needed for proper time-synchronized raw-file playback
|
||||||
|
|
@ -263,10 +271,14 @@ class rawFileHandler extends rawFileBase{
|
||||||
super.defineListeners();
|
super.defineListeners();
|
||||||
|
|
||||||
this.video.addEventListener('pause', this.onPause.bind(this));
|
this.video.addEventListener('pause', this.onPause.bind(this));
|
||||||
this.video.addEventListener('seeking', this.onSeek.bind(this));
|
this.video.addEventListener('seeked', this.onSeek.bind(this));
|
||||||
|
this.video.addEventListener('waiting', this.onBuffer.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
start(){
|
start(){
|
||||||
|
//Call derived start
|
||||||
|
super.start();
|
||||||
|
|
||||||
//Set video
|
//Set video
|
||||||
this.video.src = this.nowPlaying.rawLink;
|
this.video.src = this.nowPlaying.rawLink;
|
||||||
|
|
||||||
|
|
@ -505,7 +517,7 @@ class hlsBase extends rawFileBase{
|
||||||
}
|
}
|
||||||
|
|
||||||
buildPlayer(){
|
buildPlayer(){
|
||||||
//Call derived player
|
//Call derived buildPlayer function
|
||||||
super.buildPlayer();
|
super.buildPlayer();
|
||||||
|
|
||||||
//Instantiate HLS object
|
//Instantiate HLS object
|
||||||
|
|
@ -522,6 +534,14 @@ class hlsBase extends rawFileBase{
|
||||||
}
|
}
|
||||||
|
|
||||||
onMetadataLoad(){
|
onMetadataLoad(){
|
||||||
|
//Call derived method
|
||||||
|
super.onMetadataLoad();
|
||||||
|
}
|
||||||
|
|
||||||
|
start(){
|
||||||
|
//Call derived method
|
||||||
|
super.start();
|
||||||
|
|
||||||
//Start the video
|
//Start the video
|
||||||
this.video.play();
|
this.video.play();
|
||||||
}
|
}
|
||||||
|
|
@ -531,5 +551,74 @@ class hlsLiveStreamHandler extends hlsBase{
|
||||||
constructor(client, player, media){
|
constructor(client, player, media){
|
||||||
//Call derived constructor
|
//Call derived constructor
|
||||||
super(client, player, media, "livehls");
|
super(client, player, media, "livehls");
|
||||||
|
|
||||||
|
//Create variable to determine if we need to resync after next seek
|
||||||
|
this.reSync = false;
|
||||||
|
|
||||||
|
this.video.addEventListener('pause', this.onPause.bind(this));
|
||||||
|
this.video.addEventListener('seeked', this.onSeek.bind(this));
|
||||||
|
this.video.addEventListener('waiting', this.onBuffer.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
sync(){
|
||||||
|
//Kick the video back on if it was paused
|
||||||
|
this.video.play();
|
||||||
|
|
||||||
|
//Pull video duration
|
||||||
|
const duration = this.video.duration;
|
||||||
|
|
||||||
|
//Ignore bad timestamps
|
||||||
|
if(duration > 0){
|
||||||
|
//Seek to the end to sync up w/ the livestream
|
||||||
|
this.video.currentTime = duration;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setVideoTitle(title){
|
||||||
|
//Add title as text content for security :P
|
||||||
|
this.player.title.textContent = `: ${title}`;
|
||||||
|
|
||||||
|
//Create glow span
|
||||||
|
const glowSpan = document.createElement('span');
|
||||||
|
//Fill glow span content
|
||||||
|
glowSpan.textContent = "🔴LIVE";
|
||||||
|
//Set glowspan class
|
||||||
|
glowSpan.classList.add('critical-danger-text');
|
||||||
|
|
||||||
|
//Inject glowspan into title in a way that allows it to be easily replaced
|
||||||
|
this.player.title.prepend(glowSpan);
|
||||||
|
}
|
||||||
|
|
||||||
|
onBuffer(event){
|
||||||
|
//Call derived function
|
||||||
|
super.onBuffer(event);
|
||||||
|
|
||||||
|
|
||||||
|
//If we're synced by the end of buffering
|
||||||
|
if(this.player.syncLock){
|
||||||
|
//Throw flag to manually sync since this works entirely differently from literally every other fucking media source
|
||||||
|
this.reSync = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSeek(event){
|
||||||
|
//Call derived method
|
||||||
|
super.onSeek(event);
|
||||||
|
|
||||||
|
//Calculate distance to end of stream
|
||||||
|
const difference = this.video.duration - this.video.currentTime;
|
||||||
|
|
||||||
|
|
||||||
|
//If we where buffering under sync lock
|
||||||
|
if(this.reSync){
|
||||||
|
//Set reSync to false
|
||||||
|
this.reSync = false;
|
||||||
|
|
||||||
|
//If the difference is bigger than streamSyncTolerance
|
||||||
|
if(difference > this.player.streamSyncTolerance){
|
||||||
|
//Sync manually since we have no timestamp, and therefore the player won't do it for us
|
||||||
|
this.sync();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -42,6 +42,8 @@ class player{
|
||||||
|
|
||||||
//Numbers
|
//Numbers
|
||||||
this.syncTolerance = 0.4;
|
this.syncTolerance = 0.4;
|
||||||
|
//Might seem weird to keep this here instead of the HLS handler, but remember we may want to support other livestream services in the future...
|
||||||
|
this.streamSyncTolerance = 2;
|
||||||
this.syncDelta = 6;
|
this.syncDelta = 6;
|
||||||
this.volume = 1;
|
this.volume = 1;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue