import { Directive , OnInit , OnDestroy , Input , ChangeDetectorRef , } from '@angular/core' ; import { Observable , Subscription , Subject , fromEvent , timer , combineLatest , } from 'rxjs' ; import { map } from 'rxjs/operators' ; import { IPlayable , IMediaSubscriptions , } from '../../interfaces/vg-media-api.interface' ; import { IMediaElement } from '../../interfaces/i-media-element.interface' ; import { VgApiService } from '../../services/vg-api/vg-api.service' ; import { VgStates } from '../../services/states/vg-states.service' ; import { VgEvents } from '../../services/events/vg-events.service' ; @ Directive ( { selector : '[vgMedia]' , } ) export class VgMediaDirective implements OnInit , OnDestroy , IPlayable { elem : any ; @ Input ( ) vgMedia : IMediaElement ; @ Input ( ) vgMaster : boolean ; state : string = VgStates . VG_PAUSED ; time : any = { current : 0 , total : 0 , left : 0 } ; buffer : any = { end : 0 } ; track : any ; subscriptions : IMediaSubscriptions | any ; canPlay = false ; canPlayThrough = false ; isMetadataLoaded = false ; isWaiting = false ; isCompleted = false ; isLive = false ; isBufferDetected = false ; checkInterval = 200 ; currentPlayPos = 0 ; lastPlayPos = 0 ; specifiedDuration : number ; checkBufferSubscription : any ; syncSubscription : Subscription ; canPlayAllSubscription : any ; playAtferSync = false ; mutationObs : Subscription ; canPlayObs : Subscription ; canPlayThroughObs : Subscription ; loadedMetadataObs : Subscription ; waitingObs : Subscription ; progressObs : Subscription ; endedObs : Subscription ; playingObs : Subscription ; playObs : Subscription ; pauseObs : Subscription ; timeUpdateObs : Subscription ; volumeChangeObs : Subscription ; errorObs : Subscription ; bufferDetected : Subject < boolean > = new Subject ( ) ; playPromise : Promise < any > ; constructor ( private api , private ref ) { } ngOnInit ( ) { if ( this . vgMedia . nodeName ) { this . elem = this . vgMedia ; } else { this . elem = this . vgMedia . elem ; } this . api . registerMedia ( this ) ; this . subscriptions = { abort : fromEvent ( this . elem as any , VgEvents . VG_ABORT ) , canPlay : fromEvent ( this . elem as any , VgEvents . VG_CAN_PLAY ) , canPlayThrough : fromEvent ( this . elem as any , VgEvents . VG_CAN_PLAY_THROUGH ) , durationChange : fromEvent ( this . elem as any , VgEvents . VG_DURATION_CHANGE ) , emptied : fromEvent ( this . elem as any , VgEvents . VG_EMPTIED ) , encrypted : fromEvent ( this . elem as any , VgEvents . VG_ENCRYPTED ) , ended : fromEvent ( this . elem as any , VgEvents . VG_ENDED ) , error : fromEvent ( this . elem as any , VgEvents . VG_ERROR ) , loadedData : fromEvent ( this . elem as any , VgEvents . VG_LOADED_DATA ) , loadedMetadata : fromEvent ( this . elem as any , VgEvents . VG_LOADED_METADATA ) , loadStart : fromEvent ( this . elem as any , VgEvents . VG_LOAD_START ) , pause : fromEvent ( this . elem as any , VgEvents . VG_PAUSE ) , play : fromEvent ( this . elem as any , VgEvents . VG_PLAY ) , playing : fromEvent ( this . elem as any , VgEvents . VG_PLAYING ) , progress : fromEvent ( this . elem as any , VgEvents . VG_PROGRESS ) , rateChange : fromEvent ( this . elem as any , VgEvents . VG_RATE_CHANGE ) , seeked : fromEvent ( this . elem as any , VgEvents . VG_SEEKED ) , seeking : fromEvent ( this . elem as any , VgEvents . VG_SEEKING ) , stalled : fromEvent ( this . elem as any , VgEvents . VG_STALLED ) , suspend : fromEvent ( this . elem as any , VgEvents . VG_SUSPEND ) , timeUpdate : fromEvent ( this . elem as any , VgEvents . VG_TIME_UPDATE ) , volumeChange : fromEvent ( this . elem as any , VgEvents . VG_VOLUME_CHANGE ) , waiting : fromEvent ( this . elem as any , VgEvents . VG_WAITING ) , startAds : fromEvent ( window as any , VgEvents . VG_START_ADS ) , endAds : fromEvent ( window as any , VgEvents . VG_END_ADS ) , mutation : new Observable ( ( observer ) => { const domObs = new MutationObserver ( ( mutations ) => { observer . next ( mutations ) ; } ) ; domObs . observe ( this . elem as any , { childList : true , attributes : true } ) ; return ( ) => { domObs . disconnect ( ) ; } ; } ) , bufferDetected : this . bufferDetected , } ; this . mutationObs = this . subscriptions . mutation . subscribe ( this . onMutation . bind ( this ) ) ; this . canPlayObs = this . subscriptions . canPlay . subscribe ( this . onCanPlay . bind ( this ) ) ; this . canPlayThroughObs = this . subscriptions . canPlayThrough . subscribe ( this . onCanPlayThrough . bind ( this ) ) ; this . loadedMetadataObs = this . subscriptions . loadedMetadata . subscribe ( this . onLoadMetadata . bind ( this ) ) ; this . waitingObs = this . subscriptions . waiting . subscribe ( this . onWait . bind ( this ) ) ; this . progressObs = this . subscriptions . progress . subscribe ( this . onProgress . bind ( this ) ) ; this . endedObs = this . subscriptions . ended . subscribe ( this . onComplete . bind ( this ) ) ; this . playingObs = this . subscriptions . playing . subscribe ( this . onStartPlaying . bind ( this ) ) ; this . playObs = this . subscriptions . play . subscribe ( this . onPlay . bind ( this ) ) ; this . pauseObs = this . subscriptions . pause . subscribe ( this . onPause . bind ( this ) ) ; this . timeUpdateObs = this . subscriptions . timeUpdate . subscribe ( this . onTimeUpdate . bind ( this ) ) ; this . volumeChangeObs = this . subscriptions . volumeChange . subscribe ( this . onVolumeChange . bind ( this ) ) ; this . errorObs = this . subscriptions . error . subscribe ( this . onError . bind ( this ) ) ; if ( this . vgMaster ) { this . api . playerReadyEvent . subscribe ( ( ) => { this . prepareSync ( ) ; } ) ; } } prepareSync ( ) { const canPlayAll = [ ] ; for ( const media in this . api . medias ) { if ( this . api . medias [ media ] ) { canPlayAll . push ( this . api . medias [ media ] . subscriptions . canPlay ) ; } } this . canPlayAllSubscription = combineLatest ( canPlayAll ) . pipe ( map ( ( ... params ) => { const checkReadyState = ( event ) => { if ( ! event ?. target ) { return false ; } return event . target . readyState === 4 ; } ; const allReady = params . some ( checkReadyState ) ; if ( allReady && ! this . syncSubscription ) { this . startSync ( ) ; this . syncSubscription . unsubscribe ( ) ; } } ) ) . subscribe ( ) ; } startSync ( ) { this . syncSubscription = timer ( 0 , 1000 ) . subscribe ( ( ) => { for ( const media in this . api . medias ) { if ( this . api . medias [ media ] !== this ) { const diff = this . api . medias [ media ] . currentTime - this . currentTime ; if ( diff < - 0.3 || diff > 0.3 ) { this . playAtferSync = this . state === VgStates . VG_PLAYING ; this . pause ( ) ; this . api . medias [ media ] . pause ( ) ; this . api . medias [ media ] . currentTime = this . currentTime ; } else { if ( this . playAtferSync ) { this . play ( ) ; this . api . medias [ media ] . play ( ) ; this . playAtferSync = false ; } } } } } ) ; } onMutation ( mutations ) { for ( let i = 0 , l = mutations . length ; i < l ; i ++ ) { const mut = mutations [ i ] ; if ( mut . type === 'attributes' && mut . attributeName === 'src' ) { if ( ( mut . target as any ) . src && ( mut . target as any ) . src . length > 0 && ( mut . target as any ) . src . indexOf ( 'blob:' ) < 0 ) { this . loadMedia ( ) ; break ; } } else if ( mut . type === 'childList' && mut . removedNodes . length && mut . removedNodes [ 0 ] . nodeName . toLowerCase ( ) === 'source' ) { this . loadMedia ( ) ; break ; } } } loadMedia ( ) { this . vgMedia . pause ( ) ; this . vgMedia . currentTime = 0 ; this . stopBufferCheck ( ) ; this . isBufferDetected = true ; this . bufferDetected . next ( this . isBufferDetected ) ; setTimeout ( ( ) => this . vgMedia . load ( ) , 10 ) ; } play ( ) { if ( this . playPromise || ( this . state !== VgStates . VG_PAUSED && this . state !== VgStates . VG_ENDED ) ) { return ; } this . playPromise = this . vgMedia . play ( ) ; if ( this . playPromise && this . playPromise . then && this . playPromise . catch ) { this . playPromise . then ( ( ) => { this . playPromise = null ; } ) . catch ( ( ) => { this . playPromise = null ; } ) ; } return this . playPromise ; } pause ( ) { if ( this . playPromise ) { this . playPromise . then ( ( ) => { this . vgMedia . pause ( ) ; } ) ; } else { this . vgMedia . pause ( ) ; } } get id ( ) { let result ; if ( this . vgMedia ) { result = this . vgMedia . id ; } return result ; } get duration ( ) { return this . vgMedia . duration === Infinity ? this . specifiedDuration : this . vgMedia . duration ; } set currentTime ( seconds ) { this . vgMedia . currentTime = seconds ; } get currentTime ( ) { return this . vgMedia . currentTime ; } set volume ( volume ) { this . vgMedia . volume = volume ; } get volume ( ) { return this . vgMedia . volume ; } set playbackRate ( rate ) { this . vgMedia . playbackRate = rate ; } get playbackRate ( ) { return this . vgMedia . playbackRate ; } get buffered ( ) { return this . vgMedia . buffered ; } get textTracks ( ) { return this . vgMedia . textTracks ; } onCanPlay ( event ) { this . isBufferDetected = false ; this . bufferDetected . next ( this . isBufferDetected ) ; this . canPlay = true ; this . ref . detectChanges ( ) ; } onCanPlayThrough ( event ) { this . isBufferDetected = false ; this . bufferDetected . next ( this . isBufferDetected ) ; this . canPlayThrough = true ; this . ref . detectChanges ( ) ; } onLoadMetadata ( event ) { this . isMetadataLoaded = true ; this . time = { current : 0 , left : 0 , total : this . duration * 1000 , } ; this . state = VgStates . VG_PAUSED ; const t = Math . round ( this . time . total ) ; this . isLive = t === Infinity ; this . ref . detectChanges ( ) ; } onWait ( event ) { this . isWaiting = true ; this . ref . detectChanges ( ) ; } onComplete ( event ) { this . isCompleted = true ; this . state = VgStates . VG_ENDED ; this . ref . detectChanges ( ) ; } onStartPlaying ( event ) { this . state = VgStates . VG_PLAYING ; this . ref . detectChanges ( ) ; } onPlay ( event ) { this . state = VgStates . VG_PLAYING ; if ( this . vgMaster ) { if ( ! this . syncSubscription || this . syncSubscription . closed ) { this . startSync ( ) ; } } this . startBufferCheck ( ) ; this . ref . detectChanges ( ) ; } onPause ( event ) { this . state = VgStates . VG_PAUSED ; if ( this . vgMaster ) { if ( ! this . playAtferSync ) { this . syncSubscription . unsubscribe ( ) ; } } this . stopBufferCheck ( ) ; this . ref . detectChanges ( ) ; } onTimeUpdate ( event ) { const end = this . buffered . length - 1 ; this . time = { current : this . currentTime * 1000 , total : this . time . total , left : ( this . duration - this . currentTime ) * 1000 , } ; if ( end >= 0 ) { this . buffer = { end : this . buffered . end ( end ) * 1000 } ; } this . ref . detectChanges ( ) ; } onProgress ( event ) { const end = this . buffered . length - 1 ; if ( end >= 0 ) { this . buffer = { end : this . buffered . end ( end ) * 1000 } ; } this . ref . detectChanges ( ) ; } onVolumeChange ( event ) { this . ref . detectChanges ( ) ; } onError ( event ) { this . ref . detectChanges ( ) ; } bufferCheck ( ) { const offset = 1 / this . checkInterval ; this . currentPlayPos = this . currentTime ; if ( ! this . isBufferDetected && this . currentPlayPos < this . lastPlayPos + offset ) { this . isBufferDetected = true ; } if ( this . isBufferDetected && this . currentPlayPos > this . lastPlayPos + offset ) { this . isBufferDetected = false ; } if ( ! this . bufferDetected . closed ) { this . bufferDetected . next ( this . isBufferDetected ) ; } this . lastPlayPos = this . currentPlayPos ; } startBufferCheck ( ) { this . checkBufferSubscription = timer ( 0 , this . checkInterval ) . subscribe ( ( ) => { this . bufferCheck ( ) ; } ) ; } stopBufferCheck ( ) { if ( this . checkBufferSubscription ) { this . checkBufferSubscription . unsubscribe ( ) ; } this . isBufferDetected = false ; this . bufferDetected . next ( this . isBufferDetected ) ; } seekTime ( value , byPercent = false ) { let second ; const duration = this . duration ; if ( byPercent ) { second = ( value * duration ) / 100 ; } else { second = value ; } this . currentTime = second ; } addTextTrack ( type , label ? , language ? , mode ? ) { const newTrack = this . vgMedia . addTextTrack ( type , label , language ) ; if ( mode ) { newTrack . mode = mode ; } return newTrack ; } ngOnDestroy ( ) { this . vgMedia . src = '' ; this . mutationObs ?. unsubscribe ( ) ; this . canPlayObs ?. unsubscribe ( ) ; this . canPlayThroughObs ?. unsubscribe ( ) ; this . loadedMetadataObs ?. unsubscribe ( ) ; this . waitingObs ?. unsubscribe ( ) ; this . progressObs ?. unsubscribe ( ) ; this . endedObs ?. unsubscribe ( ) ; this . playingObs ?. unsubscribe ( ) ; this . playObs ?. unsubscribe ( ) ; this . pauseObs ?. unsubscribe ( ) ; this . timeUpdateObs ?. unsubscribe ( ) ; this . volumeChangeObs ?. unsubscribe ( ) ; this . errorObs ?. unsubscribe ( ) ; this . checkBufferSubscription ?. unsubscribe ( ) ; this . syncSubscription ?. unsubscribe ( ) ; this . bufferDetected ?. complete ( ) ; this . bufferDetected ?. unsubscribe ( ) ; this . api . unregisterMedia ( this ) ; } }