466 lines
15 KiB
JavaScript
466 lines
15 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/>.*/
|
|
|
|
/**
|
|
* Class which represents Canopy Player UX
|
|
*/
|
|
class player{
|
|
/**
|
|
* Instantiates a new Canopy Player object
|
|
* @param {channel} client - Parent client Management Object
|
|
*/
|
|
constructor (client){
|
|
/**
|
|
* Parent CLient Management Object
|
|
*/
|
|
this.client = client;
|
|
|
|
/**
|
|
* Whether or not the mouse cursor is floating over player UX
|
|
*/
|
|
this.onUI = false;
|
|
|
|
/**
|
|
* Whether or not player scrub is locked to sync signal from the server
|
|
*/
|
|
this.syncLock = true;
|
|
|
|
/**
|
|
* Player UX Stow-Away timer
|
|
*/
|
|
this.uiTimer = setTimeout(this.toggleUI.bind(this), 1500, false);
|
|
|
|
//elements
|
|
/**
|
|
* Top-Level Player Container Div
|
|
*/
|
|
this.playerDiv = document.querySelector("#media-panel-div");
|
|
|
|
/**
|
|
* Player Element Container Div
|
|
*/
|
|
this.videoContainer = document.querySelector("#media-panel-video-container")
|
|
|
|
/**
|
|
* Page Nav-Par
|
|
*/
|
|
this.navBar = document.querySelector("#navbar");
|
|
|
|
/**
|
|
* Auto-Hiding Player UI
|
|
*/
|
|
this.uiBar = document.querySelector("#media-panel-head-div");
|
|
|
|
/**
|
|
* Player Title Label
|
|
*/
|
|
this.title = document.querySelector("#media-panel-title-paragraph");
|
|
|
|
/**
|
|
* Player Show Video Icon
|
|
*/
|
|
this.showVideoIcon = document.querySelector("#chat-panel-show-video-icon");
|
|
|
|
/**
|
|
* Player Hide Video Icon
|
|
*/
|
|
this.hideVideoIcon = document.querySelector("#media-panel-div-toggle-icon");
|
|
|
|
/**
|
|
* Player Syncronization Icon
|
|
*/
|
|
this.syncIcon = document.querySelector("#media-panel-sync-icon");
|
|
|
|
/**
|
|
* Player Cinema-Mode Icon
|
|
*/
|
|
this.cinemaModeIcon = document.querySelector("#media-panel-cinema-mode-icon");
|
|
|
|
/**
|
|
* Player Filp Video Y Icon
|
|
*/
|
|
this.flipYIcon = document.querySelector("#media-panel-flip-vertical-icon")
|
|
|
|
/**
|
|
* Player Flip Video X Icon
|
|
*/
|
|
this.flipXIcon = document.querySelector("#media-panel-flip-horizontal-icon")
|
|
|
|
/**
|
|
* Player Media Reload Icon
|
|
*/
|
|
this.reloadIcon = document.querySelector("#media-panel-reload-icon");
|
|
|
|
/**
|
|
* Tolerance between timestamp from server and actual media before corrective seek for pre-recorded media
|
|
*/
|
|
this.syncTolerance = localStorage.getItem('syncTolerance');
|
|
|
|
/**
|
|
* Tolerance in livestream delay before corrective seek to live.
|
|
*
|
|
* Might seem weird to keep this here instead of the HLS handler, but remember we may want to support other livestream services in the future...
|
|
*/
|
|
this.streamSyncTolerance = localStorage.getItem('liveSyncTolerance');
|
|
|
|
/**
|
|
* Forced time to wait between sync checks, heavily decreases chance of seek-banging without reducing syncornization accuracy
|
|
*/
|
|
this.syncDelta = localStorage.getItem('syncDelta');
|
|
|
|
/**
|
|
* Current Player Volume
|
|
*/
|
|
this.volume = 1;
|
|
|
|
//run setup functions
|
|
this.setupInput();
|
|
this.defineListeners();
|
|
}
|
|
|
|
/**
|
|
* Defines Input-Related Event Listeners for the player
|
|
*/
|
|
setupInput(){
|
|
//UIBar Movement Detection
|
|
this.playerDiv.addEventListener("mousemove", this.popUI.bind(this));
|
|
this.uiBar.addEventListener("mouseenter", ()=>{this.setOnUI(true)});
|
|
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));
|
|
}
|
|
|
|
/**
|
|
* Define Network-Related Event Listeners for the player
|
|
*/
|
|
defineListeners(){
|
|
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));
|
|
this.client.socket.on("updateCurrentRawFile", this.updateCurrentRawFile.bind(this));
|
|
}
|
|
|
|
/**
|
|
* Handles command from server to start media
|
|
* @param {Object} data - Media Metadata from server
|
|
*/
|
|
start(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 youtube video and the official embedded iframe player is selected
|
|
if(data.media.type == 'yt' && localStorage.getItem("ytPlayerType") == 'embed'){
|
|
//Create a new yt handler for it
|
|
this.mediaHandler = new youtubeEmbedHandler(this.client, this, data.media);
|
|
//Sync to time stamp
|
|
this.mediaHandler.sync(data.timestamp);
|
|
//If we have an HLS Livestream
|
|
}else if(data.media.type == "livehls"){
|
|
//Create a new HLS Livestream Handler for it
|
|
this.mediaHandler = new hlsLiveStreamHandler(this.client, this, data.media);
|
|
}else if(data.media.type == 'dm'){
|
|
this.mediaHandler = new hlsDailymotionHandler(this.client, this, data.media);
|
|
//Otherwise, if we have a raw-file compatible source
|
|
}else if(data.media.type == 'ia' || data.media.type == 'raw' || data.media.type == 'yt' || data.media.type == 'dm'){
|
|
//If we're running a source from IA
|
|
if(data.media.type == 'ia'){
|
|
//Replace specified CDN with generic URL, in-case of hard reload
|
|
data.media.rawLink = data.media.rawLink.replace(/^https(.*)archive\.org(.*)items/g, "https://archive.org/download")
|
|
|
|
//If we have an IA source and a custom IA CDN Server set
|
|
if(data.media.type == 'ia' && localStorage.getItem("IACDN") != ""){
|
|
//Generate and set new link
|
|
data.media.rawLink = data.media.rawLink.replace("https://archive.org/download", `https://${localStorage.getItem("IACDN")}.archive.org/0/items`);
|
|
}
|
|
}
|
|
|
|
//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);
|
|
}
|
|
}
|
|
|
|
//Lock synchronization since everyone starts at 0, and update the UI
|
|
this.lockSync();
|
|
|
|
//Re-size to aspect since video may now be a different size
|
|
this.client.chatBox.resizeAspect();
|
|
|
|
//Sync off of starter time stamp
|
|
this.mediaHandler.sync(data.timestamp);
|
|
}
|
|
|
|
/**
|
|
* Handles synchronization command from server
|
|
* @param {Object} data - Syncrhonization Data from Server
|
|
*/
|
|
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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reloads the media player
|
|
*/
|
|
reload(){
|
|
if(this.mediaHandler != null){
|
|
this.mediaHandler.reload();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Destroys and Re-Creates media handler
|
|
*/
|
|
hardReload(){
|
|
if(this.mediaHandler != null){
|
|
//Re-create data we'd get from server
|
|
const data = {
|
|
media: this.mediaHandler.nowPlaying,
|
|
timestamp: this.mediaHandler.getTimestamp()
|
|
}
|
|
|
|
//End current media handler
|
|
this.end();
|
|
|
|
console.log(data);
|
|
|
|
//Restart from last media handlers
|
|
this.start(data);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles End-Media Commands from the Server
|
|
*/
|
|
end(){
|
|
//Call the media handler finisher
|
|
this.mediaHandler.end();
|
|
|
|
//Replace it with a null handler
|
|
this.mediaHandler = new nullHandler(client, this);
|
|
|
|
//Re-lock sync since we're probably gonna start new media soon anywho, and we need to update the UI anywho
|
|
this.lockSync();
|
|
}
|
|
|
|
/**
|
|
* Handles Raw-File Metadata Updates from the Server
|
|
* @param {Object} data - Updadated Raw-File link from Server
|
|
*/
|
|
updateCurrentRawFile(data){
|
|
//typecheck the media handler to see if we really need to do any of this shit, if not...
|
|
if(this.mediaHandler.type == 'ytEmbed'){
|
|
//Ignore it
|
|
return;
|
|
}
|
|
|
|
//Grab current item from media handler
|
|
const currentItem = this.mediaHandler.nowPlaying;
|
|
|
|
//Update raw link
|
|
currentItem.rawLink = data.file;
|
|
|
|
//Re-start the item
|
|
this.start({media: currentItem});
|
|
}
|
|
|
|
/**
|
|
* Locks player seek to synced timestamp from the server
|
|
*/
|
|
lockSync(){
|
|
//Enable syncing
|
|
this.syncLock = true;
|
|
|
|
if(this.mediaHandler != null && this.mediaHandler.type != null){
|
|
//Light up the sync icon to show that we're actively synchronized
|
|
this.syncIcon.classList.add('positive');
|
|
|
|
//Sync to last timestamp
|
|
this.mediaHandler.sync();
|
|
|
|
//Play
|
|
this.mediaHandler.play();
|
|
}else{
|
|
//Unlight the sync icon since there is nothing to sync
|
|
this.syncIcon.classList.remove('positive');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Un-locks player seek to synced timestamp from the server
|
|
*/
|
|
unlockSync(){
|
|
//Unlight the sync icon since we're no longer actively synced
|
|
this.syncIcon.classList.remove('positive');
|
|
|
|
//Disable syncing
|
|
this.syncLock = false;
|
|
}
|
|
|
|
/**
|
|
* Flips the video horizontally
|
|
*/
|
|
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)';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Flips the video vertically
|
|
*/
|
|
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)';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Displays UI after player-related input
|
|
* @param {Event} event - Event passed through by event handler
|
|
*/
|
|
popUI(event){
|
|
this.toggleUI(true);
|
|
clearTimeout(this.uiTimer);
|
|
if(!this.onUI){
|
|
this.uiTimer = setTimeout(this.toggleUI.bind(this), 1500, false);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Toggles UI-Bar on or off
|
|
* @param {Boolean} show - Whether or not to show the UI-Bar. Defaults to toggle if left unspecified.
|
|
*/
|
|
toggleUI(show = this.uiBar.style.display == "none"){
|
|
this.uiBar.style.display = show ? "flex" : "none";
|
|
}
|
|
|
|
/**
|
|
* Toggles video on or off
|
|
* @param {Boolean} show - Whether or not to show the video player. Defaults to toggle if left unspecified
|
|
*/
|
|
toggleVideo(show = !this.playerDiv.checkVisibility()){
|
|
if(show){
|
|
this.playerDiv.style.display = "flex";
|
|
this.showVideoIcon.style.display = "none";
|
|
}else{
|
|
this.playerDiv.style.display = "none";
|
|
this.showVideoIcon.style.display = "flex";
|
|
}
|
|
|
|
//Tell chatbox to handle this shit
|
|
this.client.chatBox.handleVideoToggle(show);
|
|
}
|
|
|
|
/**
|
|
* Toggles Cinema Mode on or off
|
|
* @param {Boolean} cinema - Whether or not to enter Cinema Mode. Defaults to toggle if left unspecified
|
|
*/
|
|
toggleCinemaMode(cinema = !this.navBar.checkVisibility()){
|
|
if(cinema){
|
|
this.navBar.style.display = "flex";
|
|
}else{
|
|
this.navBar.style.display = "none";
|
|
}
|
|
|
|
//Resize the video if we're aspect locked
|
|
this.client.chatBox.resizeAspect();
|
|
}
|
|
|
|
/**
|
|
* Informs the class when the user's mouse curosr enters and leaves the UI area
|
|
* @param {Boolean} onUI - Whether or not onUI should be toggled true
|
|
*/
|
|
setOnUI(onUI){
|
|
this.onUI = onUI;
|
|
this.popUI();
|
|
}
|
|
|
|
/**
|
|
* Calculates ratio of current media object
|
|
* @returns {Number} Current media aspect ratio as a single floating point number
|
|
*/
|
|
getRatio(){
|
|
//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();
|
|
}
|
|
}
|
|
} |