Merge pull request #239 from thecodingmachine/screenshare2

Fictive user screen sharing 2
This commit is contained in:
David Négrier 2020-08-21 23:16:28 +02:00 committed by GitHub
commit 87dd889e25
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 800 additions and 195 deletions

View File

@ -13,7 +13,6 @@ import {MessageUserJoined} from "../Model/Websocket/MessageUserJoined";
import {MessageUserMoved} from "../Model/Websocket/MessageUserMoved"; import {MessageUserMoved} from "../Model/Websocket/MessageUserMoved";
import si from "systeminformation"; import si from "systeminformation";
import {Gauge} from "prom-client"; import {Gauge} from "prom-client";
import os from 'os';
import {TokenInterface} from "../Controller/AuthenticateController"; import {TokenInterface} from "../Controller/AuthenticateController";
import {isJoinRoomMessageInterface} from "../Model/Websocket/JoinRoomMessage"; import {isJoinRoomMessageInterface} from "../Model/Websocket/JoinRoomMessage";
import {isPointInterface, PointInterface} from "../Model/Websocket/PointInterface"; import {isPointInterface, PointInterface} from "../Model/Websocket/PointInterface";
@ -28,6 +27,7 @@ enum SockerIoEvent {
USER_MOVED = "user-moved", // From server to client USER_MOVED = "user-moved", // From server to client
USER_LEFT = "user-left", // From server to client USER_LEFT = "user-left", // From server to client
WEBRTC_SIGNAL = "webrtc-signal", WEBRTC_SIGNAL = "webrtc-signal",
WEBRTC_SCREEN_SHARING_SIGNAL = "webrtc-screen-sharing-signal",
WEBRTC_START = "webrtc-start", WEBRTC_START = "webrtc-start",
WEBRTC_DISCONNECT = "webrtc-disconect", WEBRTC_DISCONNECT = "webrtc-disconect",
MESSAGE_ERROR = "message-error", MESSAGE_ERROR = "message-error",
@ -226,18 +226,11 @@ export class IoSocketController {
}); });
socket.on(SockerIoEvent.WEBRTC_SIGNAL, (data: unknown) => { socket.on(SockerIoEvent.WEBRTC_SIGNAL, (data: unknown) => {
if (!isWebRtcSignalMessageInterface(data)) { this.emitVideo((socket as ExSocketInterface), data);
socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: 'Invalid WEBRTC_SIGNAL message.'}); });
console.warn('Invalid WEBRTC_SIGNAL message received: ', data);
return; socket.on(SockerIoEvent.WEBRTC_SCREEN_SHARING_SIGNAL, (data: unknown) => {
} this.emitScreenSharing((socket as ExSocketInterface), data);
//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);
}); });
socket.on(SockerIoEvent.DISCONNECT, () => { socket.on(SockerIoEvent.DISCONNECT, () => {
@ -284,6 +277,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 { searchClientByIdOrFail(userId: string): ExSocketInterface {
const client: ExSocketInterface|undefined = this.sockets.get(userId); const client: ExSocketInterface|undefined = this.sockets.get(userId);
if (client === undefined) { if (client === undefined) {
@ -364,13 +393,15 @@ export class IoSocketController {
if (this.Io.sockets.adapter.rooms[roomId].length < 2 /*|| this.Io.sockets.adapter.rooms[roomId].length >= 4*/) { if (this.Io.sockets.adapter.rooms[roomId].length < 2 /*|| this.Io.sockets.adapter.rooms[roomId].length >= 4*/) {
return; return;
} }
// TODO: scanning all sockets is maybe not the most efficient
const clients: Array<ExSocketInterface> = (Object.values(this.Io.sockets.sockets) as Array<ExSocketInterface>) const clients: Array<ExSocketInterface> = (Object.values(this.Io.sockets.sockets) as Array<ExSocketInterface>)
.filter((client: ExSocketInterface) => client.webRtcRoomId && client.webRtcRoomId === roomId); .filter((client: ExSocketInterface) => client.webRtcRoomId && client.webRtcRoomId === roomId);
//send start at one client to initialise offer webrtc //send start at one client to initialise offer webrtc
//send all users in room to create PeerConnection in front //send all users in room to create PeerConnection in front
clients.forEach((client: ExSocketInterface, index: number) => { 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) { if (!clientId.userId || clientId.userId === client.userId) {
return tabs; return tabs;
} }
@ -382,7 +413,7 @@ export class IoSocketController {
return tabs; 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"; import * as tg from "generic-type-guard";
export const isSignalData =
new tg.IsInterface().withProperties({
type: tg.isOptional(tg.isString)
}).get();
export const isWebRtcSignalMessageInterface = export const isWebRtcSignalMessageInterface =
new tg.IsInterface().withProperties({ new tg.IsInterface().withProperties({
userId: tg.isString,
receiverId: tg.isString, receiverId: tg.isString,
roomId: tg.isString, signal: isSignalData
signal: tg.isUnknown }).get();
export const isWebRtcScreenSharingStartMessageInterface =
new tg.IsInterface().withProperties({
userId: tg.isString,
roomId: tg.isString
}).get(); }).get();
export type WebRtcSignalMessageInterface = tg.GuardedType<typeof isWebRtcSignalMessageInterface>; export type WebRtcSignalMessageInterface = tg.GuardedType<typeof isWebRtcSignalMessageInterface>;

10
front/dist/index.html vendored
View File

@ -68,6 +68,7 @@
<div id="div-myCamVideo" class="video-container"> <div id="div-myCamVideo" class="video-container">
<video id="myCamVideo" autoplay muted></video> <video id="myCamVideo" autoplay muted></video>
</div> </div>
</div>
<div class="btn-cam-action"> <div class="btn-cam-action">
<div class="btn-micro"> <div class="btn-micro">
<img id="microphone" src="resources/logos/microphone.svg"> <img id="microphone" src="resources/logos/microphone.svg">
@ -77,6 +78,9 @@
<img id="cinema" src="resources/logos/cinema.svg"> <img id="cinema" src="resources/logos/cinema.svg">
<img id="cinema-close" src="resources/logos/cinema-close.svg"> <img id="cinema-close" src="resources/logos/cinema-close.svg">
</div> </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>
@ -100,9 +104,15 @@
<img id="cinema" src="resources/logos/cinema.svg"> <img id="cinema" src="resources/logos/cinema.svg">
<img id="cinema-close" src="resources/logos/cinema-close.svg"> <img id="cinema-close" src="resources/logos/cinema-close.svg">
</div> </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> </div>
--> -->
<div id="activeScreenSharing" class="active-screen-sharing active">
</div>
<div id="webRtcSetup" class="webrtcsetup"> <div id="webRtcSetup" class="webrtcsetup">
<img id="webRtcSetupNoVideo" class="background-img" src="resources/logos/cinema-close.svg"> <img id="webRtcSetupNoVideo" class="background-img" src="resources/logos/cinema-close.svg">
<video id="myCamVideoSetup" autoplay muted></video> <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 animation*/
.btn-cam-action div{ .btn-cam-action div{
cursor: pointer; cursor: pointer;
@ -93,7 +100,7 @@ video#myCamVideo{
transition-timing-function: ease-in-out; transition-timing-function: ease-in-out;
bottom: 20px; bottom: 20px;
} }
#activeCam:hover .btn-cam-action div{ .btn-cam-action:hover div{
transform: translateY(0); transform: translateY(0);
} }
.btn-cam-action div:hover{ .btn-cam-action div:hover{
@ -106,9 +113,13 @@ video#myCamVideo{
right: 44px; right: 44px;
} }
.btn-video{ .btn-video{
transition: all .2s; transition: all .25s;
right: 134px; right: 134px;
} }
.btn-monitor{
transition: all .2s;
right: 224px;
}
/*.btn-call{ /*.btn-call{
transition: all .1s; transition: all .1s;
left: 0px; left: 0px;

View File

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

View File

@ -18,7 +18,7 @@ import {PlayerMovement} from "./PlayerMovement";
import {PlayersPositionInterpolator} from "./PlayersPositionInterpolator"; import {PlayersPositionInterpolator} from "./PlayersPositionInterpolator";
import {RemotePlayer} from "../Entity/RemotePlayer"; import {RemotePlayer} from "../Entity/RemotePlayer";
import {Queue} from 'queue-typescript'; import {Queue} from 'queue-typescript';
import {SimplePeer, UserSimplePeer} from "../../WebRtc/SimplePeer"; import {SimplePeer, UserSimplePeerInterface} from "../../WebRtc/SimplePeer";
import {ReconnectingSceneName} from "../Reconnecting/ReconnectingScene"; import {ReconnectingSceneName} from "../Reconnecting/ReconnectingScene";
import {FourOFourSceneName} from "../Reconnecting/FourOFourScene"; import {FourOFourSceneName} from "../Reconnecting/FourOFourScene";
import {loadAllLayers} from "../Entity/body_character"; import {loadAllLayers} from "../Entity/body_character";
@ -229,7 +229,7 @@ export class GameScene extends Phaser.Scene {
this.simplePeer = new SimplePeer(this.connection); this.simplePeer = new SimplePeer(this.connection);
const self = this; const self = this;
this.simplePeer.registerPeerConnectionListener({ this.simplePeer.registerPeerConnectionListener({
onConnect(user: UserSimplePeer) { onConnect(user: UserSimplePeerInterface) {
self.presentationModeSprite.setVisible(true); self.presentationModeSprite.setVisible(true);
self.chatModeSprite.setVisible(true); self.chatModeSprite.setVisible(true);
}, },

View File

@ -1,4 +1,5 @@
import {DivImportance, layoutManager} from "./LayoutManager"; import {DivImportance, layoutManager} from "./LayoutManager";
import {HtmlUtils} from "./HtmlUtils";
const videoConstraint: boolean|MediaTrackConstraints = { const videoConstraint: boolean|MediaTrackConstraints = {
width: { ideal: 1280 }, width: { ideal: 1280 },
@ -7,15 +8,20 @@ const videoConstraint: boolean|MediaTrackConstraints = {
}; };
type UpdatedLocalStreamCallback = (media: MediaStream) => void; 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: 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!!!!) // TODO: verify that microphone event listeners are not triggered plenty of time NOW (since MediaManager is created many times!!!!)
export class MediaManager { export class MediaManager {
localStream: MediaStream|null = null; localStream: MediaStream|null = null;
localScreenCapture: MediaStream|null = null;
private remoteVideo: Map<string, HTMLVideoElement> = new Map<string, HTMLVideoElement>(); private remoteVideo: Map<string, HTMLVideoElement> = new Map<string, HTMLVideoElement>();
myCamVideo: HTMLVideoElement; myCamVideo: HTMLVideoElement;
cinemaClose: HTMLImageElement; cinemaClose: HTMLImageElement;
cinema: HTMLImageElement; cinema: HTMLImageElement;
monitorClose: HTMLImageElement;
monitor: HTMLImageElement;
microphoneClose: HTMLImageElement; microphoneClose: HTMLImageElement;
microphone: HTMLImageElement; microphone: HTMLImageElement;
webrtcInAudio: HTMLAudioElement; webrtcInAudio: HTMLAudioElement;
@ -24,8 +30,12 @@ export class MediaManager {
video: videoConstraint video: videoConstraint
}; };
updatedLocalStreamCallBacks : Set<UpdatedLocalStreamCallback> = new Set<UpdatedLocalStreamCallback>(); updatedLocalStreamCallBacks : Set<UpdatedLocalStreamCallback> = new Set<UpdatedLocalStreamCallback>();
startScreenSharingCallBacks : Set<StartScreenSharingCallback> = new Set<StartScreenSharingCallback>();
stopScreenSharingCallBacks : Set<StopScreenSharingCallback> = new Set<StopScreenSharingCallback>();
constructor() { constructor() {
this.myCamVideo = this.getElementByIdOrFail<HTMLVideoElement>('myCamVideo'); this.myCamVideo = this.getElementByIdOrFail<HTMLVideoElement>('myCamVideo');
this.webrtcInAudio = this.getElementByIdOrFail<HTMLAudioElement>('audio-webrtc-in'); this.webrtcInAudio = this.getElementByIdOrFail<HTMLAudioElement>('audio-webrtc-in');
this.webrtcInAudio.volume = 0.2; this.webrtcInAudio.volume = 0.2;
@ -34,13 +44,13 @@ export class MediaManager {
this.microphoneClose.style.display = "none"; this.microphoneClose.style.display = "none";
this.microphoneClose.addEventListener('click', (e: MouseEvent) => { this.microphoneClose.addEventListener('click', (e: MouseEvent) => {
e.preventDefault(); e.preventDefault();
this.enabledMicrophone(); this.enableMicrophone();
//update tracking //update tracking
}); });
this.microphone = this.getElementByIdOrFail<HTMLImageElement>('microphone'); this.microphone = this.getElementByIdOrFail<HTMLImageElement>('microphone');
this.microphone.addEventListener('click', (e: MouseEvent) => { this.microphone.addEventListener('click', (e: MouseEvent) => {
e.preventDefault(); e.preventDefault();
this.disabledMicrophone(); this.disableMicrophone();
//update tracking //update tracking
}); });
@ -48,22 +58,47 @@ export class MediaManager {
this.cinemaClose.style.display = "none"; this.cinemaClose.style.display = "none";
this.cinemaClose.addEventListener('click', (e: MouseEvent) => { this.cinemaClose.addEventListener('click', (e: MouseEvent) => {
e.preventDefault(); e.preventDefault();
this.enabledCamera(); this.enableCamera();
//update tracking //update tracking
}); });
this.cinema = this.getElementByIdOrFail<HTMLImageElement>('cinema'); this.cinema = this.getElementByIdOrFail<HTMLImageElement>('cinema');
this.cinema.addEventListener('click', (e: MouseEvent) => { this.cinema.addEventListener('click', (e: MouseEvent) => {
e.preventDefault(); e.preventDefault();
this.disabledCamera(); this.disableCamera();
//update tracking
});
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 //update tracking
}); });
} }
onUpdateLocalStream(callback: UpdatedLocalStreamCallback): void { public onUpdateLocalStream(callback: UpdatedLocalStreamCallback): void {
this.updatedLocalStreamCallBacks.add(callback); 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 { removeUpdateLocalStreamEventListener(callback: UpdatedLocalStreamCallback): void {
this.updatedLocalStreamCallBacks.delete(callback); this.updatedLocalStreamCallBacks.delete(callback);
} }
@ -74,12 +109,24 @@ 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'); const gameOverlay = this.getElementByIdOrFail('game-overlay');
gameOverlay.classList.add('active'); gameOverlay.classList.add('active');
} }
enabledCamera() { private enableCamera() {
this.cinemaClose.style.display = "none"; this.cinemaClose.style.display = "none";
this.cinema.style.display = "block"; this.cinema.style.display = "block";
this.constraintsMedia.video = videoConstraint; this.constraintsMedia.video = videoConstraint;
@ -88,7 +135,7 @@ export class MediaManager {
}); });
} }
disabledCamera() { private disableCamera() {
this.cinemaClose.style.display = "block"; this.cinemaClose.style.display = "block";
this.cinema.style.display = "none"; this.cinema.style.display = "none";
this.constraintsMedia.video = false; this.constraintsMedia.video = false;
@ -103,7 +150,7 @@ export class MediaManager {
}); });
} }
enabledMicrophone() { private enableMicrophone() {
this.microphoneClose.style.display = "none"; this.microphoneClose.style.display = "none";
this.microphone.style.display = "block"; this.microphone.style.display = "block";
this.constraintsMedia.audio = true; this.constraintsMedia.audio = true;
@ -112,7 +159,7 @@ export class MediaManager {
}); });
} }
disabledMicrophone() { private disableMicrophone() {
this.microphoneClose.style.display = "block"; this.microphoneClose.style.display = "block";
this.microphone.style.display = "none"; this.microphone.style.display = "none";
this.constraintsMedia.audio = false; this.constraintsMedia.audio = false;
@ -126,6 +173,78 @@ export class MediaManager {
}); });
} }
private enableScreenSharing() {
this.monitorClose.style.display = "none";
this.monitor.style.display = "block";
this.getScreenMedia().then((stream) => {
this.triggerStartedScreenSharingCallbacks(stream);
});
}
private disableScreenSharing() {
this.monitorClose.style.display = "block";
this.monitor.style.display = "none";
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 //get camera
async getCamera(): Promise<MediaStream> { async getCamera(): Promise<MediaStream> {
if (navigator.mediaDevices === undefined) { if (navigator.mediaDevices === undefined) {
@ -205,6 +324,25 @@ export class MediaManager {
this.remoteVideo.set(userId, this.getElementByIdOrFail<HTMLVideoElement>(userId)); 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 * @param userId
@ -272,6 +410,15 @@ export class MediaManager {
} }
remoteVideo.srcObject = stream; 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 +428,9 @@ export class MediaManager {
layoutManager.remove(userId); layoutManager.remove(userId);
this.remoteVideo.delete(userId); this.remoteVideo.delete(userId);
} }
removeActiveScreenSharingVideo(userId : string) {
this.removeActiveVideo(`screen-sharing-${userId}`)
}
isConnecting(userId : string): void { isConnecting(userId : string): void {
const connectingSpinnerDiv = this.getSpinner(userId); const connectingSpinnerDiv = this.getSpinner(userId);
@ -299,6 +449,7 @@ export class MediaManager {
} }
isError(userId : string): void { isError(userId : string): void {
console.log("isError", `div-${userId}`);
const element = document.getElementById(`div-${userId}`); const element = document.getElementById(`div-${userId}`);
if(!element){ if(!element){
return; return;
@ -309,6 +460,10 @@ export class MediaManager {
} }
errorDiv.style.display = 'block'; errorDiv.style.display = 'block';
} }
isErrorScreenSharing(userId : string): void {
this.isError(`screen-sharing-${userId}`);
}
private getSpinner(userId : string): HTMLDivElement|null { private getSpinner(userId : string): HTMLDivElement|null {
const element = document.getElementById(`div-${userId}`); 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 { import {
Connection, Connection,
WebRtcDisconnectMessageInterface, WebRtcDisconnectMessageInterface,
WebRtcSignalMessageInterface, WebRtcSignalReceivedMessageInterface,
WebRtcStartMessageInterface WebRtcStartMessageInterface
} from "../Connection"; } from "../Connection";
import { mediaManager } from "./MediaManager"; import { mediaManager } from "./MediaManager";
import * as SimplePeerNamespace from "simple-peer"; import * as SimplePeerNamespace from "simple-peer";
import {ScreenSharingPeer} from "./ScreenSharingPeer";
import {VideoPeer} from "./VideoPeer";
const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer'); const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer');
export interface UserSimplePeer{ export interface UserSimplePeerInterface{
userId: string; userId: string;
name?: string; name?: string;
initiator?: boolean; initiator?: boolean;
} }
export interface PeerConnectionListener { export interface PeerConnectionListener {
onConnect(user: UserSimplePeer): void; onConnect(user: UserSimplePeerInterface): void;
onDisconnect(userId: string): void; onDisconnect(userId: string): void;
} }
@ -26,18 +28,25 @@ export interface PeerConnectionListener {
export class SimplePeer { export class SimplePeer {
private Connection: Connection; private Connection: Connection;
private WebRtcRoomId: string; 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 PeerScreenSharingConnectionArray: Map<string, ScreenSharingPeer> = new Map<string, ScreenSharingPeer>();
private readonly updateLocalStreamCallback: (media: MediaStream) => void; 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>(); private readonly peerConnectionListeners: Array<PeerConnectionListener> = new Array<PeerConnectionListener>();
constructor(Connection: Connection, WebRtcRoomId: string = "test-webrtc") { constructor(Connection: Connection, WebRtcRoomId: string = "test-webrtc") {
this.Connection = Connection; this.Connection = Connection;
this.WebRtcRoomId = WebRtcRoomId; this.WebRtcRoomId = WebRtcRoomId;
// We need to go through this weird bound function pointer in order to be able to "free" this reference later. // 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); this.sendLocalVideoStreamCallback = this.sendLocalVideoStream.bind(this);
mediaManager.onUpdateLocalStream(this.updateLocalStreamCallback); 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(); this.initialise();
} }
@ -55,11 +64,16 @@ export class SimplePeer {
private initialise() { private initialise() {
//receive signal by gemer //receive signal by gemer
this.Connection.receiveWebrtcSignal((message: WebRtcSignalMessageInterface) => { this.Connection.receiveWebrtcSignal((message: WebRtcSignalReceivedMessageInterface) => {
this.receiveWebrtcSignal(message); this.receiveWebrtcSignal(message);
}); });
mediaManager.activeVisio(); //receive signal by gemer
this.Connection.receiveWebrtcScreenSharingSignal((message: WebRtcSignalReceivedMessageInterface) => {
this.receiveWebrtcScreenSharingSignal(message);
});
mediaManager.showGameOverlay();
mediaManager.getCamera().then(() => { mediaManager.getCamera().then(() => {
//receive message start //receive message start
@ -79,7 +93,7 @@ export class SimplePeer {
private receiveWebrtcStart(data: WebRtcStartMessageInterface) { private receiveWebrtcStart(data: WebRtcStartMessageInterface) {
this.WebRtcRoomId = data.roomId; this.WebRtcRoomId = data.roomId;
this.Users = data.clients; 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) // 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). // 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. // 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 * server has two people connected, start the meet
*/ */
private startWebRtc() { 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 it's not an initiator, peer connection will be created when gamer will receive offer signal
if(!user.initiator){ if(!user.initiator){
return; return;
@ -105,102 +120,63 @@ export class SimplePeer {
/** /**
* create peer connection to bind users * create peer connection to bind users
*/ */
private createPeerConnection(user : UserSimplePeer) { private createPeerConnection(user : UserSimplePeerInterface) : VideoPeer | null{
if(this.PeerConnectionArray.has(user.userId)) { if(
return; this.PeerConnectionArray.has(user.userId)
){
return null;
} }
//console.log("Creating connection with peer "+user.userId);
let name = user.name; let name = user.name;
if(!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) { if(userSearch) {
name = userSearch.name; name = userSearch.name;
} }
} }
mediaManager.removeActiveVideo(user.userId); mediaManager.removeActiveVideo(user.userId);
mediaManager.addActiveVideo(user.userId, name); mediaManager.addActiveVideo(user.userId, name);
const peer : SimplePeerNamespace.Instance = new Peer({ const peer = new VideoPeer(user.userId, user.initiator ? user.initiator : false, this.Connection);
initiator: user.initiator ? user.initiator : false, // When a connection is established to a video stream, and if a screen sharing is taking place,
reconnectTimer: 10000, // the user sharing screen should also initiate a connection to the remote user!
config: { peer.on('connect', () => {
iceServers: [ if (mediaManager.localScreenCapture) {
{ this.sendLocalScreenSharingStreamToUser(user.userId);
urls: 'stun:stun.l.google.com:19302' }
},
{
urls: 'turn:numb.viagenie.ca',
username: 'g.parant@thecodingmachine.com',
credential: 'itcugcOHxle9Acqi$'
},
]
},
}); });
this.PeerConnectionArray.set(user.userId, peer); 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) { for (const peerConnectionListener of this.peerConnectionListeners) {
peerConnectionListener.onConnect(user); 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) { private closeConnection(userId : string) {
try { try {
mediaManager.removeActiveVideo(userId); //mediaManager.removeActiveVideo(userId);
const peer = this.PeerConnectionArray.get(userId); const peer = this.PeerConnectionArray.get(userId);
if (peer === undefined) { if (peer === undefined) {
console.warn("Tried to close connection for user "+userId+" but could not find user") console.warn("Tried to close connection for user "+userId+" but could not find user")
return; return;
} }
peer.destroy();
// FIXME: I don't understand why "Closing connection with" message is displayed TWICE before "Nb users in peerConnectionArray" // 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. // 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); //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); //console.log('Nb users in peerConnectionArray '+this.PeerConnectionArray.size);
for (const peerConnectionListener of this.peerConnectionListeners) { for (const peerConnectionListener of this.peerConnectionListeners) {
peerConnectionListener.onDisconnect(userId); 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() { public closeAllConnections() {
for (const userId of this.PeerConnectionArray.keys()) { for (const userId of this.PeerConnectionArray.keys()) {
this.closeConnection(userId); this.closeConnection(userId);
} }
for (const userId of this.PeerScreenSharingConnectionArray.keys()) {
this.closeScreenSharingConnection(userId);
}
} }
/** /**
* Unregisters any held event handler. * Unregisters any held event handler.
*/ */
public unregister() { public unregister() {
mediaManager.removeUpdateLocalStreamEventListener(this.updateLocalStreamCallback); mediaManager.removeUpdateLocalStreamEventListener(this.sendLocalVideoStreamCallback);
}
/**
*
* @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);
}
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
private receiveWebrtcSignal(data: WebRtcSignalMessageInterface) { private receiveWebrtcSignal(data: WebRtcSignalReceivedMessageInterface) {
try { try {
//if offer type, create peer connection //if offer type, create peer connection
if(data.signal.type === "offer"){ if(data.signal.type === "offer"){
@ -274,53 +266,126 @@ export class SimplePeer {
} }
} }
/** private receiveWebrtcScreenSharingSignal(data: WebRtcSignalReceivedMessageInterface) {
* console.log("receiveWebrtcScreenSharingSignal", data);
* @param userId
* @param stream
*/
private stream(userId : string, stream: MediaStream) {
if(!stream){
mediaManager.disabledVideoByUserId(userId);
mediaManager.disabledMicrophoneByUserId(userId);
return;
}
mediaManager.addStreamRemoteVideo(userId, stream);
}
/**
*
* @param userId
*/
private addMedia (userId : string) {
try { try {
const localStream: MediaStream|null = mediaManager.localStream; //if offer type, create peer connection
const peer = this.PeerConnectionArray.get(userId); if(data.signal.type === "offer"){
if(localStream === null) { this.createPeerScreenSharingConnection(data);
//send fake signal
if(peer === undefined){
return;
} }
peer.write(new Buffer(JSON.stringify({ const peer = this.PeerScreenSharingConnectionArray.get(data.userId);
type: "stream", if (peer !== undefined) {
stream: null peer.signal(data.signal);
}))); } else {
return; console.error('Could not find peer whose ID is "'+data.userId+'" in receiveWebrtcScreenSharingSignal');
}
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);
} }
} catch (e) { } catch (e) {
console.error(`addMedia => addMedia => ${userId}`, e); console.error(`receiveWebrtcSignal => ${data.userId}`, e);
} }
} }
updatedLocalStream(){ /**
this.Users.forEach((user: UserSimplePeer) => { *
this.addMedia(user.userId); * @param userId
*/
private pushVideoToRemoteUser(userId : string) {
try {
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;
}
for (const track of localStream.getTracks()) {
PeerConnection.addTrack(track, localStream);
}
}catch (e) {
console.error(`pushVideoToRemoteUser => ${userId}`, e);
}
}
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);
}
}
}

View File

@ -3,9 +3,8 @@
"outDir": "./dist/", "outDir": "./dist/",
"sourceMap": true, "sourceMap": true,
"moduleResolution": "node", "moduleResolution": "node",
"noImplicitAny": true,
"module": "CommonJS", "module": "CommonJS",
"target": "es5", "target": "es6",
"downlevelIteration": true, "downlevelIteration": true,
"jsx": "react", "jsx": "react",
"allowJs": true, "allowJs": true,