interface SourceParameters { parent : HTMLElement ; camera : THREE . Camera | null ; renderer : THREE . WebGLRenderer | null ; sourceType : "webcam" | "image" | "video" ; sourceUrl : string ; facingMode : "user" | "environment" ; deviceId : any ; sourceWidth : number ; sourceHeight : number ; displayWidth : number ; displayHeight : number ; } param parameters export class Source { public domElement : HTMLImageElement | HTMLVideoElement | undefined ; private currentTorchStatus : boolean ; private parameters : SourceParameters ; constructor ( parameters ) { if ( ! parameters . renderer ) { throw Error ( "ThreeJS Renderer is required" ) ; } if ( ! parameters . camera ) { throw Error ( "ThreeJS Camera is required" ) ; } this . currentTorchStatus = false ; this . parameters = { parent : document . body , renderer : null , camera : null , sourceType : "webcam" , sourceUrl : "" , deviceId : null , facingMode : "environment" , sourceWidth : 640 , sourceHeight : 480 , displayWidth : 640 , displayHeight : 480 , } ; this . setParameters ( parameters ) ; } public setParameters ( parameters ) { if ( ! parameters ) { return ; } for ( const key in parameters ) { if ( key ) { const newValue = parameters [ key ] ; if ( newValue === undefined ) { console . warn ( key + "' parameter is undefined." ) ; continue ; } const currentValue = ( this . parameters as any ) [ key ] ; if ( currentValue === undefined ) { console . warn ( key + "' is not a property of this Source." ) ; continue ; } ( this . parameters as any ) [ key ] = newValue ; } } } get renderer ( ) { return this . parameters . renderer ; } get camera ( ) { return this . parameters . camera ; } public dispose ( ) { if ( this . parameters . parent && this . domElement ) { this . parameters . parent . removeChild ( this . domElement ) ; } } public initialize ( ) { return new Promise ( ( resolve , reject ) => { const onReady = ( ) => { if ( ! this . domElement ) { reject ( "domElement not defined" ) ; return ; } this . onResizeElement ( ) ; this . parameters . parent . appendChild ( this . domElement ) ; resolve ( ) ; } ; const onError = ( message ) => { reject ( message ) ; } ; if ( this . parameters . sourceType === "image" ) { this . domElement = this . _initSourceImage ( onReady , onError ) ; } else if ( this . parameters . sourceType === "video" ) { this . domElement = this . _initSourceVideo ( onReady , onError ) ; } else if ( this . parameters . sourceType === "webcam" ) { const webcam = this . _initSourceWebcam ( onReady , onError ) ; if ( ! webcam ) { reject ( "Webcam source could not be established" ) ; return ; } this . domElement = webcam ; } else { reject ( "Source type not recognised. Try: 'image', 'video', 'webcam'" ) ; return ; } this . positionSourceDomElement ( ) ; return this ; } ) ; } /**
     * Determine if the device supports torch capability
     */ public hasMobileTorch ( domElement ) { const stream = domElement . srcObject ; if ( stream instanceof MediaStream ) { const videoTrack = stream . getVideoTracks ( ) [ 0 ] ; if ( videoTrack . getCapabilities === undefined ) { return false ; } const capabilities = videoTrack . getCapabilities ( ) ; return ( capabilities as any ) . torch ? true : false ; } return false ; } /**
     * Toggle the flash/torch of the mobile phone if possible.
     * See: https://www.oberhofer.co/mediastreamtrack-and-its-capabilities/
     */ public toggleMobileTorch ( domElement ) { if ( ! this . hasMobileTorch ( domElement ) === true ) { return ; } const stream = domElement . srcObject ; if ( this . currentTorchStatus === undefined ) { this . currentTorchStatus = false ; } if ( stream instanceof MediaStream ) { const videoTrack = stream . getVideoTracks ( ) [ 0 ] ; const capabilities = videoTrack . getCapabilities ( ) ; if ( ! ( capabilities as any ) . torch ) { console . warn ( "Torch is not avaiable to be toggled" ) ; return ; } this . currentTorchStatus = ! this . currentTorchStatus ; videoTrack . applyConstraints ( { advanced : [ { torch : this . currentTorchStatus } as any ] , } ) . catch ( ( error ) => { throw error ; } ) ; } } public onResizeElement ( ) { if ( ! this . domElement ) { console . warn ( "Can't resize as domElement is not defined on source" ) ; return ; } const screenWidth = window . innerWidth ; const screenHeight = window . innerHeight ; let sourceHeight = 0 ; let sourceWidth = 0 ; if ( this . domElement instanceof HTMLImageElement ) { sourceWidth = this . domElement . naturalWidth ; sourceHeight = this . domElement . naturalHeight ; } else if ( this . domElement instanceof HTMLVideoElement ) { sourceWidth = this . domElement . videoWidth ; sourceHeight = this . domElement . videoHeight ; } const sourceAspect = sourceWidth / sourceHeight ; const screenAspect = screenWidth / screenHeight ; if ( screenAspect < sourceAspect ) { const newWidth = sourceAspect * screenHeight ; this . domElement . style . width = newWidth + "px" ; this . domElement . style . marginLeft = - ( newWidth - screenWidth ) / 2 + "px" ; this . domElement . style . height = screenHeight + "px" ; this . domElement . style . marginTop = "0px" ; } else { const newHeight = 1 / ( sourceAspect / screenWidth ) ; this . domElement . style . height = newHeight + "px" ; this . domElement . style . marginTop = - ( newHeight - screenHeight ) / 2 + "px" ; this . domElement . style . width = screenWidth + "px" ; this . domElement . style . marginLeft = "0px" ; } } param otherElement public copyElementSizeTo ( otherElement ) { if ( ! this . domElement ) { console . warn ( "Cant set size to match domElement as it is not defined" ) ; return ; } if ( window . innerWidth > window . innerHeight ) { otherElement . style . width = this . domElement . style . width ; otherElement . style . height = this . domElement . style . height ; otherElement . style . marginLeft = this . domElement . style . marginLeft ; otherElement . style . marginTop = this . domElement . style . marginTop ; } else { otherElement . style . height = this . domElement . style . height ; otherElement . style . width = ( parseInt ( otherElement . style . height , 10 ) * 4 ) / 3 + "px" ; otherElement . style . marginLeft = ( window . innerWidth - parseInt ( otherElement . style . width , 10 ) ) / 2 + "px" ; otherElement . style . marginTop = 0 ; } } private _initSourceImage ( onReady , onError ) { if ( ! this . parameters . sourceUrl ) { throw Error ( "No source URL provided" ) ; } const domElement = document . createElement ( "img" ) ; domElement . src = this . parameters . sourceUrl ; domElement . width = this . parameters . sourceWidth ; domElement . height = this . parameters . sourceHeight ; domElement . style . width = this . parameters . displayWidth + "px" ; domElement . style . height = this . parameters . displayHeight + "px" ; domElement . onload = ( ) => onReady ( ) ; return domElement ; } private _initSourceVideo ( onReady , onError ) { const domElement = document . createElement ( "video" ) ; domElement . src = this . parameters . sourceUrl ; domElement . style . objectFit = "initial" ; domElement . autoplay = true ; ( domElement as any ) . webkitPlaysinline = true ; domElement . controls = false ; domElement . loop = true ; domElement . muted = true ; document . body . addEventListener ( "click" , function onClick ( ) { document . body . removeEventListener ( "click" , onClick ) ; domElement . play ( ) ; } ) ; domElement . width = this . parameters . sourceWidth ; domElement . height = this . parameters . sourceHeight ; domElement . style . width = this . parameters . displayWidth + "px" ; domElement . style . height = this . parameters . displayHeight + "px" ; domElement . addEventListener ( "loadeddata" , ( ) => { onReady ( ) ; } , false ) ; return domElement ; } private _initSourceWebcam ( onReady , onError ) { const domElement = document . createElement ( "video" ) ; domElement . setAttribute ( "autoplay" , "" ) ; domElement . setAttribute ( "muted" , "" ) ; domElement . setAttribute ( "playsinline" , "" ) ; domElement . style . width = this . parameters . displayWidth + "px" ; domElement . style . height = this . parameters . displayHeight + "px" ; if ( navigator . mediaDevices === undefined || navigator . mediaDevices . enumerateDevices === undefined || navigator . mediaDevices . getUserMedia === undefined ) { let fctName = "" ; if ( navigator . mediaDevices === undefined ) { fctName = "navigator.mediaDevices" ; } else if ( navigator . mediaDevices . enumerateDevices === undefined ) { fctName = "navigator.mediaDevices.enumerateDevices" ; } else if ( navigator . mediaDevices . getUserMedia === undefined ) { fctName = "navigator.mediaDevices.getUserMedia" ; } onError ( "WebRTC issue-! " + fctName + " not present in your browser" ) ; return ; } navigator . mediaDevices . enumerateDevices ( ) . then ( ( devices ) => { const userMediaConstraints = { audio : false , video : { facingMode : this . parameters . facingMode , width : { ideal : this . parameters . sourceWidth , } , height : { ideal : this . parameters . sourceHeight , } , } , } ; if ( null !== this . parameters . deviceId ) { ( userMediaConstraints as any ) . video . deviceId = { exact : this . parameters . deviceId , } ; } navigator . mediaDevices . getUserMedia ( userMediaConstraints ) . then ( function success ( stream ) { domElement . srcObject = stream ; document . body . addEventListener ( "click" , ( ) => { domElement . play ( ) ; } ) ; domElement . addEventListener ( "loadedmetadata" , ( event ) => { onReady ( ) ; } ) ; } ) . catch ( ( error ) => { onError ( error ) ; } ) ; } ) . catch ( ( error ) => { onError ( error ) ; } ) ; return domElement ; } private positionSourceDomElement ( ) { if ( this . domElement ) { this . domElement . style . position = "absolute" ; this . domElement . style . top = "0px" ; this . domElement . style . left = "0px" ; this . domElement . style . zIndex = "-2" ; } } } export default Source ;