/*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, type){ //Get parents this.client = client; this.player = player; //Set handler type this.type = type //Set last received timestamp to 0 this.lastTimestamp = 0; //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 = 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(){ this.nowPlaying = null; this.destroyPlayer(); } play(){ } pause(){ } 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; } getTimestamp(){ } setVideoTitle(title){ this.player.title.textContent = `Currently Playing: ${title}`; } } class nullHandler extends mediaHandler{ constructor(client, player){ //Call derived constructor super(client, player, {}, null); this.defineListeners(); } defineListeners(){ //Disable right clicking this.video.addEventListener('contextmenu', (e)=>{e.preventDefault()}); this.video.addEventListener('loadedmetadata', this.onMetadataLoad.bind(this)); } onMetadataLoad(event){ //Resize aspect (if locked), since the video doesn't properly report it's resolution until it's been loaded this.client.chatBox.resizeAspect(); } start(){ //Lock the player this.setPlayerLock(true); //Set the static placeholder this.video.src = '/video/static.webm'; //Set video title manually this.player.title.textContent = 'Channel Off Air'; //play the placeholder video this.video.play(); } } class rawFileHandler extends mediaHandler{ constructor(client, player, media){ //Call derived constructor super(client, player, media, 'raw'); //Since this media type has no way to tell between events that originate from either the user or code //That's what this boolean is for :P this.selfAct = false; //Define listeners this.defineListeners(); } defineListeners(){ this.video.addEventListener('loadedmetadata', this.onMetadataLoad.bind(this)); this.video.addEventListener('pause', this.onPause.bind(this)); this.video.addEventListener('seeking', this.onSeek.bind(this)); } start(){ //Set video this.video.src = this.nowPlaying.id; //Set video title this.setVideoTitle(this.nowPlaying.title); //Unlock player this.setPlayerLock(false); //play video this.video.play(); } play(){ this.video.play(); } pause(){ this.video.pause(); } sync(timestamp = this.lastTimestamp){ //Skip sync calls that won't seek so we don't pointlessly throw selfAct if(timestamp != this.video.currentTime){ //Set self act flag this.selfAct = true; //Set current video time based on timestamp received from server this.video.currentTime = timestamp; } } reload(){ //Throw self act flag to make sure we don't un-sync the player this.selfAct = true; //Call derived reload function super.reload(); } onMetadataLoad(event){ //Resize aspect (if locked), since the video doesn't properly report it's resolution until it's been loaded this.client.chatBox.resizeAspect(); } onPause(event){ //If the video was paused out-side of code if(!this.selfAct){ this.player.unlockSync(); } this.selfAct = false; } onSeek(event){ //If the video was seeked out-side of code if(!this.selfAct){ this.player.unlockSync(); } //reset self act flag this.selfAct = false; } getTimestamp(){ //Return current timestamp return this.video.currentTime; } end(){ //Throw self act to prevent unlock on video end this.selfAct = true; super.end(); } }