diff --git a/back/src/Controller/IoSocketController.ts b/back/src/Controller/IoSocketController.ts index ff844358..57adf92d 100644 --- a/back/src/Controller/IoSocketController.ts +++ b/back/src/Controller/IoSocketController.ts @@ -7,6 +7,7 @@ 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 {ExtWebSocket} from "../../../../publicis/sources/api/src/Entities/WebSocket/ExtWebSocket"; export class IoSocketController{ Io: socketIO.Server; @@ -84,28 +85,25 @@ export class IoSocketController{ (socket as ExSocketInterface).roomId = data.roomId; //if two persone in room share + console.log("nb user => " + data.roomId, this.Io.sockets.adapter.rooms[data.roomId].length); 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'); + clients.forEach((client: ExtWebSocket, index : number) => { + client.emit('webrtc-start', JSON.stringify({ + userId: client.userId, + initiator : index === 0 + })); + }); }); - socket.on('video-offer', (message : string) => { + socket.on('webrtc-signal', (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); + console.info('webrtc-signal', message); + socket.to(data.roomId).emit('webrtc-signal', message); }); }); } 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 0d9f998a..b6f2db72 100644 --- a/front/src/Connexion.ts +++ b/front/src/Connexion.ts @@ -4,6 +4,15 @@ const SocketIo = require('socket.io-client'); import Axios from "axios"; import {API_URL} from "./Enum/EnvironmentVariable"; +enum EventMessage{ + WEBRTC_SIGNAL = "webrtc-signal", + WEBRTC_START = "webrtc-start", + WEBRTC_ROOM = "webrtc-room", + JOIN_ROOM = "join-room", + USER_POSITION = "user-position", + MESSAGE_ERROR = "message-error" +} + class Message { userId: string; roomId: string; @@ -56,6 +65,7 @@ export interface MessageUserPositionInterface { roomId: string; position: PointInterface; } + class MessageUserPosition extends Message implements MessageUserPositionInterface{ position: PointInterface; @@ -76,14 +86,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,32 +110,47 @@ 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(roomId : string, x : number, y : number, direction : string) : void; - positionOfAllUser() : void; + startedRoom: string; + + createConnexion(): Promise; + + joinARoom(roomId: string): void; + + sharePosition(roomId: string, x: number, y: number, direction: string): void; + + positionOfAllUser(): void; + + /*webrtc*/ + sendWebrtcRomm(roomId: string): void; + + sendWebrtcSignal(signal: any, roomId: 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: GameManagerInterface; - constructor(email : string, GameManager: GameManagerInterface) { + constructor(email: string, GameManager: GameManagerInterface) { this.email = email; 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 +185,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()); } /** @@ -171,12 +197,12 @@ export class Connexion implements ConnexionInterface{ * @param y * @param direction */ - sharePosition(roomId : string, x : number, y : number, direction : string = "none") : void{ - if(!this.socket){ + sharePosition(roomId: string, x: number, y: number, direction: string = "none"): void { + if (!this.socket) { return; } let messageUserPosition = new MessageUserPosition(this.userId, roomId, new Point(x, y, direction)); - this.socket.emit('user-position', messageUserPosition.toString()); + this.socket.emit(EventMessage.USER_POSITION, messageUserPosition.toString()); } /** @@ -194,8 +220,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]); @@ -204,9 +230,29 @@ export class Connexion implements ConnexionInterface{ }); } - errorMessage() : void { - this.socket.on('message-error', (message : string) => { - console.error("message-error", message); + sendWebrtcSignal(signal: any, roomId: string) { + this.socket.emit(EventMessage.WEBRTC_SIGNAL, JSON.stringify({ + userId: this.userId, + roomId: roomId, + signal: signal + })); + } + + sendWebrtcRomm(roomId: string) { + this.socket.emit(EventMessage.WEBRTC_ROOM, JSON.stringify({roomId: roomId})); + } + + 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 fffca2e9..fbc1d5bd 100644 --- a/front/src/Phaser/Game/GameManager.ts +++ b/front/src/Phaser/Game/GameManager.ts @@ -1,7 +1,7 @@ import {GameSceneInterface, GameScene} from "./GameScene"; import {ROOM} from "../../Enum/EnvironmentVariable" import {Connexion, ConnexionInterface, ListMessageUserPositionInterface} from "../../Connexion"; -import {WebRtcEventManager} from "../../WebRtc/WebRtcEventManager"; +import {SimplePeer} from "../../WebRtc/SimplePeer"; export enum StatusGameManagerEnum { IN_PROGRESS = 1, @@ -30,7 +30,7 @@ export class GameManager implements GameManagerInterface { this.configureGame(); /** TODO add loader in the page **/ //initialise cam - new WebRtcEventManager(ConnexionInstance); + new SimplePeer(ConnexionInstance); }).catch((err) => { console.error(err); throw err; diff --git a/front/src/WebRtc/MediaManager.ts b/front/src/WebRtc/MediaManager.ts index 605be965..e8666066 100644 --- a/front/src/WebRtc/MediaManager.ts +++ b/front/src/WebRtc/MediaManager.ts @@ -47,7 +47,7 @@ export class MediaManager { let webRtc = document.getElementById('webRtc'); webRtc.classList.add('active'); - this.getCamera(); + //this.getCamera(); } enabledCamera() { @@ -56,7 +56,7 @@ export class MediaManager { this.constraintsMedia.video = true; this.localStream = null; this.myCamVideo.srcObject = null; - this.getCamera(); + //this.getCamera(); } disabledCamera() { @@ -74,14 +74,14 @@ export class MediaManager { } this.localStream = null; this.myCamVideo.srcObject = null; - this.getCamera(); + //this.getCamera(); } enabledMicrophone() { this.microphoneClose.style.display = "none"; this.microphone.style.display = "block"; this.constraintsMedia.audio = true; - this.getCamera(); + //this.getCamera(); } disabledMicrophone() { @@ -95,17 +95,17 @@ export class MediaManager { } }); } - this.getCamera(); + //this.getCamera(); } //get camera getCamera() { - this.getCameraPromise = navigator.mediaDevices.getUserMedia(this.constraintsMedia) + return this.getCameraPromise = navigator.mediaDevices.getUserMedia(this.constraintsMedia) .then((stream: MediaStream) => { - console.log("constraintsMedia", stream); this.localStream = stream; this.myCamVideo.srcObject = this.localStream; this.myCamVideo.play(); + return stream; }).catch((err) => { console.error(err); this.localStream = null; diff --git a/front/src/WebRtc/PeerConnexionManager.ts b/front/src/WebRtc/PeerConnexionManager.ts deleted file mode 100644 index f2b4b284..00000000 --- a/front/src/WebRtc/PeerConnexionManager.ts +++ /dev/null @@ -1,137 +0,0 @@ -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 { - return this.MediaManager.getCameraPromise.then(() => { - this.peerConnection = new RTCPeerConnection(); - - //init all events peer connection - this.createEventPeerConnection(); - - 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.addEventListener('icecandidate', ({candidate}) => { - let message = {message: candidate}; - if (!candidate) { - return; - } - this.WebRtcEventManager.emitIceCandidate(message); - }); - - this.peerConnection.addEventListener('iceconnectionstatechange', (e : Event) => { - console.info('oniceconnectionstatechange => iceConnectionState', this.peerConnection.iceConnectionState); - }); - - this.peerConnection.addEventListener('negotiationneeded', (e : Event) => { - console.info("Event:negotiationneeded => call()", e); - this.createOffer() - }); - - this.peerConnection.addEventListener("track", (e:RTCTrackEvent) => { - console.info('Event:track', e); - if (this.MediaManager.remoteVideo.srcObject !== e.streams[0]) { - this.MediaManager.remoteVideo.srcObject = e.streams[0]; - console.log('pc1 received remote stream'); - } - }); - - 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/SimplePeer.ts b/front/src/WebRtc/SimplePeer.ts new file mode 100644 index 00000000..9d199ccc --- /dev/null +++ b/front/src/WebRtc/SimplePeer.ts @@ -0,0 +1,106 @@ +import {ConnexionInterface} from "../Connexion"; +import {MediaManager} from "./MediaManager"; +let Peer = require('simple-peer'); + +export class SimplePeer { + Connexion: ConnexionInterface; + MediaManager: MediaManager; + RoomId: string; + + PeerConnexion: any; + + constructor(Connexion: ConnexionInterface, roomId: string = "test-webrtc") { + this.Connexion = Connexion; + this.MediaManager = new MediaManager(); + this.RoomId = roomId; + this.initialise(); + } + + /** + * server has two person connected, start the meet + */ + initialise() { + return this.MediaManager.getCamera().then(() => { + //send message to join a room + this.Connexion.sendWebrtcRomm(this.RoomId); + + //receive message start + this.Connexion.receiveWebrtcStart((message : string) => { + this.receiveWebrtcStart(message); + }); + + //receive signal by gemer + this.Connexion.receiveWebrtcSignal((message : string) => { + this.receiveWebrtcSignal(message); + }); + + }).catch((err) => { + console.error(err); + }); + } + + /** + * + */ + receiveWebrtcStart(message: string) { + let data = JSON.parse(message); + + //create pear connexion of user stared + this.createPeerConnexion(data.initiator); + } + + /** + * + * @param userId + * @param initiator + */ + createPeerConnexion(initiator : boolean = false){ + this.PeerConnexion = new Peer({initiator: initiator}); + this.addMedia(); + + this.PeerConnexion.on('signal', (data: any) => { + this.sendWebrtcSignal(data); + }); + + this.PeerConnexion.on('stream', (stream: MediaStream) => { + this.stream(stream) + }); + } + + /** + * permit to send signal + * @param data + */ + sendWebrtcSignal(data: any) { + this.Connexion.sendWebrtcSignal(data, this.RoomId); + } + + /** + * + * @param message + */ + receiveWebrtcSignal(message: string) { + let data = JSON.parse(message); + if(!this.PeerConnexion){ + return; + } + this.PeerConnexion.signal(data.signal); + } + + /** + * permit stream video + * @param stream + */ + stream(stream: MediaStream) { + this.MediaManager.remoteStream = stream; + this.MediaManager.remoteVideo.srcObject = this.MediaManager.remoteStream; + } + + /** + * Permit to update stream + * @param stream + */ + addMedia () { + this.PeerConnexion.addStream(this.MediaManager.localStream) // <- add streams to peer dynamically + } +} \ No newline at end of file diff --git a/front/src/WebRtc/WebRtcEventManager.ts b/front/src/WebRtc/WebRtcEventManager.ts deleted file mode 100644 index 28c6acfc..00000000 --- a/front/src/WebRtc/WebRtcEventManager.ts +++ /dev/null @@ -1,92 +0,0 @@ -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(); - - //start to connect on event - //TODO test - this.emitWebRtcRoom(); - } - - /** - * 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)'}`); - }); - }); - } - - emitWebRtcRoom(){ - //connect on the room to create a meet - this.Connexion.socket.emit('webrtc-room', JSON.stringify({roomId: this.RoomId})); - } - - 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 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"