partey_workadventure/front/src/WebRtc/MediaManager.ts
2021-06-17 10:07:27 +02:00

304 lines
11 KiB
TypeScript

import {DivImportance, layoutManager} from "./LayoutManager";
import {HtmlUtils} from "./HtmlUtils";
import {discussionManager, SendMessageCallback} from "./DiscussionManager";
import type {UserInputManager} from "../Phaser/UserInput/UserInputManager";
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";
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";
export class MediaManager {
startScreenSharingCallBacks : Set<StartScreenSharingCallback> = new Set<StartScreenSharingCallback>();
stopScreenSharingCallBacks : Set<StopScreenSharingCallback> = new Set<StopScreenSharingCallback>();
private focused : boolean = true;
private triggerCloseJistiFrame : Map<String, Function> = new Map<String, Function>();
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();
localStreamStore.subscribe((result) => {
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);
return;
}
});
//let isScreenSharing = false;
screenSharingLocalStreamStore.subscribe((result) => {
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);
return;
}
});
}
public showGameOverlay(): void {
const gameOverlay = HtmlUtils.getElementByIdOrFail('game-overlay');
gameOverlay.classList.add('active');
const buttonCloseFrame = HtmlUtils.getElementByIdOrFail(cowebsiteCloseButtonId);
const functionTrigger = () => {
this.triggerCloseJitsiFrameButton();
}
buttonCloseFrame.removeEventListener('click', () => {
buttonCloseFrame.blur();
functionTrigger();
});
gameOverlayVisibilityStore.showGameOverlay();
}
public hideGameOverlay(): void {
const gameOverlay = HtmlUtils.getElementByIdOrFail('game-overlay');
gameOverlay.classList.remove('active');
const buttonCloseFrame = HtmlUtils.getElementByIdOrFail(cowebsiteCloseButtonId);
const functionTrigger = () => {
this.triggerCloseJitsiFrameButton();
}
buttonCloseFrame.addEventListener('click', () => {
buttonCloseFrame.blur();
functionTrigger();
});
gameOverlayVisibilityStore.hideGameOverlay();
}
private getScreenSharingId(userId: string): string {
return `screen-sharing-${userId}`;
}
disabledMicrophoneByUserId(userId: number){
const element = document.getElementById(`microphone-${userId}`);
if(!element){
return;
}
element.classList.add('active') //todo: why does a method 'disable' add a class 'active'?
}
enabledMicrophoneByUserId(userId: number){
const element = document.getElementById(`microphone-${userId}`);
if(!element){
return;
}
element.classList.remove('active') //todo: why does a method 'enable' remove a class 'active'?
}
disabledVideoByUserId(userId: number) {
let element = document.getElementById(`${userId}`);
if (element) {
element.style.opacity = "0";
}
element = document.getElementById(`name-${userId}`);
if (element) {
element.style.display = "block";
}
}
enabledVideoByUserId(userId: number){
let element = document.getElementById(`${userId}`);
if(element){
element.style.opacity = "1";
}
element = document.getElementById(`name-${userId}`);
if(element){
element.style.display = "none";
}
}
toggleBlockLogo(userId: number, show: boolean): void {
const blockLogoElement = HtmlUtils.getElementByIdOrFail<HTMLImageElement>('blocking-'+userId);
show ? blockLogoElement.classList.add('active') : blockLogoElement.classList.remove('active');
}
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}`);
const element = document.getElementById(`div-${userId}`);
if(!element){
return;
}
const errorDiv = element.getElementsByClassName('rtc-error').item(0) as HTMLDivElement|null;
if (errorDiv === null) {
return;
}
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 connnectingSpinnerDiv = element.getElementsByClassName('connecting-spinner').item(0) as HTMLDivElement|null;
return connnectingSpinnerDiv;
}
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 addTriggerCloseJitsiFrameButton(id: String, Function: Function){
this.triggerCloseJistiFrame.set(id, Function);
}
public removeTriggerCloseJitsiFrameButton(id: String){
this.triggerCloseJistiFrame.delete(id);
}
private triggerCloseJitsiFrameButton(): void {
for (const callback of this.triggerCloseJistiFrame.values()) {
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);
//when there are new message, show discussion
if(!discussionManager.activatedDiscussion) {
discussionManager.showDiscussionPart();
}
}
public addSendMessageCallback(userId: string|number, callback: SendMessageCallback){
discussionManager.onSendMessageCallback(userId, callback);
}
get activatedDiscussion(){
return discussionManager.activatedDiscussion;
}
public setUserInputManager(userInputManager : UserInputManager){
this.userInputManager = userInputManager;
discussionManager.setUserInputManager(userInputManager);
}
public getNotification(){
//Get notification
if (!DISABLE_NOTIFICATIONS && window.Notification && Notification.permission !== "granted") {
if (this.checkNotificationPromise()) {
Notification.requestPermission().catch((err) => {
console.error(`Notification permission error`, err);
});
} else {
Notification.requestPermission();
}
}
}
/**
* Return true if the browser supports the modern version of the Notification API (which is Promise based) or false
* if we are on Safari...
*
* See https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API/Using_the_Notifications_API
*/
private checkNotificationPromise(): boolean {
try {
Notification.requestPermission().then();
} catch(e) {
return false;
}
return true;
}
public createNotification(userName: string){
if(this.focused){
return;
}
if (window.Notification && Notification.permission === "granted") {
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',
};
new Notification(title, options);
//new Notification(`Hi! ${userName} wants to discuss with you, don't be afraid!`);
}
}
}
export const mediaManager = new MediaManager();