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-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 ;
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' ;
2020-08-16 23:49:31 +02:00
HtmlUtils . getElementByIdOrFail < HTMLDivElement > ( 'chat-mode' ) . style . display = 'flex' ;
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 } {
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 ,
xEnd : window.innerWidth ,
yEnd : window.innerHeight
}
}
const lastDiv = htmlChildren [ htmlChildren . length - 1 ] ;
// Compute area between top right of the last div and bottom right of window
const area1 = ( window . innerWidth - ( lastDiv . offsetLeft + lastDiv . offsetWidth ) )
* ( window . innerHeight - lastDiv . offsetTop ) ;
// Compute area between bottom of last div and bottom of the screen on whole width
const area2 = window . innerWidth
* ( window . innerHeight - ( lastDiv . offsetTop + lastDiv . offsetHeight ) ) ;
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 ,
xEnd : window.innerWidth ,
yEnd : window.innerHeight
}
}
if ( area1 <= area2 ) {
console . log ( 'lastDiv' , lastDiv . offsetTop , lastDiv . offsetHeight ) ;
return {
xStart : 0 ,
yStart : lastDiv.offsetTop + lastDiv . offsetHeight ,
xEnd : window.innerWidth ,
yEnd : window.innerHeight
}
} else {
console . log ( 'lastDiv' , lastDiv . offsetTop ) ;
return {
xStart : lastDiv.offsetLeft + lastDiv . offsetWidth ,
yStart : lastDiv.offsetTop ,
xEnd : window.innerWidth ,
yEnd : window.innerHeight
}
}
} 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-24 18:23:02 +02:00
xEnd : window.innerWidth ,
2020-08-24 14:19:36 +02:00
yEnd : window.innerHeight
}
}
// At this point, we know we have at least one element in the main section.
const lastPresentationDiv = mainSectionChildren [ mainSectionChildren . length - 1 ] ;
const presentationArea = ( window . innerHeight - ( lastPresentationDiv . offsetTop + lastPresentationDiv . offsetHeight ) )
* ( 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 ;
}
const sideBarArea = ( window . innerWidth - leftSideBar )
* ( window . innerHeight - bottomSideBar ) ;
if ( presentationArea <= sideBarArea ) {
return {
xStart : leftSideBar ,
yStart : bottomSideBar ,
xEnd : window.innerWidth ,
yEnd : window.innerHeight
}
} else {
return {
xStart : 0 ,
yStart : lastPresentationDiv.offsetTop + lastPresentationDiv . offsetHeight ,
xEnd : /*lastPresentationDiv.offsetLeft + lastPresentationDiv.offsetWidth*/ window . innerWidth , // To avoid flickering when a chat start, we center on the center of the screen, not the center of the main content area
yEnd : window.innerHeight
}
}
}
}
2020-08-11 22:32:55 +02:00
}
2020-08-13 18:21:48 +02:00
const layoutManager = new LayoutManager ( ) ;
export { layoutManager } ;