334 lines
8.2 KiB
JavaScript
334 lines
8.2 KiB
JavaScript
/*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 <https://www.gnu.org/licenses/>.*/
|
|
|
|
//This is little more than a interface class
|
|
class mediaHandler{
|
|
constructor(client, player, media, type){
|
|
//Get parents
|
|
this.client = client;
|
|
this.player = player;
|
|
|
|
//Set handler type
|
|
this.type = type
|
|
|
|
//Denotes wether a seek call was made by the syncing function
|
|
this.selfAct = false;
|
|
|
|
//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(){
|
|
//Reset player lock
|
|
this.lock = false;
|
|
}
|
|
|
|
destroyPlayer(){
|
|
//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;
|
|
|
|
//Throw self act flag to make sure we don't un-sync the player
|
|
this.selfAct = true;
|
|
}
|
|
|
|
end(){
|
|
//Null out current media
|
|
this.nowPlaying = null;
|
|
|
|
//Throw self act to prevent unlock on video end
|
|
this.selfAct = true;
|
|
|
|
//Destroy the player
|
|
this.destroyPlayer();
|
|
}
|
|
|
|
play(){
|
|
}
|
|
|
|
pause(){
|
|
}
|
|
|
|
setPlayerLock(lock){
|
|
//set lock property
|
|
this.lock = lock;
|
|
}
|
|
|
|
getRatio(){
|
|
}
|
|
|
|
getTimestamp(){
|
|
}
|
|
|
|
setVideoTitle(title){
|
|
this.player.title.textContent = `Currently Playing: ${title}`;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
onVolumeChange(event){
|
|
}
|
|
|
|
onSeek(event){
|
|
//If the video was seeked out-side of code
|
|
if(!this.selfAct){
|
|
this.player.unlockSync();
|
|
}
|
|
|
|
//reset self act flag
|
|
this.selfAct = false;
|
|
}
|
|
}
|
|
|
|
//Basic building blocks for anything that touches a <video> tag
|
|
class rawFileBase extends mediaHandler{
|
|
constructor(client, player, media, type){
|
|
super(client, player, media, type);
|
|
}
|
|
|
|
defineListeners(){
|
|
//Resize to aspect on metadata load
|
|
this.video.addEventListener('loadedmetadata', this.onMetadataLoad.bind(this));
|
|
this.video.addEventListener('volumechange', this.onVolumeChange.bind(this));
|
|
}
|
|
|
|
buildPlayer(){
|
|
//Create player
|
|
this.video = document.createElement('video');
|
|
//Append it to page
|
|
this.player.videoContainer.appendChild(this.video);
|
|
//Run derived method
|
|
super.buildPlayer();
|
|
}
|
|
|
|
destroyPlayer(){
|
|
//Remove player from page
|
|
this.video.remove();
|
|
//Run derived method
|
|
super.destroyPlayer();
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
reload(){
|
|
//Call derived method
|
|
super.reload();
|
|
|
|
//Load video from source
|
|
this.video.load();
|
|
|
|
//Set it back to the proper time
|
|
this.video.currentTime = timestamp;
|
|
|
|
//Play the video
|
|
this.video.play();
|
|
}
|
|
|
|
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;
|
|
|
|
//Run derived method
|
|
super.setPlayerLock(lock);
|
|
}
|
|
|
|
getRatio(){
|
|
return this.video.videoWidth / this.video.videoHeight;
|
|
}
|
|
|
|
onVolumeChange(event){
|
|
//Pull volume from video
|
|
this.player.volume = this.video.volume;
|
|
}
|
|
}
|
|
|
|
//Off air static 'player'
|
|
class nullHandler extends rawFileBase{
|
|
constructor(client, player){
|
|
//Call derived constructor
|
|
super(client, player, {}, null);
|
|
|
|
this.defineListeners();
|
|
}
|
|
|
|
defineListeners(){
|
|
//Run derived method
|
|
super.defineListeners();
|
|
|
|
//Disable right clicking
|
|
this.video.addEventListener('contextmenu', (e)=>{e.preventDefault()});
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|
|
|
|
//Basic building blocks needed for proper time-synchronized raw-file playback
|
|
class rawFileHandler extends rawFileBase{
|
|
constructor(client, player, media){
|
|
//Call derived constructor
|
|
super(client, player, media, 'raw');
|
|
|
|
//Define listeners
|
|
this.defineListeners();
|
|
}
|
|
|
|
defineListeners(){
|
|
//Run derived method
|
|
super.defineListeners();
|
|
|
|
this.video.addEventListener('pause', this.onPause.bind(this));
|
|
this.video.addEventListener('seeking', this.onSeek.bind(this));
|
|
}
|
|
|
|
start(){
|
|
//Set video
|
|
this.video.src = this.nowPlaying.rawLink;
|
|
|
|
//Set video volume
|
|
this.video.volume = this.player.volume;
|
|
|
|
//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){
|
|
//Call derived sync
|
|
super.sync(timestamp);
|
|
|
|
//Skip sync calls that won't seek so we don't pointlessly throw selfAct
|
|
if(timestamp != this.video.currentTime){
|
|
//Set current video time based on timestamp received from server
|
|
this.video.currentTime = timestamp;
|
|
}
|
|
}
|
|
|
|
getTimestamp(){
|
|
//Return current timestamp
|
|
return this.video.currentTime;
|
|
}
|
|
}
|
|
|
|
class youtubeEmbedHandler extends mediaHandler{
|
|
constructor(client, player, media){
|
|
//Call derived constructor
|
|
super(client, player, media, 'ytEmbed');
|
|
}
|
|
|
|
buildPlayer(){
|
|
//If the embed API has loaded
|
|
if(this.client.ytEmbedAPILoaded){
|
|
//Create a new youtube player using the official YT iframe-embed api
|
|
this.video = new YT.Player('video', {
|
|
//Inject video id
|
|
videoID: media.id,
|
|
//Set up event listeners (NGL kinda nice of google to do it this way...)
|
|
events: {
|
|
'onReady': this.embedPlayer.bind(this)
|
|
}
|
|
});
|
|
}
|
|
|
|
//Call derived function
|
|
super.buildPlayer();
|
|
}
|
|
|
|
embedPlayer(){
|
|
}
|
|
} |