From f38eae170d1cd905bdb621ca3931111994fb00cf Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Fri, 17 Jan 2025 06:02:39 -0500 Subject: [PATCH] Finished up with player UI-Bar functionality, including 'reload' and 'sync' controls. --- src/app/channel/media/queue.js | 8 +- src/app/channel/media/yanker.js | 14 +-- src/views/partial/channel/mediaPanel.ejs | 18 ++-- www/css/channel.css | 15 ++-- www/css/theme/movie-night.css | 5 ++ www/js/channel/mediaHandler.js | 102 +++++++++++++++++---- www/js/channel/player.js | 110 +++++++++++++++++++++-- 7 files changed, 222 insertions(+), 50 deletions(-) diff --git a/src/app/channel/media/queue.js b/src/app/channel/media/queue.js index 5f39777..de989ca 100644 --- a/src/app/channel/media/queue.js +++ b/src/app/channel/media/queue.js @@ -39,10 +39,10 @@ module.exports = class{ const mediaObj = queuedMedia.fromMedia(inputMedia, new Date().getTime()); //Start playback - this.play(mediaObj); + this.start(mediaObj); } - play(mediaObj){ + start(mediaObj){ //Silently end the media this.end(true); @@ -109,11 +109,11 @@ module.exports = class{ //If a socket is specified if(socket != null){ //Send data out to specified socket - socket.emit("play", data); + socket.emit("start", data); //Otherwise }else{ //Send that shit out to the entire channel - this.server.io.in(this.channel.name).emit("play", data); + this.server.io.in(this.channel.name).emit("start", data); } } diff --git a/src/app/channel/media/yanker.js b/src/app/channel/media/yanker.js index f6014b0..e8a0ae5 100644 --- a/src/app/channel/media/yanker.js +++ b/src/app/channel/media/yanker.js @@ -45,7 +45,7 @@ module.exports = class{ //Pull media list const mediaList = await this.yankMedia(data.url); //Get active channel from server/socket - const chan = this.server.activeChannels.get(socket.chan) + const chan = this.server.activeChannels.get(socket.chan); //Queue the first media object given chan.queue.queueMedia(mediaList[0]); }catch(err){ @@ -54,11 +54,11 @@ module.exports = class{ } async yankMedia(url){ - const pullType = await this.getMediaType(url) + const pullType = await this.getMediaType(url); if(pullType == 'ia'){ //Create empty list to hold media objects - const mediaList = [] + const mediaList = []; //Pull metadata from IA const mediaInfo = await iaUtil.fetchMetadata(url); @@ -69,17 +69,17 @@ module.exports = class{ //pull filename from path const name = path[path.length - 1]; //Construct link from pulled info - const link = `https://archive.org/download/${mediaInfo.metadata.identifier}/${file.name}` + 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', Number(file.length))); } //return media object list - return mediaList + return mediaList; }else{ //return null to signify a bad url - return null + return null; } } @@ -87,7 +87,7 @@ module.exports = class{ //Check if we have a valid url if(!validator.isURL(url)){ //If not toss the fucker out - return null + return null; } //If we have link to a resource from archive.org diff --git a/src/views/partial/channel/mediaPanel.ejs b/src/views/partial/channel/mediaPanel.ejs index 4206dab..3b50c8d 100644 --- a/src/views/partial/channel/mediaPanel.ejs +++ b/src/views/partial/channel/mediaPanel.ejs @@ -16,16 +16,16 @@ along with this program. If not, see . %>
- -

Currently Playing: NULL

+ +

-

- - - - - - - + + + + + + +
diff --git a/www/css/channel.css b/www/css/channel.css index ef251f3..f959e32 100644 --- a/www/css/channel.css +++ b/www/css/channel.css @@ -52,21 +52,13 @@ div#media-panel-div{ #media-panel-head-div{ position: absolute; + z-index: 1; height: 3em; right: 0; left: 0; top: 0; } -video#media-panel-video{ - flex: 1; - min-height: 0; -} - -#media-panel-sync-button{ - height: 1.5em; -} - #media-panel-title-paragraph{ font-size: 1.2em; } @@ -90,6 +82,11 @@ div#chat-panel-main-div{ flex-direction: column; justify-content: center; height: 100%; + transform: scaleX(1) scaleY(1); +} + +#media-panel-video-container video{ + height: 100% } .drag-handle{ diff --git a/www/css/theme/movie-night.css b/www/css/theme/movie-night.css index 5b18ac7..d9eb117 100644 --- a/www/css/theme/movie-night.css +++ b/www/css/theme/movie-night.css @@ -138,6 +138,11 @@ textarea{ border-bottom-right-radius: 0; } +.positive{ + color: var(--focus0-alt0); + text-shadow: var(--focus-glow0); +} + .danger-button{ background-color: var(--danger0); color: var(--accent1); diff --git a/www/js/channel/mediaHandler.js b/www/js/channel/mediaHandler.js index 5c5bc1f..f22914a 100644 --- a/www/js/channel/mediaHandler.js +++ b/www/js/channel/mediaHandler.js @@ -19,8 +19,8 @@ class mediaHandler{ //Get parents this.client = client; this.player = player; - this.syncTolerance = 1; - this.syncDelta = 6; + + this.lastTimestamp = 0; //Ingest media object from server this.startMedia(media); @@ -64,7 +64,21 @@ class mediaHandler{ start(){ } - sync(timestamp){ + sync(timestamp = this.lastTimestamp){ + } + + reload(){ + //Get current timestamp + const timestamp = this.video.currentTime; + + //Load video from source + this.video.load(); + + //Set it back to the proper time + this.video.currentTime = timestamp; + + //Play the video + this.video.play(); } end(){ @@ -72,6 +86,12 @@ class mediaHandler{ this.destroyPlayer(); } + play(){ + } + + pause(){ + } + setPlayerLock(lock){ //toggle controls this.video.controls = !lock; @@ -89,6 +109,13 @@ class mediaHandler{ getRatio(){ return this.video.videoWidth / this.video.videoHeight; + } + + getTimestamp(){ + } + + setVideoTitle(title){ + this.player.title.textContent = `Currently Playing: ${title}`; } } @@ -100,13 +127,13 @@ class nullHandler extends mediaHandler{ start(){ //Lock the player - super.setPlayerLock(true); + this.setPlayerLock(true); //Set the static placeholder this.video.src = '/video/static.webm'; - //Set video title - this.player.title.textContent = 'NULL'; + //Set video title manually + this.player.title.textContent = 'Channel Off Air'; //play the placeholder video this.video.play(); @@ -117,6 +144,17 @@ class rawFileHandler extends mediaHandler{ constructor(client, player, media){ //Call derived constructor super(client, player, media); + + //Since this media type has no way to tell between the user and code seek events, we need a flag to mark them + this.selfSeek = false; + + //Define listeners + this.defineListeners(); + } + + defineListeners(){ + this.video.addEventListener('pause', this.onPause.bind(this)); + this.video.addEventListener('seeking', this.onSeek.bind(this)); } start(){ @@ -124,23 +162,55 @@ class rawFileHandler extends mediaHandler{ this.video.src = this.nowPlaying.id; //Set video title - this.player.title.textContent = this.nowPlaying.title; + this.setVideoTitle(this.nowPlaying.title); //Unlock player - super.setPlayerLock(false); + this.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; - } + play(){ + this.video.play(); + } + + pause(){ + this.video.pause(); + } + + sync(timestamp = this.lastTimestamp){ + //Set self seek flag + this.selfSeek = true; + + //Set current video time based on timestamp received from server + this.video.currentTime = timestamp; + } + + reload(){ + //Throw self seek flag to make sure we don't un-sync the player + this.selfSeek = true; + + //Call derived reload function + super.reload(); + } + + onPause(event){ + this.player.unlockSync(); + } + + onSeek(event){ + //If the video was seeked out-side of code + if(!this.selfSeek){ + this.player.unlockSync(); } + + //reset self seek flag + this.selfSeek = false; + } + + getTimestamp(){ + //Return current timestamp + return this.video.currentTime; } } \ No newline at end of file diff --git a/www/js/channel/player.js b/www/js/channel/player.js index ecf3b89..352b1e2 100644 --- a/www/js/channel/player.js +++ b/www/js/channel/player.js @@ -21,6 +21,7 @@ class player{ //booleans this.onUI = false; + this.syncLock = true; //timers this.uiTimer = setTimeout(this.toggleUI.bind(this), 1500, false); @@ -30,14 +31,23 @@ class player{ this.videoContainer = document.querySelector("#media-panel-video-container") this.navBar = document.querySelector("#navbar"); this.uiBar = document.querySelector("#media-panel-head-div"); - this.title = document.querySelector("#media-panel-title-span"); + this.title = document.querySelector("#media-panel-title-paragraph"); this.showVideoIcon = document.querySelector("#chat-panel-show-video-icon"); this.hideVideoIcon = document.querySelector("#media-panel-div-toggle-icon"); + this.syncIcon = document.querySelector("#media-panel-sync-icon"); this.cinemaModeIcon = document.querySelector("#media-panel-cinema-mode-icon"); + this.flipYIcon = document.querySelector("#media-panel-flip-vertical-icon") + this.flipXIcon = document.querySelector("#media-panel-flip-horizontal-icon") + this.reloadIcon = document.querySelector("#media-panel-reload-icon"); + + //Numbers + this.syncTolerance = 1; + this.syncDelta = 6; //run setup functions this.setupInput(); this.defineListeners(); + this.lockSync(); } setupInput(){ @@ -47,18 +57,23 @@ class player{ this.uiBar.addEventListener("mouseleave", ()=>{this.setOnUI(false)}); //UIBar/header icons + //Don't bind these, they want an argument that isn't an event :P this.showVideoIcon.addEventListener("click", ()=>{this.toggleVideo()}); this.hideVideoIcon.addEventListener("click", ()=>{this.toggleVideo()}); + this.syncIcon.addEventListener("click", this.lockSync.bind(this)); this.cinemaModeIcon.addEventListener("click", ()=>{this.toggleCinemaMode()}); + this.flipYIcon.addEventListener('click', this.flipY.bind(this)); + this.flipXIcon.addEventListener('click', this.flipX.bind(this)); + this.reloadIcon.addEventListener("click", this.reload.bind(this)); } defineListeners(){ - this.client.socket.on("play", this.play.bind(this)); + this.client.socket.on("start", this.start.bind(this)); this.client.socket.on("sync", this.sync.bind(this)); this.client.socket.on("end", this.end.bind(this)); } - play(data){ + start(data){ //If we have an active media handler if(this.mediaHandler != null){ //End the media handler @@ -86,6 +101,32 @@ class player{ this.client.chatBox.resizeAspect(); } + sync(data){ + if(this.mediaHandler != null){ + //Get timestamp + const timestamp = data.timestamp; + //Get difference between server and local timestamp + const difference = Math.abs(timestamp - this.mediaHandler.getTimestamp()); + + //Check if timestamp evenly devides into sync delta, effectively only checking for sync every X seconds + //Check if the difference between timestamps is larger than the sync tolerance + //Lastly, check to make sure we have sync lock + if(timestamp % this.syncDelta == 0 && difference > this.syncTolerance && this.syncLock){ + //If we need to sync, then sync the video! + this.mediaHandler.sync(timestamp); + } + + //Collect last timestamp + this.mediaHandler.lastTimestamp = timestamp; + } + } + + reload(){ + if(this.mediaHandler != null){ + this.mediaHandler.reload(); + } + } + end(){ //Call the media handler finisher this.mediaHandler.end(); @@ -94,8 +135,67 @@ class player{ this.mediaHandler = new nullHandler(client, this); } - sync(data){ - this.mediaHandler.sync(data.timestamp); + lockSync(){ + //Light up the sync icon to show that we're actively synchronized + this.syncIcon.classList.add('positive'); + + //Enable syncing + this.syncLock = true; + + //If we have a media handler + if(this.mediaHandler != null){ + //Sync to last timestamp + this.mediaHandler.sync(); + + //Play + this.mediaHandler.play(); + } + } + + unlockSync(){ + //Unlight the sync icon + this.syncIcon.classList.remove('positive'); + + //Disable syncing + this.syncLock = false; + } + + flipX(){ + //I'm lazy + const transform = this.videoContainer.style.transform; + + //If we we're specifically set to un-mirrored + if(transform.match("scaleX(1)")){ + //mirror it + this.videoContainer.style.transfrom = transform.replace('scaleX(1)', 'scaleX(-1)'); + //If we're currently mirrored + }else if(transform.match("scaleX(-1)")){ + //Un-mirror + this.videoContainer.style.transfrom = transform.replace('scaleX(-1)', 'scaleX(1)'); + //Otherwise, if it's untouched + }else{ + //Mirror it + this.videoContainer.style.transform += 'scaleX(-1)'; + } + } + + flipY(){ + //I'm lazy + const transform = this.videoContainer.style.transform; + + //If we we're specifically set to un-mirrored + if(transform.match("scaleY(1)")){ + //mirror it + this.videoContainer.style.transfrom = transform.replace('scaleY(1)', 'scaleY(-1)'); + //If we're currently mirrored + }else if(transform.match("scaleY(-1)")){ + //Un-mirror + this.videoContainer.style.transfrom = transform.replace('scaleY(-1)', 'scaleY(1)'); + //Otherwise, if it's untouched + }else{ + //Mirror it + this.videoContainer.style.transform += 'scaleY(-1)'; + } } popUI(event){