workadventure/front/src/WebRtc/MediaManager.ts

472 lines
16 KiB
TypeScript
Raw Normal View History

import * as SimplePeerNamespace from "simple-peer";
import {DivImportance, layoutManager} from "./LayoutManager";
2020-06-10 12:15:25 +02:00
const videoConstraint: boolean|MediaTrackConstraints = {
2020-05-03 14:29:45 +02:00
width: { ideal: 1280 },
height: { ideal: 720 },
facingMode: "user"
};
type UpdatedLocalStreamCallback = (media: MediaStream) => void;
type UpdatedScreenSharingCallback = (media: MediaStream) => void;
2020-06-23 12:24:36 +02:00
// TODO: Split MediaManager in 2 classes: MediaManagerUI (in charge of HTML) and MediaManager (singleton in charge of the camera only)
// TODO: verify that microphone event listeners are not triggered plenty of time NOW (since MediaManager is created many times!!!!)
export class MediaManager {
2020-06-03 11:55:31 +02:00
localStream: MediaStream|null = null;
localScreenCapture: MediaStream|null = null;
2020-06-10 12:15:25 +02:00
private remoteVideo: Map<string, HTMLVideoElement> = new Map<string, HTMLVideoElement>();
2020-06-03 11:55:31 +02:00
myCamVideo: HTMLVideoElement;
2020-06-10 12:15:25 +02:00
cinemaClose: HTMLImageElement;
cinema: HTMLImageElement;
monitorClose: HTMLImageElement;
monitor: HTMLImageElement;
2020-06-10 12:15:25 +02:00
microphoneClose: HTMLImageElement;
microphone: HTMLImageElement;
2020-06-03 11:55:31 +02:00
webrtcInAudio: HTMLAudioElement;
2020-06-10 12:15:25 +02:00
constraintsMedia : MediaStreamConstraints = {
2020-05-03 14:29:45 +02:00
audio: true,
video: videoConstraint
};
updatedLocalStreamCallBacks : Set<UpdatedLocalStreamCallback> = new Set<UpdatedLocalStreamCallback>();
updatedScreenSharingCallBacks : Set<UpdatedScreenSharingCallback> = new Set<UpdatedScreenSharingCallback>();
constructor() {
2020-06-03 11:55:31 +02:00
this.myCamVideo = this.getElementByIdOrFail<HTMLVideoElement>('myCamVideo');
this.webrtcInAudio = this.getElementByIdOrFail<HTMLAudioElement>('audio-webrtc-in');
this.webrtcInAudio.volume = 0.2;
2020-04-26 20:55:20 +02:00
2020-06-10 12:15:25 +02:00
this.microphoneClose = this.getElementByIdOrFail<HTMLImageElement>('microphone-close');
2020-05-03 17:19:42 +02:00
this.microphoneClose.style.display = "none";
2020-06-10 12:15:25 +02:00
this.microphoneClose.addEventListener('click', (e: MouseEvent) => {
e.preventDefault();
this.enableMicrophone();
//update tracking
});
2020-06-10 12:15:25 +02:00
this.microphone = this.getElementByIdOrFail<HTMLImageElement>('microphone');
this.microphone.addEventListener('click', (e: MouseEvent) => {
e.preventDefault();
this.disableMicrophone();
//update tracking
});
2020-06-10 12:15:25 +02:00
this.cinemaClose = this.getElementByIdOrFail<HTMLImageElement>('cinema-close');
2020-05-03 17:19:42 +02:00
this.cinemaClose.style.display = "none";
2020-06-10 12:15:25 +02:00
this.cinemaClose.addEventListener('click', (e: MouseEvent) => {
e.preventDefault();
this.enableCamera();
//update tracking
});
2020-06-10 12:15:25 +02:00
this.cinema = this.getElementByIdOrFail<HTMLImageElement>('cinema');
this.cinema.addEventListener('click', (e: MouseEvent) => {
e.preventDefault();
this.disableCamera();
//update tracking
});
this.monitorClose = this.getElementByIdOrFail<HTMLImageElement>('monitor-close');
this.monitorClose.style.display = "block";
2020-08-18 14:59:50 +02:00
this.monitorClose.addEventListener('click', (e: MouseEvent) => {
e.preventDefault();
this.enableScreenSharing();
//update tracking
});
this.monitor = this.getElementByIdOrFail<HTMLImageElement>('monitor');
this.monitor.style.display = "none";
2020-08-18 14:59:50 +02:00
this.monitor.addEventListener('click', (e: MouseEvent) => {
e.preventDefault();
this.disableScreenSharing();
//update tracking
});
2020-04-26 20:55:20 +02:00
}
public onUpdateLocalStream(callback: UpdatedLocalStreamCallback): void {
this.updatedLocalStreamCallBacks.add(callback);
}
public onUpdateScreenSharing(callback: UpdatedScreenSharingCallback): void {
this.updatedScreenSharingCallBacks.add(callback);
}
removeUpdateLocalStreamEventListener(callback: UpdatedLocalStreamCallback): void {
this.updatedLocalStreamCallBacks.delete(callback);
}
private triggerUpdatedLocalStreamCallbacks(stream: MediaStream): void {
for (const callback of this.updatedLocalStreamCallBacks) {
callback(stream);
}
}
private triggerUpdatedScreenSharingCallbacks(stream: MediaStream): void {
for (const callback of this.updatedScreenSharingCallBacks) {
callback(stream);
}
}
showGameOverlay(){
const gameOverlay = this.getElementByIdOrFail('game-overlay');
gameOverlay.classList.add('active');
}
private enableCamera() {
this.cinemaClose.style.display = "none";
this.cinema.style.display = "block";
2020-05-03 14:29:45 +02:00
this.constraintsMedia.video = videoConstraint;
this.getCamera().then((stream: MediaStream) => {
this.triggerUpdatedLocalStreamCallbacks(stream);
2020-05-03 17:19:42 +02:00
});
}
private disableCamera() {
this.cinemaClose.style.display = "block";
this.cinema.style.display = "none";
this.constraintsMedia.video = false;
this.myCamVideo.srcObject = null;
if (this.localStream) {
this.localStream.getVideoTracks().forEach((MediaStreamTrack: MediaStreamTrack) => {
MediaStreamTrack.stop();
2020-04-19 22:30:42 +02:00
});
}
2020-05-03 17:19:42 +02:00
this.getCamera().then((stream) => {
this.triggerUpdatedLocalStreamCallbacks(stream);
2020-05-03 17:19:42 +02:00
});
}
private enableMicrophone() {
this.microphoneClose.style.display = "none";
this.microphone.style.display = "block";
this.constraintsMedia.audio = true;
2020-05-03 17:19:42 +02:00
this.getCamera().then((stream) => {
this.triggerUpdatedLocalStreamCallbacks(stream);
2020-05-03 17:19:42 +02:00
});
}
private disableMicrophone() {
this.microphoneClose.style.display = "block";
this.microphone.style.display = "none";
this.constraintsMedia.audio = false;
2020-04-19 22:30:42 +02:00
if(this.localStream) {
this.localStream.getAudioTracks().forEach((MediaStreamTrack: MediaStreamTrack) => {
MediaStreamTrack.stop();
2020-04-19 22:30:42 +02:00
});
}
2020-05-03 17:19:42 +02:00
this.getCamera().then((stream) => {
this.triggerUpdatedLocalStreamCallbacks(stream);
2020-05-03 17:19:42 +02:00
});
2020-04-26 20:55:20 +02:00
}
private enableScreenSharing() {
this.monitorClose.style.display = "none";
this.monitor.style.display = "block";
this.getScreenMedia().then((stream) => {
this.triggerUpdatedScreenSharingCallbacks(stream);
});
}
private disableScreenSharing() {
this.monitorClose.style.display = "block";
this.monitor.style.display = "none";
this.localScreenCapture?.getTracks().forEach((track: MediaStreamTrack) => {
track.stop();
});
this.localScreenCapture = null;
this.getCamera().then((stream) => {
this.triggerUpdatedScreenSharingCallbacks(stream);
});
}
//get screen
getScreenMedia() : Promise<MediaStream>{
try {
return this._startScreenCapture()
.then((stream: MediaStream) => {
this.localScreenCapture = stream;
return stream;
})
2020-08-18 14:59:50 +02:00
.catch((err: unknown) => {
console.error("Error => getScreenMedia => ", err);
throw err;
});
}catch (err) {
return new Promise((resolve, reject) => { // eslint-disable-line no-unused-vars
reject(err);
});
}
}
private _startScreenCapture() {
2020-08-18 14:59:50 +02:00
// getDisplayMedia was moved to mediaDevices in 2018. Typescript definitions are not up to date yet.
// See: https://github.com/w3c/mediacapture-screen-share/pull/86
// https://github.com/microsoft/TypeScript/issues/31821
if ((navigator as any).getDisplayMedia) { // eslint-disable-line @typescript-eslint/no-explicit-any
return (navigator as any).getDisplayMedia({video: true}); // eslint-disable-line @typescript-eslint/no-explicit-any
} else if ((navigator.mediaDevices as any).getDisplayMedia) { // eslint-disable-line @typescript-eslint/no-explicit-any
return (navigator.mediaDevices as any).getDisplayMedia({video: true}); // eslint-disable-line @typescript-eslint/no-explicit-any
} else {
//return navigator.mediaDevices.getUserMedia(({video: {mediaSource: 'screen'}} as any));
return new Promise((resolve, reject) => { // eslint-disable-line no-unused-vars
reject("error sharing screen");
});
}
}
//get camera
2020-06-25 10:43:27 +02:00
async getCamera(): Promise<MediaStream> {
if (navigator.mediaDevices === undefined) {
2020-06-25 10:43:27 +02:00
if (window.location.protocol === 'http:') {
throw new Error('Unable to access your camera or microphone. You need to use a HTTPS connection.');
} else {
throw new Error('Unable to access your camera or microphone. Your browser is too old.');
}
}
2020-05-13 16:56:22 +02:00
try {
2020-06-25 11:35:20 +02:00
const stream = await navigator.mediaDevices.getUserMedia(this.constraintsMedia);
2020-06-25 10:43:27 +02:00
this.localStream = stream;
this.myCamVideo.srcObject = this.localStream;
return stream;
//TODO resize remote cam
/*console.log(this.localStream.getTracks());
let videoMediaStreamTrack = this.localStream.getTracks().find((media : MediaStreamTrack) => media.kind === "video");
let {width, height} = videoMediaStreamTrack.getSettings();
console.info(`${width}x${height}`); // 6*/
} catch (err) {
console.info("error get media ", this.constraintsMedia.video, this.constraintsMedia.audio, err);
this.localStream = null;
throw err;
2020-05-13 16:56:22 +02:00
}
}
2020-04-25 20:29:03 +02:00
2020-06-23 12:24:36 +02:00
setCamera(id: string): Promise<MediaStream> {
let video = this.constraintsMedia.video;
if (typeof(video) === 'boolean' || video === undefined) {
video = {}
}
2020-06-25 10:33:26 +02:00
video.deviceId = {
exact: id
};
2020-06-23 12:24:36 +02:00
return this.getCamera();
}
setMicrophone(id: string): Promise<MediaStream> {
let audio = this.constraintsMedia.audio;
if (typeof(audio) === 'boolean' || audio === undefined) {
audio = {}
}
2020-06-25 11:26:55 +02:00
audio.deviceId = {
exact: id
};
2020-06-23 12:24:36 +02:00
return this.getCamera();
}
2020-04-25 20:29:03 +02:00
/**
*
* @param userId
*/
addActiveVideo(userId : string, userName: string = ""){
this.webrtcInAudio.play();
userName = userName.toUpperCase();
2020-06-09 23:13:26 +02:00
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}" src="resources/logos/microphone-close.svg">
<video id="${userId}" autoplay></video>
</div>
`;
layoutManager.add(DivImportance.Normal, userId, html);
2020-06-10 12:15:25 +02:00
this.remoteVideo.set(userId, this.getElementByIdOrFail<HTMLVideoElement>(userId));
}
/**
*
* @param userId
*/
2020-06-11 23:18:06 +02:00
addScreenSharingActiveVideo(userId : string){
this.webrtcInAudio.play();
userId = `screen-sharing-${userId}`;
const html = `
<div id="div-${userId}" class="video-container">
<video id="${userId}" autoplay></video>
</div>
`;
layoutManager.add(DivImportance.Important, userId, html);
this.remoteVideo.set(userId, this.getElementByIdOrFail<HTMLVideoElement>(userId));
}
/**
*
* @param userId
*/
disabledMicrophoneByUserId(userId: string){
2020-06-09 23:13:26 +02:00
const element = document.getElementById(`microphone-${userId}`);
if(!element){
return;
}
element.classList.add('active')
}
/**
*
* @param userId
*/
enabledMicrophoneByUserId(userId: string){
2020-06-09 23:13:26 +02:00
const element = document.getElementById(`microphone-${userId}`);
if(!element){
return;
}
element.classList.remove('active')
}
/**
*
* @param userId
*/
2020-05-14 20:54:34 +02:00
disabledVideoByUserId(userId: string) {
let element = document.getElementById(`${userId}`);
2020-05-14 20:54:34 +02:00
if (element) {
element.style.opacity = "0";
}
element = document.getElementById(`name-${userId}`);
if (element) {
element.style.display = "block";
}
}
/**
*
* @param userId
*/
enabledVideoByUserId(userId: string){
let element = document.getElementById(`${userId}`);
2020-05-14 20:54:34 +02:00
if(element){
element.style.opacity = "1";
}
element = document.getElementById(`name-${userId}`);
if(element){
element.style.display = "none";
}
}
2020-05-02 20:46:02 +02:00
/**
*
* @param userId
* @param stream
*/
addStreamRemoteVideo(userId : string, stream : MediaStream){
2020-06-10 12:15:25 +02:00
const remoteVideo = this.remoteVideo.get(userId);
if (remoteVideo === undefined) {
console.error('Unable to find video for ', userId);
return;
}
remoteVideo.srcObject = stream;
2020-05-02 20:46:02 +02:00
}
2020-06-11 23:18:06 +02:00
addStreamRemoteScreenSharing(userId : string, stream : MediaStream){
this.addStreamRemoteVideo(`screen-sharing-${userId}`, stream);
}
2020-05-02 20:46:02 +02:00
/**
*
* @param userId
*/
removeActiveVideo(userId : string){
layoutManager.remove(userId);
2020-06-10 12:15:25 +02:00
this.remoteVideo.delete(userId);
2020-04-25 20:29:03 +02:00
}
2020-06-11 23:18:06 +02:00
removeActiveScreenSharingVideo(userId : string) {
this.removeActiveVideo(`screen-sharing-${userId}`)
}
isConnecting(userId : string): void {
2020-06-09 23:13:26 +02:00
const connectingSpinnerDiv = this.getSpinner(userId);
if (connectingSpinnerDiv === null) {
return;
}
connectingSpinnerDiv.style.display = 'block';
}
isConnected(userId : string): void {
2020-06-09 23:13:26 +02:00
const connectingSpinnerDiv = this.getSpinner(userId);
if (connectingSpinnerDiv === null) {
return;
}
connectingSpinnerDiv.style.display = 'none';
}
isError(userId : string): void {
console.log("isError", `div-${userId}`);
2020-06-09 23:13:26 +02:00
const element = document.getElementById(`div-${userId}`);
if(!element){
return;
}
2020-06-09 23:13:26 +02:00
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(`screen-sharing-${userId}`);
}
private getSpinner(userId : string): HTMLDivElement|null {
2020-06-09 23:13:26 +02:00
const element = document.getElementById(`div-${userId}`);
if(!element){
return null;
}
2020-06-09 23:13:26 +02:00
const connnectingSpinnerDiv = element.getElementsByClassName('connecting-spinner').item(0) as HTMLDivElement|null;
return connnectingSpinnerDiv;
}
/**
*
* @param str
*/
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++) {
2020-06-09 23:13:26 +02:00
const value = (hash >> (i * 8)) & 255;
color += ('00' + value.toString(16)).substr(-2);
}
return color;
}
2020-06-03 11:55:31 +02:00
private getElementByIdOrFail<T extends HTMLElement>(id: string): T {
2020-06-09 23:13:26 +02:00
const elem = document.getElementById(id);
2020-06-03 11:55:31 +02:00
if (elem === null) {
throw new Error("Cannot find HTML element with id '"+id+"'");
}
// FIXME: does not check the type of the returned type
return elem as T;
}
}
export const mediaManager = new MediaManager();