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

This commit is contained in:
_Bastler
2021-06-24 13:02:41 +02:00
112 changed files with 2860 additions and 2505 deletions
+4 -8
View File
@@ -1,9 +1,9 @@
import {HtmlUtils} from "./HtmlUtils";
import type {ShowReportCallBack} from "./MediaManager";
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;
@@ -104,11 +104,10 @@ export class DiscussionManager {
}
public addParticipant(
userId: number|string,
userId: number|'me',
name: string|undefined,
img?: string|undefined,
isMe: boolean = false,
showReportCallBack?: ShowReportCallBack
) {
const divParticipant: HTMLDivElement = document.createElement('div');
divParticipant.classList.add('participant');
@@ -132,16 +131,13 @@ export class DiscussionManager {
!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', () => {
if(showReportCallBack) {
showReportCallBack(`${userId}`, name);
}else{
console.info('report feature is not activated!');
}
showReportScreenStore.set({ userId: userId, userName: name ? name : ''});
});
divParticipant.appendChild(reportBanUserAction);
}
+2 -292
View File
@@ -16,14 +16,6 @@ export enum DivImportance {
Normal = "Normal",
}
/**
* Classes implementing this interface can be notified when the center of the screen (the player position) should be
* changed.
*/
export interface CenterListener {
onCenterChange(): void;
}
export const ON_ACTION_TRIGGER_BUTTON = 'onaction';
export const TRIGGER_WEBSITE_PROPERTIES = 'openWebsiteTrigger';
@@ -35,294 +27,12 @@ export const JITSI_MESSAGE_PROPERTIES = 'jitsiTriggerMessage';
export const AUDIO_VOLUME_PROPERTY = 'audioVolume';
export const AUDIO_LOOP_PROPERTY = 'audioLoop';
/**
* This class is in charge of the video-conference layout.
* It receives positioning requests for videos and does its best to place them on the screen depending on the active layout mode.
*/
export type Box = {xStart: number, yStart: number, xEnd: number, yEnd: number};
class LayoutManager {
private mode: LayoutMode = LayoutMode.Presentation;
private importantDivs: Map<string, HTMLDivElement> = new Map<string, HTMLDivElement>();
private normalDivs: Map<string, HTMLDivElement> = new Map<string, HTMLDivElement>();
private listener: CenterListener|null = null;
private actionButtonTrigger: Map<string, Function> = new Map<string, Function>();
private actionButtonInformation: Map<string, HTMLDivElement> = new Map<string, HTMLDivElement>();
public setListener(centerListener: CenterListener|null) {
this.listener = centerListener;
}
public add(importance: DivImportance, userId: string, html: string): void {
const div = document.createElement('div');
div.innerHTML = html;
div.id = "user-"+userId;
div.className = "media-container"
div.classList.add("nes-container", "is-rounded", "is-dark");
div.onclick = () => {
const parentId = div.parentElement?.id;
if (parentId === 'sidebar' || parentId === 'chat-mode') {
this.focusOn(userId);
} else {
this.removeFocusOn(userId);
}
}
if (importance === DivImportance.Important) {
this.importantDivs.set(userId, div);
// If this is the first video with high importance, let's switch mode automatically.
if (this.importantDivs.size === 1 && this.mode === LayoutMode.VideoChat) {
this.switchLayoutMode(LayoutMode.Presentation);
}
} else if (importance === DivImportance.Normal) {
this.normalDivs.set(userId, div);
} else {
throw new Error('Unexpected importance');
}
this.positionDiv(div, importance);
this.adjustVideoChatClass();
this.listener?.onCenterChange();
}
private positionDiv(elem: HTMLDivElement, importance: DivImportance): void {
if (this.mode === LayoutMode.VideoChat) {
const chatModeDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('chat-mode');
chatModeDiv.appendChild(elem);
} else {
if (importance === DivImportance.Important) {
const mainSectionDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('main-section');
mainSectionDiv.appendChild(elem);
} else if (importance === DivImportance.Normal) {
const sideBarDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('sidebar');
sideBarDiv.appendChild(elem);
}
}
}
/**
* Put the screen in presentation mode and move elem in presentation mode (and all other videos in normal mode)
*/
private focusOn(userId: string): void {
const focusedDiv = this.getDivByUserId(userId);
for (const [importantUserId, importantDiv] of this.importantDivs.entries()) {
//this.positionDiv(importantDiv, DivImportance.Normal);
this.importantDivs.delete(importantUserId);
this.normalDivs.set(importantUserId, importantDiv);
}
this.normalDivs.delete(userId);
this.importantDivs.set(userId, focusedDiv);
//this.positionDiv(focusedDiv, DivImportance.Important);
this.switchLayoutMode(LayoutMode.Presentation);
}
/**
* Removes userId from presentation mode
*/
private removeFocusOn(userId: string): void {
const importantDiv = this.importantDivs.get(userId);
if (importantDiv === undefined) {
throw new Error('Div with user id "'+userId+'" is not in important mode');
}
this.normalDivs.set(userId, importantDiv);
this.importantDivs.delete(userId);
this.positionDiv(importantDiv, DivImportance.Normal);
}
private getDivByUserId(userId: string): HTMLDivElement {
let div = this.importantDivs.get(userId);
if (div !== undefined) {
return div;
}
div = this.normalDivs.get(userId);
if (div !== undefined) {
return div;
}
throw new Error('Could not find media with user id '+userId);
}
/**
* Removes the DIV matching userId.
*/
public remove(userId: string): void {
console.log('Removing video for userID '+userId+'.');
let div = this.importantDivs.get(userId);
if (div !== undefined) {
div.remove();
this.importantDivs.delete(userId);
this.adjustVideoChatClass();
this.listener?.onCenterChange();
return;
}
div = this.normalDivs.get(userId);
if (div !== undefined) {
div.remove();
this.normalDivs.delete(userId);
this.adjustVideoChatClass();
this.listener?.onCenterChange();
return;
}
console.log('Cannot remove userID '+userId+'. Already removed?');
//throw new Error('Could not find user ID "'+userId+'"');
}
private adjustVideoChatClass(): void {
const chatModeDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('chat-mode');
chatModeDiv.classList.remove('one-col', 'two-col', 'three-col', 'four-col');
const nbUsers = this.importantDivs.size + this.normalDivs.size;
if (nbUsers <= 1) {
chatModeDiv.classList.add('one-col');
} else if (nbUsers <= 4) {
chatModeDiv.classList.add('two-col');
} else if (nbUsers <= 9) {
chatModeDiv.classList.add('three-col');
} else {
chatModeDiv.classList.add('four-col');
}
}
public switchLayoutMode(layoutMode: LayoutMode) {
this.mode = layoutMode;
if (layoutMode === LayoutMode.Presentation) {
HtmlUtils.getElementByIdOrFail<HTMLDivElement>('sidebar').style.display = 'flex';
HtmlUtils.getElementByIdOrFail<HTMLDivElement>('main-section').style.display = 'flex';
HtmlUtils.getElementByIdOrFail<HTMLDivElement>('chat-mode').style.display = 'none';
} else {
HtmlUtils.getElementByIdOrFail<HTMLDivElement>('sidebar').style.display = 'none';
HtmlUtils.getElementByIdOrFail<HTMLDivElement>('main-section').style.display = 'none';
HtmlUtils.getElementByIdOrFail<HTMLDivElement>('chat-mode').style.display = 'grid';
}
for (const div of this.importantDivs.values()) {
this.positionDiv(div, DivImportance.Important);
}
for (const div of this.normalDivs.values()) {
this.positionDiv(div, DivImportance.Normal);
}
this.listener?.onCenterChange();
}
public getLayoutMode(): LayoutMode {
return this.mode;
}
/*public getGameCenter(): {x: number, y: number} {
}*/
/**
* Tries to find the biggest available box of remaining space (this is a space where we can center the character)
*/
public findBiggestAvailableArray(): {xStart: number, yStart: number, xEnd: number, yEnd: number} {
const game = HtmlUtils.querySelectorOrFail<HTMLCanvasElement>('#game canvas');
if (this.mode === LayoutMode.VideoChat) {
const children = document.querySelectorAll<HTMLDivElement>('div.chat-mode > div');
const htmlChildren = Array.from(children.values());
// No chat? Let's go full center
if (htmlChildren.length === 0) {
return {
xStart: 0,
yStart: 0,
xEnd: game.offsetWidth,
yEnd: game.offsetHeight
}
}
const lastDiv = htmlChildren[htmlChildren.length - 1];
// Compute area between top right of the last div and bottom right of window
const area1 = (game.offsetWidth - (lastDiv.offsetLeft + lastDiv.offsetWidth))
* (game.offsetHeight - lastDiv.offsetTop);
// Compute area between bottom of last div and bottom of the screen on whole width
const area2 = game.offsetWidth
* (game.offsetHeight - (lastDiv.offsetTop + lastDiv.offsetHeight));
if (area1 < 0 && area2 < 0) {
// If screen is full, let's not attempt something foolish and simply center character in the middle.
return {
xStart: 0,
yStart: 0,
xEnd: game.offsetWidth,
yEnd: game.offsetHeight
}
}
if (area1 <= area2) {
console.log('lastDiv', lastDiv.offsetTop, lastDiv.offsetHeight);
return {
xStart: 0,
yStart: lastDiv.offsetTop + lastDiv.offsetHeight,
xEnd: game.offsetWidth,
yEnd: game.offsetHeight
}
} else {
console.log('lastDiv', lastDiv.offsetTop);
return {
xStart: lastDiv.offsetLeft + lastDiv.offsetWidth,
yStart: lastDiv.offsetTop,
xEnd: game.offsetWidth,
yEnd: game.offsetHeight
}
}
} else {
// Possible destinations: at the center bottom or at the right bottom.
const mainSectionChildren = Array.from(document.querySelectorAll<HTMLDivElement>('div.main-section > div').values());
const sidebarChildren = Array.from(document.querySelectorAll<HTMLDivElement>('aside.sidebar > div').values());
// No presentation? Let's center on the screen
if (mainSectionChildren.length === 0) {
return {
xStart: 0,
yStart: 0,
xEnd: game.offsetWidth,
yEnd: game.offsetHeight
}
}
// At this point, we know we have at least one element in the main section.
const lastPresentationDiv = mainSectionChildren[mainSectionChildren.length-1];
const presentationArea = (game.offsetHeight - (lastPresentationDiv.offsetTop + lastPresentationDiv.offsetHeight))
* (lastPresentationDiv.offsetLeft + lastPresentationDiv.offsetWidth);
let leftSideBar: number;
let bottomSideBar: number;
if (sidebarChildren.length === 0) {
leftSideBar = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('sidebar').offsetLeft;
bottomSideBar = 0;
} else {
const lastSideBarChildren = sidebarChildren[sidebarChildren.length - 1];
leftSideBar = lastSideBarChildren.offsetLeft;
bottomSideBar = lastSideBarChildren.offsetTop + lastSideBarChildren.offsetHeight;
}
const sideBarArea = (game.offsetWidth - leftSideBar)
* (game.offsetHeight - bottomSideBar);
if (presentationArea <= sideBarArea) {
return {
xStart: leftSideBar,
yStart: bottomSideBar,
xEnd: game.offsetWidth,
yEnd: game.offsetHeight
}
} else {
return {
xStart: 0,
yStart: lastPresentationDiv.offsetTop + lastPresentationDiv.offsetHeight,
xEnd: /*lastPresentationDiv.offsetLeft + lastPresentationDiv.offsetWidth*/ game.offsetWidth , // To avoid flickering when a chat start, we center on the center of the screen, not the center of the main content area
yEnd: game.offsetHeight
}
}
}
}
public addActionButton(id: string, text: string, callBack: Function, userInputManager: UserInputManager){
//delete previous element
this.removeActionButton(id, userInputManager);
+8 -256
View File
@@ -7,7 +7,7 @@ import type { UserSimplePeerInterface } from "./SimplePeer";
import { SoundMeter } from "../Phaser/Components/SoundMeter";
import { DISABLE_NOTIFICATIONS } from "../Enum/EnvironmentVariable";
import {
gameOverlayVisibilityStore, localStreamStore,
localStreamStore,
} from "../Stores/MediaStore";
import {
screenSharingLocalStreamStore
@@ -17,20 +17,13 @@ import {helpCameraSettingsVisibleStore} from "../Stores/HelpCameraSettingsStore"
export type UpdatedLocalStreamCallback = (media: MediaStream | null) => void;
export type StartScreenSharingCallback = (media: MediaStream) => void;
export type StopScreenSharingCallback = (media: MediaStream) => void;
export type ReportCallback = (message: string) => void;
export type ShowReportCallBack = (userId: string, userName: string | undefined) => void;
export type HelpCameraSettingsCallBack = () => void;
import {cowebsiteCloseButtonId} from "./CoWebsiteManager";
import {gameOverlayVisibilityStore} from "../Stores/GameOverlayStoreVisibility";
export class MediaManager {
private remoteVideo: Map<string, HTMLVideoElement> = new Map<string, HTMLVideoElement>();
//FIX ME SOUNDMETER: check stalability of sound meter calculation
//mySoundMeterElement: HTMLDivElement;
startScreenSharingCallBacks: Set<StartScreenSharingCallback> = new Set<StartScreenSharingCallback>();
stopScreenSharingCallBacks: Set<StopScreenSharingCallback> = new Set<StopScreenSharingCallback>();
showReportModalCallBacks: Set<ShowReportCallBack> = new Set<ShowReportCallBack>();
startScreenSharingCallBacks : Set<StartScreenSharingCallback> = new Set<StartScreenSharingCallback>();
stopScreenSharingCallBacks : Set<StopScreenSharingCallback> = new Set<StopScreenSharingCallback>();
@@ -40,21 +33,8 @@ export class MediaManager {
private userInputManager?: UserInputManager;
//FIX ME SOUNDMETER: check stalability of sound meter calculation
/*private mySoundMeter?: SoundMeter|null;
private soundMeters: Map<string, SoundMeter> = new Map<string, SoundMeter>();
private soundMeterElements: Map<string, HTMLDivElement> = new Map<string, HTMLDivElement>();*/
constructor() {
this.pingCameraStatus();
//FIX ME SOUNDMETER: check stability of sound meter calculation
/*this.mySoundMeterElement = (HtmlUtils.getElementByIdOrFail('mySoundMeter'));
this.mySoundMeterElement.childNodes.forEach((value: ChildNode, index) => {
this.mySoundMeterElement.children.item(index)?.classList.remove('active');
});*/
//Check of ask notification navigator permission
this.getNotification();
@@ -68,7 +48,6 @@ export class MediaManager {
}
});
let isScreenSharing = false;
screenSharingLocalStreamStore.subscribe((result) => {
if (result.type === 'error') {
console.error(result.error);
@@ -77,32 +56,7 @@ export class MediaManager {
}, this.userInputManager);
return;
}
if (result.stream !== null) {
isScreenSharing = true;
this.addScreenSharingActiveVideo('me', DivImportance.Normal);
HtmlUtils.getElementByIdOrFail<HTMLVideoElement>('screen-sharing-me').srcObject = result.stream;
} else {
if (isScreenSharing) {
isScreenSharing = false;
this.removeActiveScreenSharingVideo('me');
}
}
});
/*screenSharingAvailableStore.subscribe((available) => {
if (available) {
document.querySelector('.btn-monitor')?.classList.remove('hide');
} else {
document.querySelector('.btn-monitor')?.classList.add('hide');
}
});*/
}
public updateScene(){
//FIX ME SOUNDMETER: check stability of sound meter calculation
//this.updateSoudMeter();
}
public showGameOverlay(): void {
@@ -137,71 +91,6 @@ export class MediaManager {
gameOverlayVisibilityStore.hideGameOverlay();
}
addActiveVideo(user: UserSimplePeerInterface, userName: string = "") {
const userId = '' + user.userId
userName = userName.toUpperCase();
const color = this.getColorByString(userName);
const html = `
<div id="div-${userId}" class="video-container">
<div class="connecting-spinner"></div>
<div class="rtc-error" style="display: none"></div>
<i id="name-${userId}" style="background-color: ${color};">${userName}</i>
<img id="microphone-${userId}" title="mute" src="resources/logos/microphone-close.svg">
<button id="report-${userId}" class="report">
<img title="report this user" src="resources/logos/report.svg">
<span>Report/Block</span>
</button>
<video id="${userId}" autoplay playsinline></video>
<img src="resources/logos/blockSign.svg" id="blocking-${userId}" class="block-logo">
<div id="soundMeter-${userId}" class="sound-progress">
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
</div>
</div>
`;
layoutManager.add(DivImportance.Normal, userId, html);
this.remoteVideo.set(userId, HtmlUtils.getElementByIdOrFail<HTMLVideoElement>(userId));
//permit to create participant in discussion part
const showReportUser = () => {
for (const callBack of this.showReportModalCallBacks) {
callBack(userId, userName);
}
};
this.addNewParticipant(userId, userName, undefined, showReportUser);
const reportBanUserActionEl: HTMLImageElement = HtmlUtils.getElementByIdOrFail<HTMLImageElement>(`report-${userId}`);
reportBanUserActionEl.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
showReportUser();
});
}
addScreenSharingActiveVideo(userId: string, divImportance: DivImportance = DivImportance.Important){
userId = this.getScreenSharingId(userId);
const html = `
<div id="div-${userId}" class="video-container">
<video id="${userId}" autoplay playsinline></video>
</div>
`;
layoutManager.add(divImportance, userId, html);
this.remoteVideo.set(userId, HtmlUtils.getElementByIdOrFail<HTMLVideoElement>(userId));
}
private getScreenSharingId(userId: string): string {
return `screen-sharing-${userId}`;
}
@@ -248,61 +137,6 @@ export class MediaManager {
const blockLogoElement = HtmlUtils.getElementByIdOrFail<HTMLImageElement>('blocking-' + userId);
show ? blockLogoElement.classList.add('active') : blockLogoElement.classList.remove('active');
}
addStreamRemoteVideo(userId: string, stream: MediaStream): void {
const remoteVideo = this.remoteVideo.get(userId);
if (remoteVideo === undefined) {
throw `Unable to find video for ${userId}`;
}
remoteVideo.srcObject = stream;
//FIX ME SOUNDMETER: check stalability of sound meter calculation
//sound metter
/*const soundMeter = new SoundMeter();
soundMeter.connectToSource(stream, new AudioContext());
this.soundMeters.set(userId, soundMeter);
this.soundMeterElements.set(userId, HtmlUtils.getElementByIdOrFail<HTMLImageElement>('soundMeter-'+userId));*/
}
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(this.getScreenSharingId(userId));
if (remoteVideo === undefined) {
this.addScreenSharingActiveVideo(userId);
}
this.addStreamRemoteVideo(this.getScreenSharingId(userId), stream);
}
removeActiveVideo(userId: string) {
layoutManager.remove(userId);
this.remoteVideo.delete(userId);
//FIX ME SOUNDMETER: check stalability of sound meter calculation
/*this.soundMeters.get(userId)?.stop();
this.soundMeters.delete(userId);
this.soundMeterElements.delete(userId);*/
//permit to remove user in discussion part
this.removeParticipant(userId);
}
removeActiveScreenSharingVideo(userId: string) {
this.removeActiveVideo(this.getScreenSharingId(userId))
}
isConnecting(userId: string): void {
const connectingSpinnerDiv = this.getSpinner(userId);
if (connectingSpinnerDiv === null) {
return;
}
connectingSpinnerDiv.style.display = 'block';
}
isConnected(userId: string): void {
const connectingSpinnerDiv = this.getSpinner(userId);
if (connectingSpinnerDiv === null) {
return;
}
connectingSpinnerDiv.style.display = 'none';
}
isError(userId: string): void {
console.info("isError", `div-${userId}`);
@@ -326,33 +160,11 @@ export class MediaManager {
if (!element) {
return null;
}
const connnectingSpinnerDiv = element.getElementsByClassName('connecting-spinner').item(0) as HTMLDivElement | null;
return connnectingSpinnerDiv;
const connectingSpinnerDiv = element.getElementsByClassName('connecting-spinner').item(0) as HTMLDivElement|null;
return connectingSpinnerDiv;
}
private getColorByString(str: String): String | null {
let hash = 0;
if (str.length === 0) return null;
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
hash = hash & hash;
}
let color = '#';
for (let i = 0; i < 3; i++) {
const value = (hash >> (i * 8)) & 255;
color += ('00' + value.toString(16)).substr(-2);
}
return color;
}
public addNewParticipant(userId: number | string, name: string | undefined, img?: string, showReportUserCallBack?: ShowReportCallBack) {
discussionManager.addParticipant(userId, name, img, false, showReportUserCallBack);
}
public removeParticipant(userId: number | string) {
discussionManager.removeParticipant(userId);
}
public addTriggerCloseJitsiFrameButton(id: String, Function: Function) {
public addTriggerCloseJitsiFrameButton(id: String, Function: Function){
this.triggerCloseJistiFrame.set(id, Function);
}
@@ -365,16 +177,6 @@ export class MediaManager {
callback();
}
}
/**
* For some reasons, the microphone muted icon or the stream is not always up to date.
* Here, every 30 seconds, we are "reseting" the streams and sending again the constraints to the other peers via the data channel again (see SimplePeer::pushVideoToRemoteUser)
**/
private pingCameraStatus() {
/*setInterval(() => {
console.log('ping camera status');
this.triggerUpdatedLocalStreamCallbacks(this.localStream);
}, 30000);*/
}
public addNewMessage(name: string, message: string, isMe: boolean = false) {
discussionManager.addMessage(name, message, isMe);
@@ -389,61 +191,11 @@ export class MediaManager {
discussionManager.onSendMessageCallback(userId, callback);
}
get activatedDiscussion() {
return discussionManager.activatedDiscussion;
}
public setUserInputManager(userInputManager: UserInputManager) {
public setUserInputManager(userInputManager : UserInputManager){
this.userInputManager = userInputManager;
discussionManager.setUserInputManager(userInputManager);
}
public setShowReportModalCallBacks(callback: ShowReportCallBack) {
this.showReportModalCallBacks.add(callback);
}
//FIX ME SOUNDMETER: check stalability of sound meter calculation
/*updateSoudMeter(){
try{
const volume = parseInt(((this.mySoundMeter ? this.mySoundMeter.getVolume() : 0) / 10).toFixed(0));
this.setVolumeSoundMeter(volume, this.mySoundMeterElement);
for(const indexUserId of this.soundMeters.keys()){
const soundMeter = this.soundMeters.get(indexUserId);
const soundMeterElement = this.soundMeterElements.get(indexUserId);
if (!soundMeter || !soundMeterElement) {
return;
}
const volumeByUser = parseInt((soundMeter.getVolume() / 10).toFixed(0));
this.setVolumeSoundMeter(volumeByUser, soundMeterElement);
}
} catch (err) {
//console.error(err);
}
}*/
private setVolumeSoundMeter(volume: number, element: HTMLDivElement) {
if (volume <= 0 && !element.classList.contains('active')) {
return;
}
element.classList.remove('active');
if (volume <= 0) {
return;
}
element.classList.add('active');
element.childNodes.forEach((value: ChildNode, index) => {
const elementChildren = element.children.item(index);
if (!elementChildren) {
return;
}
elementChildren.classList.remove('active');
if ((index + 1) > volume) {
return;
}
elementChildren.classList.add('active');
});
}
public getNotification(){
//Get notification
if (!DISABLE_NOTIFICATIONS && window.Notification && Notification.permission !== "granted") {
+58 -24
View File
@@ -1,9 +1,11 @@
import type * as SimplePeerNamespace from "simple-peer";
import {mediaManager} from "./MediaManager";
import {STUN_SERVER, TURN_SERVER, TURN_USER, TURN_PASSWORD} from "../Enum/EnvironmentVariable";
import {STUN_SERVER, TURN_PASSWORD, TURN_SERVER, TURN_USER} from "../Enum/EnvironmentVariable";
import type {RoomConnection} from "../Connexion/RoomConnection";
import {MESSAGE_TYPE_CONSTRAINT} from "./VideoPeer";
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');
@@ -17,9 +19,12 @@ export class ScreenSharingPeer extends Peer {
private isReceivingStream:boolean = false;
public toClose: boolean = false;
public _connected: boolean = false;
private userId: number;
public readonly userId: number;
public readonly uniqueId: string;
public readonly streamStore: Readable<MediaStream | null>;
public readonly statusStore: Readable<PeerStatus>;
constructor(user: UserSimplePeerInterface, initiator: boolean, 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,
@@ -38,6 +43,55 @@ export class ScreenSharingPeer extends Peer {
});
this.userId = user.userId;
this.uniqueId = 'screensharing_'+this.userId;
this.streamStore = readable<MediaStream|null>(null, (set) => {
const onStream = (stream: MediaStream|null) => {
videoFocusStore.focus(this);
set(stream);
};
const onData = (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.
// 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'));
if (message.streamEnded !== true) {
console.error('Unexpected message on screen sharing peer connection');
return;
}
set(null);
}
this.on('stream', onStream);
this.on('data', onData);
return () => {
this.off('stream', onStream);
this.off('data', onData);
};
});
this.statusStore = readable<PeerStatus>("connecting", (set) => {
const onConnect = () => {
set('connected');
};
const onError = () => {
set('error');
};
const onClose = () => {
set('closed');
};
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);
};
});
//start listen signal for the peer connection
this.on('signal', (data: unknown) => {
@@ -54,27 +108,13 @@ export class ScreenSharingPeer extends Peer {
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');
return;
}
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', () => {
this._connected = true;
// FIXME: we need to put the loader on the screen sharing connection
mediaManager.isConnected("" + this.userId);
console.info(`connect => ${this.userId}`);
});
@@ -88,7 +128,6 @@ export class ScreenSharingPeer extends Peer {
}
private sendWebrtcScreenSharingSignal(data: unknown) {
//console.log("sendWebrtcScreenSharingSignal", data);
try {
this.connection.sendWebrtcScreenSharingSignal(data, this.userId);
}catch (e) {
@@ -100,13 +139,9 @@ export class ScreenSharingPeer extends Peer {
* 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;
}
}
@@ -121,7 +156,6 @@ export class ScreenSharingPeer extends Peer {
if(!this.toClose){
return;
}
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);
+22 -25
View File
@@ -6,19 +6,15 @@ import {
mediaManager,
StartScreenSharingCallback,
StopScreenSharingCallback,
UpdatedLocalStreamCallback
} 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 {connectionManager} from "../Connexion/ConnectionManager";
import {GameConnexionTypes} from "../Url/UrlManager";
import {blackListManager} from "./BlackListManager";
import {get} from "svelte/store";
import {localStreamStore, LocalStreamStoreValue, obtainedMediaConstraintStore} from "../Stores/MediaStore";
import {screenSharingLocalStreamStore} from "../Stores/ScreenSharingStore";
import {DivImportance, layoutManager} from "./LayoutManager";
import {HtmlUtils} from "./HtmlUtils";
import {discussionManager} from "./DiscussionManager";
export interface UserSimplePeerInterface{
userId: number;
@@ -28,8 +24,10 @@ export interface UserSimplePeerInterface{
webRtcPassword?: string|undefined;
}
export type RemotePeer = VideoPeer | ScreenSharingPeer;
export interface PeerConnectionListener {
onConnect(user: UserSimplePeerInterface): void;
onConnect(user: RemotePeer): void;
onDisconnect(userId: number): void;
}
@@ -124,7 +122,6 @@ export class SimplePeer {
// This would be symmetrical to the way we handle disconnection.
//start connection
//console.log('receiveWebrtcStart. Initiator: ', user.initiator)
if(!user.initiator){
return;
}
@@ -159,20 +156,15 @@ export class SimplePeer {
let name = user.name;
if (!name) {
const userSearch = this.Users.find((userSearch: UserSimplePeerInterface) => userSearch.userId === user.userId);
if (userSearch) {
name = userSearch.name;
}
name = this.getName(user.userId);
}
mediaManager.removeActiveVideo("" + user.userId);
mediaManager.addActiveVideo(user, name);
discussionManager.removeParticipant(user.userId);
this.lastWebrtcUserName = user.webRtcUser;
this.lastWebrtcPassword = user.webRtcPassword;
const peer = new VideoPeer(user, user.initiator ? user.initiator : false, this.Connection, localStream);
const peer = new VideoPeer(user, user.initiator ? user.initiator : false, name, this.Connection, localStream);
//permit to send message
mediaManager.addSendMessageCallback(user.userId,(message: string) => {
@@ -196,11 +188,20 @@ export class SimplePeer {
this.PeerConnectionArray.set(user.userId, peer);
for (const peerConnectionListener of this.peerConnectionListeners) {
peerConnectionListener.onConnect(user);
peerConnectionListener.onConnect(peer);
}
return peer;
}
private getName(userId: number): string {
const userSearch = this.Users.find((userSearch: UserSimplePeerInterface) => userSearch.userId === userId);
if (userSearch) {
return userSearch.name || '';
} else {
return '';
}
}
/**
* create peer connection to bind users
*/
@@ -221,23 +222,19 @@ export class SimplePeer {
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);
}
// Enrich the user with last known credentials (if they are not set in the user object, which happens when a user triggers the screen sharing)
if (user.webRtcUser === undefined) {
user.webRtcUser = this.lastWebrtcUserName;
user.webRtcPassword = this.lastWebrtcPassword;
}
const peer = new ScreenSharingPeer(user, user.initiator ? user.initiator : false, this.Connection, stream);
const name = this.getName(user.userId);
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) {
peerConnectionListener.onConnect(user);
peerConnectionListener.onConnect(peer);
}
return peer;
}
@@ -288,7 +285,7 @@ export class SimplePeer {
*/
private closeScreenSharingConnection(userId : number) {
try {
mediaManager.removeActiveScreenSharingVideo("" + userId);
//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")
+73 -9
View File
@@ -5,11 +5,14 @@ import type {RoomConnection} from "../Connexion/RoomConnection";
import {blackListManager} from "./BlackListManager";
import type {Subscription} from "rxjs";
import type {UserSimplePeerInterface} from "./SimplePeer";
import {get} from "svelte/store";
import {get, readable, Readable} from "svelte/store";
import {obtainedMediaConstraintStore} from "../Stores/MediaStore";
import {discussionManager} from "./DiscussionManager";
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';
@@ -22,12 +25,15 @@ export class VideoPeer extends Peer {
public _connected: boolean = false;
private remoteStream!: MediaStream;
private blocked: boolean = false;
private userId: number;
private userName: string;
public readonly userId: number;
public readonly uniqueId: string;
private onBlockSubscribe: Subscription;
private onUnBlockSubscribe: Subscription;
public readonly streamStore: Readable<MediaStream | null>;
public readonly statusStore: Readable<PeerStatus>;
public readonly constraintsStore: Readable<MediaStreamConstraints|null>;
constructor(public user: UserSimplePeerInterface, initiator: boolean, 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,
@@ -46,7 +52,68 @@ export class VideoPeer extends Peer {
});
this.userId = user.userId;
this.userName = user.name || '';
this.uniqueId = 'video_'+this.userId;
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'));
if (message.type === MESSAGE_TYPE_CONSTRAINT) {
if (!message.video) {
set(null);
}
}
});
}
this.on('stream', onStream);
this.on('data', onData);
return () => {
this.off('stream', onStream);
this.off('data', onData);
};
});
this.constraintsStore = readable<MediaStreamConstraints|null>(null, (set) => {
const onData = (chunk: Buffer) => {
const message = JSON.parse(chunk.toString('utf8'));
if(message.type === MESSAGE_TYPE_CONSTRAINT) {
set(message);
}
}
this.on('data', onData);
return () => {
this.off('data', onData);
};
});
this.statusStore = readable<PeerStatus>("connecting", (set) => {
const onConnect = () => {
set('connected');
};
const onError = () => {
set('error');
};
const onClose = () => {
set('closed');
};
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);
};
});
//start listen signal for the peer connection
this.on('signal', (data: unknown) => {
@@ -69,8 +136,6 @@ export class VideoPeer extends Peer {
this.on('connect', () => {
this._connected = true;
mediaManager.isConnected("" + this.userId);
console.info(`connect => ${this.userId}`);
});
this.on('data', (chunk: Buffer) => {
@@ -152,7 +217,6 @@ export class VideoPeer extends Peer {
if (blackListManager.isBlackListed(this.userId) || this.blocked) {
this.toggleRemoteStream(false);
}
mediaManager.addStreamRemoteVideo("" + this.userId, stream);
}catch (err){
console.error(err);
}
@@ -169,7 +233,7 @@ export class VideoPeer extends Peer {
}
this.onBlockSubscribe.unsubscribe();
this.onUnBlockSubscribe.unsubscribe();
mediaManager.removeActiveVideo("" + this.userId);
discussionManager.removeParticipant(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.
super.destroy(error);