diff --git a/src/app/channel/connectedUser.js b/src/app/channel/connectedUser.js index b03c614..f239aac 100644 --- a/src/app/channel/connectedUser.js +++ b/src/app/channel/connectedUser.js @@ -35,16 +35,19 @@ module.exports = class{ async handleConnection(userDB, chanDB, socket){ //send metadata to client - await this.sendClientMetadata(); + this.sendClientMetadata(); //Send out emotes - await this.sendSiteEmotes(); - await this.sendChanEmotes(chanDB); - await this.sendPersonalEmotes(userDB); + this.sendSiteEmotes(); + this.sendChanEmotes(chanDB); + this.sendPersonalEmotes(userDB); //Send out used tokes - await this.sendUsedTokes(userDB); + this.sendUsedTokes(userDB); + //Send out the currently playing item + this.channel.queue.sendQueue(socket); + //Tattoo hashed IP address to user account for seven days await userDB.tattooIPRecord(socket.handshake.address); } diff --git a/src/app/channel/media/queue.js b/src/app/channel/media/queue.js index 6e2ece0..5f39777 100644 --- a/src/app/channel/media/queue.js +++ b/src/app/channel/media/queue.js @@ -23,22 +23,29 @@ module.exports = class{ this.server = server //Set channel this.channel = channel; - //Create variable to hold sync delta + //Create variable to hold sync delta in ms this.syncDelta = 1000; + //Create variable to hold current timestamp within the video + this.timestamp = 0; //Create variable to hold sync timer this.syncTimer = null; //Create variable to hold currently playing media object this.nowPlaying = null; - //Create variable to hold current timestamp within the video - this.timestamp = 0; + } queueMedia(inputMedia){ - //Create new media object, start it now + //Create new media object, set start time to now const mediaObj = queuedMedia.fromMedia(inputMedia, new Date().getTime()); + + //Start playback + this.play(mediaObj); } play(mediaObj){ + //Silently end the media + this.end(true); + //reset current timestamp this.timestamp = 0; @@ -46,7 +53,7 @@ module.exports = class{ this.nowPlaying = mediaObj; //Send play signal out to the channel - this.server.io.in(this.channel.name).emit("play", {media: this.nowPlaying}); + this.sendQueue(); //Kick off the sync timer this.syncTimer = setTimeout(this.sync.bind(this), this.syncDelta); @@ -56,14 +63,58 @@ module.exports = class{ //Send sync signal out to the channel this.server.io.in(this.channel.name).emit("sync", {timestamp: this.timestamp}); - //If the media hasn't finished playing - if(this.timeStamp < this.nowPlaying.duration){ - + //If the media has over a second to go + if((this.timestamp + 1) < this.nowPlaying.duration){ //Increment the time stamp - this.timestamp++; + this.timestamp += (this.syncDelta / 1000); //Call the sync function in another second this.syncTimer = setTimeout(this.sync.bind(this), this.syncDelta); + }else{ + //Get leftover video length in ms + const leftover = (this.nowPlaying.duration - this.timestamp) * 1000; + + //Call the end function once the video is over + this.syncTimer = setTimeout(this.end.bind(this), leftover); } } + + end(quiet = false){ + //Call off any existing sync timer + clearTimeout(this.syncTimer); + + //Clear out the sync timer + this.syncTimer = null; + + //Clear now playing + this.nowPlaying = null; + + //Clear timestamp + this.timestamp = 0; + + //If we're not being quiet + if(!quiet){ + //Tell everyone of the end-times + this.server.io.in(this.channel.name).emit('end', {}); + } + } + + sendQueue(socket){ + //Create data object + const data = { + media: this.nowPlaying, + timestamp: this.timestamp + } + + //If a socket is specified + if(socket != null){ + //Send data out to specified socket + socket.emit("play", data); + //Otherwise + }else{ + //Send that shit out to the entire channel + this.server.io.in(this.channel.name).emit("play", data); + } + + } } \ No newline at end of file diff --git a/src/app/channel/media/yanker.js b/src/app/channel/media/yanker.js index 367f259..f6014b0 100644 --- a/src/app/channel/media/yanker.js +++ b/src/app/channel/media/yanker.js @@ -72,7 +72,7 @@ module.exports = class{ const link = `https://archive.org/download/${mediaInfo.metadata.identifier}/${file.name}` //Create new media object from file info - mediaList.push(new media(name, name, link, 'ia', file.length)); + mediaList.push(new media(name, name, link, 'ia', Number(file.length))); } //return media object list diff --git a/src/utils/media/internetArchiveUtils.js b/src/utils/media/internetArchiveUtils.js index 3bad9f8..32b68ee 100644 --- a/src/utils/media/internetArchiveUtils.js +++ b/src/utils/media/internetArchiveUtils.js @@ -74,7 +74,7 @@ module.exports.fetchMetadata = async function(link){ function compatibilityFilter(file){ //return true for all files that match for web-safe formats - return file.format == "h.264" + return file.format == "h.264" || file.format == "Ogg Video" || file.format.match("MPEG4"); } function pathFilter(file){ diff --git a/src/views/channel.ejs b/src/views/channel.ejs index 7509195..4f89406 100644 --- a/src/views/channel.ejs +++ b/src/views/channel.ejs @@ -39,6 +39,7 @@ along with this program. If not, see . %> + diff --git a/src/views/partial/channel/mediaPanel.ejs b/src/views/partial/channel/mediaPanel.ejs index e612fd5..4206dab 100644 --- a/src/views/partial/channel/mediaPanel.ejs +++ b/src/views/partial/channel/mediaPanel.ejs @@ -17,7 +17,7 @@ along with this program. If not, see . %>
-

Currently Playing: NULL

+

Currently Playing: NULL

@@ -27,5 +27,6 @@ along with this program. If not, see . %>
- +
+
\ No newline at end of file diff --git a/src/views/partial/styles.ejs b/src/views/partial/styles.ejs index 7063618..9cc8977 100644 --- a/src/views/partial/styles.ejs +++ b/src/views/partial/styles.ejs @@ -13,6 +13,8 @@ GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . %> +<%# Technically favicon has nothing to do with .css, but it's still looks related, uses a link tag, and globally used :P %> + diff --git a/www/css/channel.css b/www/css/channel.css index 9f8b226..ef251f3 100644 --- a/www/css/channel.css +++ b/www/css/channel.css @@ -85,6 +85,13 @@ div#chat-panel-main-div{ height: 1%; } +#media-panel-video-container{ + display: flex; + flex-direction: column; + justify-content: center; + height: 100%; +} + .drag-handle{ position: absolute; cursor: ew-resize; diff --git a/www/js/channel/mediaHandler.js b/www/js/channel/mediaHandler.js new file mode 100644 index 0000000..5c5bc1f --- /dev/null +++ b/www/js/channel/mediaHandler.js @@ -0,0 +1,146 @@ +/*Canopy - The next generation of stoner streaming software +Copyright (C) 2024-2025 Rainbownapkin and the TTN Community + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see .*/ + +class mediaHandler{ + constructor(client, player, media){ + //Get parents + this.client = client; + this.player = player; + this.syncTolerance = 1; + this.syncDelta = 6; + + //Ingest media object from server + this.startMedia(media); + } + + startMedia(media){ + //If we properly ingested the media + if(this.ingestMedia(media)){ + //Build the video player + this.buildPlayer(); + + //Call the start function + this.start(); + } + } + + buildPlayer(){ + //Create player + this.video = document.createElement('video'); + //Append it to page + this.player.videoContainer.appendChild(this.video); + //Reset player lock + this.lock = false; + } + + destroyPlayer(){ + //Remove player from page + this.video.remove(); + //Null out video property + this.video = null; + } + + ingestMedia(media){ + //Set now playing + this.nowPlaying = media; + + //return true to signify success + return true; + } + + start(){ + } + + sync(timestamp){ + } + + end(){ + this.nowPlaying = null; + this.destroyPlayer(); + } + + setPlayerLock(lock){ + //toggle controls + this.video.controls = !lock; + //Only toggle mute if we're locking, or if we're unlocking after being locked + //If this is ran twice without locking we don't want to surprise unmute on the user + if(lock || this.lock){ + //toggle mute + this.video.muted = lock; + } + //toggle looping + this.video.loop = lock; + //set lock property + this.lock = lock; + } + + getRatio(){ + return this.video.videoWidth / this.video.videoHeight; + } +} + +class nullHandler extends mediaHandler{ + constructor(client, player){ + //Call derived constructor + super(client, player, {}); + } + + start(){ + //Lock the player + super.setPlayerLock(true); + + //Set the static placeholder + this.video.src = '/video/static.webm'; + + //Set video title + this.player.title.textContent = 'NULL'; + + //play the placeholder video + this.video.play(); + } +} + +class rawFileHandler extends mediaHandler{ + constructor(client, player, media){ + //Call derived constructor + super(client, player, media); + } + + start(){ + //Set video + this.video.src = this.nowPlaying.id; + + //Set video title + this.player.title.textContent = this.nowPlaying.title; + + //Unlock player + super.setPlayerLock(false); + + //play video + this.video.play(); + } + + sync(timestamp){ + //Check if timestamp evenly devides into sync delta, effectively only checking for sync every X seconds + if(timestamp % this.syncDelta == 0){ + //Get absolute difference between syncronization timestamp and actual video timestamp, and check if it's over the sync tolerance + if(Math.abs(timestamp - this.video.currentTime) > this.syncTolerance){ + //If we need to sync, then sync the video! + this.video.currentTime = timestamp; + } + } + } +} \ No newline at end of file diff --git a/www/js/channel/player.js b/www/js/channel/player.js index e8ca80d..ecf3b89 100644 --- a/www/js/channel/player.js +++ b/www/js/channel/player.js @@ -27,15 +27,17 @@ class player{ //elements this.playerDiv = document.querySelector("#media-panel-div"); + this.videoContainer = document.querySelector("#media-panel-video-container") this.navBar = document.querySelector("#navbar"); - this.video = document.querySelector("#media-panel-video"); this.uiBar = document.querySelector("#media-panel-head-div"); + this.title = document.querySelector("#media-panel-title-span"); this.showVideoIcon = document.querySelector("#chat-panel-show-video-icon"); this.hideVideoIcon = document.querySelector("#media-panel-div-toggle-icon"); this.cinemaModeIcon = document.querySelector("#media-panel-cinema-mode-icon"); //run setup functions this.setupInput(); + this.defineListeners(); } setupInput(){ @@ -50,6 +52,52 @@ class player{ this.cinemaModeIcon.addEventListener("click", ()=>{this.toggleCinemaMode()}); } + defineListeners(){ + this.client.socket.on("play", this.play.bind(this)); + this.client.socket.on("sync", this.sync.bind(this)); + this.client.socket.on("end", this.end.bind(this)); + } + + play(data){ + //If we have an active media handler + if(this.mediaHandler != null){ + //End the media handler + this.mediaHandler.end(); + } + + //Ignore null media + if(data.media == null){ + //Set null handler + this.mediaHandler = new nullHandler(client, this); + //Otherwise + }else{ + //If we have a raw-file compatible source + if(data.media.type == 'ia' || data.media.type == 'raw'){ + //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); + }else{ + this.mediaHandler = new nullHandler(client, this); + } + } + + //Re-size to aspect since video may now be a different size + this.client.chatBox.resizeAspect(); + } + + end(){ + //Call the media handler finisher + this.mediaHandler.end(); + + //Replace it with a null handler + this.mediaHandler = new nullHandler(client, this); + } + + sync(data){ + this.mediaHandler.sync(data.timestamp); + } + popUI(event){ this.toggleUI(true); clearTimeout(this.uiTimer); @@ -94,7 +142,14 @@ class player{ this.popUI(); } + //This way other classes don't need to worry about media handler getRatio(){ - return this.video.videoWidth / this.video.videoHeight; + //If we have no media handler + if(this.mediaHandler == null){ + //Return a 4/3 aspect to get a decent chat size + return 4/3; + }else{ + return this.mediaHandler.getRatio(); + } } } \ No newline at end of file