Merge branch 'develop' of github.com:thecodingmachine/workadventure into outline

This commit is contained in:
David Négrier 2020-08-24 18:15:44 +02:00
commit fca93663b4
13 changed files with 991 additions and 201 deletions

View file

@ -13,7 +13,6 @@ import {MessageUserJoined} from "../Model/Websocket/MessageUserJoined";
import {MessageUserMoved} from "../Model/Websocket/MessageUserMoved";
import si from "systeminformation";
import {Gauge} from "prom-client";
import os from 'os';
import {TokenInterface} from "../Controller/AuthenticateController";
import {isJoinRoomMessageInterface} from "../Model/Websocket/JoinRoomMessage";
import {isPointInterface, PointInterface} from "../Model/Websocket/PointInterface";
@ -29,6 +28,7 @@ enum SockerIoEvent {
USER_MOVED = "user-moved", // From server to client
USER_LEFT = "user-left", // From server to client
WEBRTC_SIGNAL = "webrtc-signal",
WEBRTC_SCREEN_SHARING_SIGNAL = "webrtc-screen-sharing-signal",
WEBRTC_START = "webrtc-start",
WEBRTC_DISCONNECT = "webrtc-disconect",
MESSAGE_ERROR = "message-error",
@ -237,18 +237,11 @@ export class IoSocketController {
});
socket.on(SockerIoEvent.WEBRTC_SIGNAL, (data: unknown) => {
if (!isWebRtcSignalMessageInterface(data)) {
socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: 'Invalid WEBRTC_SIGNAL message.'});
console.warn('Invalid WEBRTC_SIGNAL message received: ', data);
return;
}
//send only at user
const client = this.sockets.get(data.receiverId);
if (client === undefined) {
console.warn("While exchanging a WebRTC signal: client with id ", data.receiverId, " does not exist. This might be a race condition.");
return;
}
return client.emit(SockerIoEvent.WEBRTC_SIGNAL, data);
this.emitVideo((socket as ExSocketInterface), data);
});
socket.on(SockerIoEvent.WEBRTC_SCREEN_SHARING_SIGNAL, (data: unknown) => {
this.emitScreenSharing((socket as ExSocketInterface), data);
});
socket.on(SockerIoEvent.DISCONNECT, () => {
@ -318,6 +311,42 @@ export class IoSocketController {
});
}
emitVideo(socket: ExSocketInterface, data: unknown){
if (!isWebRtcSignalMessageInterface(data)) {
socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: 'Invalid WEBRTC_SIGNAL message.'});
console.warn('Invalid WEBRTC_SIGNAL message received: ', data);
return;
}
//send only at user
const client = this.sockets.get(data.receiverId);
if (client === undefined) {
console.warn("While exchanging a WebRTC signal: client with id ", data.receiverId, " does not exist. This might be a race condition.");
return;
}
return client.emit(SockerIoEvent.WEBRTC_SIGNAL, {
userId: socket.userId,
signal: data.signal
});
}
emitScreenSharing(socket: ExSocketInterface, data: unknown){
if (!isWebRtcSignalMessageInterface(data)) {
socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: 'Invalid WEBRTC_SCREEN_SHARING message.'});
console.warn('Invalid WEBRTC_SCREEN_SHARING message received: ', data);
return;
}
//send only at user
const client = this.sockets.get(data.receiverId);
if (client === undefined) {
console.warn("While exchanging a WEBRTC_SCREEN_SHARING signal: client with id ", data.receiverId, " does not exist. This might be a race condition.");
return;
}
return client.emit(SockerIoEvent.WEBRTC_SCREEN_SHARING_SIGNAL, {
userId: socket.userId,
signal: data.signal
});
}
searchClientByIdOrFail(userId: string): ExSocketInterface {
const client: ExSocketInterface|undefined = this.sockets.get(userId);
if (client === undefined) {
@ -398,13 +427,15 @@ export class IoSocketController {
if (this.Io.sockets.adapter.rooms[roomId].length < 2 /*|| this.Io.sockets.adapter.rooms[roomId].length >= 4*/) {
return;
}
// TODO: scanning all sockets is maybe not the most efficient
const clients: Array<ExSocketInterface> = (Object.values(this.Io.sockets.sockets) as Array<ExSocketInterface>)
.filter((client: ExSocketInterface) => client.webRtcRoomId && client.webRtcRoomId === roomId);
//send start at one client to initialise offer webrtc
//send all users in room to create PeerConnection in front
clients.forEach((client: ExSocketInterface, index: number) => {
const clientsId = clients.reduce((tabs: Array<UserInGroupInterface>, clientId: ExSocketInterface, indexClientId: number) => {
const peerClients = clients.reduce((tabs: Array<UserInGroupInterface>, clientId: ExSocketInterface, indexClientId: number) => {
if (!clientId.userId || clientId.userId === client.userId) {
return tabs;
}
@ -416,7 +447,7 @@ export class IoSocketController {
return tabs;
}, []);
client.emit(SockerIoEvent.WEBRTC_START, {clients: clientsId, roomId: roomId});
client.emit(SockerIoEvent.WEBRTC_START, {clients: peerClients, roomId: roomId});
});
}

View file

@ -1,10 +1,18 @@
import * as tg from "generic-type-guard";
export const isSignalData =
new tg.IsInterface().withProperties({
type: tg.isOptional(tg.isString)
}).get();
export const isWebRtcSignalMessageInterface =
new tg.IsInterface().withProperties({
userId: tg.isString,
receiverId: tg.isString,
roomId: tg.isString,
signal: tg.isUnknown
signal: isSignalData
}).get();
export const isWebRtcScreenSharingStartMessageInterface =
new tg.IsInterface().withProperties({
userId: tg.isString,
roomId: tg.isString
}).get();
export type WebRtcSignalMessageInterface = tg.GuardedType<typeof isWebRtcSignalMessageInterface>;

28
front/dist/index.html vendored
View file

@ -68,15 +68,19 @@
<div id="div-myCamVideo" class="video-container">
<video id="myCamVideo" autoplay muted></video>
</div>
<div class="btn-cam-action">
<div class="btn-micro">
<img id="microphone" src="resources/logos/microphone.svg">
<img id="microphone-close" src="resources/logos/microphone-close.svg">
</div>
<div class="btn-video">
<img id="cinema" src="resources/logos/cinema.svg">
<img id="cinema-close" src="resources/logos/cinema-close.svg">
</div>
</div>
<div class="btn-cam-action">
<div id="btn-micro" class="btn-micro">
<img id="microphone" src="resources/logos/microphone.svg">
<img id="microphone-close" src="resources/logos/microphone-close.svg">
</div>
<div id="btn-video" class="btn-video">
<img id="cinema" src="resources/logos/cinema.svg">
<img id="cinema-close" src="resources/logos/cinema-close.svg">
</div>
<div id="btn-monitor" class="btn-monitor">
<img id="monitor" src="resources/logos/monitor.svg">
<img id="monitor-close" src="resources/logos/monitor-close.svg">
</div>
</div>
@ -100,9 +104,15 @@
<img id="cinema" src="resources/logos/cinema.svg">
<img id="cinema-close" src="resources/logos/cinema-close.svg">
</div>
<div class="btn-monitor">
<img id="monitor" src="resources/logos/monitor.svg">
<img id="monitor-close" src="resources/logos/monitor-close.svg">
</div>
</div>
</div>
-->
<div id="activeScreenSharing" class="active-screen-sharing active">
</div>
<div id="webRtcSetup" class="webrtcsetup">
<img id="webRtcSetupNoVideo" class="background-img" src="resources/logos/cinema-close.svg">
<video id="myCamVideoSetup" autoplay muted></video>

View file

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.1.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 480" style="enable-background:new 0 0 512 480;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
</style>
<g>
<path class="st0" d="M411.1,384.2c-12.2,0-24.3,0-36.5,0C259.6,257.6,144.5,130.9,29.5,4.3c11.4-0.8,22.9-1.6,34.3-2.4
C179.6,129.3,295.3,256.8,411.1,384.2z"/>
<g>
<path class="st0" d="M352,152.5c-8.8-8.7-34.2-31.6-74.5-38.1c-32.3-5.2-58.1,2.7-70,7.3C170.9,81.5,134.3,41.3,97.8,1
C220.4,1,343,1,465.6,1C427.7,51.5,389.8,102,352,152.5z"/>
<path class="st0" d="M511.5,338.3c0,4.7-0.8,12.2-4.7,20.2c-1.2,2.4-3.4,6.3-7.1,10.5c-4,4.4-7.9,7.1-10.2,8.5
c-5.6,3.5-10.7,4.9-13.5,5.6c-3.8,0.9-6.7,1-10.2,1.2c-3.6,0.2-5.3,0-13.1,0c-3,0-5.4,0-7,0C414.5,307,383.2,229.8,352,152.5
C402.9,62.7,448.7,1,465.6,1c7.5,0,14.3,2.3,14.3,2.3c14.5,4.8,22.3,15.8,23.6,17.8c7.4,10.8,8,21.7,8,25.9
C511.5,144.1,511.5,241.2,511.5,338.3z"/>
<path class="st0" d="M312.5,192c-5.2-5.2-15.6-14.1-31.4-19.4c-12.8-4.2-24-4.3-30.9-3.8c-6.2-7.1-12.4-14.2-18.6-21.3
c10.3-2.4,36.5-6.8,65.1,5.3c15.3,6.5,26.1,15.5,32.7,22.2C323.7,180.8,318.1,186.4,312.5,192z"/>
<path class="st0" d="M329.4,175.1c38.8,69.7,77.6,139.4,116.4,209.1c-50.3-55.4-100.6-110.8-151-166.2c6.9,2.9,14.9,0.7,19.2-5.2
c4.6-6.2,4-15.1-1.6-20.8C318.1,186.4,323.7,180.8,329.4,175.1z"/>
<path class="st0" d="M445.8,384.2L445.8,384.2c-38.8-69.7-77.6-139.4-116.4-209.1c5.3,4.9,12.9,6,18.9,2.7
c7.8-4.2,8.3-13.4,8.3-13.8C386.4,237.4,416.1,310.8,445.8,384.2z"/>
</g>
<path class="st0" d="M162.2,150.4C108.3,213,54.4,275.7,0.5,338.3c0-97.1,0-194.3,0-291.4c0-4,0.6-15.1,8.1-26
C16,10.2,25.7,5.8,29.5,4.3C73.7,53,118,101.7,162.2,150.4z"/>
<path class="st0" d="M199.5,192c-5.3-6-10.6-12-15.8-18C122.6,228.8,61.6,283.6,0.5,338.3c0,4.1,0.6,15.5,8.6,26.7
c1.7,2.4,9.6,12.9,24.1,17.2c5.3,1.6,10,1.9,13.1,1.9C97.5,320.2,148.5,256.1,199.5,192z"/>
<path class="st0" d="M84.7,384.2c-12.7,0-25.5,0-38.2,0c58.2-56.2,116.5-112.5,174.7-168.7c8.3,9.1,16.6,18.2,24.9,27.2
c-2.2,1.1-5.5,3-8.4,6.4c-3.1,3.7-4.2,7.3-4.4,7.8C231.3,262.4,194.5,295.2,84.7,384.2z"/>
<path class="st0" d="M46.4,384.2c-15.3-15.3-30.6-30.6-45.9-45.9C52.8,277.5,105.1,216.8,157.4,156c-3.7,6.7-2.2,15.1,3.5,20
c5.4,4.6,13.3,5.1,19.4,1C135.7,246.1,91.1,315.1,46.4,384.2z"/>
<path class="st0" d="M49.6,384.3c-1.1,0-2.1,0-3.2-0.1c50.1-62.9,100.2-125.8,150.3-188.7c-3.5,6.6-2.1,14.8,3.4,19.7
c5.8,5.2,14.8,5.3,21,0.3C164,271.7,106.8,328,49.6,384.3z"/>
<path class="st0" d="M374.6,384.2c-96.6,0-193.3,0-289.9,0C16.5,308,1.3,223,28.5,194.5C57,164.7,150.6,177,233.3,256.9
c-3.9,11.8,2,24.8,13.3,29.6c11,4.7,24,0.4,30.2-10C309.3,312.4,342,348.3,374.6,384.2z"/>
<path class="st0" d="M219.7,226.7"/>
<path class="st0" d="M368.9,480c-74.9,0-149.8,0-224.7,0c-8.8,0-16-7.2-16-16c0-8.8,7.2-16,16-16c74.9,0,149.9,0,224.8,0.1
c8.3,0.7,14.7,7.6,14.7,15.9C383.7,472.3,377.2,479.3,368.9,480z"/>
<rect x="208.1" y="384.2" class="st0" width="31.9" height="63.9"/>
<rect x="272" y="384.2" class="st0" width="32" height="63.9"/>
<path class="st0" d="M410.3,395.5"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

15
front/dist/resources/logos/monitor.svg vendored Normal file
View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.1.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 480" style="enable-background:new 0 0 512 480;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
</style>
<path class="st0" d="M466,0H46C20.6,0,0,20.6,0,46v292c0,25.4,20.6,46,46,46h162v64h-64c-8.8,0-16,7.2-16,16s7.2,16,16,16h224
c8.8,0,16-7.2,16-16s-7.2-16-16-16h-64v-64h162c25.4,0,46-20.6,46-46V46C512,20.6,491.4,0,466,0z M232,264c0-13.3,10.7-24,24-24
c13.3,0,24,10.7,24,24s-10.7,24-24,24C242.7,288,232,277.3,232,264z M272,448h-32v-64h32V448z M312.6,214.1
c-6.2,6.2-16.4,6.2-22.6,0c-18.7-18.8-49.1-18.8-67.9,0c0,0,0,0,0,0c-6.4,6.1-16.5,5.8-22.6-0.6c-5.9-6.2-5.9-15.9,0-22
c31.2-31.2,81.9-31.2,113.1,0c0,0,0,0,0,0C318.8,197.7,318.8,207.8,312.6,214.1z M352.2,174.5c-6.2,6.2-16.4,6.3-22.6,0c0,0,0,0,0,0
c-40.6-40.6-106.4-40.6-147.1,0c-6.2,6.3-16.4,6.3-22.6,0c-6.3-6.2-6.3-16.4,0-22.6c53.1-53.1,139.2-53.1,192.3,0c0,0,0,0,0,0
C358.4,158.1,358.4,168.2,352.2,174.5C352.2,174.5,352.2,174.5,352.2,174.5L352.2,174.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -79,6 +79,13 @@ video#myCamVideo{
}
.btn-cam-action {
position: absolute;
bottom: 0px;
right: 0px;
width: 450px;
height: 150px;
}
/*btn animation*/
.btn-cam-action div{
cursor: pointer;
@ -89,11 +96,17 @@ video#myCamVideo{
background: #666;
box-shadow: 2px 2px 24px #444;
border-radius: 48px;
transform: translateY(12vh);
transform: translateY(40px);
transition-timing-function: ease-in-out;
bottom: 20px;
}
#activeCam:hover .btn-cam-action div{
.btn-cam-action div.disabled {
background: #d75555;
}
.btn-cam-action div.enabled {
background: #73c973;
}
.btn-cam-action:hover div{
transform: translateY(0);
}
.btn-cam-action div:hover{
@ -106,9 +119,13 @@ video#myCamVideo{
right: 44px;
}
.btn-video{
transition: all .2s;
transition: all .25s;
right: 134px;
}
.btn-monitor{
transition: all .2s;
right: 224px;
}
/*.btn-call{
transition: all .1s;
left: 0px;
@ -300,13 +317,13 @@ body {
flex: 0 0 75%;
display: flex;
justify-content: start;
/*align-items: flex-start;*/
align-items: flex-start;
flex-wrap: wrap;
}
.main-section > div {
margin: 5%;
flex-basis: 90%;
margin: 2%;
flex-basis: 96%;
/*flex-shrink: 2;*/
}

View file

@ -6,12 +6,12 @@ import {SetPlayerDetailsMessage} from "./Messages/SetPlayerDetailsMessage";
const SocketIo = require('socket.io-client');
import Socket = SocketIOClient.Socket;
import {PlayerAnimationNames} from "./Phaser/Player/Animation";
import {UserSimplePeer} from "./WebRtc/SimplePeer";
import {UserSimplePeerInterface} from "./WebRtc/SimplePeer";
import {SignalData} from "simple-peer";
enum EventMessage{
WEBRTC_SIGNAL = "webrtc-signal",
WEBRTC_SCREEN_SHARING_SIGNAL = "webrtc-screen-sharing-signal",
WEBRTC_START = "webrtc-start",
JOIN_ROOM = "join-room", // bi-directional
USER_POSITION = "user-position", // bi-directional
@ -73,17 +73,20 @@ export interface GroupCreatedUpdatedMessageInterface {
export interface WebRtcStartMessageInterface {
roomId: string,
clients: UserSimplePeer[]
clients: UserSimplePeerInterface[]
}
export interface WebRtcDisconnectMessageInterface {
userId: string
}
export interface WebRtcSignalMessageInterface {
userId: string,
export interface WebRtcSignalSentMessageInterface {
receiverId: string,
roomId: string,
signal: SignalData
}
export interface WebRtcSignalReceivedMessageInterface {
userId: string,
signal: SignalData
}
@ -201,23 +204,32 @@ export class Connection implements Connection {
this.socket.on(EventMessage.CONNECT_ERROR, callback)
}
public sendWebrtcSignal(signal: unknown, roomId: string, userId? : string|null, receiverId? : string) {
public sendWebrtcSignal(signal: unknown, receiverId : string) {
return this.socket.emit(EventMessage.WEBRTC_SIGNAL, {
userId: userId ? userId : this.userId,
receiverId: receiverId ? receiverId : this.userId,
roomId: roomId,
receiverId: receiverId,
signal: signal
});
} as WebRtcSignalSentMessageInterface);
}
public sendWebrtcScreenSharingSignal(signal: unknown, receiverId : string) {
return this.socket.emit(EventMessage.WEBRTC_SCREEN_SHARING_SIGNAL, {
receiverId: receiverId,
signal: signal
} as WebRtcSignalSentMessageInterface);
}
public receiveWebrtcStart(callback: (message: WebRtcStartMessageInterface) => void) {
this.socket.on(EventMessage.WEBRTC_START, callback);
}
public receiveWebrtcSignal(callback: (message: WebRtcSignalMessageInterface) => void) {
public receiveWebrtcSignal(callback: (message: WebRtcSignalReceivedMessageInterface) => void) {
return this.socket.on(EventMessage.WEBRTC_SIGNAL, callback);
}
public receiveWebrtcScreenSharingSignal(callback: (message: WebRtcSignalReceivedMessageInterface) => void) {
return this.socket.on(EventMessage.WEBRTC_SCREEN_SHARING_SIGNAL, callback);
}
public onServerDisconnected(callback: (reason: string) => void): void {
this.socket.on('disconnect', (reason: string) => {
if (reason === 'io client disconnect') {

View file

@ -10,7 +10,7 @@ import {
RoomJoinedMessageInterface
} from "../../Connection";
import {CurrentGamerInterface, hasMovedEventName, Player} from "../Player/Player";
import {DEBUG_MODE, POSITION_DELAY, ZOOM_LEVEL} from "../../Enum/EnvironmentVariable";
import {DEBUG_MODE, POSITION_DELAY, RESOLUTION, ZOOM_LEVEL} from "../../Enum/EnvironmentVariable";
import {
ITiledMap,
ITiledMapLayer,
@ -24,10 +24,10 @@ import {PlayerMovement} from "./PlayerMovement";
import {PlayersPositionInterpolator} from "./PlayersPositionInterpolator";
import {RemotePlayer} from "../Entity/RemotePlayer";
import {Queue} from 'queue-typescript';
import {SimplePeer, UserSimplePeer} from "../../WebRtc/SimplePeer";
import {SimplePeer, UserSimplePeerInterface} from "../../WebRtc/SimplePeer";
import {ReconnectingSceneName} from "../Reconnecting/ReconnectingScene";
import {loadAllLayers} from "../Entity/body_character";
import {layoutManager, LayoutMode} from "../../WebRtc/LayoutManager";
import {CenterListener, layoutManager, LayoutMode} from "../../WebRtc/LayoutManager";
import Texture = Phaser.Textures.Texture;
import Sprite = Phaser.GameObjects.Sprite;
import CanvasTexture = Phaser.Textures.CanvasTexture;
@ -78,7 +78,7 @@ interface DeleteGroupEventInterface {
groupId: string
}
export class GameScene extends Phaser.Scene {
export class GameScene extends Phaser.Scene implements CenterListener {
GameManager : GameManager;
Terrains : Array<Phaser.Tilemaps.Tileset>;
CurrentPlayer!: CurrentGamerInterface;
@ -263,7 +263,7 @@ export class GameScene extends Phaser.Scene {
this.simplePeer = new SimplePeer(this.connection);
const self = this;
this.simplePeer.registerPeerConnectionListener({
onConnect(user: UserSimplePeer) {
onConnect(user: UserSimplePeerInterface) {
self.presentationModeSprite.setVisible(true);
self.chatModeSprite.setVisible(true);
},
@ -537,6 +537,9 @@ export class GameScene extends Phaser.Scene {
this.repositionCallback = this.reposition.bind(this);
window.addEventListener('resize', this.repositionCallback);
this.reposition();
// From now, this game scene will be notified of reposition events
layoutManager.setListener(this);
}
private switchLayoutMode(): void {
@ -656,7 +659,7 @@ export class GameScene extends Phaser.Scene {
//todo: in a dedicated class/function?
initCamera() {
this.cameras.main.setBounds(0,0, this.Map.widthInPixels, this.Map.heightInPixels);
this.cameras.main.startFollow(this.CurrentPlayer);
this.updateCameraOffset();
this.cameras.main.setZoom(ZOOM_LEVEL);
}
@ -1058,5 +1061,30 @@ export class GameScene extends Phaser.Scene {
private reposition(): void {
this.presentationModeSprite.setY(this.game.renderer.height - 2);
this.chatModeSprite.setY(this.game.renderer.height - 2);
// Recompute camera offset if needed
this.updateCameraOffset();
}
/**
* Updates the offset of the character compared to the center of the screen according to the layout mananger
* (tries to put the character in the center of the reamining space if there is a discussion going on.
*/
private updateCameraOffset(): void {
const array = layoutManager.findBiggestAvailableArray();
let xCenter = (array.xEnd - array.xStart) / 2 + array.xStart;
let yCenter = (array.yEnd - array.yStart) / 2 + array.yStart;
// Let's put this in Game coordinates by applying the zoom level:
xCenter /= ZOOM_LEVEL * RESOLUTION;
yCenter /= ZOOM_LEVEL * RESOLUTION;
//console.log("updateCameraOffset", array, xCenter, yCenter, this.game.renderer.width, this.game.renderer.height);
this.cameras.main.startFollow(this.CurrentPlayer, true, 1, 1, xCenter - this.game.renderer.width / 2, yCenter - this.game.renderer.height / 2);
}
public onCenterChange(): void {
this.updateCameraOffset();
}
}

View file

@ -14,6 +14,14 @@ export enum DivImportance {
Normal = "Normal",
}
/**
* Classes implementing this interface can be notified when the center of the screen (the player position) should be
* changed.
*/
export interface CenterListener {
onCenterChange(): void;
}
/**
* 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.
@ -23,6 +31,11 @@ class LayoutManager {
private importantDivs: Map<string, HTMLDivElement> = new Map<string, HTMLDivElement>();
private normalDivs: Map<string, HTMLDivElement> = new Map<string, HTMLDivElement>();
private listener: CenterListener|null = null;
public setListener(centerListener: CenterListener|null) {
this.listener = centerListener;
}
public add(importance: DivImportance, userId: string, html: string): void {
const div = document.createElement('div');
@ -45,6 +58,7 @@ class LayoutManager {
this.positionDiv(div, importance);
this.adjustVideoChatClass();
this.listener?.onCenterChange();
}
private positionDiv(elem: HTMLDivElement, importance: DivImportance): void {
@ -72,6 +86,7 @@ class LayoutManager {
div.remove();
this.importantDivs.delete(userId);
this.adjustVideoChatClass();
this.listener?.onCenterChange();
return;
}
@ -80,6 +95,7 @@ class LayoutManager {
div.remove();
this.normalDivs.delete(userId);
this.adjustVideoChatClass();
this.listener?.onCenterChange();
return;
}
@ -123,11 +139,133 @@ class LayoutManager {
for (const div of this.normalDivs.values()) {
this.positionDiv(div, DivImportance.Normal);
}
this.listener?.onCenterChange();
}
public getLayoutMode(): LayoutMode {
return this.mode;
}
/*public getGameCenter(): {x: number, y: number} {
}*/
/**
* 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());
// Nothing? Let's center
if (mainSectionChildren.length === 0 && sidebarChildren.length === 0) {
return {
xStart: 0,
yStart: 0,
xEnd: window.innerWidth,
yEnd: window.innerHeight
}
}
if (mainSectionChildren.length === 0) {
const lastSidebarDiv = sidebarChildren[sidebarChildren.length-1];
// No presentation? Let's center on the main-section space
return {
xStart: 0,
yStart: 0,
xEnd: lastSidebarDiv.offsetLeft,
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
}
}
}
}
}
const layoutManager = new LayoutManager();

View file

@ -1,4 +1,5 @@
import {DivImportance, layoutManager} from "./LayoutManager";
import {HtmlUtils} from "./HtmlUtils";
const videoConstraint: boolean|MediaTrackConstraints = {
width: { ideal: 1280 },
@ -7,15 +8,20 @@ const videoConstraint: boolean|MediaTrackConstraints = {
};
type UpdatedLocalStreamCallback = (media: MediaStream) => void;
type StartScreenSharingCallback = (media: MediaStream) => void;
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!!!!)
export class MediaManager {
localStream: MediaStream|null = null;
localScreenCapture: MediaStream|null = null;
private remoteVideo: Map<string, HTMLVideoElement> = new Map<string, HTMLVideoElement>();
myCamVideo: HTMLVideoElement;
cinemaClose: HTMLImageElement;
cinema: HTMLImageElement;
monitorClose: HTMLImageElement;
monitor: HTMLImageElement;
microphoneClose: HTMLImageElement;
microphone: HTMLImageElement;
webrtcInAudio: HTMLAudioElement;
@ -24,46 +30,81 @@ export class MediaManager {
video: videoConstraint
};
updatedLocalStreamCallBacks : Set<UpdatedLocalStreamCallback> = new Set<UpdatedLocalStreamCallback>();
startScreenSharingCallBacks : Set<StartScreenSharingCallback> = new Set<StartScreenSharingCallback>();
stopScreenSharingCallBacks : Set<StopScreenSharingCallback> = new Set<StopScreenSharingCallback>();
private microphoneBtn: HTMLDivElement;
private cinemaBtn: HTMLDivElement;
private monitorBtn: HTMLDivElement;
constructor() {
this.myCamVideo = this.getElementByIdOrFail<HTMLVideoElement>('myCamVideo');
this.webrtcInAudio = this.getElementByIdOrFail<HTMLAudioElement>('audio-webrtc-in');
this.webrtcInAudio.volume = 0.2;
this.microphoneBtn = this.getElementByIdOrFail<HTMLDivElement>('btn-micro');
this.microphoneClose = this.getElementByIdOrFail<HTMLImageElement>('microphone-close');
this.microphoneClose.style.display = "none";
this.microphoneClose.addEventListener('click', (e: MouseEvent) => {
e.preventDefault();
this.enabledMicrophone();
this.enableMicrophone();
//update tracking
});
this.microphone = this.getElementByIdOrFail<HTMLImageElement>('microphone');
this.microphone.addEventListener('click', (e: MouseEvent) => {
e.preventDefault();
this.disabledMicrophone();
this.disableMicrophone();
//update tracking
});
this.cinemaBtn = this.getElementByIdOrFail<HTMLDivElement>('btn-video');
this.cinemaClose = this.getElementByIdOrFail<HTMLImageElement>('cinema-close');
this.cinemaClose.style.display = "none";
this.cinemaClose.addEventListener('click', (e: MouseEvent) => {
e.preventDefault();
this.enabledCamera();
this.enableCamera();
//update tracking
});
this.cinema = this.getElementByIdOrFail<HTMLImageElement>('cinema');
this.cinema.addEventListener('click', (e: MouseEvent) => {
e.preventDefault();
this.disabledCamera();
this.disableCamera();
//update tracking
});
this.monitorBtn = this.getElementByIdOrFail<HTMLDivElement>('btn-monitor');
this.monitorClose = this.getElementByIdOrFail<HTMLImageElement>('monitor-close');
this.monitorClose.style.display = "block";
this.monitorClose.addEventListener('click', (e: MouseEvent) => {
e.preventDefault();
this.enableScreenSharing();
//update tracking
});
this.monitor = this.getElementByIdOrFail<HTMLImageElement>('monitor');
this.monitor.style.display = "none";
this.monitor.addEventListener('click', (e: MouseEvent) => {
e.preventDefault();
this.disableScreenSharing();
//update tracking
});
}
onUpdateLocalStream(callback: UpdatedLocalStreamCallback): void {
public onUpdateLocalStream(callback: UpdatedLocalStreamCallback): void {
this.updatedLocalStreamCallBacks.add(callback);
}
public onStartScreenSharing(callback: StartScreenSharingCallback): void {
this.startScreenSharingCallBacks.add(callback);
}
public onStopScreenSharing(callback: StopScreenSharingCallback): void {
this.stopScreenSharingCallBacks.add(callback);
}
removeUpdateLocalStreamEventListener(callback: UpdatedLocalStreamCallback): void {
this.updatedLocalStreamCallBacks.delete(callback);
}
@ -74,13 +115,26 @@ export class MediaManager {
}
}
activeVisio(){
private triggerStartedScreenSharingCallbacks(stream: MediaStream): void {
for (const callback of this.startScreenSharingCallBacks) {
callback(stream);
}
}
private triggerStoppedScreenSharingCallbacks(stream: MediaStream): void {
for (const callback of this.stopScreenSharingCallBacks) {
callback(stream);
}
}
showGameOverlay(){
const gameOverlay = this.getElementByIdOrFail('game-overlay');
gameOverlay.classList.add('active');
}
enabledCamera() {
private enableCamera() {
this.cinemaClose.style.display = "none";
this.cinemaBtn.classList.remove("disabled");
this.cinema.style.display = "block";
this.constraintsMedia.video = videoConstraint;
this.getCamera().then((stream: MediaStream) => {
@ -88,9 +142,10 @@ export class MediaManager {
});
}
disabledCamera() {
private 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) {
@ -103,18 +158,20 @@ export class MediaManager {
});
}
enabledMicrophone() {
private enableMicrophone() {
this.microphoneClose.style.display = "none";
this.microphone.style.display = "block";
this.microphoneBtn.classList.remove("disabled");
this.constraintsMedia.audio = true;
this.getCamera().then((stream) => {
this.triggerUpdatedLocalStreamCallbacks(stream);
});
}
disabledMicrophone() {
private 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) => {
@ -126,6 +183,80 @@ export class MediaManager {
});
}
private enableScreenSharing() {
this.monitorClose.style.display = "none";
this.monitor.style.display = "block";
this.monitorBtn.classList.add("enabled");
this.getScreenMedia().then((stream) => {
this.triggerStartedScreenSharingCallbacks(stream);
});
}
private disableScreenSharing() {
this.monitorClose.style.display = "block";
this.monitor.style.display = "none";
this.monitorBtn.classList.remove("enabled");
this.removeActiveScreenSharingVideo('me');
this.localScreenCapture?.getTracks().forEach((track: MediaStreamTrack) => {
track.stop();
});
if (this.localScreenCapture === null) {
console.warn('Weird: trying to remove a screen sharing that is not enabled');
return;
}
const localScreenCapture = this.localScreenCapture;
this.getCamera().then((stream) => {
this.triggerStoppedScreenSharingCallbacks(localScreenCapture);
});
this.localScreenCapture = null;
}
//get screen
getScreenMedia() : Promise<MediaStream>{
try {
return this._startScreenCapture()
.then((stream: MediaStream) => {
this.localScreenCapture = stream;
// If stream ends (for instance if user clicks the stop screen sharing button in the browser), let's close the view
for (const track of stream.getTracks()) {
track.onended = () => {
this.disableScreenSharing();
};
}
this.addScreenSharingActiveVideo('me', DivImportance.Normal);
HtmlUtils.getElementByIdOrFail<HTMLVideoElement>('screen-sharing-me').srcObject = stream;
return stream;
})
.catch((err: unknown) => {
console.error("Error => getScreenMedia => ", err);
throw err;
});
}catch (err) {
return new Promise((resolve, reject) => { // eslint-disable-line no-unused-vars
reject(err);
});
}
}
private _startScreenCapture() {
// getDisplayMedia was moved to mediaDevices in 2018. Typescript definitions are not up to date yet.
// See: https://github.com/w3c/mediacapture-screen-share/pull/86
// https://github.com/microsoft/TypeScript/issues/31821
if ((navigator as any).getDisplayMedia) { // eslint-disable-line @typescript-eslint/no-explicit-any
return (navigator as any).getDisplayMedia({video: true}); // eslint-disable-line @typescript-eslint/no-explicit-any
} else if ((navigator.mediaDevices as any).getDisplayMedia) { // eslint-disable-line @typescript-eslint/no-explicit-any
return (navigator.mediaDevices as any).getDisplayMedia({video: true}); // eslint-disable-line @typescript-eslint/no-explicit-any
} else {
//return navigator.mediaDevices.getUserMedia(({video: {mediaSource: 'screen'}} as any));
return new Promise((resolve, reject) => { // eslint-disable-line no-unused-vars
reject("error sharing screen");
});
}
}
//get camera
async getCamera(): Promise<MediaStream> {
if (navigator.mediaDevices === undefined) {
@ -205,6 +336,25 @@ export class MediaManager {
this.remoteVideo.set(userId, this.getElementByIdOrFail<HTMLVideoElement>(userId));
}
/**
*
* @param userId
*/
addScreenSharingActiveVideo(userId : string, divImportance: DivImportance = DivImportance.Important){
//this.webrtcInAudio.play();
userId = `screen-sharing-${userId}`;
const html = `
<div id="div-${userId}" class="video-container">
<video id="${userId}" autoplay></video>
</div>
`;
layoutManager.add(divImportance, userId, html);
this.remoteVideo.set(userId, this.getElementByIdOrFail<HTMLVideoElement>(userId));
}
/**
*
* @param userId
@ -272,6 +422,15 @@ export class MediaManager {
}
remoteVideo.srcObject = stream;
}
addStreamRemoteScreenSharing(userId : string, stream : MediaStream){
// In the case of screen sharing (going both ways), we may need to create the HTML element if it does not exist yet
const remoteVideo = this.remoteVideo.get(`screen-sharing-${userId}`);
if (remoteVideo === undefined) {
this.addScreenSharingActiveVideo(userId);
}
this.addStreamRemoteVideo(`screen-sharing-${userId}`, stream);
}
/**
*
@ -281,6 +440,9 @@ export class MediaManager {
layoutManager.remove(userId);
this.remoteVideo.delete(userId);
}
removeActiveScreenSharingVideo(userId : string) {
this.removeActiveVideo(`screen-sharing-${userId}`)
}
isConnecting(userId : string): void {
const connectingSpinnerDiv = this.getSpinner(userId);
@ -299,6 +461,7 @@ export class MediaManager {
}
isError(userId : string): void {
console.log("isError", `div-${userId}`);
const element = document.getElementById(`div-${userId}`);
if(!element){
return;
@ -309,6 +472,10 @@ export class MediaManager {
}
errorDiv.style.display = 'block';
}
isErrorScreenSharing(userId : string): void {
this.isError(`screen-sharing-${userId}`);
}
private getSpinner(userId : string): HTMLDivElement|null {
const element = document.getElementById(`div-${userId}`);

View file

@ -0,0 +1,127 @@
import * as SimplePeerNamespace from "simple-peer";
import {mediaManager} from "./MediaManager";
import {Connection} from "../Connection";
const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer');
/**
* A peer connection used to transmit video / audio signals between 2 peers.
*/
export class ScreenSharingPeer extends Peer {
/**
* Whether this connection is currently receiving a video stream from a remote user.
*/
private isReceivingStream:boolean = false;
constructor(private userId: string, initiator: boolean, private connection: Connection) {
super({
initiator: initiator ? initiator : false,
reconnectTimer: 10000,
config: {
iceServers: [
{
urls: 'stun:stun.l.google.com:19302'
},
{
urls: 'turn:numb.viagenie.ca',
username: 'g.parant@thecodingmachine.com',
credential: 'itcugcOHxle9Acqi$'
},
]
}
});
//start listen signal for the peer connection
this.on('signal', (data: unknown) => {
this.sendWebrtcScreenSharingSignal(data);
});
this.on('stream', (stream: MediaStream) => {
this.stream(stream);
});
this.on('close', () => {
this.destroy();
});
this.on('data', (chunk: Buffer) => {
// We unfortunately need to rely on an event to let the other party know a stream has stopped.
// It seems there is no native way to detect that.
const message = JSON.parse(chunk.toString('utf8'));
if (message.streamEnded !== true) {
console.error('Unexpected message on screen sharing peer connection');
}
mediaManager.removeActiveScreenSharingVideo(this.userId);
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.on('error', (err: any) => {
console.error(`screen sharing error => ${this.userId} => ${err.code}`, err);
//mediaManager.isErrorScreenSharing(this.userId);
});
this.on('connect', () => {
// FIXME: we need to put the loader on the screen sharing connection
mediaManager.isConnected(this.userId);
console.info(`connect => ${this.userId}`);
});
this.pushScreenSharingToRemoteUser();
}
private sendWebrtcScreenSharingSignal(data: unknown) {
console.log("sendWebrtcScreenSharingSignal", data);
try {
this.connection.sendWebrtcScreenSharingSignal(data, this.userId);
}catch (e) {
console.error(`sendWebrtcScreenSharingSignal => ${this.userId}`, e);
}
}
/**
* Sends received stream to screen.
*/
private stream(stream?: MediaStream) {
console.log(`ScreenSharingPeer::stream => ${this.userId}`, stream);
console.log(`stream => ${this.userId} => `, stream);
if(!stream){
mediaManager.removeActiveScreenSharingVideo(this.userId);
this.isReceivingStream = false;
} else {
mediaManager.addStreamRemoteScreenSharing(this.userId, stream);
this.isReceivingStream = true;
}
}
public isReceivingScreenSharingStream(): boolean {
return this.isReceivingStream;
}
public destroy(error?: Error): void {
try {
mediaManager.removeActiveScreenSharingVideo(this.userId);
// FIXME: I don't understand why "Closing connection with" message is displayed TWICE before "Nb users in peerConnectionArray"
// I do understand the method closeConnection is called twice, but I don't understand how they manage to run in parallel.
//console.log('Closing connection with '+userId);
super.destroy(error);
//console.log('Nb users in peerConnectionArray '+this.PeerConnectionArray.size);
} catch (err) {
console.error("ScreenSharingPeer::destroy", err)
}
}
private pushScreenSharingToRemoteUser() {
const localScreenCapture: MediaStream | null = mediaManager.localScreenCapture;
if(!localScreenCapture){
return;
}
this.addStream(localScreenCapture);
return;
}
public stopPushingScreenSharingToRemoteUser(stream: MediaStream) {
this.removeStream(stream);
this.write(new Buffer(JSON.stringify({streamEnded: true})));
}
}

View file

@ -1,21 +1,23 @@
import {
Connection,
WebRtcDisconnectMessageInterface,
WebRtcSignalMessageInterface,
WebRtcSignalReceivedMessageInterface,
WebRtcStartMessageInterface
} from "../Connection";
import { mediaManager } from "./MediaManager";
import * as SimplePeerNamespace from "simple-peer";
import {ScreenSharingPeer} from "./ScreenSharingPeer";
import {VideoPeer} from "./VideoPeer";
const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer');
export interface UserSimplePeer{
export interface UserSimplePeerInterface{
userId: string;
name?: string;
initiator?: boolean;
}
export interface PeerConnectionListener {
onConnect(user: UserSimplePeer): void;
onConnect(user: UserSimplePeerInterface): void;
onDisconnect(userId: string): void;
}
@ -26,18 +28,25 @@ export interface PeerConnectionListener {
export class SimplePeer {
private Connection: Connection;
private WebRtcRoomId: string;
private Users: Array<UserSimplePeer> = new Array<UserSimplePeer>();
private Users: Array<UserSimplePeerInterface> = new Array<UserSimplePeerInterface>();
private PeerConnectionArray: Map<string, SimplePeerNamespace.Instance> = new Map<string, SimplePeerNamespace.Instance>();
private readonly updateLocalStreamCallback: (media: MediaStream) => void;
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 peerConnectionListeners: Array<PeerConnectionListener> = new Array<PeerConnectionListener>();
constructor(Connection: Connection, WebRtcRoomId: string = "test-webrtc") {
this.Connection = Connection;
this.WebRtcRoomId = WebRtcRoomId;
// We need to go through this weird bound function pointer in order to be able to "free" this reference later.
this.updateLocalStreamCallback = this.updatedLocalStream.bind(this);
mediaManager.onUpdateLocalStream(this.updateLocalStreamCallback);
this.sendLocalVideoStreamCallback = this.sendLocalVideoStream.bind(this);
this.sendLocalScreenSharingStreamCallback = this.sendLocalScreenSharingStream.bind(this);
this.stopLocalScreenSharingStreamCallback = this.stopLocalScreenSharingStream.bind(this);
mediaManager.onUpdateLocalStream(this.sendLocalVideoStreamCallback);
mediaManager.onStartScreenSharing(this.sendLocalScreenSharingStreamCallback);
mediaManager.onStopScreenSharing(this.stopLocalScreenSharingStreamCallback);
this.initialise();
}
@ -55,11 +64,16 @@ export class SimplePeer {
private initialise() {
//receive signal by gemer
this.Connection.receiveWebrtcSignal((message: WebRtcSignalMessageInterface) => {
this.Connection.receiveWebrtcSignal((message: WebRtcSignalReceivedMessageInterface) => {
this.receiveWebrtcSignal(message);
});
mediaManager.activeVisio();
//receive signal by gemer
this.Connection.receiveWebrtcScreenSharingSignal((message: WebRtcSignalReceivedMessageInterface) => {
this.receiveWebrtcScreenSharingSignal(message);
});
mediaManager.showGameOverlay();
mediaManager.getCamera().then(() => {
//receive message start
@ -79,7 +93,7 @@ export class SimplePeer {
private receiveWebrtcStart(data: WebRtcStartMessageInterface) {
this.WebRtcRoomId = data.roomId;
this.Users = data.clients;
// Note: the clients array contain the list of all clients (event the ones we are already connected to in case a user joints a group)
// Note: the clients array contain the list of all clients (even the ones we are already connected to in case a user joints a group)
// So we can receive a request we already had before. (which will abort at the first line of createPeerConnection)
// TODO: refactor this to only send a message to connect to one user (rather than several users).
// This would be symmetrical to the way we handle disconnection.
@ -93,7 +107,8 @@ export class SimplePeer {
* server has two people connected, start the meet
*/
private startWebRtc() {
this.Users.forEach((user: UserSimplePeer) => {
console.warn('startWebRtc startWebRtc');
this.Users.forEach((user: UserSimplePeerInterface) => {
//if it's not an initiator, peer connection will be created when gamer will receive offer signal
if(!user.initiator){
return;
@ -105,102 +120,63 @@ export class SimplePeer {
/**
* create peer connection to bind users
*/
private createPeerConnection(user : UserSimplePeer) {
if(this.PeerConnectionArray.has(user.userId)) {
return;
private createPeerConnection(user : UserSimplePeerInterface) : VideoPeer | null{
if(
this.PeerConnectionArray.has(user.userId)
){
return null;
}
//console.log("Creating connection with peer "+user.userId);
let name = user.name;
if(!name){
const userSearch = this.Users.find((userSearch: UserSimplePeer) => userSearch.userId === user.userId);
const userSearch = this.Users.find((userSearch: UserSimplePeerInterface) => userSearch.userId === user.userId);
if(userSearch) {
name = userSearch.name;
}
}
mediaManager.removeActiveVideo(user.userId);
mediaManager.addActiveVideo(user.userId, name);
const peer : SimplePeerNamespace.Instance = new Peer({
initiator: user.initiator ? user.initiator : false,
reconnectTimer: 10000,
config: {
iceServers: [
{
urls: 'stun:stun.l.google.com:19302'
},
{
urls: 'turn:numb.viagenie.ca',
username: 'g.parant@thecodingmachine.com',
credential: 'itcugcOHxle9Acqi$'
},
]
},
const peer = new VideoPeer(user.userId, user.initiator ? user.initiator : false, this.Connection);
// When a connection is established to a video stream, and if a screen sharing is taking place,
// the user sharing screen should also initiate a connection to the remote user!
peer.on('connect', () => {
if (mediaManager.localScreenCapture) {
this.sendLocalScreenSharingStreamToUser(user.userId);
}
});
this.PeerConnectionArray.set(user.userId, peer);
//start listen signal for the peer connection
peer.on('signal', (data: unknown) => {
this.sendWebrtcSignal(data, user.userId);
});
peer.on('stream', (stream: MediaStream) => {
let videoActive = false;
let microphoneActive = false;
stream.getTracks().forEach((track : MediaStreamTrack) => {
if(track.kind === "audio"){
microphoneActive = true;
}
if(track.kind === "video"){
videoActive = true;
}
});
if(microphoneActive){
mediaManager.enabledMicrophoneByUserId(user.userId);
}else{
mediaManager.disabledMicrophoneByUserId(user.userId);
}
if(videoActive){
mediaManager.enabledVideoByUserId(user.userId);
}else{
mediaManager.disabledVideoByUserId(user.userId);
}
this.stream(user.userId, stream);
});
/*peer.on('track', (track: MediaStreamTrack, stream: MediaStream) => {
this.stream(user.userId, stream);
});*/
peer.on('close', () => {
this.closeConnection(user.userId);
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
peer.on('error', (err: any) => {
console.error(`error => ${user.userId} => ${err.code}`, err);
mediaManager.isError(user.userId);
});
peer.on('connect', () => {
mediaManager.isConnected(user.userId);
console.info(`connect => ${user.userId}`);
});
peer.on('data', (chunk: Buffer) => {
const data = JSON.parse(chunk.toString('utf8'));
if(data.type === "stream"){
this.stream(user.userId, data.stream);
}
});
this.addMedia(user.userId);
for (const peerConnectionListener of this.peerConnectionListeners) {
peerConnectionListener.onConnect(user);
}
return peer;
}
/**
* create peer connection to bind users
*/
private createPeerScreenSharingConnection(user : UserSimplePeerInterface) : ScreenSharingPeer | null{
if(
this.PeerScreenSharingConnectionArray.has(user.userId)
){
return null;
}
// We should display the screen sharing ONLY if we are not initiator
if (!user.initiator) {
mediaManager.removeActiveScreenSharingVideo(user.userId);
mediaManager.addScreenSharingActiveVideo(user.userId);
}
const peer = new ScreenSharingPeer(user.userId, user.initiator ? user.initiator : false, this.Connection);
this.PeerScreenSharingConnectionArray.set(user.userId, peer);
for (const peerConnectionListener of this.peerConnectionListeners) {
peerConnectionListener.onConnect(user);
}
return peer;
}
/**
@ -210,17 +186,18 @@ export class SimplePeer {
*/
private closeConnection(userId : string) {
try {
mediaManager.removeActiveVideo(userId);
//mediaManager.removeActiveVideo(userId);
const peer = this.PeerConnectionArray.get(userId);
if (peer === undefined) {
console.warn("Tried to close connection for user "+userId+" but could not find user")
return;
}
peer.destroy();
// FIXME: I don't understand why "Closing connection with" message is displayed TWICE before "Nb users in peerConnectionArray"
// I do understand the method closeConnection is called twice, but I don't understand how they manage to run in parallel.
//console.log('Closing connection with '+userId);
peer.destroy();
this.PeerConnectionArray.delete(userId)
this.PeerConnectionArray.delete(userId);
this.closeScreenSharingConnection(userId);
//console.log('Nb users in peerConnectionArray '+this.PeerConnectionArray.size);
for (const peerConnectionListener of this.peerConnectionListeners) {
peerConnectionListener.onDisconnect(userId);
@ -230,34 +207,49 @@ export class SimplePeer {
}
}
/**
* This is triggered twice. Once by the server, and once by a remote client disconnecting
*
* @param userId
*/
private closeScreenSharingConnection(userId : string) {
try {
mediaManager.removeActiveScreenSharingVideo(userId);
const peer = this.PeerScreenSharingConnectionArray.get(userId);
if (peer === undefined) {
console.warn("Tried to close connection for user "+userId+" but could not find user")
return;
}
// FIXME: I don't understand why "Closing connection with" message is displayed TWICE before "Nb users in peerConnectionArray"
// I do understand the method closeConnection is called twice, but I don't understand how they manage to run in parallel.
//console.log('Closing connection with '+userId);
peer.destroy();
this.PeerScreenSharingConnectionArray.delete(userId)
//console.log('Nb users in peerConnectionArray '+this.PeerConnectionArray.size);
} catch (err) {
console.error("closeConnection", err)
}
}
public closeAllConnections() {
for (const userId of this.PeerConnectionArray.keys()) {
this.closeConnection(userId);
}
for (const userId of this.PeerScreenSharingConnectionArray.keys()) {
this.closeScreenSharingConnection(userId);
}
}
/**
* Unregisters any held event handler.
*/
public unregister() {
mediaManager.removeUpdateLocalStreamEventListener(this.updateLocalStreamCallback);
}
/**
*
* @param userId
* @param data
*/
private sendWebrtcSignal(data: unknown, userId : string) {
try {
this.Connection.sendWebrtcSignal(data, this.WebRtcRoomId, null, userId);
}catch (e) {
console.error(`sendWebrtcSignal => ${userId}`, e);
}
mediaManager.removeUpdateLocalStreamEventListener(this.sendLocalVideoStreamCallback);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private receiveWebrtcSignal(data: WebRtcSignalMessageInterface) {
private receiveWebrtcSignal(data: WebRtcSignalReceivedMessageInterface) {
try {
//if offer type, create peer connection
if(data.signal.type === "offer"){
@ -274,53 +266,126 @@ export class SimplePeer {
}
}
/**
*
* @param userId
* @param stream
*/
private stream(userId : string, stream: MediaStream) {
if(!stream){
mediaManager.disabledVideoByUserId(userId);
mediaManager.disabledMicrophoneByUserId(userId);
return;
private receiveWebrtcScreenSharingSignal(data: WebRtcSignalReceivedMessageInterface) {
console.log("receiveWebrtcScreenSharingSignal", data);
try {
//if offer type, create peer connection
if(data.signal.type === "offer"){
this.createPeerScreenSharingConnection(data);
}
const peer = this.PeerScreenSharingConnectionArray.get(data.userId);
if (peer !== undefined) {
peer.signal(data.signal);
} else {
console.error('Could not find peer whose ID is "'+data.userId+'" in receiveWebrtcScreenSharingSignal');
}
} catch (e) {
console.error(`receiveWebrtcSignal => ${data.userId}`, e);
}
mediaManager.addStreamRemoteVideo(userId, stream);
}
/**
*
* @param userId
*/
private addMedia (userId : string) {
private pushVideoToRemoteUser(userId : string) {
try {
const localStream: MediaStream|null = mediaManager.localStream;
const peer = this.PeerConnectionArray.get(userId);
if(localStream === null) {
//send fake signal
if(peer === undefined){
return;
}
peer.write(new Buffer(JSON.stringify({
type: "stream",
stream: null
})));
const PeerConnection = this.PeerConnectionArray.get(userId);
if (!PeerConnection) {
throw new Error('While adding media, cannot find user with ID ' + userId);
}
const localStream: MediaStream | null = mediaManager.localStream;
PeerConnection.write(new Buffer(JSON.stringify(mediaManager.constraintsMedia)));
if(!localStream){
return;
}
if (peer === undefined) {
throw new Error('While adding media, cannot find user with ID '+userId);
}
for (const track of localStream.getTracks()) {
peer.addTrack(track, localStream);
PeerConnection.addTrack(track, localStream);
}
}catch (e) {
console.error(`addMedia => addMedia => ${userId}`, e);
console.error(`pushVideoToRemoteUser => ${userId}`, e);
}
}
updatedLocalStream(){
this.Users.forEach((user: UserSimplePeer) => {
this.addMedia(user.userId);
private pushScreenSharingToRemoteUser(userId : string) {
const PeerConnection = this.PeerScreenSharingConnectionArray.get(userId);
if (!PeerConnection) {
throw new Error('While pushing screen sharing, cannot find user with ID ' + userId);
}
const localScreenCapture: MediaStream | null = mediaManager.localScreenCapture;
if(!localScreenCapture){
return;
}
for (const track of localScreenCapture.getTracks()) {
PeerConnection.addTrack(track, localScreenCapture);
}
return;
}
public sendLocalVideoStream(){
this.Users.forEach((user: UserSimplePeerInterface) => {
this.pushVideoToRemoteUser(user.userId);
})
}
/**
* Triggered locally when clicking on the screen sharing button
*/
public sendLocalScreenSharingStream() {
if (!mediaManager.localScreenCapture) {
console.error('Could not find localScreenCapture to share')
return;
}
for (const user of this.Users) {
this.sendLocalScreenSharingStreamToUser(user.userId);
}
}
/**
* Triggered locally when clicking on the screen sharing button
*/
public stopLocalScreenSharingStream(stream: MediaStream) {
for (const user of this.Users) {
this.stopLocalScreenSharingStreamToUser(user.userId, stream);
}
}
private sendLocalScreenSharingStreamToUser(userId: string): void {
// If a connection already exists with user (because it is already sharing a screen with us... let's use this connection)
if (this.PeerScreenSharingConnectionArray.has(userId)) {
this.pushScreenSharingToRemoteUser(userId);
return;
}
const screenSharingUser: UserSimplePeerInterface = {
userId,
initiator: true
};
const PeerConnectionScreenSharing = this.createPeerScreenSharingConnection(screenSharingUser);
if (!PeerConnectionScreenSharing) {
return;
}
}
private stopLocalScreenSharingStreamToUser(userId: string, stream: MediaStream): void {
const PeerConnectionScreenSharing = this.PeerScreenSharingConnectionArray.get(userId);
if (!PeerConnectionScreenSharing) {
throw new Error('Weird, screen sharing connection to user ' + userId + 'not found')
}
console.log("updatedScreenSharing => destroy", PeerConnectionScreenSharing);
// Stop sending stream and close peer connection if peer is not sending stream too
PeerConnectionScreenSharing.stopPushingScreenSharingToRemoteUser(stream);
if (!PeerConnectionScreenSharing.isReceivingScreenSharingStream()) {
PeerConnectionScreenSharing.destroy();
this.PeerScreenSharingConnectionArray.delete(userId);
}
}
}

View file

@ -0,0 +1,128 @@
import * as SimplePeerNamespace from "simple-peer";
import {mediaManager} from "./MediaManager";
import {Connection} from "../Connection";
const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer');
/**
* A peer connection used to transmit video / audio signals between 2 peers.
*/
export class VideoPeer extends Peer {
constructor(private userId: string, initiator: boolean, private connection: Connection) {
super({
initiator: initiator ? initiator : false,
reconnectTimer: 10000,
config: {
iceServers: [
{
urls: 'stun:stun.l.google.com:19302'
},
{
urls: 'turn:numb.viagenie.ca',
username: 'g.parant@thecodingmachine.com',
credential: 'itcugcOHxle9Acqi$'
},
]
}
});
//start listen signal for the peer connection
this.on('signal', (data: unknown) => {
this.sendWebrtcSignal(data);
});
this.on('stream', (stream: MediaStream) => {
this.stream(stream);
});
/*peer.on('track', (track: MediaStreamTrack, stream: MediaStream) => {
});*/
this.on('close', () => {
this.destroy();
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.on('error', (err: any) => {
console.error(`error => ${this.userId} => ${err.code}`, err);
mediaManager.isError(userId);
});
this.on('connect', () => {
mediaManager.isConnected(this.userId);
console.info(`connect => ${this.userId}`);
});
this.on('data', (chunk: Buffer) => {
const constraint = JSON.parse(chunk.toString('utf8'));
console.log("data", constraint);
if (constraint.audio) {
mediaManager.enabledMicrophoneByUserId(this.userId);
} else {
mediaManager.disabledMicrophoneByUserId(this.userId);
}
if (constraint.video || constraint.screen) {
mediaManager.enabledVideoByUserId(this.userId);
} else {
this.stream(undefined);
mediaManager.disabledVideoByUserId(this.userId);
}
});
this.pushVideoToRemoteUser();
}
private sendWebrtcSignal(data: unknown) {
try {
this.connection.sendWebrtcSignal(data, this.userId);
}catch (e) {
console.error(`sendWebrtcSignal => ${this.userId}`, e);
}
}
/**
* Sends received stream to screen.
*/
private stream(stream?: MediaStream) {
console.log(`VideoPeer::stream => ${this.userId}`, stream);
if(!stream){
mediaManager.disabledVideoByUserId(this.userId);
mediaManager.disabledMicrophoneByUserId(this.userId);
} else {
mediaManager.addStreamRemoteVideo(this.userId, stream);
}
}
/**
* This is triggered twice. Once by the server, and once by a remote client disconnecting
*/
public destroy(error?: Error): void {
try {
mediaManager.removeActiveVideo(this.userId);
// FIXME: I don't understand why "Closing connection with" message is displayed TWICE before "Nb users in peerConnectionArray"
// I do understand the method closeConnection is called twice, but I don't understand how they manage to run in parallel.
//console.log('Closing connection with '+userId);
super.destroy(error);
} catch (err) {
console.error("VideoPeer::destroy", err)
}
}
private pushVideoToRemoteUser() {
try {
const localStream: MediaStream | null = mediaManager.localStream;
this.write(new Buffer(JSON.stringify(mediaManager.constraintsMedia)));
if(!localStream){
return;
}
for (const track of localStream.getTracks()) {
this.addTrack(track, localStream);
}
}catch (e) {
console.error(`pushVideoToRemoteUser => ${this.userId}`, e);
}
}
}