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