Merge
This commit is contained in:
commit
3a9196fb82
37 changed files with 1601 additions and 213 deletions
|
@ -14,7 +14,7 @@ enum EventMessage{
|
|||
WEBRTC_SCREEN_SHARING_SIGNAL = "webrtc-screen-sharing-signal",
|
||||
WEBRTC_START = "webrtc-start",
|
||||
JOIN_ROOM = "join-room", // bi-directional
|
||||
USER_POSITION = "user-position", // bi-directional
|
||||
USER_POSITION = "user-position", // From client to server
|
||||
USER_MOVED = "user-moved", // From server to client
|
||||
USER_LEFT = "user-left", // From server to client
|
||||
MESSAGE_ERROR = "message-error",
|
||||
|
@ -25,6 +25,9 @@ enum EventMessage{
|
|||
ITEM_EVENT = 'item-event',
|
||||
|
||||
CONNECT_ERROR = "connect_error",
|
||||
SET_SILENT = "set_silent", // Set or unset the silent mode for this user.
|
||||
SET_VIEWPORT = "set-viewport",
|
||||
BATCH = "batch",
|
||||
}
|
||||
|
||||
export interface PointInterface {
|
||||
|
@ -95,6 +98,23 @@ export interface StartMapInterface {
|
|||
startInstance: string
|
||||
}
|
||||
|
||||
export interface ViewportInterface {
|
||||
left: number,
|
||||
top: number,
|
||||
right: number,
|
||||
bottom: number,
|
||||
}
|
||||
|
||||
export interface UserMovesInterface {
|
||||
position: PositionInterface,
|
||||
viewport: ViewportInterface,
|
||||
}
|
||||
|
||||
export interface BatchedMessageInterface {
|
||||
event: string,
|
||||
payload: unknown
|
||||
}
|
||||
|
||||
export interface ItemEventMessageInterface {
|
||||
itemId: number,
|
||||
event: string,
|
||||
|
@ -123,6 +143,18 @@ export class Connection implements Connection {
|
|||
this.socket.on(EventMessage.MESSAGE_ERROR, (message: string) => {
|
||||
console.error(EventMessage.MESSAGE_ERROR, message);
|
||||
})
|
||||
|
||||
/**
|
||||
* Messages inside batched messages are extracted and sent to listeners directly.
|
||||
*/
|
||||
this.socket.on(EventMessage.BATCH, (batchedMessages: BatchedMessageInterface[]) => {
|
||||
for (const message of batchedMessages) {
|
||||
const listeners = this.socket.listeners(message.event);
|
||||
for (const listener of listeners) {
|
||||
listener(message.payload);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public static createConnection(name: string, characterLayersSelected: string[]): Promise<Connection> {
|
||||
|
@ -163,21 +195,33 @@ export class Connection implements Connection {
|
|||
}
|
||||
|
||||
|
||||
public joinARoom(roomId: string, startX: number, startY: number, direction: string, moving: boolean): Promise<RoomJoinedMessageInterface> {
|
||||
public joinARoom(roomId: string, startX: number, startY: number, direction: string, moving: boolean, viewport: ViewportInterface): Promise<RoomJoinedMessageInterface> {
|
||||
const promise = new Promise<RoomJoinedMessageInterface>((resolve, reject) => {
|
||||
this.socket.emit(EventMessage.JOIN_ROOM, { roomId, position: {x: startX, y: startY, direction, moving }}, (roomJoinedMessage: RoomJoinedMessageInterface) => {
|
||||
resolve(roomJoinedMessage);
|
||||
});
|
||||
this.socket.emit(EventMessage.JOIN_ROOM, {
|
||||
roomId,
|
||||
position: {x: startX, y: startY, direction, moving },
|
||||
viewport,
|
||||
}, (roomJoinedMessage: RoomJoinedMessageInterface) => {
|
||||
resolve(roomJoinedMessage);
|
||||
});
|
||||
})
|
||||
return promise;
|
||||
}
|
||||
|
||||
public sharePosition(x : number, y : number, direction : string, moving: boolean) : void{
|
||||
public sharePosition(x : number, y : number, direction : string, moving: boolean, viewport: ViewportInterface) : void{
|
||||
if(!this.socket){
|
||||
return;
|
||||
}
|
||||
const point = new Point(x, y, direction, moving);
|
||||
this.socket.emit(EventMessage.USER_POSITION, point);
|
||||
this.socket.emit(EventMessage.USER_POSITION, { position: point, viewport } as UserMovesInterface);
|
||||
}
|
||||
|
||||
public setSilent(silent: boolean): void {
|
||||
this.socket.emit(EventMessage.SET_SILENT, silent);
|
||||
}
|
||||
|
||||
public setViewport(viewport: ViewportInterface): void {
|
||||
this.socket.emit(EventMessage.SET_VIEWPORT, viewport);
|
||||
}
|
||||
|
||||
public onUserJoins(callback: (message: MessageUserJoined) => void): void {
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
const DEBUG_MODE: boolean = process.env.DEBUG_MODE == "true";
|
||||
const API_URL = (typeof(window) !== 'undefined' ? window.location.protocol : 'http:') + '//' + (process.env.API_URL || "api.workadventure.localhost");
|
||||
const TURN_SERVER: string = process.env.TURN_SERVER || "turn:numb.viagenie.ca";
|
||||
const TURN_USER: string = process.env.TURN_USER || 'g.parant@thecodingmachine.com';
|
||||
const TURN_PASSWORD: string = process.env.TURN_PASSWORD || 'itcugcOHxle9Acqi$';
|
||||
const JITSI_URL : string|undefined = (process.env.JITSI_URL === '') ? undefined : process.env.JITSI_URL;
|
||||
const RESOLUTION = 3;
|
||||
const ZOOM_LEVEL = 1/*3/4*/;
|
||||
const POSITION_DELAY = 200; // Wait 200ms between sending position events
|
||||
|
@ -11,5 +15,9 @@ export {
|
|||
RESOLUTION,
|
||||
ZOOM_LEVEL,
|
||||
POSITION_DELAY,
|
||||
MAX_EXTRAPOLATION_TIME
|
||||
MAX_EXTRAPOLATION_TIME,
|
||||
TURN_SERVER,
|
||||
TURN_USER,
|
||||
TURN_PASSWORD,
|
||||
JITSI_URL
|
||||
}
|
||||
|
|
97
front/src/Phaser/Game/GameMap.ts
Normal file
97
front/src/Phaser/Game/GameMap.ts
Normal file
|
@ -0,0 +1,97 @@
|
|||
import {ITiledMap} from "../Map/ITiledMap";
|
||||
|
||||
export type PropertyChangeCallback = (newValue: string | number | boolean | undefined, oldValue: string | number | boolean | undefined) => void;
|
||||
|
||||
/**
|
||||
* A wrapper around a ITiledMap interface to provide additional capabilities.
|
||||
* It is used to handle layer properties.
|
||||
*/
|
||||
export class GameMap {
|
||||
private key: number|undefined;
|
||||
private lastProperties = new Map<string, string|boolean|number>();
|
||||
private callbacks = new Map<string, Array<PropertyChangeCallback>>();
|
||||
|
||||
public constructor(private map: ITiledMap) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the position of the current player (in pixels)
|
||||
* This will trigger events if properties are changing.
|
||||
*/
|
||||
public setPosition(x: number, y: number) {
|
||||
const xMap = Math.floor(x / this.map.tilewidth);
|
||||
const yMap = Math.floor(y / this.map.tileheight);
|
||||
const key = xMap + yMap * this.map.width;
|
||||
if (key === this.key) {
|
||||
return;
|
||||
}
|
||||
this.key = key;
|
||||
|
||||
const newProps = this.getProperties(key);
|
||||
const oldProps = this.lastProperties;
|
||||
|
||||
// Let's compare the 2 maps:
|
||||
// First new properties vs oldProperties
|
||||
for (const [newPropName, newPropValue] of newProps.entries()) {
|
||||
const oldPropValue = oldProps.get(newPropName);
|
||||
if (oldPropValue !== newPropValue) {
|
||||
this.trigger(newPropName, oldPropValue, newPropValue);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [oldPropName, oldPropValue] of oldProps.entries()) {
|
||||
if (!newProps.has(oldPropName)) {
|
||||
// We found a property that disappeared
|
||||
this.trigger(oldPropName, oldPropValue, undefined);
|
||||
}
|
||||
}
|
||||
|
||||
this.lastProperties = newProps;
|
||||
}
|
||||
|
||||
private getProperties(key: number): Map<string, string|boolean|number> {
|
||||
const properties = new Map<string, string|boolean|number>();
|
||||
|
||||
for (const layer of this.map.layers) {
|
||||
if (layer.type !== 'tilelayer') {
|
||||
continue;
|
||||
}
|
||||
const tiles = layer.data as number[];
|
||||
if (tiles[key] == 0) {
|
||||
continue;
|
||||
}
|
||||
// There is a tile in this layer, let's embed the properties
|
||||
if (layer.properties !== undefined) {
|
||||
for (const layerProperty of layer.properties) {
|
||||
if (layerProperty.value === undefined) {
|
||||
continue;
|
||||
}
|
||||
properties.set(layerProperty.name, layerProperty.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return properties;
|
||||
}
|
||||
|
||||
private trigger(propName: string, oldValue: string | number | boolean | undefined, newValue: string | number | boolean | undefined) {
|
||||
const callbacksArray = this.callbacks.get(propName);
|
||||
if (callbacksArray !== undefined) {
|
||||
for (const callback of callbacksArray) {
|
||||
callback(newValue, oldValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a callback called when the user moves to a tile where the property propName is different from the last tile the user was on.
|
||||
*/
|
||||
public onPropertyChange(propName: string, callback: PropertyChangeCallback) {
|
||||
let callbacksArray = this.callbacks.get(propName);
|
||||
if (callbacksArray === undefined) {
|
||||
callbacksArray = new Array<PropertyChangeCallback>();
|
||||
this.callbacks.set(propName, callbacksArray);
|
||||
}
|
||||
callbacksArray.push(callback);
|
||||
}
|
||||
}
|
|
@ -10,7 +10,7 @@ import {
|
|||
RoomJoinedMessageInterface
|
||||
} from "../../Connection";
|
||||
import {CurrentGamerInterface, hasMovedEventName, Player} from "../Player/Player";
|
||||
import {DEBUG_MODE, POSITION_DELAY, RESOLUTION, ZOOM_LEVEL} from "../../Enum/EnvironmentVariable";
|
||||
import {DEBUG_MODE, JITSI_URL, POSITION_DELAY, RESOLUTION, ZOOM_LEVEL} from "../../Enum/EnvironmentVariable";
|
||||
import {
|
||||
ITiledMap,
|
||||
ITiledMapLayer,
|
||||
|
@ -33,6 +33,9 @@ import Sprite = Phaser.GameObjects.Sprite;
|
|||
import CanvasTexture = Phaser.Textures.CanvasTexture;
|
||||
import GameObject = Phaser.GameObjects.GameObject;
|
||||
import FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR;
|
||||
import {GameMap} from "./GameMap";
|
||||
import {CoWebsiteManager} from "../../WebRtc/CoWebsiteManager";
|
||||
import {mediaManager} from "../../WebRtc/MediaManager";
|
||||
import {FourOFourSceneName} from "../Reconnecting/FourOFourScene";
|
||||
import {ItemFactoryInterface} from "../Items/ItemFactoryInterface";
|
||||
import {ActionableItem} from "../Items/ActionableItem";
|
||||
|
@ -122,7 +125,8 @@ export class GameScene extends Phaser.Scene implements CenterListener {
|
|||
private startLayerName: string|undefined;
|
||||
private presentationModeSprite!: Sprite;
|
||||
private chatModeSprite!: Sprite;
|
||||
private repositionCallback!: (this: Window, ev: UIEvent) => void;
|
||||
private onResizeCallback!: (this: Window, ev: UIEvent) => void;
|
||||
private gameMap!: GameMap;
|
||||
private actionableItems: Map<number, ActionableItem> = new Map<number, ActionableItem>();
|
||||
// The item that can be selected by pressing the space key.
|
||||
private outlinedItem: ActionableItem|null = null;
|
||||
|
@ -247,7 +251,7 @@ export class GameScene extends Phaser.Scene implements CenterListener {
|
|||
|
||||
this.scene.stop(this.scene.key);
|
||||
this.scene.remove(this.scene.key);
|
||||
window.removeEventListener('resize', this.repositionCallback);
|
||||
window.removeEventListener('resize', this.onResizeCallback);
|
||||
})
|
||||
|
||||
connection.onActionableEvent((message => {
|
||||
|
@ -398,6 +402,7 @@ export class GameScene extends Phaser.Scene implements CenterListener {
|
|||
create(): void {
|
||||
//initalise map
|
||||
this.Map = this.add.tilemap(this.MapKey);
|
||||
this.gameMap = new GameMap(this.mapFile);
|
||||
const mapDirUrl = this.MapUrlFile.substr(0, this.MapUrlFile.lastIndexOf('/'));
|
||||
this.mapFile.tilesets.forEach((tileset: ITiledTileSet) => {
|
||||
this.Terrains.push(this.Map.addTilesetImage(tileset.name, `${mapDirUrl}/${tileset.image}`, tileset.tilewidth, tileset.tileheight, tileset.margin, tileset.spacing/*, tileset.firstgid*/));
|
||||
|
@ -521,12 +526,14 @@ export class GameScene extends Phaser.Scene implements CenterListener {
|
|||
this.presentationModeSprite.setOrigin(0, 1);
|
||||
this.presentationModeSprite.setInteractive();
|
||||
this.presentationModeSprite.setVisible(false);
|
||||
this.presentationModeSprite.setDepth(99999);
|
||||
this.presentationModeSprite.on('pointerup', this.switchLayoutMode.bind(this));
|
||||
this.chatModeSprite = this.add.sprite(36, this.game.renderer.height - 2, 'layout_modes', 3);
|
||||
this.chatModeSprite.setScrollFactor(0, 0);
|
||||
this.chatModeSprite.setOrigin(0, 1);
|
||||
this.chatModeSprite.setInteractive();
|
||||
this.chatModeSprite.setVisible(false);
|
||||
this.chatModeSprite.setDepth(99999);
|
||||
this.chatModeSprite.on('pointerup', this.switchLayoutMode.bind(this));
|
||||
|
||||
// FIXME: change this to use the UserInputManager class for input
|
||||
|
@ -534,12 +541,58 @@ export class GameScene extends Phaser.Scene implements CenterListener {
|
|||
this.switchLayoutMode();
|
||||
});
|
||||
|
||||
this.repositionCallback = this.reposition.bind(this);
|
||||
window.addEventListener('resize', this.repositionCallback);
|
||||
this.onResizeCallback = this.onResize.bind(this);
|
||||
window.addEventListener('resize', this.onResizeCallback);
|
||||
this.reposition();
|
||||
|
||||
// From now, this game scene will be notified of reposition events
|
||||
layoutManager.setListener(this);
|
||||
|
||||
this.gameMap.onPropertyChange('openWebsite', (newValue, oldValue) => {
|
||||
if (newValue === undefined) {
|
||||
CoWebsiteManager.closeCoWebsite();
|
||||
} else {
|
||||
CoWebsiteManager.loadCoWebsite(newValue as string);
|
||||
}
|
||||
});
|
||||
let jitsiApi: any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
this.gameMap.onPropertyChange('jitsiRoom', (newValue, oldValue) => {
|
||||
if (newValue === undefined) {
|
||||
this.connection.setSilent(false);
|
||||
jitsiApi?.dispose();
|
||||
CoWebsiteManager.closeCoWebsite();
|
||||
mediaManager.showGameOverlay();
|
||||
} else {
|
||||
CoWebsiteManager.insertCoWebsite((cowebsiteDiv => {
|
||||
const domain = JITSI_URL;
|
||||
const options = {
|
||||
roomName: this.instance + "-" + newValue,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
parentNode: cowebsiteDiv,
|
||||
configOverwrite: {
|
||||
prejoinPageEnabled: false
|
||||
},
|
||||
interfaceConfigOverwrite: {
|
||||
SHOW_CHROME_EXTENSION_BANNER: false,
|
||||
MOBILE_APP_PROMO: false
|
||||
}
|
||||
};
|
||||
jitsiApi = new (window as any).JitsiMeetExternalAPI(domain, options); // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
jitsiApi.executeCommand('displayName', gameManager.getPlayerName());
|
||||
}));
|
||||
this.connection.setSilent(true);
|
||||
mediaManager.hideGameOverlay();
|
||||
}
|
||||
})
|
||||
|
||||
this.gameMap.onPropertyChange('silent', (newValue, oldValue) => {
|
||||
if (newValue === undefined || newValue === false || newValue === '') {
|
||||
this.connection.setSilent(false);
|
||||
} else {
|
||||
this.connection.setSilent(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private switchLayoutMode(): void {
|
||||
|
@ -713,7 +766,17 @@ export class GameScene extends Phaser.Scene implements CenterListener {
|
|||
|
||||
//join room
|
||||
this.connectionPromise.then((connection: Connection) => {
|
||||
connection.joinARoom(this.RoomId, this.startX, this.startY, PlayerAnimationNames.WalkDown, false).then((roomJoinedMessage: RoomJoinedMessageInterface) => {
|
||||
const camera = this.cameras.main;
|
||||
connection.joinARoom(this.RoomId,
|
||||
this.startX,
|
||||
this.startY,
|
||||
PlayerAnimationNames.WalkDown,
|
||||
false, {
|
||||
left: camera.scrollX,
|
||||
top: camera.scrollY,
|
||||
right: camera.scrollX + camera.width,
|
||||
bottom: camera.scrollY + camera.height,
|
||||
}).then((roomJoinedMessage: RoomJoinedMessageInterface) => {
|
||||
this.initUsersPosition(roomJoinedMessage.users);
|
||||
this.connectionAnswerPromiseResolve(roomJoinedMessage);
|
||||
});
|
||||
|
@ -721,6 +784,9 @@ export class GameScene extends Phaser.Scene implements CenterListener {
|
|||
//listen event to share position of user
|
||||
this.CurrentPlayer.on(hasMovedEventName, this.pushPlayerPosition.bind(this))
|
||||
this.CurrentPlayer.on(hasMovedEventName, this.outlineItem.bind(this))
|
||||
this.CurrentPlayer.on(hasMovedEventName, (event: HasMovedEvent) => {
|
||||
this.gameMap.setPosition(event.x, event.y);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -796,7 +862,13 @@ export class GameScene extends Phaser.Scene implements CenterListener {
|
|||
private doPushPlayerPosition(event: HasMovedEvent): void {
|
||||
this.lastMoveEventSent = event;
|
||||
this.lastSentTick = this.currentTick;
|
||||
this.connection.sharePosition(event.x, event.y, event.direction, event.moving);
|
||||
const camera = this.cameras.main;
|
||||
this.connection.sharePosition(event.x, event.y, event.direction, event.moving, {
|
||||
left: camera.scrollX,
|
||||
top: camera.scrollY,
|
||||
right: camera.scrollX + camera.width,
|
||||
bottom: camera.scrollY + camera.height,
|
||||
});
|
||||
}
|
||||
|
||||
EventToClickOnTile(){
|
||||
|
@ -860,7 +932,7 @@ export class GameScene extends Phaser.Scene implements CenterListener {
|
|||
this.simplePeer.unregister();
|
||||
this.scene.stop();
|
||||
this.scene.remove(this.scene.key);
|
||||
window.removeEventListener('resize', this.repositionCallback);
|
||||
window.removeEventListener('resize', this.onResizeCallback);
|
||||
this.scene.start(nextSceneKey.key, {
|
||||
startLayerName: nextSceneKey.hash
|
||||
});
|
||||
|
@ -1058,6 +1130,19 @@ export class GameScene extends Phaser.Scene implements CenterListener {
|
|||
this.connection.emitActionableEvent(itemId, eventName, state, parameters);
|
||||
}
|
||||
|
||||
private onResize(): void {
|
||||
this.reposition();
|
||||
|
||||
// Send new viewport to server
|
||||
const camera = this.cameras.main;
|
||||
this.connection.setViewport({
|
||||
left: camera.scrollX,
|
||||
top: camera.scrollY,
|
||||
right: camera.scrollX + camera.width,
|
||||
bottom: camera.scrollY + camera.height,
|
||||
});
|
||||
}
|
||||
|
||||
private reposition(): void {
|
||||
this.presentationModeSprite.setY(this.game.renderer.height - 2);
|
||||
this.chatModeSprite.setY(this.game.renderer.height - 2);
|
||||
|
|
|
@ -266,6 +266,9 @@ export class EnableCameraScene extends Phaser.Scene {
|
|||
this.soundMeter.stop();
|
||||
window.removeEventListener('resize', this.repositionCallback);
|
||||
|
||||
mediaManager.stopCamera();
|
||||
mediaManager.stopMicrophone();
|
||||
|
||||
// Do we have a start URL in the address bar? If so, let's redirect to this address
|
||||
const instanceAndMapUrl = this.findMapUrl();
|
||||
if (instanceAndMapUrl !== null) {
|
||||
|
|
|
@ -14,7 +14,24 @@ export class CoWebsiteManager {
|
|||
iframe.id = 'cowebsite-iframe';
|
||||
iframe.src = url;
|
||||
cowebsiteDiv.appendChild(iframe);
|
||||
//iframe.onload = () => {
|
||||
// onload can be long to trigger. Maybe we should display the website, whatever happens, after 1 second?
|
||||
CoWebsiteManager.fire();
|
||||
//}
|
||||
}
|
||||
|
||||
/**
|
||||
* Just like loadCoWebsite but the div can be filled by the user.
|
||||
*/
|
||||
public static insertCoWebsite(callback: (cowebsite: HTMLDivElement) => void): void {
|
||||
const cowebsiteDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>("cowebsite");
|
||||
cowebsiteDiv.innerHTML = '';
|
||||
|
||||
callback(cowebsiteDiv);
|
||||
//iframe.onload = () => {
|
||||
// onload can be long to trigger. Maybe we should display the website, whatever happens, after 1 second?
|
||||
CoWebsiteManager.fire();
|
||||
//}
|
||||
}
|
||||
|
||||
public static closeCoWebsite(): void {
|
||||
|
@ -24,8 +41,8 @@ export class CoWebsiteManager {
|
|||
}
|
||||
|
||||
public static getGameSize(): {width: number, height: number} {
|
||||
const iframe = document.getElementById('cowebsite-iframe');
|
||||
if (iframe === null) {
|
||||
const hasChildren = HtmlUtils.getElementByIdOrFail<HTMLDivElement>("cowebsite").children.length > 0;
|
||||
if (hasChildren === false) {
|
||||
return {
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight
|
||||
|
|
|
@ -42,6 +42,14 @@ class LayoutManager {
|
|||
div.innerHTML = html;
|
||||
div.id = "user-"+userId;
|
||||
div.className = "media-container"
|
||||
div.onclick = () => {
|
||||
const parentId = div.parentElement?.id;
|
||||
if (parentId === 'sidebar' || parentId === 'chat-mode') {
|
||||
this.focusOn(userId);
|
||||
} else {
|
||||
this.removeFocusOn(userId);
|
||||
}
|
||||
}
|
||||
|
||||
if (importance === DivImportance.Important) {
|
||||
this.importantDivs.set(userId, div);
|
||||
|
@ -76,6 +84,48 @@ class LayoutManager {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the DIV matching userId.
|
||||
*/
|
||||
|
@ -154,6 +204,7 @@ class LayoutManager {
|
|||
* 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} {
|
||||
const game = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('game');
|
||||
if (this.mode === LayoutMode.VideoChat) {
|
||||
const children = document.querySelectorAll<HTMLDivElement>('div.chat-mode > div');
|
||||
const htmlChildren = Array.from(children.values());
|
||||
|
@ -163,27 +214,27 @@ class LayoutManager {
|
|||
return {
|
||||
xStart: 0,
|
||||
yStart: 0,
|
||||
xEnd: window.innerWidth,
|
||||
yEnd: window.innerHeight
|
||||
xEnd: game.offsetWidth,
|
||||
yEnd: game.offsetHeight
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
const area1 = (game.offsetWidth - (lastDiv.offsetLeft + lastDiv.offsetWidth))
|
||||
* (game.offsetHeight - 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));
|
||||
const area2 = game.offsetWidth
|
||||
* (game.offsetHeight - (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
|
||||
xEnd: game.offsetWidth,
|
||||
yEnd: game.offsetHeight
|
||||
}
|
||||
}
|
||||
if (area1 <= area2) {
|
||||
|
@ -191,16 +242,16 @@ class LayoutManager {
|
|||
return {
|
||||
xStart: 0,
|
||||
yStart: lastDiv.offsetTop + lastDiv.offsetHeight,
|
||||
xEnd: window.innerWidth,
|
||||
yEnd: window.innerHeight
|
||||
xEnd: game.offsetWidth,
|
||||
yEnd: game.offsetHeight
|
||||
}
|
||||
} else {
|
||||
console.log('lastDiv', lastDiv.offsetTop);
|
||||
return {
|
||||
xStart: lastDiv.offsetLeft + lastDiv.offsetWidth,
|
||||
yStart: lastDiv.offsetTop,
|
||||
xEnd: window.innerWidth,
|
||||
yEnd: window.innerHeight
|
||||
xEnd: game.offsetWidth,
|
||||
yEnd: game.offsetHeight
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -213,15 +264,15 @@ class LayoutManager {
|
|||
return {
|
||||
xStart: 0,
|
||||
yStart: 0,
|
||||
xEnd: window.innerWidth,
|
||||
yEnd: window.innerHeight
|
||||
xEnd: game.offsetWidth,
|
||||
yEnd: game.offsetHeight
|
||||
}
|
||||
}
|
||||
|
||||
// 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))
|
||||
const presentationArea = (game.offsetHeight - (lastPresentationDiv.offsetTop + lastPresentationDiv.offsetHeight))
|
||||
* (lastPresentationDiv.offsetLeft + lastPresentationDiv.offsetWidth);
|
||||
|
||||
let leftSideBar: number;
|
||||
|
@ -234,22 +285,22 @@ class LayoutManager {
|
|||
leftSideBar = lastSideBarChildren.offsetLeft;
|
||||
bottomSideBar = lastSideBarChildren.offsetTop + lastSideBarChildren.offsetHeight;
|
||||
}
|
||||
const sideBarArea = (window.innerWidth - leftSideBar)
|
||||
* (window.innerHeight - bottomSideBar);
|
||||
const sideBarArea = (game.offsetWidth - leftSideBar)
|
||||
* (game.offsetHeight - bottomSideBar);
|
||||
|
||||
if (presentationArea <= sideBarArea) {
|
||||
return {
|
||||
xStart: leftSideBar,
|
||||
yStart: bottomSideBar,
|
||||
xEnd: window.innerWidth,
|
||||
yEnd: window.innerHeight
|
||||
xEnd: game.offsetWidth,
|
||||
yEnd: game.offsetHeight
|
||||
}
|
||||
} 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
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,9 +7,9 @@ const videoConstraint: boolean|MediaTrackConstraints = {
|
|||
facingMode: "user"
|
||||
};
|
||||
|
||||
type UpdatedLocalStreamCallback = (media: MediaStream) => void;
|
||||
type StartScreenSharingCallback = (media: MediaStream) => void;
|
||||
type StopScreenSharingCallback = (media: MediaStream) => void;
|
||||
export type UpdatedLocalStreamCallback = (media: MediaStream|null) => void;
|
||||
export type StartScreenSharingCallback = (media: MediaStream) => void;
|
||||
export type StopScreenSharingCallback = (media: MediaStream) => void;
|
||||
|
||||
// TODO: Split MediaManager in 2 classes: MediaManagerUI (in charge of HTML) and MediaManager (singleton in charge of the camera only)
|
||||
// TODO: verify that microphone event listeners are not triggered plenty of time NOW (since MediaManager is created many times!!!!)
|
||||
|
@ -109,7 +109,7 @@ export class MediaManager {
|
|||
this.updatedLocalStreamCallBacks.delete(callback);
|
||||
}
|
||||
|
||||
private triggerUpdatedLocalStreamCallbacks(stream: MediaStream): void {
|
||||
private triggerUpdatedLocalStreamCallbacks(stream: MediaStream|null): void {
|
||||
for (const callback of this.updatedLocalStreamCallBacks) {
|
||||
callback(stream);
|
||||
}
|
||||
|
@ -127,11 +127,16 @@ export class MediaManager {
|
|||
}
|
||||
}
|
||||
|
||||
showGameOverlay(){
|
||||
public showGameOverlay(){
|
||||
const gameOverlay = this.getElementByIdOrFail('game-overlay');
|
||||
gameOverlay.classList.add('active');
|
||||
}
|
||||
|
||||
public hideGameOverlay(){
|
||||
const gameOverlay = this.getElementByIdOrFail('game-overlay');
|
||||
gameOverlay.classList.remove('active');
|
||||
}
|
||||
|
||||
private enableCamera() {
|
||||
this.cinemaClose.style.display = "none";
|
||||
this.cinemaBtn.classList.remove("disabled");
|
||||
|
@ -142,20 +147,20 @@ export class MediaManager {
|
|||
});
|
||||
}
|
||||
|
||||
private disableCamera() {
|
||||
private async disableCamera() {
|
||||
this.cinemaClose.style.display = "block";
|
||||
this.cinema.style.display = "none";
|
||||
this.cinemaBtn.classList.add("disabled");
|
||||
this.constraintsMedia.video = false;
|
||||
this.myCamVideo.srcObject = null;
|
||||
if (this.localStream) {
|
||||
this.localStream.getVideoTracks().forEach((MediaStreamTrack: MediaStreamTrack) => {
|
||||
MediaStreamTrack.stop();
|
||||
});
|
||||
}
|
||||
this.getCamera().then((stream) => {
|
||||
this.stopCamera();
|
||||
|
||||
if (this.constraintsMedia.audio !== false) {
|
||||
const stream = await this.getCamera();
|
||||
this.triggerUpdatedLocalStreamCallbacks(stream);
|
||||
});
|
||||
} else {
|
||||
this.triggerUpdatedLocalStreamCallbacks(null);
|
||||
}
|
||||
}
|
||||
|
||||
private enableMicrophone() {
|
||||
|
@ -163,24 +168,25 @@ export class MediaManager {
|
|||
this.microphone.style.display = "block";
|
||||
this.microphoneBtn.classList.remove("disabled");
|
||||
this.constraintsMedia.audio = true;
|
||||
|
||||
this.getCamera().then((stream) => {
|
||||
this.triggerUpdatedLocalStreamCallbacks(stream);
|
||||
});
|
||||
}
|
||||
|
||||
private disableMicrophone() {
|
||||
private async disableMicrophone() {
|
||||
this.microphoneClose.style.display = "block";
|
||||
this.microphone.style.display = "none";
|
||||
this.microphoneBtn.classList.add("disabled");
|
||||
this.constraintsMedia.audio = false;
|
||||
if(this.localStream) {
|
||||
this.localStream.getAudioTracks().forEach((MediaStreamTrack: MediaStreamTrack) => {
|
||||
MediaStreamTrack.stop();
|
||||
});
|
||||
}
|
||||
this.getCamera().then((stream) => {
|
||||
this.stopMicrophone();
|
||||
|
||||
if (this.constraintsMedia.video !== false) {
|
||||
const stream = await this.getCamera();
|
||||
this.triggerUpdatedLocalStreamCallbacks(stream);
|
||||
});
|
||||
} else {
|
||||
this.triggerUpdatedLocalStreamCallbacks(null);
|
||||
}
|
||||
}
|
||||
|
||||
private enableScreenSharing() {
|
||||
|
@ -287,6 +293,28 @@ export class MediaManager {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the camera from filming
|
||||
*/
|
||||
public stopCamera(): void {
|
||||
if (this.localStream) {
|
||||
for (const track of this.localStream.getVideoTracks()) {
|
||||
track.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the microphone from listening
|
||||
*/
|
||||
public stopMicrophone(): void {
|
||||
if (this.localStream) {
|
||||
for (const track of this.localStream.getAudioTracks()) {
|
||||
track.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setCamera(id: string): Promise<MediaStream> {
|
||||
let video = this.constraintsMedia.video;
|
||||
if (typeof(video) === 'boolean' || video === undefined) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import * as SimplePeerNamespace from "simple-peer";
|
||||
import {mediaManager} from "./MediaManager";
|
||||
import {Connection} from "../Connection";
|
||||
import {TURN_SERVER, TURN_USER, TURN_PASSWORD} from "../Enum/EnvironmentVariable";
|
||||
|
||||
const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer');
|
||||
|
||||
|
@ -23,9 +24,9 @@ export class ScreenSharingPeer extends Peer {
|
|||
urls: 'stun:stun.l.google.com:19302'
|
||||
},
|
||||
{
|
||||
urls: 'turn:numb.viagenie.ca',
|
||||
username: 'g.parant@thecodingmachine.com',
|
||||
credential: 'itcugcOHxle9Acqi$'
|
||||
urls: TURN_SERVER.split(','),
|
||||
username: TURN_USER,
|
||||
credential: TURN_PASSWORD
|
||||
},
|
||||
]
|
||||
}
|
||||
|
@ -70,7 +71,7 @@ export class ScreenSharingPeer extends Peer {
|
|||
}
|
||||
|
||||
private sendWebrtcScreenSharingSignal(data: unknown) {
|
||||
console.log("sendWebrtcScreenSharingSignal", data);
|
||||
//console.log("sendWebrtcScreenSharingSignal", data);
|
||||
try {
|
||||
this.connection.sendWebrtcScreenSharingSignal(data, this.userId);
|
||||
}catch (e) {
|
||||
|
@ -82,8 +83,8 @@ export class ScreenSharingPeer extends Peer {
|
|||
* Sends received stream to screen.
|
||||
*/
|
||||
private stream(stream?: MediaStream) {
|
||||
console.log(`ScreenSharingPeer::stream => ${this.userId}`, stream);
|
||||
console.log(`stream => ${this.userId} => `, stream);
|
||||
//console.log(`ScreenSharingPeer::stream => ${this.userId}`, stream);
|
||||
//console.log(`stream => ${this.userId} => `, stream);
|
||||
if(!stream){
|
||||
mediaManager.removeActiveScreenSharingVideo(this.userId);
|
||||
this.isReceivingStream = false;
|
||||
|
|
|
@ -4,7 +4,12 @@ import {
|
|||
WebRtcSignalReceivedMessageInterface,
|
||||
WebRtcStartMessageInterface
|
||||
} from "../Connection";
|
||||
import { mediaManager } from "./MediaManager";
|
||||
import {
|
||||
mediaManager,
|
||||
StartScreenSharingCallback,
|
||||
StopScreenSharingCallback,
|
||||
UpdatedLocalStreamCallback
|
||||
} from "./MediaManager";
|
||||
import * as SimplePeerNamespace from "simple-peer";
|
||||
import {ScreenSharingPeer} from "./ScreenSharingPeer";
|
||||
import {VideoPeer} from "./VideoPeer";
|
||||
|
@ -32,9 +37,9 @@ export class SimplePeer {
|
|||
|
||||
private PeerScreenSharingConnectionArray: Map<string, ScreenSharingPeer> = new Map<string, ScreenSharingPeer>();
|
||||
private PeerConnectionArray: Map<string, VideoPeer> = new Map<string, VideoPeer>();
|
||||
private readonly sendLocalVideoStreamCallback: (media: MediaStream) => void;
|
||||
private readonly sendLocalScreenSharingStreamCallback: (media: MediaStream) => void;
|
||||
private readonly stopLocalScreenSharingStreamCallback: (media: MediaStream) => void;
|
||||
private readonly sendLocalVideoStreamCallback: UpdatedLocalStreamCallback;
|
||||
private readonly sendLocalScreenSharingStreamCallback: StartScreenSharingCallback;
|
||||
private readonly stopLocalScreenSharingStreamCallback: StopScreenSharingCallback;
|
||||
private readonly peerConnectionListeners: Array<PeerConnectionListener> = new Array<PeerConnectionListener>();
|
||||
|
||||
constructor(Connection: Connection, WebRtcRoomId: string = "test-webrtc") {
|
||||
|
@ -326,9 +331,9 @@ export class SimplePeer {
|
|||
}
|
||||
|
||||
public sendLocalVideoStream(){
|
||||
this.Users.forEach((user: UserSimplePeerInterface) => {
|
||||
for (const user of this.Users) {
|
||||
this.pushVideoToRemoteUser(user.userId);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import * as SimplePeerNamespace from "simple-peer";
|
||||
import {mediaManager} from "./MediaManager";
|
||||
import {Connection} from "../Connection";
|
||||
import {TURN_PASSWORD, TURN_SERVER, TURN_USER} from "../Enum/EnvironmentVariable";
|
||||
|
||||
const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer');
|
||||
|
||||
|
@ -18,14 +19,31 @@ export class VideoPeer extends Peer {
|
|||
urls: 'stun:stun.l.google.com:19302'
|
||||
},
|
||||
{
|
||||
urls: 'turn:numb.viagenie.ca',
|
||||
username: 'g.parant@thecodingmachine.com',
|
||||
credential: 'itcugcOHxle9Acqi$'
|
||||
urls: TURN_SERVER.split(','),
|
||||
username: TURN_USER,
|
||||
credential: TURN_PASSWORD
|
||||
},
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
console.log('PEER SETUP ', {
|
||||
initiator: initiator ? initiator : false,
|
||||
reconnectTimer: 10000,
|
||||
config: {
|
||||
iceServers: [
|
||||
{
|
||||
urls: 'stun:stun.l.google.com:19302'
|
||||
},
|
||||
{
|
||||
urls: TURN_SERVER,
|
||||
username: TURN_USER,
|
||||
credential: TURN_PASSWORD
|
||||
},
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
//start listen signal for the peer connection
|
||||
this.on('signal', (data: unknown) => {
|
||||
this.sendWebrtcSignal(data);
|
||||
|
@ -85,7 +103,7 @@ export class VideoPeer extends Peer {
|
|||
* Sends received stream to screen.
|
||||
*/
|
||||
private stream(stream?: MediaStream) {
|
||||
console.log(`VideoPeer::stream => ${this.userId}`, stream);
|
||||
//console.log(`VideoPeer::stream => ${this.userId}`, stream);
|
||||
if(!stream){
|
||||
mediaManager.disabledVideoByUserId(this.userId);
|
||||
mediaManager.disabledMicrophoneByUserId(this.userId);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import 'phaser';
|
||||
import GameConfig = Phaser.Types.Core.GameConfig;
|
||||
import {DEBUG_MODE, RESOLUTION} from "./Enum/EnvironmentVariable";
|
||||
import {DEBUG_MODE, JITSI_URL, RESOLUTION} from "./Enum/EnvironmentVariable";
|
||||
import {cypressAsserter} from "./Cypress/CypressAsserter";
|
||||
import {LoginScene} from "./Phaser/Login/LoginScene";
|
||||
import {ReconnectingScene} from "./Phaser/Reconnecting/ReconnectingScene";
|
||||
|
@ -15,6 +15,13 @@ import {CoWebsiteManager} from "./WebRtc/CoWebsiteManager";
|
|||
|
||||
//CoWebsiteManager.loadCoWebsite('https://thecodingmachine.com');
|
||||
|
||||
// Load Jitsi if the environment variable is set.
|
||||
if (JITSI_URL) {
|
||||
const jitsiScript = document.createElement('script');
|
||||
jitsiScript.src = 'https://' + JITSI_URL + '/external_api.js';
|
||||
document.head.appendChild(jitsiScript);
|
||||
}
|
||||
|
||||
const {width, height} = CoWebsiteManager.getGameSize();
|
||||
|
||||
const config: GameConfig = {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue