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-22 22:55:28 +02:00
getCamera ( ) : Promise < MediaStream > {
2020-05-13 16:56:22 +02:00
let promise = null ;
2020-06-22 22:55:28 +02:00
if ( navigator . mediaDevices === undefined ) {
return Promise . reject < MediaStream > ( new Error ( 'Unable to access your camera or microphone. Your browser is too old (or you are running a development version of WorkAdventure on Firefox)' ) ) ;
}
2020-05-13 16:56:22 +02:00
try {
promise = navigator . mediaDevices . getUserMedia ( this . constraintsMedia )
. then ( ( stream : MediaStream ) = > {
this . localStream = stream ;
this . myCamVideo . srcObject = this . localStream ;
//TODO resize remote cam
/ * c o n s o l e . l o g ( t h i s . l o c a l S t r e a m . g e t T r a c k s ( ) ) ;
let videoMediaStreamTrack = this . localStream . getTracks ( ) . find ( ( media : MediaStreamTrack ) = > media . kind === "video" ) ;
let { width , height } = videoMediaStreamTrack . getSettings ( ) ;
console . info ( ` ${ width } x ${ height } ` ) ; // 6*/
return stream ;
} ) . catch ( ( err ) = > {
2020-06-22 22:55:28 +02:00
console . info ( "error get media " , this . constraintsMedia . video , this . constraintsMedia . audio , err ) ;
2020-05-13 16:56:22 +02:00
this . localStream = null ;
2020-06-22 22:55:28 +02:00
throw err ;
2020-05-13 16:56:22 +02:00
} ) ;
} catch ( e ) {
2020-06-22 22:55:28 +02:00
promise = Promise . reject < MediaStream > ( e ) ;
2020-05-13 16:56:22 +02:00
}
2020-06-03 11:55:31 +02:00
return promise ;
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 ( ) ;