diff --git a/back/src/App.ts b/back/src/App.ts index 4b3b3c53..82ca317a 100644 --- a/back/src/App.ts +++ b/back/src/App.ts @@ -28,7 +28,7 @@ class App { private config(): void { this.app.use(bodyParser.json()); this.app.use(bodyParser.urlencoded({extended: false})); - this.app.use(function (req: Request, res: Response, next) { + this.app.use((req: Request, res: Response, next) => { res.header("Access-Control-Allow-Origin", "*"); // update to match the domain you will make the request from res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); next(); diff --git a/back/src/Controller/IoSocketController.ts b/back/src/Controller/IoSocketController.ts index acd5843f..23a238e6 100644 --- a/back/src/Controller/IoSocketController.ts +++ b/back/src/Controller/IoSocketController.ts @@ -6,10 +6,23 @@ import {ExSocketInterface} from "../Model/Websocket/ExSocketInterface"; //TODO f import Jwt, {JsonWebTokenError} from "jsonwebtoken"; import {SECRET_KEY} from "../Enum/EnvironmentVariable"; //TODO fix import by "_Enum/..." import {ExtRooms, RefreshUserPositionFunction} from "../Model/Websocket/ExtRoom"; -import {ExtRoomsInterface} from "_Model/Websocket/ExtRoomsInterface"; +import {ExtRoomsInterface} from "../Model/Websocket/ExtRoomsInterface"; +import {World} from "../Model/World"; +import { uuid } from 'uuidv4'; + +enum SockerIoEvent { + CONNECTION = "connection", + DISCONNECTION = "disconnect", + JOIN_ROOM = "join-room", + USER_POSITION = "user-position", + WEBRTC_SIGNAL = "webrtc-signal", + WEBRTC_START = "webrtc-start", + MESSAGE_ERROR = "message-error", +} export class IoSocketController{ Io: socketIO.Server; + World: World; constructor(server : http.Server) { this.Io = socketIO(server); @@ -29,10 +42,17 @@ export class IoSocketController{ this.ioConnection(); this.shareUsersPosition(); + + //don't send only function because the context will be not this + this.World = new World((user1 : string, user2 : string) => { + this.connectedUser(user1, user2); + }, (user1 : string, user2 : string) => { + this.disConnectedUser(user1, user2); + }); } ioConnection() { - this.Io.on('connection', (socket: Socket) => { + this.Io.on(SockerIoEvent.CONNECTION, (socket: Socket) => { /*join-rom event permit to join one room. message : userId : user identification @@ -41,15 +61,18 @@ export class IoSocketController{ x: user x position on map y: user y position on map */ - socket.on('join-room', (message : string) => { + socket.on(SockerIoEvent.JOIN_ROOM, (message : string) => { let messageUserPosition = this.hydrateMessageReceive(message); if(messageUserPosition instanceof Error){ - return socket.emit("message-error", JSON.stringify({message: messageUserPosition.message})) + return socket.emit(SockerIoEvent.MESSAGE_ERROR, JSON.stringify({message: messageUserPosition.message})) } //join user in room socket.join(messageUserPosition.roomId); + //join user in world + this.World.join(messageUserPosition); + // sending to all clients in room except sender this.saveUserInformation((socket as ExSocketInterface), messageUserPosition); @@ -58,22 +81,96 @@ export class IoSocketController{ rooms.refreshUserPosition = RefreshUserPositionFunction; rooms.refreshUserPosition(rooms, this.Io); - socket.to(messageUserPosition.roomId).emit('join-room', messageUserPosition.toString()); + socket.to(messageUserPosition.roomId).emit(SockerIoEvent.JOIN_ROOM, messageUserPosition.toString()); }); - socket.on('user-position', (message : string) => { + socket.on(SockerIoEvent.USER_POSITION, (message : string) => { let messageUserPosition = this.hydrateMessageReceive(message); if (messageUserPosition instanceof Error) { - return socket.emit("message-error", JSON.stringify({message: messageUserPosition.message})); + return socket.emit(SockerIoEvent.MESSAGE_ERROR, JSON.stringify({message: messageUserPosition.message})); } + // update position in the worl + this.World.updatePosition(messageUserPosition); + // sending to all clients in room except sender this.saveUserInformation((socket as ExSocketInterface), messageUserPosition); //refresh position of all user in all rooms in real time - let rooms = (this.Io.sockets.adapter.rooms as ExtRoomsInterface) + let rooms = (this.Io.sockets.adapter.rooms as ExtRoomsInterface); + if(!rooms.refreshUserPosition){ + rooms.refreshUserPosition = RefreshUserPositionFunction; + } rooms.refreshUserPosition(rooms, this.Io); }); + + socket.on(SockerIoEvent.WEBRTC_SIGNAL, (message : string) => { + let data : any = JSON.parse(message); + + //send only at user + let clients: Array = Object.values(this.Io.sockets.sockets); + for(let i = 0; i < clients.length; i++){ + let client : ExSocketInterface = clients[i]; + if(client.userId !== data.receiverId){ + continue + } + client.emit(SockerIoEvent.WEBRTC_SIGNAL, message); + break; + } + }); + + socket.on(SockerIoEvent.DISCONNECTION, (reason : string) => { + let Client = (socket as ExSocketInterface); + //leave group of user + this.World.leave(Client); + + //leave room + socket.leave(Client.roomId); + socket.leave(Client.webRtcRoomId); + + //delete all socket information + delete Client.userId; + delete Client.webRtcRoomId; + delete Client.roomId; + delete Client.token; + delete Client.position; + }); + }); + } + + /** + * + * @param socket + * @param roomId + */ + joinWebRtcRoom(socket : ExSocketInterface, roomId : string) { + if(socket.webRtcRoomId === roomId){ + return; + } + socket.join(roomId); + socket.webRtcRoomId = roomId; + //if two persone in room share + if (this.Io.sockets.adapter.rooms[roomId].length < 2) { + return; + } + let clients: Array = Object.values(this.Io.sockets.sockets); + + //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) => { + + let clientsId = clients.reduce((tabs: Array, clientId: ExSocketInterface, indexClientId: number) => { + if (!clientId.userId || clientId.userId === client.userId) { + return tabs; + } + tabs.push({ + userId: clientId.userId, + initiator: index <= indexClientId + }); + return tabs; + }, []); + + client.emit(SockerIoEvent.WEBRTC_START, JSON.stringify({clients: clientsId, roomId: roomId})); }); } @@ -87,8 +184,7 @@ export class IoSocketController{ //Hydrate and manage error hydrateMessageReceive(message : string) : MessageUserPosition | Error{ try { - let data = JSON.parse(message); - return new MessageUserPosition(data); + return new MessageUserPosition(JSON.parse(message)); }catch (err) { //TODO log error return new Error(err); @@ -132,4 +228,26 @@ export class IoSocketController{ this.shareUsersPosition(); }, 10); } + + //connected user + connectedUser(user1 : string, user2 : string){ + /* TODO manager room and group user to enter and leave */ + let roomId = uuid(); + let clients : Array = Object.values(this.Io.sockets.sockets); + let User1 = clients.find((user : ExSocketInterface) => user.userId === user1); + let User2 = clients.find((user : ExSocketInterface) => user.userId === user2); + + if(User1) { + this.joinWebRtcRoom(User1, roomId); + } + if(User2) { + this.joinWebRtcRoom(User2, roomId); + } + } + + //connected user + disConnectedUser(user1 : string, user2 : string){ + console.log("disConnectedUser => user1", user1); + console.log("disConnectedUser => user2", user2); + } } diff --git a/back/src/Model/Websocket/ExSocketInterface.ts b/back/src/Model/Websocket/ExSocketInterface.ts index 095d3cbc..4d1d9fee 100644 --- a/back/src/Model/Websocket/ExSocketInterface.ts +++ b/back/src/Model/Websocket/ExSocketInterface.ts @@ -4,6 +4,7 @@ import {PointInterface} from "./PointInterface"; export interface ExSocketInterface extends Socket { token: any; roomId: string; + webRtcRoomId: string; userId: string; position: PointInterface; } \ No newline at end of file diff --git a/back/src/Model/Websocket/Message.ts b/back/src/Model/Websocket/Message.ts index d726968f..da265464 100644 --- a/back/src/Model/Websocket/Message.ts +++ b/back/src/Model/Websocket/Message.ts @@ -3,7 +3,7 @@ export class Message { roomId: string; constructor(data: any) { - if(!data.userId || !data.roomId){ + if (!data.userId || !data.roomId) { throw Error("userId or roomId cannot be null"); } this.userId = data.userId; @@ -13,7 +13,7 @@ export class Message { toJson() { return { userId: this.userId, - roomId: this.roomId, + roomId: this.roomId } } } \ No newline at end of file diff --git a/back/src/Model/Websocket/MessageUserPosition.ts b/back/src/Model/Websocket/MessageUserPosition.ts index a3161b69..1b534620 100644 --- a/back/src/Model/Websocket/MessageUserPosition.ts +++ b/back/src/Model/Websocket/MessageUserPosition.ts @@ -27,9 +27,9 @@ export class Point implements PointInterface{ export class MessageUserPosition extends Message{ position: PointInterface; - constructor(data: any) { - super(data); - this.position = new Point(data.position.x, data.position.y, data.position.direction); + constructor(message: any) { + super(message); + this.position = new Point(message.position.x, message.position.y, message.position.direction); } toString() { diff --git a/back/src/Model/World.ts b/back/src/Model/World.ts index 804a176b..ff1f58ff 100644 --- a/back/src/Model/World.ts +++ b/back/src/Model/World.ts @@ -3,6 +3,7 @@ import {PointInterface} from "./Websocket/PointInterface"; import {Group} from "./Group"; import {Distance} from "./Distance"; import {UserInterface} from "./UserInterface"; +import {ExSocketInterface} from "_Model/Websocket/ExSocketInterface"; export class World { static readonly MIN_DISTANCE = 160; @@ -29,8 +30,12 @@ export class World { }); } + public leave(user : ExSocketInterface){ + /*TODO leaver user in group*/ + this.users.delete(user.userId); + } + public updatePosition(userPosition: MessageUserPosition): void { - let context = this; let user = this.users.get(userPosition.userId); if(typeof user === 'undefined') { return; diff --git a/front/dist/index.html b/front/dist/index.html index 61213be6..a9f51d4d 100644 --- a/front/dist/index.html +++ b/front/dist/index.html @@ -1,11 +1,37 @@ - - - - - Document - - + + + + + + Document + + + +
+
+
+
+ +
+
+
+ + +
+
+ + +
+
+ +
+
+
+
+ +
+ diff --git a/front/dist/resources/logos/cinema-close.svg b/front/dist/resources/logos/cinema-close.svg new file mode 100644 index 00000000..aa1d9b17 --- /dev/null +++ b/front/dist/resources/logos/cinema-close.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/front/dist/resources/logos/cinema.svg b/front/dist/resources/logos/cinema.svg new file mode 100644 index 00000000..1167d09d --- /dev/null +++ b/front/dist/resources/logos/cinema.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + diff --git a/front/dist/resources/logos/microphone-close.svg b/front/dist/resources/logos/microphone-close.svg new file mode 100644 index 00000000..16731829 --- /dev/null +++ b/front/dist/resources/logos/microphone-close.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + diff --git a/front/dist/resources/logos/microphone.svg b/front/dist/resources/logos/microphone.svg new file mode 100644 index 00000000..ff5727ca --- /dev/null +++ b/front/dist/resources/logos/microphone.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + diff --git a/front/dist/resources/logos/phone-open.svg b/front/dist/resources/logos/phone-open.svg new file mode 100644 index 00000000..73b08951 --- /dev/null +++ b/front/dist/resources/logos/phone-open.svg @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/front/dist/resources/logos/phone.svg b/front/dist/resources/logos/phone.svg new file mode 100644 index 00000000..ac8e595a --- /dev/null +++ b/front/dist/resources/logos/phone.svg @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/front/dist/resources/style/style.css b/front/dist/resources/style/style.css new file mode 100644 index 00000000..9e2d2daa --- /dev/null +++ b/front/dist/resources/style/style.css @@ -0,0 +1,153 @@ +.webrtc{ + display: none; +} +.webrtc.active{ + display: block; +} +.webrtc, .activeCam{ + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + background: black; +} +.activeCam video{ + position: absolute; + width: 100%; + height: 100%; +} + +/*CSS size for 2 - 3 elements*/ +video:nth-child(1):nth-last-child(3), +video:nth-child(2):nth-last-child(2), +video:nth-child(3):nth-last-child(1), +video:nth-child(1):nth-last-child(2), +video:nth-child(2):nth-last-child(1){ + width: 50%; +} +video:nth-child(1):nth-last-child(3), +video:nth-child(2):nth-last-child(2), +video:nth-child(3):nth-last-child(1){ + height: 50%; +} + +/*CSS position for 2 elements*/ +video:nth-child(1):nth-last-child(2){ + left: 0; +} +video:nth-child(2):nth-last-child(1){ + left: 50%; +} + +/*CSS position for 3 elements*/ +video:nth-child(1):nth-last-child(3){ + top: 0; + left: 0; +} +video:nth-child(2):nth-last-child(2){ + top: 0; + left: 50%; +} +video:nth-child(3):nth-last-child(1) { + top: 50%; + left: 25%; +} + +.myCam{ + height: 200px; + width: 300px; + position: absolute; + right: 10px; + background: black; + border: none; + bottom: 20px; + max-height: 17%; + max-width: 17%; + opacity: 1; + display: block; + transition: opacity 1s; +} +.myCam video{ + width: 100%; + height: 100%; +} +.btn-cam-action div{ + cursor: pointer; + position: absolute; + border: solid 0px black; + width: 64px; + height: 64px; + background: #666; + left: 6vw; + box-shadow: 2px 2px 24px #444; + border-radius: 48px; + transform: translateX(calc(-6vw - 96px)); + transition-timing-function: ease-in-out; +} +.webrtc:hover .btn-cam-action.active div{ + transform: translateX(0); +} +.btn-cam-action div:hover{ + background: #407cf7; + box-shadow: 4px 4px 48px #666; + transition: 280ms; +} +.btn-micro{ + bottom: 277px; + transition: all .3s; +} +.btn-video{ + bottom: 177px; + transition: all .2s; +} +.btn-call{ + bottom: 77px; + transition: all .1s; +} +.btn-cam-action div img{ + height: 32px; + width: 40px; + top: calc(48px - 32px); + left: calc(48px - 35px); + position: relative; +} +.phone-open{ + position: absolute; + border-radius: 50%; + width: 50px; + height: 50px; + left: calc(50% - 70px); + padding: 20px; + bottom: 20px; + box-shadow: 2px 2px 24px #444; + background-color: green; + opacity: 0; + transition: all .4s ease-in-out; +} +.phone-open.active{ + opacity: 1; + animation-name: phone-move; + animation-duration: 0.4s; + animation-iteration-count: infinite; + animation-timing-function: linear; +} +.phone-open:hover{ + animation: none; + cursor: pointer; +} + +@keyframes phone-move { + 0% { + left: calc(50% - 70px); + bottom: 20px; + } + 25% { + left: calc(50% - 65px); + bottom: 15px; + } + 25% { + left: calc(50% - 75px); + bottom: 25px; + } +} \ No newline at end of file diff --git a/front/package.json b/front/package.json index 25d613e6..17c08137 100644 --- a/front/package.json +++ b/front/package.json @@ -15,8 +15,10 @@ }, "dependencies": { "@types/axios": "^0.14.0", + "@types/simple-peer": "^9.6.0", "@types/socket.io-client": "^1.4.32", "phaser": "^3.22.0", + "simple-peer": "^9.6.2", "socket.io-client": "^2.3.0" }, "scripts": { diff --git a/front/src/Connexion.ts b/front/src/Connexion.ts index d3c4b875..dc48833c 100644 --- a/front/src/Connexion.ts +++ b/front/src/Connexion.ts @@ -4,6 +4,14 @@ const SocketIo = require('socket.io-client'); import Axios from "axios"; import {API_URL, ROOM} from "./Enum/EnvironmentVariable"; +enum EventMessage{ + WEBRTC_SIGNAL = "webrtc-signal", + WEBRTC_START = "webrtc-start", + JOIN_ROOM = "join-room", + USER_POSITION = "user-position", + MESSAGE_ERROR = "message-error" +} + class Message { userId: string; roomId: string; @@ -56,6 +64,7 @@ export interface MessageUserPositionInterface { roomId: string; position: PointInterface; } + class MessageUserPosition extends Message implements MessageUserPositionInterface{ position: PointInterface; @@ -76,14 +85,15 @@ class MessageUserPosition extends Message implements MessageUserPositionInterfac } export interface ListMessageUserPositionInterface { - roomId : string; + roomId: string; listUsersPosition: Array; } -class ListMessageUserPosition{ - roomId : string; + +class ListMessageUserPosition { + roomId: string; listUsersPosition: Array; - constructor(roomId : string, data : any) { + constructor(roomId: string, data: any) { this.roomId = roomId; this.listUsersPosition = new Array(); data.forEach((userPosition: any) => { @@ -99,23 +109,36 @@ class ListMessageUserPosition{ }); } } + export interface ConnexionInterface { - socket : any; - token : string; - email : string; + socket: any; + token: string; + email: string; userId: string; - startedRoom : string; - createConnexion() : Promise; - joinARoom(roomId : string) : void; - sharePosition(x : number, y : number, direction : string) : void; - positionOfAllUser() : void; + startedRoom: string; + + createConnexion(): Promise; + + joinARoom(roomId: string): void; + + sharePosition(x: number, y: number, direction: string): void; + + positionOfAllUser(): void; + + /*webrtc*/ + sendWebrtcSignal(signal: any, roomId: string, userId?: string, receiverId?: string): void; + + receiveWebrtcSignal(callBack: Function): void; + + receiveWebrtcStart(callBack: Function): void; } -export class Connexion implements ConnexionInterface{ - socket : any; - token : string; - email : string; + +export class Connexion implements ConnexionInterface { + socket: any; + token: string; + email: string; userId: string; - startedRoom : string; + startedRoom: string; GameManager: GameManager; @@ -124,7 +147,7 @@ export class Connexion implements ConnexionInterface{ this.GameManager = GameManager; } - createConnexion() : Promise{ + createConnexion(): Promise { return Axios.post(`${API_URL}/login`, {email: this.email}) .then((res) => { this.token = res.data.token; @@ -159,9 +182,9 @@ export class Connexion implements ConnexionInterface{ * Permit to join a room * @param roomId */ - joinARoom(roomId : string) : void { + joinARoom(roomId: string): void { let messageUserPosition = new MessageUserPosition(this.userId, this.startedRoom, new Point(0, 0)); - this.socket.emit('join-room', messageUserPosition.toString()); + this.socket.emit(EventMessage.JOIN_ROOM, messageUserPosition.toString()); } /** @@ -175,7 +198,7 @@ export class Connexion implements ConnexionInterface{ return; } let messageUserPosition = new MessageUserPosition(this.userId, ROOM[0], new Point(x, y, direction)); - this.socket.emit('user-position', messageUserPosition.toString()); + this.socket.emit(EventMessage.USER_POSITION, messageUserPosition.toString()); } /** @@ -193,8 +216,8 @@ export class Connexion implements ConnexionInterface{ * ... * ] **/ - positionOfAllUser() : void { - this.socket.on("user-position", (message: string) => { + positionOfAllUser(): void { + this.socket.on(EventMessage.USER_POSITION, (message: string) => { let dataList = JSON.parse(message); dataList.forEach((UserPositions: any) => { let listMessageUserPosition = new ListMessageUserPosition(UserPositions[0], UserPositions[1]); @@ -203,9 +226,26 @@ export class Connexion implements ConnexionInterface{ }); } - errorMessage() : void { - this.socket.on('message-error', (message : string) => { - console.error("message-error", message); + sendWebrtcSignal(signal: any, roomId: string, userId? : string, receiverId? : string) { + this.socket.emit(EventMessage.WEBRTC_SIGNAL, JSON.stringify({ + userId: userId ? userId : this.userId, + receiverId: receiverId ? receiverId : this.userId, + roomId: roomId, + signal: signal + })); + } + + receiveWebrtcStart(callback: Function) { + this.socket.on(EventMessage.WEBRTC_START, callback); + } + + receiveWebrtcSignal(callback: Function) { + this.socket.on(EventMessage.WEBRTC_SIGNAL, callback); + } + + errorMessage(): void { + this.socket.on(EventMessage.MESSAGE_ERROR, (message: string) => { + console.error(EventMessage.MESSAGE_ERROR, message); }) } } \ No newline at end of file diff --git a/front/src/Phaser/Game/GameManager.ts b/front/src/Phaser/Game/GameManager.ts index 916840fc..1877b29a 100644 --- a/front/src/Phaser/Game/GameManager.ts +++ b/front/src/Phaser/Game/GameManager.ts @@ -1,6 +1,8 @@ import {GameScene} from "./GameScene"; import {ROOM} from "../../Enum/EnvironmentVariable" import {Connexion, ConnexionInterface, ListMessageUserPositionInterface} from "../../Connexion"; +import {SimplePeerInterface, SimplePeer} from "../../WebRtc/SimplePeer"; +import {LogincScene} from "../Login/LogincScene"; export enum StatusGameManagerEnum { IN_PROGRESS = 1, @@ -13,6 +15,7 @@ export class GameManager { status: number; private ConnexionInstance: Connexion; private currentGameScene: GameScene; + SimplePeer : SimplePeerInterface; constructor() { this.status = StatusGameManagerEnum.IN_PROGRESS; @@ -21,9 +24,11 @@ export class GameManager { connect(email:string) { this.ConnexionInstance = new Connexion(email, this); ConnexionInstance = this.ConnexionInstance; - return this.ConnexionInstance.createConnexion() + return this.ConnexionInstance.createConnexion().then(() => { + this.SimplePeer = new SimplePeer(ConnexionInstance); + }); } - + setCurrentGameScene(gameScene: GameScene) { this.currentGameScene = gameScene; } diff --git a/front/src/Phaser/Login/LogincScene.ts b/front/src/Phaser/Login/LogincScene.ts index 0fb4d2a4..1aa1e0af 100644 --- a/front/src/Phaser/Login/LogincScene.ts +++ b/front/src/Phaser/Login/LogincScene.ts @@ -1,9 +1,9 @@ -import KeyboardKeydownCallback = Phaser.Types.Input.Keyboard.KeyboardKeydownCallback; import {gameManager} from "../Game/GameManager"; -import {ROOM} from "../../Enum/EnvironmentVariable"; import {TextField} from "../Components/TextField"; import {TextInput} from "../Components/TextInput"; import {ClickButton} from "../Components/ClickButton"; +import {GameSceneInterface} from "../Game/GameScene"; +import {MessageUserPositionInterface} from "../../Connexion"; //todo: put this constants in a dedicated file export const LoginSceneName = "LoginScene"; @@ -11,21 +11,22 @@ enum LoginTextures { playButton = "play_button", } -export class LogincScene extends Phaser.Scene { +export class LogincScene extends Phaser.Scene implements GameSceneInterface { private emailInput: TextInput; private textField: TextField; private playButton: ClickButton; private infoTextField: TextField; + constructor() { super({ key: LoginSceneName }); } - + preload() { this.load.image(LoginTextures.playButton, "resources/objects/play_button.png"); } - + create() { this.textField = new TextField(this, 10, 10, 'Enter your email:'); this.emailInput = new TextInput(this, 10, 50); @@ -37,15 +38,25 @@ export class LogincScene extends Phaser.Scene { let infoText = "Commandes de base: \n - Z,Q,S,D (ou les flèches de direction) pour bouger\n - SHIFT pour accélerer"; this.infoTextField = new TextField(this, 10, 300, infoText); } - + update(time: number, delta: number): void { - + } - + async login() { let email = this.emailInput.text; if (!email) return; - await gameManager.connect(email); - this.scene.start("GameScene"); + gameManager.connect(email).then(() => { + this.scene.start("GameScene"); + }); + } + + Map: Phaser.Tilemaps.Tilemap; + RoomId: string; + + createCurrentPlayer(UserId: string): void { + } + + shareUserPosition(UsersPosition: Array): void { } } \ No newline at end of file diff --git a/front/src/WebRtc/MediaManager.ts b/front/src/WebRtc/MediaManager.ts new file mode 100644 index 00000000..5b8d0076 --- /dev/null +++ b/front/src/WebRtc/MediaManager.ts @@ -0,0 +1,141 @@ +export class MediaManager { + localStream: MediaStream; + remoteVideo: Array = new Array(); + myCamVideo: any; + cinemaClose: any = null; + cinema: any = null; + microphoneClose: any = null; + microphone: any = null; + constraintsMedia = {audio: false, video: true}; + getCameraPromise : Promise = null; + + constructor() { + this.myCamVideo = document.getElementById('myCamVideo'); + this.microphoneClose = document.getElementById('microphone-close'); + + this.microphoneClose.addEventListener('click', (e: any) => { + e.preventDefault(); + this.enabledMicrophone(); + //update tracking + }); + this.microphone = document.getElementById('microphone'); + this.microphone.addEventListener('click', (e: any) => { + e.preventDefault(); + this.disabledMicrophone(); + //update tracking + }); + + this.cinemaClose = document.getElementById('cinema-close'); + this.cinemaClose.addEventListener('click', (e: any) => { + e.preventDefault(); + this.enabledCamera(); + //update tracking + }); + this.cinema = document.getElementById('cinema'); + this.cinema.addEventListener('click', (e: any) => { + e.preventDefault(); + this.disabledCamera(); + //update tracking + }); + + this.enabledCamera(); + this.enabledMicrophone(); + } + + activeVisio(){ + let webRtc = document.getElementById('webRtc'); + webRtc.classList.add('active'); + } + + enabledCamera() { + this.cinemaClose.style.display = "none"; + this.cinema.style.display = "block"; + this.constraintsMedia.video = true; + this.localStream = null; + this.myCamVideo.srcObject = null; + } + + disabledCamera() { + this.cinemaClose.style.display = "block"; + this.cinema.style.display = "none"; + this.constraintsMedia.video = false; + + this.myCamVideo.pause(); + if(this.localStream) { + this.localStream.getTracks().forEach((MediaStreamTrack: MediaStreamTrack) => { + if (MediaStreamTrack.kind === "video") { + MediaStreamTrack.stop(); + } + }); + } + this.localStream = null; + this.myCamVideo.srcObject = null; + } + + enabledMicrophone() { + this.microphoneClose.style.display = "none"; + this.microphone.style.display = "block"; + this.constraintsMedia.audio = true; + } + + disabledMicrophone() { + this.microphoneClose.style.display = "block"; + this.microphone.style.display = "none"; + this.constraintsMedia.audio = false; + if(this.localStream) { + this.localStream.getTracks().forEach((MediaStreamTrack: MediaStreamTrack) => { + if (MediaStreamTrack.kind === "audio") { + MediaStreamTrack.stop(); + } + }); + } + } + + getElementActivePhone(){ + return document.getElementById('phone-open'); + } + + activePhoneOpen(){ + return this.getElementActivePhone().classList.add("active"); + } + + disablePhoneOpen(){ + return this.getElementActivePhone().classList.remove("active"); + } + + //get camera + getCamera() { + return this.getCameraPromise = navigator.mediaDevices.getUserMedia(this.constraintsMedia) + .then((stream: MediaStream) => { + this.localStream = stream; + this.myCamVideo.srcObject = this.localStream; + return stream; + }).catch((err) => { + console.error(err); + this.localStream = null; + throw err; + }); + } + + /** + * + * @param userId + */ + addActiveVideo(userId : string){ + let elementRemoteVideo = document.getElementById("activeCam"); + elementRemoteVideo.insertAdjacentHTML('beforeend', ''); + this.remoteVideo[(userId as any)] = document.getElementById(userId); + } + + /** + * + * @param userId + */ + removeActiveVideo(userId : string){ + let element = document.getElementById(userId); + if(!element){ + return; + } + element.remove(); + } +} \ No newline at end of file diff --git a/front/src/WebRtc/SimplePeer.ts b/front/src/WebRtc/SimplePeer.ts new file mode 100644 index 00000000..3c94bbaf --- /dev/null +++ b/front/src/WebRtc/SimplePeer.ts @@ -0,0 +1,150 @@ +import {ConnexionInterface} from "../Connexion"; +import {MediaManager} from "./MediaManager"; +let Peer = require('simple-peer'); + +export interface SimplePeerInterface { +} + +export class SimplePeer { + Connexion: ConnexionInterface; + MediaManager: MediaManager; + WebRtcRoomId: string; + Users: Array; + + PeerConnexionArray: Array = new Array(); + + constructor(Connexion: ConnexionInterface, WebRtcRoomId: string = "test-webrtc") { + this.Connexion = Connexion; + this.WebRtcRoomId = WebRtcRoomId; + this.MediaManager = new MediaManager(); + this.initialise(); + } + + /** + * permit to listen when user could start visio + */ + private initialise(){ + + //receive message start + this.Connexion.receiveWebrtcStart((message: string) => { + this.receiveWebrtcStart(message); + }); + + //when button to call is clicked, start video + this.MediaManager.getElementActivePhone().addEventListener("click", () => { + this.startWebRtc(); + this.disablePhone(); + }); + } + /** + * server has two person connected, start the meet + */ + startWebRtc() { + this.MediaManager.activeVisio(); + return this.MediaManager.getCamera().then((stream: MediaStream) => { + this.MediaManager.localStream = stream; + + //create pear connexion + this.createPeerConnexion(); + + //receive signal by gemer + this.Connexion.receiveWebrtcSignal((message: string) => { + this.receiveWebrtcSignal(message); + }); + }).catch((err) => { + console.error(err); + }); + } + + /** + * + * @param message + */ + receiveWebrtcStart(message: string) { + let data = JSON.parse(message); + this.WebRtcRoomId = data.roomId; + this.Users = data.clients; + + //active button for player + this.activePhone(); + } + + + createPeerConnexion() { + this.Users.forEach((user: any) => { + if(this.PeerConnexionArray[user.userId]){ + return; + } + this.MediaManager.addActiveVideo(user.userId); + + this.PeerConnexionArray[user.userId] = new Peer({initiator: user.initiator}); + + this.PeerConnexionArray[user.userId].on('signal', (data: any) => { + this.sendWebrtcSignal(data, user.userId); + }); + + this.PeerConnexionArray[user.userId].on('stream', (stream: MediaStream) => { + this.stream(user.userId, stream); + }); + + this.PeerConnexionArray[user.userId].on('close', () => { + this.closeConnexion(user.userId); + }); + + this.addMedia(user.userId); + }); + + } + + closeConnexion(userId : string){ + // @ts-ignore + this.PeerConnexionArray[userId] = null; + this.MediaManager.removeActiveVideo(userId) + } + + /** + * + * @param userId + * @param data + */ + sendWebrtcSignal(data: any, userId : string) { + this.Connexion.sendWebrtcSignal(data, this.WebRtcRoomId, null, userId); + } + + /** + * + * @param message + */ + receiveWebrtcSignal(message: string) { + let data = JSON.parse(message); + if(!this.PeerConnexionArray[data.userId]){ + return; + } + this.PeerConnexionArray[data.userId].signal(data.signal); + } + + /** + * + * @param userId + * @param stream + */ + stream(userId : any, stream: MediaStream) { + this.MediaManager.remoteVideo[userId].srcObject = stream; + } + + /** + * + * @param userId + */ + addMedia (userId : any) { + this.PeerConnexionArray[userId].addStream(this.MediaManager.localStream) // <- add streams to peer dynamically + } + + activePhone(){ + this.MediaManager.activePhoneOpen(); + } + + disablePhone(){ + this.MediaManager.disablePhoneOpen(); + } +} \ No newline at end of file diff --git a/front/yarn.lock b/front/yarn.lock index c9b34f3f..5b3a1cc0 100644 --- a/front/yarn.lock +++ b/front/yarn.lock @@ -65,6 +65,13 @@ version "13.11.0" resolved "https://registry.yarnpkg.com/@types/node/-/node-13.11.0.tgz#390ea202539c61c8fa6ba4428b57e05bc36dc47b" +"@types/simple-peer@^9.6.0": + version "9.6.0" + resolved "https://registry.yarnpkg.com/@types/simple-peer/-/simple-peer-9.6.0.tgz#b5828d835b7f42dde27db584ba127e7a9f9072f4" + integrity sha512-X2y6s+vE/3j03hkI90oqld2JH2J/m1L7yFCYYPyFV/whrOK1h4neYvJL3GIE+UcACJacXZqzdmDKudwec18RbA== + dependencies: + "@types/node" "*" + "@types/socket.io-client@^1.4.32": version "1.4.32" resolved "https://registry.yarnpkg.com/@types/socket.io-client/-/socket.io-client-1.4.32.tgz#988a65a0386c274b1c22a55377fab6a30789ac14" @@ -1718,6 +1725,11 @@ functional-red-black-tree@^1.0.1: resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= +get-browser-rtc@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/get-browser-rtc/-/get-browser-rtc-1.0.2.tgz#bbcd40c8451a7ed4ef5c373b8169a409dd1d11d9" + integrity sha1-u81AyEUaftTvXDc7gWmkCd0dEdk= + get-caller-file@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" @@ -3097,7 +3109,12 @@ querystringify@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e" -randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: +queue-microtask@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.1.2.tgz#139bf8186db0c545017ec66c2664ac646d5c571e" + integrity sha512-F9wwNePtXrzZenAB3ax0Y8TSKGvuB7Qw16J30hspEUTbfUM+H827XyN3rlpwhVmtm5wuZtbKIHjOnwDn7MUxWQ== + +randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.3, randombytes@^2.0.5: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" dependencies: @@ -3135,7 +3152,7 @@ raw-body@2.4.0: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.0.6: +readable-stream@^3.0.6, readable-stream@^3.4.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" dependencies: @@ -3418,6 +3435,17 @@ signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" +simple-peer@^9.6.2: + version "9.6.2" + resolved "https://registry.yarnpkg.com/simple-peer/-/simple-peer-9.6.2.tgz#42418e77cf8f9184e4fa22ef1017b195c2bf84d7" + integrity sha512-EOKoImCaqtNvXIntxT1CBBK/3pVi7tMAoJ3shdyd9qk3zLm3QPiRLb/sPC1G2xvKJkJc5fkQjCXqRZ0AknwTig== + dependencies: + debug "^4.0.1" + get-browser-rtc "^1.0.0" + queue-microtask "^1.1.0" + randombytes "^2.0.3" + readable-stream "^3.4.0" + slice-ansi@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636"