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"
|
|
|
|
};
|
2020-06-23 14:56:57 +02:00
|
|
|
|
|
|
|
type UpdatedLocalStreamCallback = (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;
|
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;
|
|
|
|
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
|
|
|
|
};
|
2020-06-23 14:56:57 +02:00
|
|
|
updatedLocalStreamCallBacks : Set<UpdatedLocalStreamCallback> = new Set<UpdatedLocalStreamCallback>();
|
2020-04-19 19:32:38 +02:00
|
|
|
|
2020-06-23 14:56:57 +02:00
|
|
|
constructor() {
|
2020-06-03 11:55:31 +02:00
|
|
|
this.myCamVideo = this.getElementByIdOrFail<HTMLVideoElement>('myCamVideo');
|
|
|
|
this.webrtcInAudio = this.getElementByIdOrFail<HTMLAudioElement>('audio-webrtc-in');
|
2020-05-14 20:39:30 +02:00
|
|
|
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) => {
|
2020-04-19 19:32:38 +02:00
|
|
|
e.preventDefault();
|
|
|
|
this.enabledMicrophone();
|
|
|
|
//update tracking
|
|
|
|
});
|
2020-06-10 12:15:25 +02:00
|
|
|
this.microphone = this.getElementByIdOrFail<HTMLImageElement>('microphone');
|
|
|
|
this.microphone.addEventListener('click', (e: MouseEvent) => {
|
2020-04-19 19:32:38 +02:00
|
|
|
e.preventDefault();
|
|
|
|
this.disabledMicrophone();
|
|
|
|
//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) => {
|
2020-04-19 19:32:38 +02:00
|
|
|
e.preventDefault();
|
|
|
|
this.enabledCamera();
|
|
|
|
//update tracking
|
|
|
|
});
|
2020-06-10 12:15:25 +02:00
|
|
|
this.cinema = this.getElementByIdOrFail<HTMLImageElement>('cinema');
|
|
|
|
this.cinema.addEventListener('click', (e: MouseEvent) => {
|
2020-04-19 19:32:38 +02:00
|
|
|
e.preventDefault();
|
|
|
|
this.disabledCamera();
|
|
|
|
//update tracking
|
|
|
|
});
|
2020-04-26 20:55:20 +02:00
|
|
|
}
|
2020-04-19 19:32:38 +02:00
|
|
|
|
2020-06-23 14:56:57 +02:00
|
|
|
onUpdateLocalStream(callback: UpdatedLocalStreamCallback): void {
|
|
|
|
|
|
|
|
this.updatedLocalStreamCallBacks.add(callback);
|
|
|
|
}
|
|
|
|
|
|
|
|
removeUpdateLocalStreamEventListener(callback: UpdatedLocalStreamCallback): void {
|
|
|
|
this.updatedLocalStreamCallBacks.delete(callback);
|
|
|
|
}
|
|
|
|
|
|
|
|
private triggerUpdatedLocalStreamCallbacks(stream: MediaStream): void {
|
|
|
|
for (const callback of this.updatedLocalStreamCallBacks) {
|
|
|
|
callback(stream);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-26 20:55:20 +02:00
|
|
|
activeVisio(){
|
2020-06-09 23:13:26 +02:00
|
|
|
const webRtc = this.getElementByIdOrFail('webRtc');
|
2020-04-19 19:32:38 +02:00
|
|
|
webRtc.classList.add('active');
|
|
|
|
}
|
|
|
|
|
|
|
|
enabledCamera() {
|
|
|
|
this.cinemaClose.style.display = "none";
|
|
|
|
this.cinema.style.display = "block";
|
2020-05-03 14:29:45 +02:00
|
|
|
this.constraintsMedia.video = videoConstraint;
|
2020-06-22 22:55:28 +02:00
|
|
|
this.getCamera().then((stream: MediaStream) => {
|
2020-06-23 14:56:57 +02:00
|
|
|
this.triggerUpdatedLocalStreamCallbacks(stream);
|
2020-05-03 17:19:42 +02:00
|
|
|
});
|
2020-04-19 19:32:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
disabledCamera() {
|
|
|
|
this.cinemaClose.style.display = "block";
|
|
|
|
this.cinema.style.display = "none";
|
|
|
|
this.constraintsMedia.video = false;
|
2020-05-14 20:39:30 +02:00
|
|
|
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) => {
|
2020-06-23 14:56:57 +02:00
|
|
|
this.triggerUpdatedLocalStreamCallbacks(stream);
|
2020-05-03 17:19:42 +02:00
|
|
|
});
|
2020-04-19 19:32:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
enabledMicrophone() {
|
|
|
|
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) => {
|
2020-06-23 14:56:57 +02:00
|
|
|
this.triggerUpdatedLocalStreamCallbacks(stream);
|
2020-05-03 17:19:42 +02:00
|
|
|
});
|
2020-04-19 19:32:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
disabledMicrophone() {
|
|
|
|
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) {
|
2020-05-14 20:39:30 +02:00
|
|
|
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) => {
|
2020-06-23 14:56:57 +02:00
|
|
|
this.triggerUpdatedLocalStreamCallbacks(stream);
|
2020-05-03 17:19:42 +02:00
|
|
|
});
|
2020-04-26 20:55:20 +02:00
|
|
|
}
|
|
|
|
|
2020-04-19 19:32:38 +02:00
|
|
|
//get camera
|
2020-06-25 10:43:27 +02:00
|
|
|
async getCamera(): Promise<MediaStream> {
|
2020-06-22 22:55:28 +02:00
|
|
|
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-06-22 22:55:28 +02:00
|
|
|
}
|
|
|
|
|
2020-05-13 16:56:22 +02:00
|
|
|
try {
|
2020-06-25 10:43:27 +02:00
|
|
|
let stream = await navigator.mediaDevices.getUserMedia(this.constraintsMedia);
|
|
|
|
|
|
|
|
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-19 19:32:38 +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 = {}
|
|
|
|
}
|
|
|
|
audio.deviceId = id;
|
|
|
|
|
|
|
|
return this.getCamera();
|
|
|
|
}
|
|
|
|
|
2020-04-25 20:29:03 +02:00
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @param userId
|
|
|
|
*/
|
2020-05-14 20:39:30 +02:00
|
|
|
addActiveVideo(userId : string, userName: string = ""){
|
|
|
|
this.webrtcInAudio.play();
|
2020-06-09 23:13:26 +02:00
|
|
|
const elementRemoteVideo = this.getElementByIdOrFail("activeCam");
|
2020-05-14 20:39:30 +02:00
|
|
|
userName = userName.toUpperCase();
|
2020-06-09 23:13:26 +02:00
|
|
|
const color = this.getColorByString(userName);
|
2020-05-14 20:39:30 +02:00
|
|
|
elementRemoteVideo.insertAdjacentHTML('beforeend', `
|
|
|
|
<div id="div-${userId}" class="video-container" style="border-color: ${color};">
|
2020-06-06 22:49:55 +02:00
|
|
|
<div class="connecting-spinner"></div>
|
|
|
|
<div class="rtc-error" style="display: none"></div>
|
2020-05-14 20:39:30 +02:00
|
|
|
<i style="background-color: ${color};">${userName}</i>
|
|
|
|
<img id="microphone-${userId}" src="resources/logos/microphone-close.svg">
|
|
|
|
<video id="${userId}" autoplay></video>
|
|
|
|
</div>
|
|
|
|
`);
|
2020-06-10 12:15:25 +02:00
|
|
|
this.remoteVideo.set(userId, this.getElementByIdOrFail<HTMLVideoElement>(userId));
|
2020-04-26 19:12:01 +02:00
|
|
|
}
|
|
|
|
|
2020-05-14 20:39:30 +02:00
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @param userId
|
|
|
|
*/
|
|
|
|
disabledMicrophoneByUserId(userId: string){
|
2020-06-09 23:13:26 +02:00
|
|
|
const element = document.getElementById(`microphone-${userId}`);
|
2020-05-14 20:39:30 +02:00
|
|
|
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}`);
|
2020-05-14 20:39:30 +02:00
|
|
|
if(!element){
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
element.classList.remove('active')
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @param userId
|
|
|
|
*/
|
2020-05-14 20:54:34 +02:00
|
|
|
disabledVideoByUserId(userId: string) {
|
2020-05-14 20:39:30 +02:00
|
|
|
let element = document.getElementById(`${userId}`);
|
2020-05-14 20:54:34 +02:00
|
|
|
if (element) {
|
|
|
|
element.style.opacity = "0";
|
|
|
|
}
|
|
|
|
element = document.getElementById(`div-${userId}`);
|
|
|
|
if (!element) {
|
2020-05-14 20:39:30 +02:00
|
|
|
return;
|
|
|
|
}
|
2020-05-14 20:54:34 +02:00
|
|
|
element.style.borderStyle = "solid";
|
2020-05-14 20:39:30 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @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(`div-${userId}`);
|
2020-05-14 20:39:30 +02:00
|
|
|
if(!element){
|
|
|
|
return;
|
|
|
|
}
|
2020-05-14 20:54:34 +02:00
|
|
|
element.style.borderStyle = "none";
|
2020-05-14 20:39:30 +02:00
|
|
|
}
|
|
|
|
|
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-04-26 19:12:01 +02:00
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @param userId
|
|
|
|
*/
|
|
|
|
removeActiveVideo(userId : string){
|
2020-06-09 23:13:26 +02:00
|
|
|
const element = document.getElementById(`div-${userId}`);
|
2020-04-26 19:12:01 +02:00
|
|
|
if(!element){
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
element.remove();
|
2020-06-10 12:15:25 +02:00
|
|
|
this.remoteVideo.delete(userId);
|
2020-04-25 20:29:03 +02:00
|
|
|
}
|
2020-05-14 20:39:30 +02:00
|
|
|
|
2020-06-06 22:49:55 +02:00
|
|
|
isConnecting(userId : string): void {
|
2020-06-09 23:13:26 +02:00
|
|
|
const connectingSpinnerDiv = this.getSpinner(userId);
|
2020-06-06 22:49:55 +02:00
|
|
|
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);
|
2020-06-06 22:49:55 +02:00
|
|
|
if (connectingSpinnerDiv === null) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
connectingSpinnerDiv.style.display = 'none';
|
|
|
|
}
|
|
|
|
|
|
|
|
isError(userId : string): void {
|
2020-06-09 23:13:26 +02:00
|
|
|
const element = document.getElementById(`div-${userId}`);
|
2020-06-06 22:49:55 +02:00
|
|
|
if(!element){
|
|
|
|
return;
|
|
|
|
}
|
2020-06-09 23:13:26 +02:00
|
|
|
const errorDiv = element.getElementsByClassName('rtc-error').item(0) as HTMLDivElement|null;
|
2020-06-06 22:49:55 +02:00
|
|
|
if (errorDiv === null) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
errorDiv.style.display = 'block';
|
|
|
|
}
|
|
|
|
|
|
|
|
private getSpinner(userId : string): HTMLDivElement|null {
|
2020-06-09 23:13:26 +02:00
|
|
|
const element = document.getElementById(`div-${userId}`);
|
2020-06-06 22:49:55 +02:00
|
|
|
if(!element){
|
|
|
|
return null;
|
|
|
|
}
|
2020-06-09 23:13:26 +02:00
|
|
|
const connnectingSpinnerDiv = element.getElementsByClassName('connecting-spinner').item(0) as HTMLDivElement|null;
|
2020-06-06 22:49:55 +02:00
|
|
|
return connnectingSpinnerDiv;
|
|
|
|
}
|
|
|
|
|
2020-05-14 20:39:30 +02:00
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @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;
|
2020-05-14 20:39:30 +02:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
2020-06-23 14:56:57 +02:00
|
|
|
|
|
|
|
export const mediaManager = new MediaManager();
|