import DefaultBrowserBehavior from '../browserbehavior/DefaultBrowserBehavior' ; import DevicePixelRatioMonitor from '../devicepixelratiomonitor/DevicePixelRatioMonitor' ; import DevicePixelRatioObserver from '../devicepixelratioobserver/DevicePixelRatioObserver' ; import DefaultModality from '../modality/DefaultModality' ; import AsyncScheduler from '../scheduler/AsyncScheduler' ; import VideoTileController from '../videotilecontroller/VideoTileController' ; import VideoTile from './VideoTile' ; import VideoTileState from './VideoTileState' ; export default class DefaultVideoTile implements DevicePixelRatioObserver , VideoTile { private tileState : VideoTileState = new VideoTileState ( ) ; param videoStream param videoElement param localTile static connectVideoStreamToVideoElement ( videoStream , videoElement , localTile ) { const transform = localTile && videoStream . getVideoTracks ( ) [ 0 ] . getSettings ( ) . facingMode !== 'environment' ? 'rotateY(180deg)' : '' ; DefaultVideoTile . setVideoElementFlag ( videoElement , 'disablePictureInPicture' , localTile ) ; DefaultVideoTile . setVideoElementFlag ( videoElement , 'disableRemotePlayback' , localTile ) ; if ( videoElement . style . transform !== transform ) { videoElement . style . transform = transform ; } if ( videoElement . hasAttribute ( 'controls' ) ) { videoElement . removeAttribute ( 'controls' ) ; } if ( ! videoElement . hasAttribute ( 'autoplay' ) ) { videoElement . setAttribute ( 'autoplay' , 'true' ) ; } if ( ! videoElement . hasAttribute ( 'playsinline' ) ) { videoElement . setAttribute ( 'playsinline' , 'true' ) ; } if ( ! videoElement . hasAttribute ( 'muted' ) ) { videoElement . setAttribute ( 'muted' , 'true' ) ; videoElement . muted = true ; } if ( videoElement . srcObject !== videoStream ) { videoElement . srcObject = videoStream ; } if ( new DefaultBrowserBehavior ( ) . requiresVideoElementWorkaround ( ) ) { AsyncScheduler . nextTick ( async ( ) => { try { await videoElement . play ( ) ; } catch ( error ) { } } ) ; } } param videoElement param dueToPause param keepLastFrameWhenPaused static disconnectVideoStreamFromVideoElement ( videoElement , dueToPause , keepLastFrameWhenPaused = false ) { if ( ! videoElement ) { return ; } if ( dueToPause ) { if ( ! keepLastFrameWhenPaused ) { videoElement . srcObject = null ; videoElement . style . transform = '' ; } } else { if ( ! videoElement . srcObject ) { return ; } videoElement . pause ( ) ; videoElement . style . transform = '' ; DefaultVideoTile . setVideoElementFlag ( videoElement , 'disablePictureInPicture' , false ) ; DefaultVideoTile . setVideoElementFlag ( videoElement , 'disableRemotePlayback' , false ) ; const mediaStream = videoElement . srcObject as MediaStream ; const tracks = mediaStream . getTracks ( ) ; for ( const track of tracks ) { track . stop ( ) ; mediaStream . removeTrack ( track ) ; } if ( new DefaultBrowserBehavior ( ) . requiresVideoElementWorkaround ( ) ) { const prevSrcObject = videoElement . srcObject ; AsyncScheduler . nextTick ( ( ) => { if ( videoElement . srcObject === prevSrcObject ) { videoElement . srcObject = null ; } } ) ; } else { videoElement . srcObject = null ; } } } constructor ( tileId , localTile , private tileController , private devicePixelRatioMonitor ) { this . tileState . tileId = tileId ; this . tileState . localTile = localTile ; this . devicePixelRatioMonitor . registerObserver ( this ) ; } destroy ( ) { this . devicePixelRatioMonitor . removeObserver ( this ) ; if ( this . tileState . boundVideoElement && this . tileState . boundVideoElement . srcObject === this . tileState . boundVideoStream ) { DefaultVideoTile . disconnectVideoStreamFromVideoElement ( this . tileState . boundVideoElement , false ) ; } this . tileState = new VideoTileState ( ) ; } devicePixelRatioChanged ( newDevicePixelRatio ) { this . tileState . devicePixelRatio = newDevicePixelRatio ; this . sendTileStateUpdate ( ) ; } id ( ) { return this . tileState . tileId ; } state ( ) { return this . tileState . clone ( ) ; } stateRef ( ) { return this . tileState ; } bindVideoStream ( attendeeId , localTile , mediaStream , contentWidth , contentHeight , streamId , externalUserId ? ) { let tileUpdated = false ; if ( this . tileState . boundAttendeeId !== attendeeId ) { this . tileState . boundAttendeeId = attendeeId ; if ( new DefaultModality ( attendeeId ) . hasModality ( DefaultModality . MODALITY_CONTENT ) ) { this . tileState . isContent = true ; } tileUpdated = true ; } if ( this . tileState . boundExternalUserId !== externalUserId ) { this . tileState . boundExternalUserId = externalUserId ; tileUpdated = true ; } if ( this . tileState . localTile !== localTile ) { this . tileState . localTile = localTile ; tileUpdated = true ; } if ( this . tileState . boundVideoStream !== mediaStream ) { this . tileState . boundVideoStream = mediaStream ; tileUpdated = true ; } if ( this . tileState . videoStreamContentWidth !== contentWidth ) { this . tileState . videoStreamContentWidth = contentWidth ; tileUpdated = true ; } if ( this . tileState . videoStreamContentHeight !== contentHeight ) { this . tileState . videoStreamContentHeight = contentHeight ; tileUpdated = true ; } if ( this . tileState . streamId !== streamId ) { this . tileState . streamId = streamId ; tileUpdated = true ; } if ( tileUpdated ) { this . sendTileStateUpdate ( ) ; } } bindVideoElement ( videoElement ) { let tileUpdated = false ; if ( this . tileState . boundVideoElement !== videoElement ) { this . tileState . boundVideoElement = videoElement ; tileUpdated = true ; } if ( this . tileState . boundVideoElement !== null ) { if ( this . tileState . videoElementCSSWidthPixels !== videoElement . clientWidth ) { this . tileState . videoElementCSSWidthPixels = videoElement . clientWidth ; tileUpdated = true ; } if ( this . tileState . videoElementCSSHeightPixels !== videoElement . clientHeight ) { this . tileState . videoElementCSSHeightPixels = videoElement . clientHeight ; tileUpdated = true ; } } else { this . tileState . videoElementCSSWidthPixels = null ; this . tileState . videoElementCSSHeightPixels = null ; } if ( tileUpdated ) { this . sendTileStateUpdate ( ) ; } } pause ( ) { if ( ! this . tileState . paused ) { this . tileState . paused = true ; this . sendTileStateUpdate ( ) ; } } unpause ( ) { if ( this . tileState . paused ) { this . tileState . paused = false ; this . sendTileStateUpdate ( ) ; } } markPoorConnection ( ) { if ( this . tileState . poorConnection ) { return false ; } this . tileState . poorConnection = true ; this . sendTileStateUpdate ( ) ; return true ; } unmarkPoorConnection ( ) { if ( ! this . tileState . poorConnection ) { return false ; } this . tileState . poorConnection = false ; this . sendTileStateUpdate ( ) ; return true ; } capture ( ) { if ( ! this . tileState . active ) { return null ; } const canvas = document . createElement ( 'canvas' ) as HTMLCanvasElement ; const video = this . tileState . boundVideoElement ; canvas . width = video . videoWidth || video . width ; canvas . height = video . videoHeight || video . height ; const ctx = canvas . getContext ( '2d' ) ; ctx . drawImage ( video , 0 , 0 , canvas . width , canvas . height ) ; return ctx . getImageData ( 0 , 0 , canvas . width , canvas . height ) ; } setStreamId ( id ) { this . tileState . streamId = id ; this . tileController . sendTileStateUpdate ( this . state ( ) ) ; } private sendTileStateUpdate ( ) { this . updateActiveState ( ) ; this . updateVideoStreamOnVideoElement ( ) ; this . updateVideoElementPhysicalPixels ( ) ; this . tileController . sendTileStateUpdate ( this . state ( ) ) ; } private updateActiveState ( ) { this . tileState . active = ! ! ( ! this . tileState . paused && ! this . tileState . poorConnection && this . tileState . boundAttendeeId && this . tileState . boundVideoElement && this . tileState . boundVideoStream ) ; } private updateVideoElementPhysicalPixels ( ) { if ( typeof this . tileState . videoElementCSSWidthPixels === 'number' && typeof this . tileState . videoElementCSSHeightPixels === 'number' ) { this . tileState . videoElementPhysicalWidthPixels = this . tileState . devicePixelRatio * this . tileState . videoElementCSSWidthPixels ; this . tileState . videoElementPhysicalHeightPixels = this . tileState . devicePixelRatio * this . tileState . videoElementCSSHeightPixels ; } else { this . tileState . videoElementPhysicalWidthPixels = null ; this . tileState . videoElementPhysicalHeightPixels = null ; } } private updateVideoStreamOnVideoElement ( ) { if ( this . tileState . active ) { DefaultVideoTile . connectVideoStreamToVideoElement ( this . tileState . boundVideoStream , this . tileState . boundVideoElement , this . tileState . localTile ) ; } else { DefaultVideoTile . disconnectVideoStreamFromVideoElement ( this . tileState . boundVideoElement , this . tileState . paused , this . tileController . keepLastFrameWhenPaused ) ; } } private static setVideoElementFlag ( videoElement , flag , value ) { if ( flag in videoElement ) { videoElement [ flag ] = value ; } } }