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

This commit is contained in:
_Bastler
2021-07-02 20:59:39 +02:00
58 changed files with 2227 additions and 1746 deletions
+82 -81
View File
@@ -1,11 +1,11 @@
import {HtmlUtils} from "./HtmlUtils";
import type {UserInputManager} from "../Phaser/UserInput/UserInputManager";
import {connectionManager} from "../Connexion/ConnectionManager";
import {GameConnexionTypes} from "../Url/UrlManager";
import {iframeListener} from "../Api/IframeListener";
import {showReportScreenStore} from "../Stores/ShowReportScreenStore";
import { HtmlUtils } from "./HtmlUtils";
import type { UserInputManager } from "../Phaser/UserInput/UserInputManager";
import { connectionManager } from "../Connexion/ConnectionManager";
import { GameConnexionTypes } from "../Url/UrlManager";
import { iframeListener } from "../Api/IframeListener";
import { showReportScreenStore } from "../Stores/ShowReportScreenStore";
export type SendMessageCallback = (message:string) => void;
export type SendMessageCallback = (message: string) => void;
export class DiscussionManager {
private mainContainer: HTMLDivElement;
@@ -15,80 +15,81 @@ export class DiscussionManager {
private nbpParticipants?: HTMLParagraphElement;
private divMessages?: HTMLParagraphElement;
private participants: Map<number|string, HTMLDivElement> = new Map<number|string, HTMLDivElement>();
private participants: Map<number | string, HTMLDivElement> = new Map<number | string, HTMLDivElement>();
private activeDiscussion: boolean = false;
private sendMessageCallBack : Map<number|string, SendMessageCallback> = new Map<number|string, SendMessageCallback>();
private sendMessageCallBack: Map<number | string, SendMessageCallback> = new Map<
number | string,
SendMessageCallback
>();
private userInputManager?: UserInputManager;
constructor() {
this.mainContainer = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('main-container');
this.createDiscussPart(''); //todo: why do we always use empty string?
this.mainContainer = HtmlUtils.getElementByIdOrFail<HTMLDivElement>("main-container");
this.createDiscussPart(""); //todo: why do we always use empty string?
iframeListener.chatStream.subscribe((chatEvent) => {
this.addMessage(chatEvent.author, chatEvent.message, false);
this.showDiscussion();
});
this.onSendMessageCallback('iframe_listener', (message) => {
this.onSendMessageCallback("iframe_listener", (message) => {
iframeListener.sendUserInputChat(message);
})
});
}
private createDiscussPart(name: string) {
this.divDiscuss = document.createElement('div');
this.divDiscuss.classList.add('discussion');
this.divDiscuss = document.createElement("div");
this.divDiscuss.classList.add("discussion");
const buttonCloseDiscussion: HTMLButtonElement = document.createElement('button');
buttonCloseDiscussion.classList.add('close-btn');
const buttonCloseDiscussion: HTMLButtonElement = document.createElement("button");
buttonCloseDiscussion.classList.add("close-btn");
buttonCloseDiscussion.innerHTML = `<img src="resources/logos/close.svg"/>`;
buttonCloseDiscussion.addEventListener('click', () => {
buttonCloseDiscussion.addEventListener("click", () => {
this.hideDiscussion();
});
this.divDiscuss.appendChild(buttonCloseDiscussion);
const myName: HTMLParagraphElement = document.createElement('p');
const myName: HTMLParagraphElement = document.createElement("p");
myName.innerText = name.toUpperCase();
this.nbpParticipants = document.createElement('p');
this.nbpParticipants.innerText = 'PARTICIPANTS (1)';
this.nbpParticipants = document.createElement("p");
this.nbpParticipants.innerText = "PARTICIPANTS (1)";
this.divParticipants = document.createElement('div');
this.divParticipants.classList.add('participants');
this.divParticipants = document.createElement("div");
this.divParticipants.classList.add("participants");
this.divMessages = document.createElement('div');
this.divMessages.classList.add('messages');
this.divMessages.innerHTML = "<h2>Local messages</h2>"
this.divMessages = document.createElement("div");
this.divMessages.classList.add("messages");
this.divMessages.innerHTML = "<h2>Local messages</h2>";
this.divDiscuss.appendChild(myName);
this.divDiscuss.appendChild(this.nbpParticipants);
this.divDiscuss.appendChild(this.divParticipants);
this.divDiscuss.appendChild(this.divMessages);
const sendDivMessage: HTMLDivElement = document.createElement('div');
sendDivMessage.classList.add('send-message');
const inputMessage: HTMLInputElement = document.createElement('input');
const sendDivMessage: HTMLDivElement = document.createElement("div");
sendDivMessage.classList.add("send-message");
const inputMessage: HTMLInputElement = document.createElement("input");
inputMessage.onfocus = () => {
if(this.userInputManager) {
if (this.userInputManager) {
this.userInputManager.disableControls();
}
}
};
inputMessage.onblur = () => {
if(this.userInputManager) {
if (this.userInputManager) {
this.userInputManager.restoreControls();
}
}
};
inputMessage.type = "text";
inputMessage.addEventListener('keyup', (event: KeyboardEvent) => {
if (event.key === 'Enter') {
inputMessage.addEventListener("keyup", (event: KeyboardEvent) => {
if (event.key === "Enter") {
event.preventDefault();
if(inputMessage.value === null
|| inputMessage.value === ''
|| inputMessage.value === undefined) {
if (inputMessage.value === null || inputMessage.value === "" || inputMessage.value === undefined) {
return;
}
this.addMessage(name, inputMessage.value, true);
for(const callback of this.sendMessageCallBack.values()) {
for (const callback of this.sendMessageCallBack.values()) {
callback(inputMessage.value);
}
inputMessage.value = "";
@@ -100,44 +101,44 @@ export class DiscussionManager {
//append in main container
this.mainContainer.appendChild(this.divDiscuss);
this.addParticipant('me', 'Moi', undefined, true);
this.addParticipant("me", "Moi", undefined, true);
}
public addParticipant(
userId: number|'me',
name: string|undefined,
img?: string|undefined,
isMe: boolean = false,
userId: number | "me",
name: string | undefined,
img?: string | undefined,
isMe: boolean = false
) {
const divParticipant: HTMLDivElement = document.createElement('div');
divParticipant.classList.add('participant');
const divParticipant: HTMLDivElement = document.createElement("div");
divParticipant.classList.add("participant");
divParticipant.id = `participant-${userId}`;
const divImgParticipant: HTMLImageElement = document.createElement('img');
divImgParticipant.src = 'resources/logos/boy.svg';
const divImgParticipant: HTMLImageElement = document.createElement("img");
divImgParticipant.src = "resources/logos/boy.svg";
if (img !== undefined) {
divImgParticipant.src = img;
}
const divPParticipant: HTMLParagraphElement = document.createElement('p');
if(!name){
name = 'Anonymous';
const divPParticipant: HTMLParagraphElement = document.createElement("p");
if (!name) {
name = "Anonymous";
}
divPParticipant.innerText = name;
divParticipant.appendChild(divImgParticipant);
divParticipant.appendChild(divPParticipant);
if(
!isMe
&& connectionManager.getConnexionType
&& connectionManager.getConnexionType !== GameConnexionTypes.anonymous
&& userId !== 'me'
if (
!isMe &&
connectionManager.getConnexionType &&
connectionManager.getConnexionType !== GameConnexionTypes.anonymous &&
userId !== "me"
) {
const reportBanUserAction: HTMLButtonElement = document.createElement('button');
reportBanUserAction.classList.add('report-btn')
reportBanUserAction.innerText = 'Report';
reportBanUserAction.addEventListener('click', () => {
showReportScreenStore.set({ userId: userId, userName: name ? name : ''});
const reportBanUserAction: HTMLButtonElement = document.createElement("button");
reportBanUserAction.classList.add("report-btn");
reportBanUserAction.innerText = "Report";
reportBanUserAction.addEventListener("click", () => {
showReportScreenStore.set({ userId: userId, userName: name ? name : "" });
});
divParticipant.appendChild(reportBanUserAction);
}
@@ -157,16 +158,16 @@ export class DiscussionManager {
}
public addMessage(name: string, message: string, isMe: boolean = false) {
const divMessage: HTMLDivElement = document.createElement('div');
divMessage.classList.add('message');
if(isMe){
divMessage.classList.add('me');
const divMessage: HTMLDivElement = document.createElement("div");
divMessage.classList.add("message");
if (isMe) {
divMessage.classList.add("me");
}
const pMessage: HTMLParagraphElement = document.createElement('p');
const pMessage: HTMLParagraphElement = document.createElement("p");
const date = new Date();
if(isMe){
name = 'Me';
if (isMe) {
name = "Me";
} else {
name = HtmlUtils.escapeHtml(name);
}
@@ -176,15 +177,15 @@ export class DiscussionManager {
</span>`;
divMessage.appendChild(pMessage);
const userMessage: HTMLParagraphElement = document.createElement('p');
const userMessage: HTMLParagraphElement = document.createElement("p");
userMessage.innerHTML = HtmlUtils.urlify(message);
userMessage.classList.add('body');
userMessage.classList.add('nes-balloon');
if (isMe) {
userMessage.classList.add('from-left');
} else {
userMessage.classList.add('from-right');
}
userMessage.classList.add("body");
divMessage.appendChild(userMessage);
this.divMessages?.appendChild(divMessage);
@@ -192,14 +193,14 @@ export class DiscussionManager {
setTimeout(() => {
this.divMessages?.scroll({
top: this.divMessages?.scrollTop + divMessage.getBoundingClientRect().y,
behavior: 'smooth'
behavior: "smooth",
});
}, 200);
}
public removeParticipant(userId: number|string){
public removeParticipant(userId: number | string) {
const element = this.participants.get(userId);
if(element){
if (element) {
element.remove();
this.participants.delete(userId);
}
@@ -208,29 +209,29 @@ export class DiscussionManager {
this.sendMessageCallBack.delete(userId);
}
public onSendMessageCallback(userId: string|number, callback: SendMessageCallback): void {
public onSendMessageCallback(userId: string | number, callback: SendMessageCallback): void {
this.sendMessageCallBack.set(userId, callback);
}
get activatedDiscussion(){
get activatedDiscussion() {
return this.activeDiscussion;
}
private showDiscussion(){
private showDiscussion() {
this.activeDiscussion = true;
this.divDiscuss?.classList.add('active');
this.divDiscuss?.classList.add("active");
}
private hideDiscussion(){
private hideDiscussion() {
this.activeDiscussion = false;
this.divDiscuss?.classList.remove('active');
this.divDiscuss?.classList.remove("active");
}
public setUserInputManager(userInputManager : UserInputManager){
public setUserInputManager(userInputManager: UserInputManager) {
this.userInputManager = userInputManager;
}
public showDiscussionPart(){
public showDiscussionPart() {
this.showDiscussion();
}
}
+25 -25
View File
@@ -1,5 +1,5 @@
import type { UserInputManager } from "../Phaser/UserInput/UserInputManager";
import {HtmlUtils} from "./HtmlUtils";
import { HtmlUtils } from "./HtmlUtils";
const sanitizeHtml = require('sanitize-html');
export enum LayoutMode {
@@ -16,24 +16,24 @@ export enum DivImportance {
Normal = "Normal",
}
export const ON_ACTION_TRIGGER_BUTTON = 'onaction';
export const ON_ACTION_TRIGGER_BUTTON = "onaction";
export const TRIGGER_WEBSITE_PROPERTIES = 'openWebsiteTrigger';
export const TRIGGER_JITSI_PROPERTIES = 'jitsiTrigger';
export const TRIGGER_WEBSITE_PROPERTIES = "openWebsiteTrigger";
export const TRIGGER_JITSI_PROPERTIES = "jitsiTrigger";
export const WEBSITE_MESSAGE_PROPERTIES = 'openWebsiteTriggerMessage';
export const JITSI_MESSAGE_PROPERTIES = 'jitsiTriggerMessage';
export const WEBSITE_MESSAGE_PROPERTIES = "openWebsiteTriggerMessage";
export const JITSI_MESSAGE_PROPERTIES = "jitsiTriggerMessage";
export const AUDIO_VOLUME_PROPERTY = 'audioVolume';
export const AUDIO_LOOP_PROPERTY = 'audioLoop';
export const AUDIO_VOLUME_PROPERTY = "audioVolume";
export const AUDIO_LOOP_PROPERTY = "audioLoop";
export type Box = {xStart: number, yStart: number, xEnd: number, yEnd: number};
export type Box = { xStart: number; yStart: number; xEnd: number; yEnd: number };
class LayoutManager {
private actionButtonTrigger: Map<string, Function> = new Map<string, Function>();
private actionButtonInformation: Map<string, HTMLDivElement> = new Map<string, HTMLDivElement>();
public addActionButton(id: string, text: string, callBack: Function, userInputManager: UserInputManager){
public addActionButton(id: string, text: string, callBack: Function, userInputManager: UserInputManager) {
//delete previous element
this.removeActionButton(id, userInputManager);
@@ -44,14 +44,14 @@ class LayoutManager {
p.classList.add('is-dark');
p.innerHTML = sanitizeHtml(text);
const div = document.createElement('div');
div.classList.add('action');
const div = document.createElement("div");
div.classList.add("action");
div.id = id;
div.appendChild(p);
this.actionButtonInformation.set(id, div);
const mainContainer = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('main-container');
const mainContainer = HtmlUtils.getElementByIdOrFail<HTMLDivElement>("main-container");
mainContainer.appendChild(div);
//add trigger action
@@ -60,42 +60,42 @@ class LayoutManager {
userInputManager.addSpaceEventListner(callBack);
}
public removeActionButton(id: string, userInputManager?: UserInputManager){
public removeActionButton(id: string, userInputManager?: UserInputManager) {
//delete previous element
const previousDiv = this.actionButtonInformation.get(id);
if(previousDiv){
if (previousDiv) {
previousDiv.remove();
this.actionButtonInformation.delete(id);
}
const previousEventCallback = this.actionButtonTrigger.get(id);
if(previousEventCallback && userInputManager){
if (previousEventCallback && userInputManager) {
userInputManager.removeSpaceEventListner(previousEventCallback);
}
}
public addInformation(id: string, text: string, callBack?: Function, userInputManager?: UserInputManager){
public addInformation(id: string, text: string, callBack?: Function, userInputManager?: UserInputManager) {
//delete previous element
for ( const [key, value] of this.actionButtonInformation ) {
for (const [key, value] of this.actionButtonInformation) {
this.removeActionButton(key, userInputManager);
}
//create div and text html component
const p = document.createElement('p');
p.classList.add('action-body');
const p = document.createElement("p");
p.classList.add("action-body");
p.innerText = text;
const div = document.createElement('div');
div.classList.add('action');
const div = document.createElement("div");
div.classList.add("action");
div.classList.add(id);
div.id = id;
div.appendChild(p);
this.actionButtonInformation.set(id, div);
const mainContainer = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('main-container');
const mainContainer = HtmlUtils.getElementByIdOrFail<HTMLDivElement>("main-container");
mainContainer.appendChild(div);
//add trigger action
if(callBack){
if (callBack) {
div.onpointerdown = () => {
callBack();
this.removeActionButton(id, userInputManager);
@@ -105,7 +105,7 @@ class LayoutManager {
//remove it after 10 sec
setTimeout(() => {
this.removeActionButton(id, userInputManager);
}, 10000)
}, 10000);
}
}
+52 -48
View File
@@ -6,26 +6,20 @@ import { localUserStore } from "../Connexion/LocalUserStore";
import type { UserSimplePeerInterface } from "./SimplePeer";
import { SoundMeter } from "../Phaser/Components/SoundMeter";
import { DISABLE_NOTIFICATIONS } from "../Enum/EnvironmentVariable";
import {
localStreamStore,
} from "../Stores/MediaStore";
import {
screenSharingLocalStreamStore
} from "../Stores/ScreenSharingStore";
import {helpCameraSettingsVisibleStore} from "../Stores/HelpCameraSettingsStore";
import { localStreamStore } from "../Stores/MediaStore";
import { screenSharingLocalStreamStore } from "../Stores/ScreenSharingStore";
import { helpCameraSettingsVisibleStore } from "../Stores/HelpCameraSettingsStore";
export type UpdatedLocalStreamCallback = (media: MediaStream | null) => void;
export type StartScreenSharingCallback = (media: MediaStream) => void;
export type StopScreenSharingCallback = (media: MediaStream) => void;
import {cowebsiteCloseButtonId} from "./CoWebsiteManager";
import {gameOverlayVisibilityStore} from "../Stores/GameOverlayStoreVisibility";
import { cowebsiteCloseButtonId } from "./CoWebsiteManager";
import { gameOverlayVisibilityStore } from "../Stores/GameOverlayStoreVisibility";
export class MediaManager {
startScreenSharingCallBacks : Set<StartScreenSharingCallback> = new Set<StartScreenSharingCallback>();
stopScreenSharingCallBacks : Set<StopScreenSharingCallback> = new Set<StopScreenSharingCallback>();
startScreenSharingCallBacks: Set<StartScreenSharingCallback> = new Set<StartScreenSharingCallback>();
stopScreenSharingCallBacks: Set<StopScreenSharingCallback> = new Set<StopScreenSharingCallback>();
private focused: boolean = true;
@@ -34,40 +28,49 @@ export class MediaManager {
private userInputManager?: UserInputManager;
constructor() {
//Check of ask notification navigator permission
this.getNotification();
localStreamStore.subscribe((result) => {
if (result.type === 'error') {
if (result.type === "error") {
console.error(result.error);
layoutManager.addInformation('warning', 'Camera access denied. Click here and check your browser permissions.', () => {
helpCameraSettingsVisibleStore.set(true);
}, this.userInputManager);
layoutManager.addInformation(
"warning",
"Camera access denied. Click here and check your browser permissions.",
() => {
helpCameraSettingsVisibleStore.set(true);
},
this.userInputManager
);
return;
}
});
screenSharingLocalStreamStore.subscribe((result) => {
if (result.type === 'error') {
if (result.type === "error") {
console.error(result.error);
layoutManager.addInformation('warning', 'Screen sharing denied. Click here and check your browser permissions.', () => {
helpCameraSettingsVisibleStore.set(true);
}, this.userInputManager);
layoutManager.addInformation(
"warning",
"Screen sharing denied. Click here and check your browser permissions.",
() => {
helpCameraSettingsVisibleStore.set(true);
},
this.userInputManager
);
return;
}
});
}
public showGameOverlay(): void {
const gameOverlay = HtmlUtils.getElementByIdOrFail('game-overlay');
gameOverlay.classList.add('active');
const gameOverlay = HtmlUtils.getElementByIdOrFail("game-overlay");
gameOverlay.classList.add("active");
const buttonCloseFrame = HtmlUtils.getElementByIdOrFail(cowebsiteCloseButtonId);
const functionTrigger = () => {
this.triggerCloseJitsiFrameButton();
}
buttonCloseFrame.removeEventListener('click', () => {
};
buttonCloseFrame.removeEventListener("click", () => {
buttonCloseFrame.blur();
functionTrigger();
});
@@ -76,14 +79,14 @@ export class MediaManager {
}
public hideGameOverlay(): void {
const gameOverlay = HtmlUtils.getElementByIdOrFail('game-overlay');
gameOverlay.classList.remove('active');
const gameOverlay = HtmlUtils.getElementByIdOrFail("game-overlay");
gameOverlay.classList.remove("active");
const buttonCloseFrame = HtmlUtils.getElementByIdOrFail(cowebsiteCloseButtonId);
const functionTrigger = () => {
this.triggerCloseJitsiFrameButton();
}
buttonCloseFrame.addEventListener('click', () => {
};
buttonCloseFrame.addEventListener("click", () => {
buttonCloseFrame.blur();
functionTrigger();
});
@@ -100,7 +103,7 @@ export class MediaManager {
if (!element) {
return;
}
element.classList.add('active') //todo: why does a method 'disable' add a class 'active'?
element.classList.add("active"); //todo: why does a method 'disable' add a class 'active'?
}
enabledMicrophoneByUserId(userId: number) {
@@ -108,7 +111,7 @@ export class MediaManager {
if (!element) {
return;
}
element.classList.remove('active') //todo: why does a method 'enable' remove a class 'active'?
element.classList.remove("active"); //todo: why does a method 'enable' remove a class 'active'?
}
disabledVideoByUserId(userId: number) {
@@ -134,8 +137,8 @@ export class MediaManager {
}
toggleBlockLogo(userId: number, show: boolean): void {
const blockLogoElement = HtmlUtils.getElementByIdOrFail<HTMLImageElement>('blocking-' + userId);
show ? blockLogoElement.classList.add('active') : blockLogoElement.classList.remove('active');
const blockLogoElement = HtmlUtils.getElementByIdOrFail<HTMLImageElement>("blocking-" + userId);
show ? blockLogoElement.classList.add("active") : blockLogoElement.classList.remove("active");
}
isError(userId: string): void {
@@ -144,27 +147,28 @@ export class MediaManager {
if (!element) {
return;
}
const errorDiv = element.getElementsByClassName('rtc-error').item(0) as HTMLDivElement | null;
const errorDiv = element.getElementsByClassName("rtc-error").item(0) as HTMLDivElement | null;
if (errorDiv === null) {
return;
}
errorDiv.style.display = 'block';
errorDiv.style.display = "block";
}
isErrorScreenSharing(userId: string): void {
this.isError(this.getScreenSharingId(userId));
}
private getSpinner(userId: string): HTMLDivElement | null {
const element = document.getElementById(`div-${userId}`);
if (!element) {
return null;
}
const connectingSpinnerDiv = element.getElementsByClassName('connecting-spinner').item(0) as HTMLDivElement|null;
const connectingSpinnerDiv = element
.getElementsByClassName("connecting-spinner")
.item(0) as HTMLDivElement | null;
return connectingSpinnerDiv;
}
public addTriggerCloseJitsiFrameButton(id: String, Function: Function){
public addTriggerCloseJitsiFrameButton(id: String, Function: Function) {
this.triggerCloseJistiFrame.set(id, Function);
}
@@ -191,12 +195,12 @@ export class MediaManager {
discussionManager.onSendMessageCallback(userId, callback);
}
public setUserInputManager(userInputManager : UserInputManager){
public setUserInputManager(userInputManager: UserInputManager) {
this.userInputManager = userInputManager;
discussionManager.setUserInputManager(userInputManager);
}
public getNotification(){
public getNotification() {
//Get notification
if (!DISABLE_NOTIFICATIONS && window.Notification && Notification.permission !== "granted") {
if (this.checkNotificationPromise()) {
@@ -218,24 +222,24 @@ export class MediaManager {
private checkNotificationPromise(): boolean {
try {
Notification.requestPermission().then();
} catch(e) {
} catch (e) {
return false;
}
return true;
}
public createNotification(userName: string){
if(this.focused){
public createNotification(userName: string) {
if (this.focused) {
return;
}
if (window.Notification && Notification.permission === "granted") {
const title = 'WorkAdventure';
const title = "WorkAdventure";
const options = {
body: `Hi! ${userName} wants to discuss with you, don't be afraid!`,
icon: '/resources/logos/logo-WA-min.png',
image: '/resources/logos/logo-WA-min.png',
badge: '/resources/logos/logo-WA-min.png',
icon: "/resources/logos/logo-WA-min.png",
image: "/resources/logos/logo-WA-min.png",
badge: "/resources/logos/logo-WA-min.png",
};
new Notification(title, options);
//new Notification(`Hi! ${userName} wants to discuss with you, don't be afraid!`);
+61 -53
View File
@@ -1,13 +1,13 @@
import type * as SimplePeerNamespace from "simple-peer";
import {mediaManager} from "./MediaManager";
import {STUN_SERVER, TURN_PASSWORD, TURN_SERVER, TURN_USER} from "../Enum/EnvironmentVariable";
import type {RoomConnection} from "../Connexion/RoomConnection";
import {MESSAGE_TYPE_CONSTRAINT, PeerStatus} from "./VideoPeer";
import type {UserSimplePeerInterface} from "./SimplePeer";
import {Readable, readable, writable, Writable} from "svelte/store";
import {videoFocusStore} from "../Stores/VideoFocusStore";
import { mediaManager } from "./MediaManager";
import { STUN_SERVER, TURN_PASSWORD, TURN_SERVER, TURN_USER } from "../Enum/EnvironmentVariable";
import type { RoomConnection } from "../Connexion/RoomConnection";
import { MESSAGE_TYPE_CONSTRAINT, PeerStatus } from "./VideoPeer";
import type { UserSimplePeerInterface } from "./SimplePeer";
import { Readable, readable, writable, Writable } from "svelte/store";
import { videoFocusStore } from "../Stores/VideoFocusStore";
const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer');
const Peer: SimplePeerNamespace.SimplePeer = require("simple-peer");
/**
* A peer connection used to transmit video / audio signals between 2 peers.
@@ -16,7 +16,7 @@ export class ScreenSharingPeer extends Peer {
/**
* Whether this connection is currently receiving a video stream from a remote user.
*/
private isReceivingStream:boolean = false;
private isReceivingStream: boolean = false;
public toClose: boolean = false;
public _connected: boolean = false;
public readonly userId: number;
@@ -24,29 +24,37 @@ export class ScreenSharingPeer extends Peer {
public readonly streamStore: Readable<MediaStream | null>;
public readonly statusStore: Readable<PeerStatus>;
constructor(user: UserSimplePeerInterface, initiator: boolean, public readonly userName: string, private connection: RoomConnection, stream: MediaStream | null) {
constructor(
user: UserSimplePeerInterface,
initiator: boolean,
public readonly userName: string,
private connection: RoomConnection,
stream: MediaStream | null
) {
super({
initiator: initiator ? initiator : false,
//reconnectTimer: 10000,
config: {
iceServers: [
{
urls: STUN_SERVER.split(',')
urls: STUN_SERVER.split(","),
},
TURN_SERVER !== '' ? {
urls: TURN_SERVER.split(','),
username: user.webRtcUser || TURN_USER,
credential: user.webRtcPassword || TURN_PASSWORD
} : undefined,
].filter((value) => value !== undefined)
}
TURN_SERVER !== ""
? {
urls: TURN_SERVER.split(","),
username: user.webRtcUser || TURN_USER,
credential: user.webRtcPassword || TURN_PASSWORD,
}
: undefined,
].filter((value) => value !== undefined),
},
});
this.userId = user.userId;
this.uniqueId = 'screensharing_'+this.userId;
this.uniqueId = "screensharing_" + this.userId;
this.streamStore = readable<MediaStream|null>(null, (set) => {
const onStream = (stream: MediaStream|null) => {
this.streamStore = readable<MediaStream | null>(null, (set) => {
const onStream = (stream: MediaStream | null) => {
videoFocusStore.focus(this);
set(stream);
};
@@ -54,71 +62,71 @@ export class ScreenSharingPeer extends Peer {
// 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.
// TODO: we might rely on the "ended" event: https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/ended_event
const message = JSON.parse(chunk.toString('utf8'));
const message = JSON.parse(chunk.toString("utf8"));
if (message.streamEnded !== true) {
console.error('Unexpected message on screen sharing peer connection');
console.error("Unexpected message on screen sharing peer connection");
return;
}
set(null);
}
};
this.on('stream', onStream);
this.on('data', onData);
this.on("stream", onStream);
this.on("data", onData);
return () => {
this.off('stream', onStream);
this.off('data', onData);
this.off("stream", onStream);
this.off("data", onData);
};
});
this.statusStore = readable<PeerStatus>("connecting", (set) => {
const onConnect = () => {
set('connected');
set("connected");
};
const onError = () => {
set('error');
set("error");
};
const onClose = () => {
set('closed');
set("closed");
};
this.on('connect', onConnect);
this.on('error', onError);
this.on('close', onClose);
this.on("connect", onConnect);
this.on("error", onError);
this.on("close", onClose);
return () => {
this.off('connect', onConnect);
this.off('error', onError);
this.off('close', onClose);
this.off("connect", onConnect);
this.off("error", onError);
this.off("close", onClose);
};
});
//start listen signal for the peer connection
this.on('signal', (data: unknown) => {
this.on("signal", (data: unknown) => {
this.sendWebrtcScreenSharingSignal(data);
});
this.on('stream', (stream: MediaStream) => {
this.on("stream", (stream: MediaStream) => {
this.stream(stream);
});
this.on('close', () => {
this.on("close", () => {
this._connected = false;
this.toClose = true;
this.destroy();
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.on('error', (err: any) => {
this.on("error", (err: any) => {
console.error(`screen sharing error => ${this.userId} => ${err.code}`, err);
});
this.on('connect', () => {
this.on("connect", () => {
this._connected = true;
console.info(`connect => ${this.userId}`);
});
this.once('finish', () => {
this.once("finish", () => {
this._onFinish();
});
@@ -130,7 +138,7 @@ export class ScreenSharingPeer extends Peer {
private sendWebrtcScreenSharingSignal(data: unknown) {
try {
this.connection.sendWebrtcScreenSharingSignal(data, this.userId);
}catch (e) {
} catch (e) {
console.error(`sendWebrtcScreenSharingSignal => ${this.userId}`, e);
}
}
@@ -139,7 +147,7 @@ export class ScreenSharingPeer extends Peer {
* Sends received stream to screen.
*/
private stream(stream?: MediaStream) {
if(!stream){
if (!stream) {
this.isReceivingStream = false;
} else {
this.isReceivingStream = true;
@@ -152,8 +160,8 @@ export class ScreenSharingPeer extends Peer {
public destroy(error?: Error): void {
try {
this._connected = false
if(!this.toClose){
this._connected = false;
if (!this.toClose) {
return;
}
// FIXME: I don't understand why "Closing connection with" message is displayed TWICE before "Nb users in peerConnectionArray"
@@ -162,24 +170,24 @@ export class ScreenSharingPeer extends Peer {
super.destroy(error);
//console.log('Nb users in peerConnectionArray '+this.PeerConnectionArray.size);
} catch (err) {
console.error("ScreenSharingPeer::destroy", err)
console.error("ScreenSharingPeer::destroy", err);
}
}
_onFinish () {
if (this.destroyed) return
_onFinish() {
if (this.destroyed) return;
const destroySoon = () => {
this.destroy();
}
};
if (this._connected) {
destroySoon();
} else {
this.once('connect', destroySoon);
this.once("connect", destroySoon);
}
}
public stopPushingScreenSharingToRemoteUser(stream: MediaStream) {
this.removeStream(stream);
this.write(new Buffer(JSON.stringify({type: MESSAGE_TYPE_CONSTRAINT, streamEnded: true})));
this.write(new Buffer(JSON.stringify({ type: MESSAGE_TYPE_CONSTRAINT, streamEnded: true })));
}
}
+118 -88
View File
@@ -2,26 +2,22 @@ import type {
WebRtcDisconnectMessageInterface,
WebRtcSignalReceivedMessageInterface,
} from "../Connexion/ConnexionModels";
import {
mediaManager,
StartScreenSharingCallback,
StopScreenSharingCallback,
} from "./MediaManager";
import {ScreenSharingPeer} from "./ScreenSharingPeer";
import {MESSAGE_TYPE_BLOCKED, MESSAGE_TYPE_CONSTRAINT, MESSAGE_TYPE_MESSAGE, VideoPeer} from "./VideoPeer";
import type {RoomConnection} from "../Connexion/RoomConnection";
import {blackListManager} from "./BlackListManager";
import {get} from "svelte/store";
import {localStreamStore, LocalStreamStoreValue, obtainedMediaConstraintStore} from "../Stores/MediaStore";
import {screenSharingLocalStreamStore} from "../Stores/ScreenSharingStore";
import {discussionManager} from "./DiscussionManager";
import { mediaManager, StartScreenSharingCallback, StopScreenSharingCallback } from "./MediaManager";
import { ScreenSharingPeer } from "./ScreenSharingPeer";
import { MESSAGE_TYPE_BLOCKED, MESSAGE_TYPE_CONSTRAINT, MESSAGE_TYPE_MESSAGE, VideoPeer } from "./VideoPeer";
import type { RoomConnection } from "../Connexion/RoomConnection";
import { blackListManager } from "./BlackListManager";
import { get } from "svelte/store";
import { localStreamStore, LocalStreamStoreValue, obtainedMediaConstraintStore } from "../Stores/MediaStore";
import { screenSharingLocalStreamStore } from "../Stores/ScreenSharingStore";
import { discussionManager } from "./DiscussionManager";
export interface UserSimplePeerInterface{
export interface UserSimplePeerInterface {
userId: number;
name?: string;
initiator?: boolean;
webRtcUser?: string|undefined;
webRtcPassword?: string|undefined;
webRtcUser?: string | undefined;
webRtcPassword?: string | undefined;
}
export type RemotePeer = VideoPeer | ScreenSharingPeer;
@@ -45,36 +41,40 @@ export class SimplePeer {
private readonly unsubscribers: (() => void)[] = [];
private readonly peerConnectionListeners: Array<PeerConnectionListener> = new Array<PeerConnectionListener>();
private readonly userId: number;
private lastWebrtcUserName: string|undefined;
private lastWebrtcPassword: string|undefined;
private lastWebrtcUserName: string | undefined;
private lastWebrtcPassword: string | undefined;
constructor(private Connection: RoomConnection, private enableReporting: boolean, private myName: string) {
// We need to go through this weird bound function pointer in order to be able to "free" this reference later.
this.sendLocalScreenSharingStreamCallback = this.sendLocalScreenSharingStream.bind(this);
this.stopLocalScreenSharingStreamCallback = this.stopLocalScreenSharingStream.bind(this);
this.unsubscribers.push(localStreamStore.subscribe((streamResult) => {
this.sendLocalVideoStream(streamResult);
}));
this.unsubscribers.push(
localStreamStore.subscribe((streamResult) => {
this.sendLocalVideoStream(streamResult);
})
);
let localScreenCapture: MediaStream|null = null;
let localScreenCapture: MediaStream | null = null;
this.unsubscribers.push(screenSharingLocalStreamStore.subscribe((streamResult) => {
if (streamResult.type === 'error') {
// Let's ignore screen sharing errors, we will deal with those in a different way.
return;
}
if (streamResult.stream !== null) {
localScreenCapture = streamResult.stream;
this.sendLocalScreenSharingStream(localScreenCapture);
} else {
if (localScreenCapture) {
this.stopLocalScreenSharingStream(localScreenCapture);
localScreenCapture = null;
this.unsubscribers.push(
screenSharingLocalStreamStore.subscribe((streamResult) => {
if (streamResult.type === "error") {
// Let's ignore screen sharing errors, we will deal with those in a different way.
return;
}
}
}));
if (streamResult.stream !== null) {
localScreenCapture = streamResult.stream;
this.sendLocalScreenSharingStream(localScreenCapture);
} else {
if (localScreenCapture) {
this.stopLocalScreenSharingStream(localScreenCapture);
localScreenCapture = null;
}
}
})
);
this.userId = Connection.getUserId();
this.initialise();
@@ -92,7 +92,6 @@ export class SimplePeer {
* permit to listen when user could start visio
*/
private initialise() {
//receive signal by gemer
this.Connection.receiveWebrtcSignal((message: WebRtcSignalReceivedMessageInterface) => {
this.receiveWebrtcSignal(message);
@@ -122,12 +121,12 @@ export class SimplePeer {
// This would be symmetrical to the way we handle disconnection.
//start connection
if(!user.initiator){
if (!user.initiator) {
return;
}
const streamResult = get(localStreamStore);
let stream : MediaStream | null = null;
if (streamResult.type === 'success' && streamResult.stream) {
let stream: MediaStream | null = null;
if (streamResult.type === "success" && streamResult.stream) {
stream = streamResult.stream;
}
@@ -137,15 +136,15 @@ export class SimplePeer {
/**
* create peer connection to bind users
*/
private createPeerConnection(user : UserSimplePeerInterface, localStream: MediaStream | null) : VideoPeer | null {
const peerConnection = this.PeerConnectionArray.get(user.userId)
private createPeerConnection(user: UserSimplePeerInterface, localStream: MediaStream | null): VideoPeer | null {
const peerConnection = this.PeerConnectionArray.get(user.userId);
if (peerConnection) {
if (peerConnection.destroyed) {
peerConnection.toClose = true;
peerConnection.destroy();
const peerConnexionDeleted = this.PeerConnectionArray.delete(user.userId);
if (!peerConnexionDeleted) {
throw 'Error to delete peer connection';
throw "Error to delete peer connection";
}
//return this.createPeerConnection(user, localStream);
} else {
@@ -167,23 +166,32 @@ export class SimplePeer {
const peer = new VideoPeer(user, user.initiator ? user.initiator : false, name, this.Connection, localStream);
//permit to send message
mediaManager.addSendMessageCallback(user.userId,(message: string) => {
peer.write(new Buffer(JSON.stringify({type: MESSAGE_TYPE_MESSAGE, name: this.myName.toUpperCase(), userId: this.userId, message: message})));
mediaManager.addSendMessageCallback(user.userId, (message: string) => {
peer.write(
new Buffer(
JSON.stringify({
type: MESSAGE_TYPE_MESSAGE,
name: this.myName.toUpperCase(),
userId: this.userId,
message: message,
})
)
);
});
peer.toClose = false;
// When a connection is established to a video stream, and if a screen sharing is taking place,
// the user sharing screen should also initiate a connection to the remote user!
peer.on('connect', () => {
peer.on("connect", () => {
const streamResult = get(screenSharingLocalStreamStore);
if (streamResult.type === 'success' && streamResult.stream !== null) {
if (streamResult.type === "success" && streamResult.stream !== null) {
this.sendLocalScreenSharingStreamToUser(user.userId, streamResult.stream);
}
});
//Create a notification for first user in circle discussion
if(this.PeerConnectionArray.size === 0){
mediaManager.createNotification(user.name??'');
if (this.PeerConnectionArray.size === 0) {
mediaManager.createNotification(user.name ?? "");
}
this.PeerConnectionArray.set(user.userId, peer);
@@ -196,27 +204,30 @@ export class SimplePeer {
private getName(userId: number): string {
const userSearch = this.Users.find((userSearch: UserSimplePeerInterface) => userSearch.userId === userId);
if (userSearch) {
return userSearch.name || '';
return userSearch.name || "";
} else {
return '';
return "";
}
}
/**
* create peer connection to bind users
*/
private createPeerScreenSharingConnection(user : UserSimplePeerInterface, stream: MediaStream | null) : ScreenSharingPeer | null{
private createPeerScreenSharingConnection(
user: UserSimplePeerInterface,
stream: MediaStream | null
): ScreenSharingPeer | null {
const peerConnection = this.PeerScreenSharingConnectionArray.get(user.userId);
if(peerConnection){
if(peerConnection.destroyed){
if (peerConnection) {
if (peerConnection.destroyed) {
peerConnection.toClose = true;
peerConnection.destroy();
const peerConnexionDeleted = this.PeerScreenSharingConnectionArray.delete(user.userId);
if(!peerConnexionDeleted){
throw 'Error to delete peer connection';
if (!peerConnexionDeleted) {
throw "Error to delete peer connection";
}
this.createPeerConnection(user, stream);
}else {
} else {
peerConnection.toClose = false;
}
return null;
@@ -230,7 +241,13 @@ export class SimplePeer {
const name = this.getName(user.userId);
const peer = new ScreenSharingPeer(user, user.initiator ? user.initiator : false, name, this.Connection, stream);
const peer = new ScreenSharingPeer(
user,
user.initiator ? user.initiator : false,
name,
this.Connection,
stream
);
this.PeerScreenSharingConnectionArray.set(user.userId, peer);
for (const peerConnectionListener of this.peerConnectionListeners) {
@@ -242,11 +259,13 @@ export class SimplePeer {
/**
* This is triggered twice. Once by the server, and once by a remote client disconnecting
*/
private closeConnection(userId : number) {
private closeConnection(userId: number) {
try {
const peer = this.PeerConnectionArray.get(userId);
if (peer === undefined) {
console.warn("closeConnection => Tried to close connection for user "+userId+" but could not find user");
console.warn(
"closeConnection => Tried to close connection for user " + userId + " but could not find user"
);
return;
}
//create temp perr to close
@@ -257,18 +276,18 @@ export class SimplePeer {
this.closeScreenSharingConnection(userId);
const userIndex = this.Users.findIndex(user => user.userId === userId);
if(userIndex < 0){
throw 'Couldn\'t delete user';
const userIndex = this.Users.findIndex((user) => user.userId === userId);
if (userIndex < 0) {
throw "Couldn't delete user";
} else {
this.Users.splice(userIndex, 1);
}
} catch (err) {
console.error("closeConnection", err)
console.error("closeConnection", err);
}
//if user left discussion, clear array peer connection of sharing
if(this.Users.length === 0) {
if (this.Users.length === 0) {
for (const userId of this.PeerScreenSharingConnectionArray.keys()) {
this.closeScreenSharingConnection(userId);
this.PeerScreenSharingConnectionArray.delete(userId);
@@ -283,12 +302,16 @@ export class SimplePeer {
/**
* This is triggered twice. Once by the server, and once by a remote client disconnecting
*/
private closeScreenSharingConnection(userId : number) {
private closeScreenSharingConnection(userId: number) {
try {
//mediaManager.removeActiveScreenSharingVideo("" + userId);
const peer = this.PeerScreenSharingConnectionArray.get(userId);
if (peer === undefined) {
console.warn("closeScreenSharingConnection => Tried to close connection for user "+userId+" but could not find user")
console.warn(
"closeScreenSharingConnection => 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"
@@ -301,7 +324,7 @@ export class SimplePeer {
}*/
//console.log('Nb users in peerConnectionArray '+this.PeerConnectionArray.size);
} catch (err) {
console.error("closeConnection", err)
console.error("closeConnection", err);
}
}
@@ -328,10 +351,10 @@ export class SimplePeer {
private receiveWebrtcSignal(data: WebRtcSignalReceivedMessageInterface) {
try {
//if offer type, create peer connection
if(data.signal.type === "offer"){
if (data.signal.type === "offer") {
const streamResult = get(localStreamStore);
let stream : MediaStream | null = null;
if (streamResult.type === 'success' && streamResult.stream) {
let stream: MediaStream | null = null;
if (streamResult.type === "success" && streamResult.stream) {
stream = streamResult.stream;
}
@@ -341,7 +364,7 @@ export class SimplePeer {
if (peer !== undefined) {
peer.signal(data.signal);
} else {
console.error('Could not find peer whose ID is "'+data.userId+'" in PeerConnectionArray');
console.error('Could not find peer whose ID is "' + data.userId + '" in PeerConnectionArray');
}
} catch (e) {
console.error(`receiveWebrtcSignal => ${data.userId}`, e);
@@ -352,22 +375,24 @@ export class SimplePeer {
if (blackListManager.isBlackListed(data.userId)) return;
console.log("receiveWebrtcScreenSharingSignal", data);
const streamResult = get(screenSharingLocalStreamStore);
let stream : MediaStream | null = null;
if (streamResult.type === 'success' && streamResult.stream !== null) {
let stream: MediaStream | null = null;
if (streamResult.type === "success" && streamResult.stream !== null) {
stream = streamResult.stream;
}
try {
//if offer type, create peer connection
if(data.signal.type === "offer"){
if (data.signal.type === "offer") {
this.createPeerScreenSharingConnection(data, stream);
}
const peer = this.PeerScreenSharingConnectionArray.get(data.userId);
if (peer !== undefined) {
peer.signal(data.signal);
} else {
console.error('Could not find peer whose ID is "'+data.userId+'" in receiveWebrtcScreenSharingSignal');
console.info('Attempt to create new peer connexion');
console.error(
'Could not find peer whose ID is "' + data.userId + '" in receiveWebrtcScreenSharingSignal'
);
console.info("Attempt to create new peer connexion");
if (stream) {
this.sendLocalScreenSharingStreamToUser(data.userId, stream);
}
@@ -384,17 +409,19 @@ export class SimplePeer {
try {
const PeerConnection = this.PeerConnectionArray.get(userId);
if (!PeerConnection) {
throw new Error('While adding media, cannot find user with ID ' + userId);
throw new Error("While adding media, cannot find user with ID " + userId);
}
PeerConnection.write(new Buffer(JSON.stringify({type: MESSAGE_TYPE_CONSTRAINT, ...streamResult.constraints})));
PeerConnection.write(
new Buffer(JSON.stringify({ type: MESSAGE_TYPE_CONSTRAINT, ...streamResult.constraints }))
);
if (streamResult.type === 'error') {
if (streamResult.type === "error") {
return;
}
const localStream: MediaStream | null = streamResult.stream;
if(!localStream){
if (!localStream) {
return;
}
@@ -404,7 +431,7 @@ export class SimplePeer {
(track as any).added = true; // eslint-disable-line @typescript-eslint/no-explicit-any
PeerConnection.addTrack(track, localStream);
}
}catch (e) {
} catch (e) {
console.error(`pushVideoToRemoteUser => ${userId}`, e);
}
}
@@ -412,7 +439,7 @@ export class SimplePeer {
private pushScreenSharingToRemoteUser(userId: number, localScreenCapture: MediaStream) {
const PeerConnection = this.PeerScreenSharingConnectionArray.get(userId);
if (!PeerConnection) {
throw new Error('While pushing screen sharing, cannot find user with ID ' + userId);
throw new Error("While pushing screen sharing, cannot find user with ID " + userId);
}
for (const track of localScreenCapture.getTracks()) {
@@ -421,7 +448,7 @@ export class SimplePeer {
return;
}
public sendLocalVideoStream(streamResult: LocalStreamStoreValue){
public sendLocalVideoStream(streamResult: LocalStreamStoreValue) {
for (const user of this.Users) {
this.pushVideoToRemoteUser(user.userId, streamResult);
}
@@ -455,9 +482,12 @@ export class SimplePeer {
const screenSharingUser: UserSimplePeerInterface = {
userId,
initiator: true
initiator: true,
};
const PeerConnectionScreenSharing = this.createPeerScreenSharingConnection(screenSharingUser, localScreenCapture);
const PeerConnectionScreenSharing = this.createPeerScreenSharingConnection(
screenSharingUser,
localScreenCapture
);
if (!PeerConnectionScreenSharing) {
return;
}
@@ -466,7 +496,7 @@ export class SimplePeer {
private stopLocalScreenSharingStreamToUser(userId: number, stream: MediaStream): void {
const PeerConnectionScreenSharing = this.PeerScreenSharingConnectionArray.get(userId);
if (!PeerConnectionScreenSharing) {
throw new Error('Weird, screen sharing connection to user ' + userId + 'not found')
throw new Error("Weird, screen sharing connection to user " + userId + "not found");
}
console.log("updatedScreenSharing => destroy", PeerConnectionScreenSharing);
+95 -76
View File
@@ -1,22 +1,22 @@
import type * as SimplePeerNamespace from "simple-peer";
import {mediaManager} from "./MediaManager";
import {STUN_SERVER, TURN_PASSWORD, TURN_SERVER, TURN_USER} from "../Enum/EnvironmentVariable";
import type {RoomConnection} from "../Connexion/RoomConnection";
import {blackListManager} from "./BlackListManager";
import type {Subscription} from "rxjs";
import type {UserSimplePeerInterface} from "./SimplePeer";
import {get, readable, Readable} from "svelte/store";
import {obtainedMediaConstraintStore} from "../Stores/MediaStore";
import {discussionManager} from "./DiscussionManager";
import { mediaManager } from "./MediaManager";
import { STUN_SERVER, TURN_PASSWORD, TURN_SERVER, TURN_USER } from "../Enum/EnvironmentVariable";
import type { RoomConnection } from "../Connexion/RoomConnection";
import { blackListManager } from "./BlackListManager";
import type { Subscription } from "rxjs";
import type { UserSimplePeerInterface } from "./SimplePeer";
import { get, readable, Readable } from "svelte/store";
import { obtainedMediaConstraintStore } from "../Stores/MediaStore";
import { discussionManager } from "./DiscussionManager";
const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer');
const Peer: SimplePeerNamespace.SimplePeer = require("simple-peer");
export type PeerStatus = "connecting" | "connected" | "error" | "closed";
export const MESSAGE_TYPE_CONSTRAINT = 'constraint';
export const MESSAGE_TYPE_MESSAGE = 'message';
export const MESSAGE_TYPE_BLOCKED = 'blocked';
export const MESSAGE_TYPE_UNBLOCKED = 'unblocked';
export const MESSAGE_TYPE_CONSTRAINT = "constraint";
export const MESSAGE_TYPE_MESSAGE = "message";
export const MESSAGE_TYPE_BLOCKED = "blocked";
export const MESSAGE_TYPE_UNBLOCKED = "unblocked";
/**
* A peer connection used to transmit video / audio signals between 2 peers.
*/
@@ -31,116 +31,124 @@ export class VideoPeer extends Peer {
private onUnBlockSubscribe: Subscription;
public readonly streamStore: Readable<MediaStream | null>;
public readonly statusStore: Readable<PeerStatus>;
public readonly constraintsStore: Readable<MediaStreamConstraints|null>;
public readonly constraintsStore: Readable<MediaStreamConstraints | null>;
constructor(public user: UserSimplePeerInterface, initiator: boolean, public readonly userName: string, private connection: RoomConnection, localStream: MediaStream | null) {
constructor(
public user: UserSimplePeerInterface,
initiator: boolean,
public readonly userName: string,
private connection: RoomConnection,
localStream: MediaStream | null
) {
super({
initiator: initiator ? initiator : false,
//reconnectTimer: 10000,
config: {
iceServers: [
{
urls: STUN_SERVER.split(',')
urls: STUN_SERVER.split(","),
},
TURN_SERVER !== '' ? {
urls: TURN_SERVER.split(','),
username: user.webRtcUser || TURN_USER,
credential: user.webRtcPassword || TURN_PASSWORD
} : undefined,
].filter((value) => value !== undefined)
}
TURN_SERVER !== ""
? {
urls: TURN_SERVER.split(","),
username: user.webRtcUser || TURN_USER,
credential: user.webRtcPassword || TURN_PASSWORD,
}
: undefined,
].filter((value) => value !== undefined),
},
});
this.userId = user.userId;
this.uniqueId = 'video_'+this.userId;
this.uniqueId = "video_" + this.userId;
this.streamStore = readable<MediaStream|null>(null, (set) => {
const onStream = (stream: MediaStream|null) => {
this.streamStore = readable<MediaStream | null>(null, (set) => {
const onStream = (stream: MediaStream | null) => {
set(stream);
};
const onData = (chunk: Buffer) => {
this.on('data', (chunk: Buffer) => {
const message = JSON.parse(chunk.toString('utf8'));
this.on("data", (chunk: Buffer) => {
const message = JSON.parse(chunk.toString("utf8"));
if (message.type === MESSAGE_TYPE_CONSTRAINT) {
if (!message.video) {
set(null);
}
}
});
}
};
this.on('stream', onStream);
this.on('data', onData);
this.on("stream", onStream);
this.on("data", onData);
return () => {
this.off('stream', onStream);
this.off('data', onData);
this.off("stream", onStream);
this.off("data", onData);
};
});
this.constraintsStore = readable<MediaStreamConstraints|null>(null, (set) => {
this.constraintsStore = readable<MediaStreamConstraints | null>(null, (set) => {
const onData = (chunk: Buffer) => {
const message = JSON.parse(chunk.toString('utf8'));
if(message.type === MESSAGE_TYPE_CONSTRAINT) {
const message = JSON.parse(chunk.toString("utf8"));
if (message.type === MESSAGE_TYPE_CONSTRAINT) {
set(message);
}
}
};
this.on('data', onData);
this.on("data", onData);
return () => {
this.off('data', onData);
this.off("data", onData);
};
});
this.statusStore = readable<PeerStatus>("connecting", (set) => {
const onConnect = () => {
set('connected');
set("connected");
};
const onError = () => {
set('error');
set("error");
};
const onClose = () => {
set('closed');
set("closed");
};
this.on('connect', onConnect);
this.on('error', onError);
this.on('close', onClose);
this.on("connect", onConnect);
this.on("error", onError);
this.on("close", onClose);
return () => {
this.off('connect', onConnect);
this.off('error', onError);
this.off('close', onClose);
this.off("connect", onConnect);
this.off("error", onError);
this.off("close", onClose);
};
});
//start listen signal for the peer connection
this.on('signal', (data: unknown) => {
this.on("signal", (data: unknown) => {
this.sendWebrtcSignal(data);
});
this.on('stream', (stream: MediaStream) => this.stream(stream));
this.on("stream", (stream: MediaStream) => this.stream(stream));
this.on('close', () => {
this.on("close", () => {
this._connected = false;
this.toClose = true;
this.destroy();
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.on('error', (err: any) => {
this.on("error", (err: any) => {
console.error(`error => ${this.userId} => ${err.code}`, err);
mediaManager.isError("" + this.userId);
});
this.on('connect', () => {
this.on("connect", () => {
this._connected = true;
});
this.on('data', (chunk: Buffer) => {
const message = JSON.parse(chunk.toString('utf8'));
if(message.type === MESSAGE_TYPE_CONSTRAINT) {
this.on("data", (chunk: Buffer) => {
const message = JSON.parse(chunk.toString("utf8"));
if (message.type === MESSAGE_TYPE_CONSTRAINT) {
if (message.audio) {
mediaManager.enabledMicrophoneByUserId(this.userId);
} else {
@@ -152,23 +160,23 @@ export class VideoPeer extends Peer {
} else {
mediaManager.disabledVideoByUserId(this.userId);
}
} else if(message.type === MESSAGE_TYPE_MESSAGE) {
} else if (message.type === MESSAGE_TYPE_MESSAGE) {
if (!blackListManager.isBlackListed(message.userId)) {
mediaManager.addNewMessage(message.name, message.message);
}
} else if(message.type === MESSAGE_TYPE_BLOCKED) {
} else if (message.type === MESSAGE_TYPE_BLOCKED) {
//FIXME when A blacklists B, the output stream from A is muted in B's js client. This is insecure since B can manipulate the code to unmute A stream.
// Find a way to block A's output stream in A's js client
//However, the output stream stream B is correctly blocked in A client
this.blocked = true;
this.toggleRemoteStream(false);
} else if(message.type === MESSAGE_TYPE_UNBLOCKED) {
} else if (message.type === MESSAGE_TYPE_UNBLOCKED) {
this.blocked = false;
this.toggleRemoteStream(true);
}
});
this.once('finish', () => {
this.once("finish", () => {
this._onFinish();
});
@@ -187,23 +195,32 @@ export class VideoPeer extends Peer {
});
if (blackListManager.isBlackListed(this.userId)) {
this.sendBlockMessage(true)
this.sendBlockMessage(true);
}
}
private sendBlockMessage(blocking: boolean) {
this.write(new Buffer(JSON.stringify({type: blocking ? MESSAGE_TYPE_BLOCKED : MESSAGE_TYPE_UNBLOCKED, name: this.userName.toUpperCase(), userId: this.userId, message: ''})));
this.write(
new Buffer(
JSON.stringify({
type: blocking ? MESSAGE_TYPE_BLOCKED : MESSAGE_TYPE_UNBLOCKED,
name: this.userName.toUpperCase(),
userId: this.userId,
message: "",
})
)
);
}
private toggleRemoteStream(enable: boolean) {
this.remoteStream.getTracks().forEach(track => track.enabled = enable);
this.remoteStream.getTracks().forEach((track) => (track.enabled = enable));
mediaManager.toggleBlockLogo(this.userId, !enable);
}
private sendWebrtcSignal(data: unknown) {
try {
this.connection.sendWebrtcSignal(data, this.userId);
}catch (e) {
} catch (e) {
console.error(`sendWebrtcSignal => ${this.userId}`, e);
}
}
@@ -217,7 +234,7 @@ export class VideoPeer extends Peer {
if (blackListManager.isBlackListed(this.userId) || this.blocked) {
this.toggleRemoteStream(false);
}
}catch (err){
} catch (err) {
console.error(err);
}
}
@@ -227,8 +244,8 @@ export class VideoPeer extends Peer {
*/
public destroy(error?: Error): void {
try {
this._connected = false
if(!this.toClose){
this._connected = false;
if (!this.toClose) {
return;
}
this.onBlockSubscribe.unsubscribe();
@@ -238,34 +255,36 @@ export class VideoPeer extends Peer {
// I do understand the method closeConnection is called twice, but I don't understand how they manage to run in parallel.
super.destroy(error);
} catch (err) {
console.error("VideoPeer::destroy", err)
console.error("VideoPeer::destroy", err);
}
}
_onFinish () {
if (this.destroyed) return
_onFinish() {
if (this.destroyed) return;
const destroySoon = () => {
this.destroy();
}
};
if (this._connected) {
destroySoon();
} else {
this.once('connect', destroySoon);
this.once("connect", destroySoon);
}
}
private pushVideoToRemoteUser(localStream: MediaStream | null) {
try {
this.write(new Buffer(JSON.stringify({type: MESSAGE_TYPE_CONSTRAINT, ...get(obtainedMediaConstraintStore)})));
this.write(
new Buffer(JSON.stringify({ type: MESSAGE_TYPE_CONSTRAINT, ...get(obtainedMediaConstraintStore) }))
);
if(!localStream){
if (!localStream) {
return;
}
for (const track of localStream.getTracks()) {
this.addTrack(track, localStream);
}
}catch (e) {
} catch (e) {
console.error(`pushVideoToRemoteUser => ${this.userId}`, e);
}
}