2021-05-12 09:13:25 +02:00
import type { UserInputManager } from "../Phaser/UserInput/UserInputManager" ;
2020-08-11 22:32:55 +02:00
import { HtmlUtils } from "./HtmlUtils" ;
export enum LayoutMode {
// All videos are displayed on the right side of the screen. If there is a screen sharing, it is displayed in the middle.
Presentation = "Presentation" ,
// Videos take the whole page.
VideoChat = "VideoChat" ,
}
export enum DivImportance {
// For screen sharing
Important = "Important" ,
// For normal video
Normal = "Normal" ,
}
2020-08-24 14:19:36 +02:00
/ * *
* Classes implementing this interface can be notified when the center of the screen ( the player position ) should be
* changed .
* /
export interface CenterListener {
onCenterChange ( ) : void ;
}
2020-11-23 20:34:05 +01:00
export const ON_ACTION_TRIGGER_BUTTON = 'onaction' ;
2021-01-25 13:18:57 +01:00
2020-11-23 20:34:05 +01:00
export const TRIGGER_WEBSITE_PROPERTIES = 'openWebsiteTrigger' ;
export const TRIGGER_JITSI_PROPERTIES = 'jitsiTrigger' ;
2021-01-25 12:21:40 +01:00
export const WEBSITE_MESSAGE_PROPERTIES = 'openWebsiteTriggerMessage' ;
export const JITSI_MESSAGE_PROPERTIES = 'jitsiTriggerMessage' ;
2020-11-23 20:34:05 +01:00
2021-03-11 22:34:49 +01:00
export const AUDIO_VOLUME_PROPERTY = 'audioVolume' ;
export const AUDIO_LOOP_PROPERTY = 'audioLoop' ;
2020-08-11 22:32:55 +02:00
/ * *
* This class is in charge of the video - conference layout .
* It receives positioning requests for videos and does its best to place them on the screen depending on the active layout mode .
* /
2020-08-13 18:21:48 +02:00
class LayoutManager {
2020-08-11 22:32:55 +02:00
private mode : LayoutMode = LayoutMode . Presentation ;
private importantDivs : Map < string , HTMLDivElement > = new Map < string , HTMLDivElement > ( ) ;
private normalDivs : Map < string , HTMLDivElement > = new Map < string , HTMLDivElement > ( ) ;
2020-08-24 14:19:36 +02:00
private listener : CenterListener | null = null ;
2020-10-31 14:04:55 +01:00
private actionButtonTrigger : Map < string , Function > = new Map < string , Function > ( ) ;
private actionButtonInformation : Map < string , HTMLDivElement > = new Map < string , HTMLDivElement > ( ) ;
2020-08-24 14:19:36 +02:00
public setListener ( centerListener : CenterListener | null ) {
this . listener = centerListener ;
}
2020-08-11 22:32:55 +02:00
public add ( importance : DivImportance , userId : string , html : string ) : void {
const div = document . createElement ( 'div' ) ;
2020-08-13 18:21:48 +02:00
div . innerHTML = html ;
2020-08-11 22:32:55 +02:00
div . id = "user-" + userId ;
2020-08-16 23:45:03 +02:00
div . className = "media-container"
2020-08-27 10:09:47 +02:00
div . onclick = ( ) = > {
const parentId = div . parentElement ? . id ;
if ( parentId === 'sidebar' || parentId === 'chat-mode' ) {
this . focusOn ( userId ) ;
} else {
this . removeFocusOn ( userId ) ;
}
}
2020-08-11 22:32:55 +02:00
if ( importance === DivImportance . Important ) {
this . importantDivs . set ( userId , div ) ;
// If this is the first video with high importance, let's switch mode automatically.
if ( this . importantDivs . size === 1 && this . mode === LayoutMode . VideoChat ) {
this . switchLayoutMode ( LayoutMode . Presentation ) ;
}
} else if ( importance === DivImportance . Normal ) {
this . normalDivs . set ( userId , div ) ;
} else {
throw new Error ( 'Unexpected importance' ) ;
}
this . positionDiv ( div , importance ) ;
2020-08-11 22:40:54 +02:00
this . adjustVideoChatClass ( ) ;
2020-08-24 14:19:36 +02:00
this . listener ? . onCenterChange ( ) ;
2020-08-11 22:32:55 +02:00
}
private positionDiv ( elem : HTMLDivElement , importance : DivImportance ) : void {
if ( this . mode === LayoutMode . VideoChat ) {
const chatModeDiv = HtmlUtils . getElementByIdOrFail < HTMLDivElement > ( 'chat-mode' ) ;
chatModeDiv . appendChild ( elem ) ;
} else {
if ( importance === DivImportance . Important ) {
const mainSectionDiv = HtmlUtils . getElementByIdOrFail < HTMLDivElement > ( 'main-section' ) ;
mainSectionDiv . appendChild ( elem ) ;
} else if ( importance === DivImportance . Normal ) {
const sideBarDiv = HtmlUtils . getElementByIdOrFail < HTMLDivElement > ( 'sidebar' ) ;
sideBarDiv . appendChild ( elem ) ;
}
}
}
2020-08-27 10:09:47 +02:00
/ * *
* Put the screen in presentation mode and move elem in presentation mode ( and all other videos in normal mode )
* /
private focusOn ( userId : string ) : void {
const focusedDiv = this . getDivByUserId ( userId ) ;
for ( const [ importantUserId , importantDiv ] of this . importantDivs . entries ( ) ) {
//this.positionDiv(importantDiv, DivImportance.Normal);
this . importantDivs . delete ( importantUserId ) ;
this . normalDivs . set ( importantUserId , importantDiv ) ;
}
this . normalDivs . delete ( userId ) ;
this . importantDivs . set ( userId , focusedDiv ) ;
//this.positionDiv(focusedDiv, DivImportance.Important);
this . switchLayoutMode ( LayoutMode . Presentation ) ;
}
/ * *
* Removes userId from presentation mode
* /
private removeFocusOn ( userId : string ) : void {
const importantDiv = this . importantDivs . get ( userId ) ;
if ( importantDiv === undefined ) {
throw new Error ( 'Div with user id "' + userId + '" is not in important mode' ) ;
}
this . normalDivs . set ( userId , importantDiv ) ;
this . importantDivs . delete ( userId ) ;
this . positionDiv ( importantDiv , DivImportance . Normal ) ;
}
private getDivByUserId ( userId : string ) : HTMLDivElement {
let div = this . importantDivs . get ( userId ) ;
if ( div !== undefined ) {
return div ;
}
div = this . normalDivs . get ( userId ) ;
if ( div !== undefined ) {
return div ;
}
throw new Error ( 'Could not find media with user id ' + userId ) ;
}
2020-08-11 22:32:55 +02:00
/ * *
* Removes the DIV matching userId .
* /
public remove ( userId : string ) : void {
2020-08-13 18:21:48 +02:00
console . log ( 'Removing video for userID ' + userId + '.' ) ;
2020-08-11 22:32:55 +02:00
let div = this . importantDivs . get ( userId ) ;
if ( div !== undefined ) {
div . remove ( ) ;
this . importantDivs . delete ( userId ) ;
2020-08-11 22:40:54 +02:00
this . adjustVideoChatClass ( ) ;
2020-08-24 14:19:36 +02:00
this . listener ? . onCenterChange ( ) ;
2020-08-11 22:32:55 +02:00
return ;
}
div = this . normalDivs . get ( userId ) ;
if ( div !== undefined ) {
div . remove ( ) ;
this . normalDivs . delete ( userId ) ;
2020-08-11 22:40:54 +02:00
this . adjustVideoChatClass ( ) ;
2020-08-24 14:19:36 +02:00
this . listener ? . onCenterChange ( ) ;
2020-08-11 22:32:55 +02:00
return ;
}
2020-08-13 18:21:48 +02:00
console . log ( 'Cannot remove userID ' + userId + '. Already removed?' ) ;
//throw new Error('Could not find user ID "'+userId+'"');
2020-08-11 22:32:55 +02:00
}
2020-08-11 22:40:54 +02:00
private adjustVideoChatClass ( ) : void {
const chatModeDiv = HtmlUtils . getElementByIdOrFail < HTMLDivElement > ( 'chat-mode' ) ;
chatModeDiv . classList . remove ( 'one-col' , 'two-col' , 'three-col' , 'four-col' ) ;
const nbUsers = this . importantDivs . size + this . normalDivs . size ;
if ( nbUsers <= 1 ) {
chatModeDiv . classList . add ( 'one-col' ) ;
} else if ( nbUsers <= 4 ) {
chatModeDiv . classList . add ( 'two-col' ) ;
} else if ( nbUsers <= 9 ) {
chatModeDiv . classList . add ( 'three-col' ) ;
} else {
chatModeDiv . classList . add ( 'four-col' ) ;
}
}
2020-08-16 23:19:04 +02:00
public switchLayoutMode ( layoutMode : LayoutMode ) {
2020-08-11 22:32:55 +02:00
this . mode = layoutMode ;
2020-08-13 18:21:48 +02:00
if ( layoutMode === LayoutMode . Presentation ) {
2020-08-16 23:49:31 +02:00
HtmlUtils . getElementByIdOrFail < HTMLDivElement > ( 'sidebar' ) . style . display = 'flex' ;
HtmlUtils . getElementByIdOrFail < HTMLDivElement > ( 'main-section' ) . style . display = 'flex' ;
2020-08-13 18:21:48 +02:00
HtmlUtils . getElementByIdOrFail < HTMLDivElement > ( 'chat-mode' ) . style . display = 'none' ;
} else {
HtmlUtils . getElementByIdOrFail < HTMLDivElement > ( 'sidebar' ) . style . display = 'none' ;
HtmlUtils . getElementByIdOrFail < HTMLDivElement > ( 'main-section' ) . style . display = 'none' ;
2021-02-13 21:19:45 +01:00
HtmlUtils . getElementByIdOrFail < HTMLDivElement > ( 'chat-mode' ) . style . display = 'grid' ;
2020-08-13 18:21:48 +02:00
}
2020-08-17 16:18:39 +02:00
for ( const div of this . importantDivs . values ( ) ) {
2020-08-11 22:32:55 +02:00
this . positionDiv ( div , DivImportance . Important ) ;
}
2020-08-17 16:18:39 +02:00
for ( const div of this . normalDivs . values ( ) ) {
2020-08-11 22:32:55 +02:00
this . positionDiv ( div , DivImportance . Normal ) ;
}
2020-08-24 14:19:36 +02:00
this . listener ? . onCenterChange ( ) ;
2020-08-11 22:32:55 +02:00
}
2020-08-16 23:19:04 +02:00
public getLayoutMode ( ) : LayoutMode {
return this . mode ;
}
2020-08-24 14:19:36 +02:00
/ * p u b l i c g e t G a m e C e n t e r ( ) : { x : n u m b e r , y : n u m b e r } {
} * /
/ * *
* Tries to find the biggest available box of remaining space ( this is a space where we can center the character )
* /
public findBiggestAvailableArray ( ) : { xStart : number , yStart : number , xEnd : number , yEnd : number } {
2021-01-28 21:09:41 +01:00
const game = HtmlUtils . querySelectorOrFail < HTMLCanvasElement > ( '#game canvas' ) ;
2020-08-24 14:19:36 +02:00
if ( this . mode === LayoutMode . VideoChat ) {
const children = document . querySelectorAll < HTMLDivElement > ( 'div.chat-mode > div' ) ;
const htmlChildren = Array . from ( children . values ( ) ) ;
// No chat? Let's go full center
if ( htmlChildren . length === 0 ) {
return {
xStart : 0 ,
yStart : 0 ,
2020-08-31 17:56:11 +02:00
xEnd : game.offsetWidth ,
yEnd : game.offsetHeight
2020-08-24 14:19:36 +02:00
}
}
const lastDiv = htmlChildren [ htmlChildren . length - 1 ] ;
// Compute area between top right of the last div and bottom right of window
2020-08-31 17:56:11 +02:00
const area1 = ( game . offsetWidth - ( lastDiv . offsetLeft + lastDiv . offsetWidth ) )
* ( game . offsetHeight - lastDiv . offsetTop ) ;
2020-08-24 14:19:36 +02:00
// Compute area between bottom of last div and bottom of the screen on whole width
2020-08-31 17:56:11 +02:00
const area2 = game . offsetWidth
* ( game . offsetHeight - ( lastDiv . offsetTop + lastDiv . offsetHeight ) ) ;
2020-08-24 14:19:36 +02:00
if ( area1 < 0 && area2 < 0 ) {
// If screen is full, let's not attempt something foolish and simply center character in the middle.
return {
xStart : 0 ,
yStart : 0 ,
2020-08-31 17:56:11 +02:00
xEnd : game.offsetWidth ,
yEnd : game.offsetHeight
2020-08-24 14:19:36 +02:00
}
}
if ( area1 <= area2 ) {
console . log ( 'lastDiv' , lastDiv . offsetTop , lastDiv . offsetHeight ) ;
return {
xStart : 0 ,
yStart : lastDiv.offsetTop + lastDiv . offsetHeight ,
2020-08-31 17:56:11 +02:00
xEnd : game.offsetWidth ,
yEnd : game.offsetHeight
2020-08-24 14:19:36 +02:00
}
} else {
console . log ( 'lastDiv' , lastDiv . offsetTop ) ;
return {
xStart : lastDiv.offsetLeft + lastDiv . offsetWidth ,
yStart : lastDiv.offsetTop ,
2020-08-31 17:56:11 +02:00
xEnd : game.offsetWidth ,
yEnd : game.offsetHeight
2020-08-24 14:19:36 +02:00
}
}
} else {
// Possible destinations: at the center bottom or at the right bottom.
const mainSectionChildren = Array . from ( document . querySelectorAll < HTMLDivElement > ( 'div.main-section > div' ) . values ( ) ) ;
const sidebarChildren = Array . from ( document . querySelectorAll < HTMLDivElement > ( 'aside.sidebar > div' ) . values ( ) ) ;
2020-08-24 18:23:02 +02:00
// No presentation? Let's center on the screen
2020-08-24 14:19:36 +02:00
if ( mainSectionChildren . length === 0 ) {
return {
xStart : 0 ,
yStart : 0 ,
2020-08-31 17:56:11 +02:00
xEnd : game.offsetWidth ,
yEnd : game.offsetHeight
2020-08-24 14:19:36 +02:00
}
}
// At this point, we know we have at least one element in the main section.
const lastPresentationDiv = mainSectionChildren [ mainSectionChildren . length - 1 ] ;
2020-08-31 17:56:11 +02:00
const presentationArea = ( game . offsetHeight - ( lastPresentationDiv . offsetTop + lastPresentationDiv . offsetHeight ) )
2020-08-24 14:19:36 +02:00
* ( lastPresentationDiv . offsetLeft + lastPresentationDiv . offsetWidth ) ;
let leftSideBar : number ;
let bottomSideBar : number ;
if ( sidebarChildren . length === 0 ) {
leftSideBar = HtmlUtils . getElementByIdOrFail < HTMLDivElement > ( 'sidebar' ) . offsetLeft ;
bottomSideBar = 0 ;
} else {
const lastSideBarChildren = sidebarChildren [ sidebarChildren . length - 1 ] ;
leftSideBar = lastSideBarChildren . offsetLeft ;
bottomSideBar = lastSideBarChildren . offsetTop + lastSideBarChildren . offsetHeight ;
}
2020-08-31 17:56:11 +02:00
const sideBarArea = ( game . offsetWidth - leftSideBar )
* ( game . offsetHeight - bottomSideBar ) ;
2020-08-24 14:19:36 +02:00
if ( presentationArea <= sideBarArea ) {
return {
xStart : leftSideBar ,
yStart : bottomSideBar ,
2020-08-31 17:56:11 +02:00
xEnd : game.offsetWidth ,
yEnd : game.offsetHeight
2020-08-24 14:19:36 +02:00
}
} else {
return {
xStart : 0 ,
yStart : lastPresentationDiv.offsetTop + lastPresentationDiv . offsetHeight ,
2020-08-31 17:56:11 +02:00
xEnd : /*lastPresentationDiv.offsetLeft + lastPresentationDiv.offsetWidth*/ game . offsetWidth , // To avoid flickering when a chat start, we center on the center of the screen, not the center of the main content area
yEnd : game.offsetHeight
2020-08-24 14:19:36 +02:00
}
}
}
}
2020-10-31 14:04:55 +01:00
public addActionButton ( id : string , text : string , callBack : Function , userInputManager : UserInputManager ) {
//delete previous element
this . removeActionButton ( id , userInputManager ) ;
2021-05-12 09:13:25 +02:00
2020-10-31 14:04:55 +01:00
//create div and text html component
const p = document . createElement ( 'p' ) ;
p . classList . add ( 'action-body' ) ;
p . innerText = text ;
const div = document . createElement ( 'div' ) ;
div . classList . add ( 'action' ) ;
div . id = id ;
div . appendChild ( p ) ;
this . actionButtonInformation . set ( id , div ) ;
const mainContainer = HtmlUtils . getElementByIdOrFail < HTMLDivElement > ( 'main-container' ) ;
mainContainer . appendChild ( div ) ;
//add trigger action
2021-03-09 20:01:48 +01:00
div . onpointerdown = ( ) = > callBack ( ) ;
this . actionButtonTrigger . set ( id , callBack ) ;
userInputManager . addSpaceEventListner ( callBack ) ;
2020-10-31 14:04:55 +01:00
}
2021-05-05 01:49:04 +02:00
public removeActionButton ( id : string , userInputManager? : UserInputManager ) {
2020-10-31 14:04:55 +01:00
//delete previous element
const previousDiv = this . actionButtonInformation . get ( id ) ;
if ( previousDiv ) {
previousDiv . remove ( ) ;
this . actionButtonInformation . delete ( id ) ;
}
const previousEventCallback = this . actionButtonTrigger . get ( id ) ;
2021-05-05 01:49:04 +02:00
if ( previousEventCallback && userInputManager ) {
2020-10-31 14:04:55 +01:00
userInputManager . removeSpaceEventListner ( previousEventCallback ) ;
}
}
2021-05-05 01:49:04 +02:00
public addInformation ( id : string , text : string , callBack? : Function , userInputManager? : UserInputManager ) {
//delete previous element
for ( const [ key , value ] of this . actionButtonInformation ) {
this . removeActionButton ( key , userInputManager ) ;
}
//create div and text html component
const p = document . createElement ( 'p' ) ;
p . classList . add ( 'action-body' ) ;
p . innerText = text ;
const div = document . createElement ( 'div' ) ;
div . classList . add ( 'action' ) ;
div . classList . add ( id ) ;
div . id = id ;
div . appendChild ( p ) ;
this . actionButtonInformation . set ( id , div ) ;
const mainContainer = HtmlUtils . getElementByIdOrFail < HTMLDivElement > ( 'main-container' ) ;
mainContainer . appendChild ( div ) ;
//add trigger action
if ( callBack ) {
div . onpointerdown = ( ) = > {
callBack ( ) ;
this . removeActionButton ( id , userInputManager ) ;
} ;
}
//remove it after 10 sec
setTimeout ( ( ) = > {
this . removeActionButton ( id , userInputManager ) ;
} , 10000 )
}
2020-08-11 22:32:55 +02:00
}
2020-08-13 18:21:48 +02:00
const layoutManager = new LayoutManager ( ) ;
export { layoutManager } ;