diff --git a/back/src/Controller/IoSocketController.ts b/back/src/Controller/IoSocketController.ts index 1fc114a4..88af801a 100644 --- a/back/src/Controller/IoSocketController.ts +++ b/back/src/Controller/IoSocketController.ts @@ -74,6 +74,36 @@ export class IoSocketController{ let rooms = (this.Io.sockets.adapter.rooms as ExtRoomsInterface) rooms.refreshUserPosition(rooms, this.Io); }); + + socket.on('webrtc-room', (message : string) => { + let data = JSON.parse(message); + socket.join(data.roomId); + (socket as ExSocketInterface).roomId = data.roomId; + + //if two persone in room share + if(this.Io.sockets.adapter.rooms[data.roomId].length < 2) { + return; + } + let clients : Array = Object.values(this.Io.sockets.sockets); + + //send start at one client to initialise offer webrtc + clients[0].emit('webrtc-start'); + }); + + socket.on('video-offer', (message : string) => { + let data : any = JSON.parse(message); + socket.to(data.roomId).emit('video-offer', message); + }); + + socket.on('video-answer', (message : string) => { + let data : any = JSON.parse(message); + socket.to(data.roomId).emit('video-answer', message); + }); + + socket.on('ice-candidate', (message : string) => { + let data : any = JSON.parse(message); + socket.to(data.roomId).emit('ice-candidate', message); + }); }); } diff --git a/front/dist/index.html b/front/dist/index.html index 61213be6..656c903b 100644 --- a/front/dist/index.html +++ b/front/dist/index.html @@ -1,11 +1,35 @@ - - - - - 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.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..4c27c584 --- /dev/null +++ b/front/dist/resources/style/style.css @@ -0,0 +1,81 @@ +.webrtc{ + display: none; +} +.webrtc.active{ + display: block; +} +.myCam{ + display: none; +} +.myCam.active{ + display: block; +} +.webrtc, .activeCam{ + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + background: black; +} +.webrtc video{ + width: 100%; + height: 100%; +} +.myCam{ + height: 200px; + width: 300px; + position: absolute; + right: 10px; + background: black; + border: none; + bottom: 20px; + max-height: 17%; + max-width: 17%; + opacity: 1; + 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; +} \ No newline at end of file diff --git a/front/src/Phaser/Game/GameManager.ts b/front/src/Phaser/Game/GameManager.ts index d03a3152..fffca2e9 100644 --- a/front/src/Phaser/Game/GameManager.ts +++ b/front/src/Phaser/Game/GameManager.ts @@ -1,6 +1,7 @@ import {GameSceneInterface, GameScene} from "./GameScene"; import {ROOM} from "../../Enum/EnvironmentVariable" import {Connexion, ConnexionInterface, ListMessageUserPositionInterface} from "../../Connexion"; +import {WebRtcEventManager} from "../../WebRtc/WebRtcEventManager"; export enum StatusGameManagerEnum { IN_PROGRESS = 1, @@ -28,6 +29,8 @@ export class GameManager implements GameManagerInterface { return ConnexionInstance.createConnexion().then(() => { this.configureGame(); /** TODO add loader in the page **/ + //initialise cam + new WebRtcEventManager(ConnexionInstance); }).catch((err) => { console.error(err); throw err; diff --git a/front/src/WebRtc/Index.ts b/front/src/WebRtc/Index.ts new file mode 100644 index 00000000..cc636bb8 --- /dev/null +++ b/front/src/WebRtc/Index.ts @@ -0,0 +1,183 @@ +/*import {ConnexionInterface} from "../Connexion"; + +const Peer = require('simple-peer'); + +let cinemaClose : any = null; +let cinema : any = null; +let microphoneClose : any = null; +let microphone : any = null; + +let localStream : MediaStream = null; +let remoteStream : MediaStream = null; +let remoteVideo : any = null; +let myCamVideo : any = null; + +let promiseGetCam : Promise = null; + +let peer : any = null; + +let Connexion : ConnexionInterface = null; + +let roomId = "test-wertc"; + +let gettingCamera : Promise = null; +let constraintsMedia = {audio: true, video: true}; + +function joinRoom(){ + Connexion.JoinRoomWebRtc(roomId); + Connexion.startRoomWebRtc(initialiseWebSocket) +} + +function initialiseWebSocket(message : any){ + console.log('initialiseWebSocket => message', message); + peer = new Peer({ + initiator: message.initiator + }); + + peer.on('signal', (data : any) => { + //send signal + //permit to send message and initialise peer connexion + console.log('signal sended', data); + Connexion.shareSignalWebRtc({ + roomId: roomId, + signal: data + }); + }); + + //permit to receive message and initialise peer connexion + Connexion.receiveSignalWebRtc((data : any) => { + let signal = JSON.parse(data); + console.log('receiveSignalWebRtc => signal', signal); + peer.signal(signal.signal); + }); + + peer.on('stream', (stream : MediaStream) => { + // got remote video stream, now let's show it in a video tag + console.log("peer => stream", stream); + + //set local stream in little cam + myCamVideo.srcObject = localStream; + + //set remote stream in remote video + remoteStream = stream; + remoteVideo.srcObject = stream; + }); + + peer.on('connect', () => { + console.log('CONNECT') + peer.send('whatever' + Math.random()) + }); + + peer.on('data', (data : any) => { + console.log('data: ' + data) + }); + + peer.on('close', (err : any) => console.error('close', err)); + peer.on('error', (err : any) => console.error('error', err)); + + + peer.on('track', (track : any, stream : any) => { + remoteStream = stream; + remoteVideo.srcObject = stream; + track.onended = (e : any) => remoteVideo.srcObject = remoteVideo.srcObject; // Chrome/Firefox bug + }); + + gettingCamera.then(() => { + addMedia(); + }); +} + +//get camera +function getCamera() { + gettingCamera = navigator.mediaDevices.getUserMedia(constraintsMedia) + .then((stream: MediaStream) => { + localStream = stream; + remoteVideo.srcObject = stream; + }).catch((err) => { + console.error(err); + localStream = null; + throw err; + }); + return gettingCamera; +} + +function addMedia () { + if(peer) { + peer.addStream(localStream) // <- add streams to peer dynamically + } +} + +function enabledCamera(){ + cinemaClose.style.display = "none"; + cinema.style.display = "block"; + constraintsMedia.video = true; +} + +function disabledCamera(){ + cinemaClose.style.display = "block"; + cinema.style.display = "none"; + constraintsMedia.video = false; +} + +function enabledMicrophone(){ + microphoneClose.style.display = "none"; + microphone.style.display = "block"; + constraintsMedia.audio = true; +} + +function disabledMicrophone(){ + microphoneClose.style.display = "block"; + microphone.style.display = "none"; + constraintsMedia.audio = false; +} + +function showWebRtc(){ + remoteVideo = document.getElementById('activeCamVideo'); + myCamVideo = document.getElementById('myCamVideo'); + + microphoneClose = document.getElementById('microphone-close'); + microphoneClose.addEventListener('click', (e : any) => { + e.preventDefault(); + enabledMicrophone(); + //update tracking + }); + + microphone = document.getElementById('microphone'); + microphone.addEventListener('click', (e : any) => { + e.preventDefault(); + disabledMicrophone(); + //update tracking + }); + + cinemaClose = document.getElementById('cinema-close'); + cinemaClose.addEventListener('click', (e : any) => { + e.preventDefault(); + enabledCamera(); + //update tracking + }); + cinema = document.getElementById('cinema'); + cinema.addEventListener('click', (e : any) => { + e.preventDefault(); + disabledCamera(); + //update tracking + }); + + enabledMicrophone(); + enabledCamera(); + + let webRtc = document.getElementById('webRtc'); + webRtc.classList.add('active'); +} + +export const initialisation = (ConnexionInterface : ConnexionInterface) => { + Connexion = ConnexionInterface; + + //show camera + showWebRtc(); + + //open the camera + getCamera(); + + //join room to create webrtc + joinRoom(); +};*/ \ 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..bab1c7c8 --- /dev/null +++ b/front/src/WebRtc/MediaManager.ts @@ -0,0 +1,89 @@ +export class MediaManager { + localStream: MediaStream; + remoteStream: MediaStream; + remoteVideo: any; + myCamVideo: any; + cinemaClose: any = null; + cinema: any = null; + microphoneClose: any = null; + microphone: any = null; + constraintsMedia = {audio: true, video: true}; + getCameraPromise : Promise = null; + + constructor() { + this.remoteVideo = document.getElementById('activeCamVideo'); + 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.enabledMicrophone(); + this.enabledCamera(); + + let webRtc = document.getElementById('webRtc'); + webRtc.classList.add('active'); + + this.getCamera(); + } + + enabledCamera() { + this.cinemaClose.style.display = "none"; + this.cinema.style.display = "block"; + this.constraintsMedia.video = true; + } + + disabledCamera() { + this.cinemaClose.style.display = "block"; + this.cinema.style.display = "none"; + this.constraintsMedia.video = false; + } + + 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; + } + + //get camera + getCamera() { + this.getCameraPromise = navigator.mediaDevices.getUserMedia(this.constraintsMedia) + .then((stream: MediaStream) => { + this.localStream = stream; + this.myCamVideo.srcObject = this.localStream; + }).catch((err) => { + console.error(err); + this.localStream = null; + throw err; + }); + } +} \ No newline at end of file diff --git a/front/src/WebRtc/PeerConnexionManager.ts b/front/src/WebRtc/PeerConnexionManager.ts new file mode 100644 index 00000000..e6b531a9 --- /dev/null +++ b/front/src/WebRtc/PeerConnexionManager.ts @@ -0,0 +1,136 @@ +import {WebRtcEventManager} from "./WebRtcEventManager"; +import {MediaManager} from "./MediaManager"; +const offerOptions = { + offerToReceiveAudio: 1, + offerToReceiveVideo: 1, + iceServers: [{url:'stun:stun.l.google.com:19302'}], +}; + +export class PeerConnexionManager { + + WebRtcEventManager: WebRtcEventManager; + MediaManager : MediaManager; + + peerConnection: RTCPeerConnection; + + constructor(WebRtcEventManager : WebRtcEventManager) { + this.WebRtcEventManager = WebRtcEventManager; + this.MediaManager = new MediaManager(); + } + + createPeerConnection(data: any = null): Promise { + this.peerConnection = new RTCPeerConnection(); + + //init all events peer connection + this.createEventPeerConnection(); + + this.MediaManager.getCameraPromise.then(() => { + this.MediaManager.localStream.getTracks().forEach( + (track : MediaStreamTrack) => this.peerConnection.addTrack(track, this.MediaManager.localStream) + ); + }); + + //if no data, create offer + if (!data || !data.message) { + return this.createOffer(); + } + + let description = new RTCSessionDescription(data.message); + return this.peerConnection.setRemoteDescription(description).catch((err) => { + console.error("createPeerConnection => setRemoteDescription", err); + throw err; + }) + } + + createOffer(): Promise { + console.log('pc1 createOffer start'); + // @ts-ignore + return this.peerConnection.createOffer(offerOptions).then((offer: RTCSessionDescriptionInit) => { + this.peerConnection.setLocalDescription(offer).then(() => { + let message = {message: this.peerConnection.localDescription}; + this.WebRtcEventManager.emitVideoOffer(message); + }).catch((err) => { + console.error("createOffer => setLocalDescription", err); + throw err; + }); + }).catch((err: Error) => { + console.error("createOffer => createOffer", err); + throw err; + }); + } + + createAnswer(): Promise { + return this.peerConnection.createAnswer().then((answer : RTCSessionDescriptionInit) => { + this.peerConnection.setLocalDescription(answer).then(() => { + //push video-answer + let messageSend = {message: this.peerConnection.localDescription}; + this.WebRtcEventManager.emitVideoAnswer(messageSend); + console.info("video-answer => send", messageSend); + }).catch((err) => { + console.error("eventVideoOffer => createAnswer => setLocalDescription", err); + throw err; + }) + }).catch((err) => { + console.error("eventVideoOffer => createAnswer", err); + throw err; + }) + } + + setRemoteDescription(data: any): Promise { + let description = new RTCSessionDescription(data.message); + return this.peerConnection.setRemoteDescription(description).catch((err) => { + console.error("PeerConnexionManager => setRemoteDescription", err); + throw err; + }) + } + + addIceCandidate(data: any): Promise { + return this.peerConnection.addIceCandidate(data.message) + .catch((err) => { + console.error("PeerConnexionManager => addIceCandidate", err); + throw err; + }) + } + + hangup() { + console.log('Ending call'); + if (this.peerConnection) { + this.peerConnection.close(); + } + this.peerConnection = null; + } + + createEventPeerConnection(){ + //define creator of offer + this.peerConnection.onicecandidate = (event: RTCPeerConnectionIceEvent) => { + let message = {message: event.candidate}; + if (!event.candidate) { + return; + } + this.WebRtcEventManager.emitIceCandidate(message); + }; + + this.peerConnection.ontrack = (e:RTCTrackEvent) => { + console.info('Event:track', e); + this.MediaManager.remoteVideo.srcObject = e.streams[0]; + this.MediaManager.myCamVideo.srcObject = e.streams[0]; + }; + + this.peerConnection.onnegotiationneeded = (e : Event) => { + console.info("Event:negotiationneeded => call()", e); + this.createOffer() + }; + this.peerConnection.oniceconnectionstatechange = (e) => { + console.info('ICE state change event: ', e); + }; + this.peerConnection.oniceconnectionstatechange = (e:Event) => { + console.info('oniceconnectionstatechange => iceConnectionState', this.peerConnection.iceConnectionState); + }; + this.peerConnection.onicegatheringstatechange = () => { + console.info('onicegatheringstatechange => iceConnectionState', this.peerConnection.iceConnectionState); + }; + this.peerConnection.onsignalingstatechange = () => { + console.info('onsignalingstatechange => iceConnectionState', this.peerConnection.iceConnectionState); + }; + } +} \ No newline at end of file diff --git a/front/src/WebRtc/WebRtcEventManager.ts b/front/src/WebRtc/WebRtcEventManager.ts new file mode 100644 index 00000000..19c46701 --- /dev/null +++ b/front/src/WebRtc/WebRtcEventManager.ts @@ -0,0 +1,86 @@ +import {ConnexionInterface} from "../Connexion"; +import {PeerConnexionManager} from "./PeerConnexionManager"; + +export class WebRtcEventManager { + Connexion: ConnexionInterface; + PeerConnexionManager: PeerConnexionManager; + RoomId : string; + + constructor(Connexion : ConnexionInterface, roomId : string = "test-webrtc") { + this.RoomId = roomId; + this.Connexion = Connexion; + this.PeerConnexionManager = new PeerConnexionManager(this); + + this.start(); + this.eventVideoOffer(); + this.eventVideoAnswer(); + this.eventIceCandidate(); + + //connect on the room to create a meet + Connexion.socket.emit('webrtc-room', JSON.stringify({roomId: roomId})); + } + + /** + * server has two person connected, start the meet + */ + start(){ + this.Connexion.socket.on('webrtc-start', () => { + return this.PeerConnexionManager.createPeerConnection(); + }); + } + + /** + * Receive video offer + */ + eventVideoOffer() { + this.Connexion.socket.on("video-offer", (message : any) => { + let data = JSON.parse(message); + console.info("video-offer", data); + this.PeerConnexionManager.createPeerConnection(data).then(() => { + return this.PeerConnexionManager.createAnswer(); + }); + }); + } + + /** + * Receive video answer + */ + eventVideoAnswer() { + this.Connexion.socket.on("video-answer", (message : any) => { + let data = JSON.parse(message); + console.info("video-answer", data); + this.PeerConnexionManager.setRemoteDescription(data) + .catch((err) => { + console.error("video-answer => setRemoteDescription", err) + }) + }); + } + + /** + * Receive ice candidate + */ + eventIceCandidate() { + this.Connexion.socket.on("ice-candidate", (message : any) => { + let data = JSON.parse(message); + console.info("ice-candidate", data); + this.PeerConnexionManager.addIceCandidate(data).then(() => { + console.log(`ICE candidate:\n${data.message ? data.message.candidate : '(null)'}`); + }); + }); + } + + emitIceCandidate(message : any){ + message.roomId = this.RoomId; + this.Connexion.socket.emit('ice-candidate', JSON.stringify(message)); + } + + emitVideoOffer(message : any){ + message.roomId = this.RoomId; + this.Connexion.socket.emit('video-offer', JSON.stringify(message)); + } + + emitVideoAnswer(message : any){ + message.roomId = this.RoomId; + this.Connexion.socket.emit("video-answer", JSON.stringify(message)); + } +} \ No newline at end of file