import { Injectable , Inject , forwardRef , OnInit , NgZone } from '@angular/core' ; import { EventData } from 'data/observable' ; import { isIOS } from 'platform' ; import { TNSPlayer } from 'nativescript-audio' ; import { File } from 'file-system' ; var TNSEZAudioPlayer ; if ( isIOS ) { let ezAudio = require ( 'nativescript-ezaudio' ) ; TNSEZAudioPlayer = ezAudio . TNSEZAudioPlayer ; } import { Store , ActionReducer , Action } from '@ngrx/store' ; import { Effect , Actions } from '@ngrx/effects' ; import { Observable } from 'rxjs/Observable' ; import { BehaviorSubject } from 'rxjs/BehaviorSubject' ; import 'rxjs/add/operator/take' ; import { TNSSpotifyConstants , TNSSpotifyAuth , TNSSpotifyPlayer } from 'nativescript-spotify' ; import { isString , isObject , throttle } from 'lodash' ; import { Analytics , AnalyticsService } from '../../analytics/index' ; import { Config , LogService , FancyAlertService , TextService , Utils , PROGRESS_ACTIONS } from '../../core/index' ; import { AUTH_ACTIONS , ISearchState , PLAYLIST_ACTIONS , FIREBASE_ACTIONS } from '../../shoutoutplay/index' ; import { IListViewResult , ListViewHelper } from './list-view-helper' ; import { CommandCenterHandler } from './command-center' ; declare var zonedCallback , MPNowPlayingInfoCenter , interop ; const CATEGORY = 'Player' ; /**
 * ngrx setup start --
 */ export interface IPlayerState { currentTrackId ? : string ; activeList ? : string ; activeShoutOutPath ? : string ; playing ? : boolean ; stopped ? : boolean ; } const initialState = { playing : false } ; interface IPLAYER_ACTIONS { TOGGLE_PLAY : string ; LIST_TOGGLE_PLAY : string ; STOP : string ; } export const PLAYER_ACTIONS = { TOGGLE_PLAY : `${ CATEGORY }_TOGGLE_PLAY` , LIST_TOGGLE_PLAY : `${ CATEGORY }_LIST_TOGGLE_PLAY` , STOP : `${ CATEGORY }_STOP` } ; export const playerReducer = ( state = initialState , action ) => { let changeState = ( ) => { if ( ! action . payload ) { action . payload = { } ; } if ( ! action . payload . stopped ) { action . payload . stopped = false ; } return Object . assign ( { } , state , action . payload ) ; } ; switch ( action . type ) { case PLAYER_ACTIONS . TOGGLE_PLAY : if ( ! action . payload . activeShoutOutPath ) action . payload . activeShoutOutPath = undefined ; return changeState ( ) ; case PLAYER_ACTIONS . STOP : let payload = { playing : false , stopped : true } ; if ( action . payload && action . payload . reset ) { payload . currentTrackId = undefined ; payload . activeList = undefined ; payload . activeShoutOutPath = undefined ; } action . payload = payload ; return changeState ( ) ; default : return state ; } } ; /**
 * ngrx end --
 */ @ Injectable ( ) export class PlayerService extends Analytics { public static SHOUTOUT_START : number ; public static currentTrackId : string ; public static isPreview : boolean ; public static isPlaying : boolean ; public state$ : Observable < any > ; public currentTrack$ : BehaviorSubject < any > = new BehaviorSubject ( null ) ; public albumArtUrl$ : BehaviorSubject < string > = new BehaviorSubject ( null ) ; private _spotify : TNSSpotifyPlayer ; private _shoutOutPlayer : any ; private _shoutOutPlayerOptions : any ; private _currentTrackId : string ; private _currentShoutOutPath : string ; private _shoutoutTimeout : any ; private _nowPlayingInfo : any ; private _playConnectingTimeout : any ; private _cmdCenterHandler : any ; private _currentTrack : any ; private _throttledLoginSuccess : Function ; constructor ( public analytics , private store , private logger , private ngZone , private fancyalert ) { super ( analytics ) ; this . category = CATEGORY ; this . state$ = store . select ( 'player' ) ; PlayerService . SHOUTOUT_START = Config . SHOUTOUT_START_TIME ( ) ; this . _spotify = new TNSSpotifyPlayer ( ) ; this . _spotify . initPlayer ( true ) ; this . setupEvents ( ) ; if ( isIOS ) { this . _shoutOutPlayer = new TNSEZAudioPlayer ( true ) ; this . _shoutOutPlayer . delegate ( ) . audioEvents . on ( 'reachedEnd' , zonedCallback ( ( eventData ) => { this . logger . debug ( `audioEvents.on('reachedEnd')` ) ; this . toggleShoutOutPlay ( ) ; } ) ) ; } else { this . _shoutOutPlayer = new TNSPlayer ( ) ; this . _shoutOutPlayerOptions = { completeCallback : ( ) => { this . logger . debug ( `_shoutOutPlayer completeCallback` ) ; this . toggleShoutOutPlay ( ) ; } , errorCallback : ( err ) => { this . logger . debug ( err ) ; } , infoCallback : ( info ) => { this . logger . debug ( 'Info callback: ' + info . msg ) ; this . logger . debug ( "what: " + info ) ; } } ; } this . state$ . subscribe ( ( player ) => { if ( player . currentTrackId ) { this . togglePlay ( player . currentTrackId , player . activeList , player . playing , player . activeShoutOutPath ) ; } else { this . _currentShoutOutPath = undefined ; if ( this . _spotify && PlayerService . isPlaying ) { this . _spotify . togglePlay ( null ) ; } PlayerService . isPlaying = false ; this . store . dispatch ( { type : FIREBASE_ACTIONS . RESET_LISTS } ) ; } } ) ; this . _throttledLoginSuccess = throttle ( this . loginSuccess . bind ( this ) , 800 ) ; if ( isIOS ) { this . initCommandCenter ( ) ; } } public togglePlay ( trackId , activeList ? , playing ? , activeShoutOutPath ? ) { if ( playing ) { this . toggleLoader ( true ) ; this . resetConnectionTimeout ( ) ; this . _playConnectingTimeout = setTimeout ( ( ) => { if ( ! this . _spotify . isPlaying ( ) ) { this . fancyalert . show ( TextService . SPOTIFY_PLAY_DELAY_NOTICE ) ; this . playerUIStateReset ( ) ; } } , 5500 ) ; } else { this . resetConnectionTimeout ( ) ; } let trackUri = `spotify:track:${ trackId }` ; PlayerService . currentTrackId = trackId ; PlayerService . isPreview = activeList == 'search' ; PlayerService . isPlaying = playing ; this . cancelShoutOutQueue ( ) ; this . setSpotifyVolume ( 1 ) ; this . logger . debug ( `player.service togglePlay...` ) ; this . logger . debug ( trackId ) ; this . logger . debug ( `playing: ${ playing }` ) ; this . _spotify . togglePlay ( trackUri , playing ) . then ( ( isPlaying ) => { this . playerUIStateReset ( ) ; if ( ! PlayerService . isPreview && PlayerService . isPlaying ) { this . queueShoutOut ( trackId , activeShoutOutPath ) ; } } , ( error ) => { this . logger . debug ( `togglePlay error:` ) ; this . logger . debug ( error ) ; this . playerUIStateReset ( ) ; this . track ( `${ CATEGORY }_ERROR` , { label : error . toString ( ) } ) ; if ( error === 'login' ) { this . updateLogin ( false ) ; } } ) ; } public cmdTogglePlay ( playing ? ) { this . logger . debug ( `this.cmdTogglePlay:` ) ; this . logger . debug ( playing ) ; if ( typeof playing === 'undefined' ) playing = ! PlayerService . isPlaying ; this . logger . debug ( `playing: ${ playing }` ) ; this . ngZone . run ( ( ) => { this . store . dispatch ( { type : PLAYER_ACTIONS . TOGGLE_PLAY , payload : { currentTrackId : PlayerService . currentTrackId , playing : playing } } ) ; } ) ; } public cmdPrevNext ( direction ) { this . ngZone . run ( ( ) => { if ( direction ) { if ( PlayerService . isPreview ) { } else { this . store . dispatch ( { type : PLAYLIST_ACTIONS . SKIP_NEXT } ) ; } } else { if ( PlayerService . isPreview ) { if ( isIOS ) { this . _spotify . player . seekToOffsetCallback ( 0 , ( error ) => { this . logger . debug ( 'seeked to 0.' ) ; } ) ; } else { } } else { this . store . dispatch ( { type : PLAYLIST_ACTIONS . SKIP_BACK } ) ; } } } ) ; } public cmdSeek ( args ) { this . ngZone . run ( ( ) => { this . logger . debug ( `cmdSeek...` ) ; this . logger . debug ( args ) ; } ) ; } public showAlert ( msg ) { this . fancyalert . show ( msg ) ; } private resetConnectionTimeout ( ) { if ( this . _playConnectingTimeout ) { clearTimeout ( this . _playConnectingTimeout ) ; this . _playConnectingTimeout = undefined ; } } private playerUIStateReset ( ) { this . toggleLoader ( false ) ; this . resetConnectionTimeout ( ) ; } private queueShoutOut ( trackId , activeShoutOutPath ? ) { if ( trackId !== this . _currentTrackId ) { this . _currentTrackId = trackId ; if ( activeShoutOutPath ) { let filename = activeShoutOutPath ; if ( activeShoutOutPath . indexOf ( '/' ) > - 1 ) { filename = Utils . getFilename ( activeShoutOutPath ) ; } this . _currentShoutOutPath = Utils . documentsPath ( filename ) ; if ( ! File . exists ( this . _currentShoutOutPath ) ) { setTimeout ( ( ) => { this . fancyalert . show ( TextService . SHOUTOUT_NOT_FOUND ) ; } , 1000 ) ; } else { this . _shoutoutTimeout = setTimeout ( ( ) => { this . logger . debug ( `queueShoutOut toggleShoutOutPlay(true)` ) ; this . toggleShoutOutPlay ( true ) ; } , PlayerService . SHOUTOUT_START ) ; } } } } private toggleShoutOutPlay ( reload ? ) { if ( this . _currentShoutOutPath && this . _spotify . player ) { this . logger . debug ( `calling this.toggleShoutOutPlay();` ) ; if ( reload === true ) { this . logger . debug ( `_shoutOutPlayer.togglePlay` ) ; this . logger . debug ( this . _currentShoutOutPath ) ; if ( isIOS ) { this . _shoutOutPlayer . togglePlay ( this . _currentShoutOutPath , true ) ; } else { if ( this . _shoutOutPlayer . isAudioPlaying ( ) ) { this . _shoutOutPlayer . pause ( ) ; } else { this . _shoutOutPlayerOptions . audioFile = this . _currentShoutOutPath ; this . _shoutOutPlayer . playFromFile ( this . _shoutOutPlayerOptions ) ; } } this . setSpotifyVolume ( 0.6 ) ; } else { this . setSpotifyVolume ( 1 ) ; } } } private cancelShoutOutQueue ( ) { if ( isIOS ) { if ( this . _shoutOutPlayer . isPlaying ( ) ) { this . _shoutOutPlayer . togglePlay ( this . _currentShoutOutPath ) ; } } else { if ( this . _shoutOutPlayer . isAudioPlaying ( ) ) { this . _shoutOutPlayer . pause ( ) ; } } if ( this . _shoutoutTimeout ) { clearTimeout ( this . _shoutoutTimeout ) ; this . _shoutoutTimeout = undefined ; } } private setSpotifyVolume ( volume ) { if ( this . _spotify ) { this . _spotify . setVolume ( volume ) . then ( ( ) => { } , ( ) => { } ) ; } } private updateAlbumArt ( url ) { this . logger . debug ( url ) ; this . albumArtUrl$ . next ( url ) ; if ( isIOS ) { let nsUrl = NSURL . URLWithString ( url ) ; let data = NSData . dataWithContentsOfURL ( nsUrl ) ; if ( ! data ) { this . logger . debug ( `Failed to load data from URL: ${ url }` ) ; return ; } let image = UIImage . imageWithData ( data ) ; let artwork = MPMediaItemArtwork . alloc ( ) . initWithImage ( image ) ; this . updateiOSControlCenter ( artwork ) ; } } private updateiOSControlCenter ( artwork ? ) { if ( this . _currentTrack ) { if ( ! this . _nowPlayingInfo ) { this . _nowPlayingInfo = NSMutableDictionary . alloc ( ) . init ( ) ; } this . _nowPlayingInfo . setValueForKey ( NSString . stringWithString ( this . _currentTrack . trackName ) , MPMediaItemPropertyTitle ) ; this . _nowPlayingInfo . setValueForKey ( NSString . stringWithString ( this . _currentTrack . albumName ) , MPMediaItemPropertyAlbumTitle ) ; this . _nowPlayingInfo . setValueForKey ( NSString . stringWithString ( this . _currentTrack . artistName ) , MPMediaItemPropertyArtist ) ; this . _nowPlayingInfo . setValueForKey ( NSNumber . numberWithFloat ( this . _currentTrack . trackDuration * .001 ) , MPMediaItemPropertyPlaybackDuration ) ; this . _nowPlayingInfo . setValueForKey ( NSNumber . numberWithDouble ( 1.0 ) , MPNowPlayingInfoPropertyPlaybackRate ) ; if ( artwork ) { this . _nowPlayingInfo . setValueForKey ( artwork , MPMediaItemPropertyArtwork ) ; } MPNowPlayingInfoCenter . defaultCenter ( ) . nowPlayingInfo = this . _nowPlayingInfo ; } } private initCommandCenter ( ) { let errorRef = new interop . Reference ( ) ; ( < any > AVAudioSession . sharedInstance ( ) ) . setCategoryError ( AVAudioSessionCategoryPlayback , errorRef ) ; if ( errorRef ) { console . log ( `setCategoryError: ${ errorRef . value }` ) ; } ( < any > AVAudioSession . sharedInstance ( ) ) . setActiveError ( true , errorRef ) ; if ( errorRef ) { console . log ( `setActiveError: ${ errorRef . value }` ) ; } this . _cmdCenterHandler = ( < any > CommandCenterHandler ) . initWithOwner ( new WeakRef ( < any > this ) ) ; MPRemoteCommandCenter . sharedCommandCenter ( ) . pauseCommand . addTargetAction ( this . _cmdCenterHandler , 'cmdPause' ) ; MPRemoteCommandCenter . sharedCommandCenter ( ) . playCommand . addTargetAction ( this . _cmdCenterHandler , 'cmdPlay' ) ; MPRemoteCommandCenter . sharedCommandCenter ( ) . stopCommand . addTargetAction ( this . _cmdCenterHandler , 'cmdStop' ) ; MPRemoteCommandCenter . sharedCommandCenter ( ) . togglePlayPauseCommand . addTargetAction ( this . _cmdCenterHandler , 'cmdTogglePlay' ) ; MPRemoteCommandCenter . sharedCommandCenter ( ) . nextTrackCommand . addTargetAction ( this . _cmdCenterHandler , 'cmdNext' ) ; MPRemoteCommandCenter . sharedCommandCenter ( ) . previousTrackCommand . addTargetAction ( this . _cmdCenterHandler , 'cmdPrev' ) ; MPRemoteCommandCenter . sharedCommandCenter ( ) . seekForwardCommand . addTargetAction ( this . _cmdCenterHandler , 'cmdSeekFwd' ) ; MPRemoteCommandCenter . sharedCommandCenter ( ) . seekBackwardCommand . addTargetAction ( this . _cmdCenterHandler , 'cmdSeekBack' ) ; } private updateLogin ( loggedIn ) { this . toggleLoader ( false ) ; this . logger . debug ( `player.service updateLogin: ${ loggedIn }` ) ; setTimeout ( ( ) => { this . store . dispatch ( { type : AUTH_ACTIONS . LOGGED_IN_CHANGE , payload : { loggedIn } } ) ; if ( ! loggedIn ) { this . store . dispatch ( { type : PLAYER_ACTIONS . STOP , payload : { reset : true } } ) ; } } , 300 ) ; } private loginSuccess ( ) { this . logger . debug ( `loginSuccess!` ) ; this . updateLogin ( true ) ; } private loginError ( error ) { this . logger . debug ( `loginError!` ) ; this . logger . debug ( error ) ; let label = '' ; if ( isString ( error ) ) { label = error ; if ( label . length > 35 ) { label = label . substring ( 0 , 35 ) ; } } else if ( isObject ( error ) ) { label = error . localizedDescription || error . description || error . message ; } this . track ( `${ CATEGORY }_LOGIN_ERROR` , { label } ) ; this . updateLogin ( false ) ; } private trackEnded ( url ) { this . store . take ( 1 ) . subscribe ( ( s ) => { let currentTrackId = s . player . currentTrackId ; let trackEndedId = url . split ( ':' ) [ 2 ] ; if ( currentTrackId === trackEndedId ) { this . logger . debug ( `*****     currentTrackId: ${ currentTrackId }` ) ; this . logger . debug ( `*****     trackEndedId: ${ trackEndedId }` ) ; this . logger . debug ( `*****     spotify.isPlaying(): ${ this . _spotify . isPlaying ( ) }` ) ; if ( ! this . _spotify . isPlaying ( ) ) { this . ngZone . run ( ( ) => { this . store . dispatch ( { type : PLAYLIST_ACTIONS . LOOP_NEXT } ) ; } ) ; } } } ) ; } private tmpConnectionError ( ) { this . logger . debug ( 'Temporary connection error.' ) ; this . playerUIStateReset ( ) ; } private streamError ( error ) { this . logger . debug ( 'Stream error:' ) ; this . logger . debug ( error ) ; if ( error ) { if ( error . toString ( ) . indexOf ( 'requires a Spotify Premium' ) > - 1 ) { this . fancyalert . show ( TextService . SPOTIFY_PREMIUM_MSG ) ; TNSSpotifyAuth . CLEAR_COOKIES = true ; TNSSpotifyAuth . LOGOUT ( ) ; } } this . playerUIStateReset ( ) ; } private receivedMessage ( message ) { this . logger . debug ( 'Spotify service message:' ) ; this . logger . debug ( message ) ; } private streamDisconnected ( ) { this . logger . debug ( 'Stream has disconnected.' ) ; PlayerService . isPlaying = false ; this . playerUIStateReset ( ) ; this . fancyalert . show ( TextService . STREAM_DISCONNECT ) ; } private updatePlayerState ( state ) { if ( state && state . currentTrack ) { if ( ! this . _currentTrack || ( this . _currentTrack && this . _currentTrack . uri !== state . currentTrack . uri ) ) { this . _currentTrack = { uri : state . currentTrack . uri . trim ( ) , trackName : state . currentTrack . name . trim ( ) , albumName : state . currentTrack . albumName . trim ( ) , artistName : state . currentTrack . artistName . trim ( ) , trackDuration : state . currentTrack . durationMs } ; this . ngZone . run ( ( ) => { this . currentTrack$ . next ( this . _currentTrack ) ; } ) ; } this . logger . debug ( `----------` ) ; let totalDurationSeconds = state . currentTrack . durationMs * .001 ; let currentPlayback = 0 ; if ( isIOS ) { currentPlayback = this . _spotify . player . currentPlaybackPosition ; this . logger . debug ( `player state change, totalDurationSeconds: ${ totalDurationSeconds }` ) ; this . logger . debug ( `player state change, currentPlaybackPosition: ${ currentPlayback }` ) ; this . logger . debug ( `player state change, track.uri: ${ state . currentTrack . uri }` ) ; let diff = totalDurationSeconds - currentPlayback ; if ( diff < 2.5 && diff > 0 ) { this . trackEnded ( state . currentTrack . uri ) ; } } else { currentPlayback = state . currentTrack . positionInMs * .001 ; if ( state . currentTrack . eventType === 'TRACK_END' ) { this . trackEnded ( state . currentTrack . uri . trim ( ) ) ; } } } } private toggleLoader ( enable ) { this . store . dispatch ( { type : enable ? PROGRESS_ACTIONS . SHOW : PROGRESS_ACTIONS . HIDE } ) ; } private setupEvents ( ) { this . _spotify . events . on ( 'albumArtChange' , ( eventData ) => { this . ngZone . run ( ( ) => { this . updateAlbumArt ( eventData . data . url ) ; } ) ; } ) ; this . _spotify . events . on ( 'changedPlaybackState' , ( eventData ) => { this . updatePlayerState ( eventData . data . state ) ; } ) ; this . _spotify . events . on ( 'playerReady' , ( eventData ) => { this . ngZone . run ( ( ) => { this . toggleLoader ( false ) ; } ) ; } ) ; this . _spotify . events . on ( 'temporaryConnectionError' , ( eventData ) => { this . ngZone . run ( ( ) => { this . tmpConnectionError ( ) ; } ) ; } ) ; this . _spotify . events . on ( 'streamError' , ( eventData ) => { this . ngZone . run ( ( ) => { this . streamError ( eventData . data . error ) ; } ) ; } ) ; this . _spotify . events . on ( 'receivedMessage' , ( eventData ) => { this . ngZone . run ( ( ) => { this . receivedMessage ( eventData . data . message ) ; } ) ; } ) ; this . _spotify . events . on ( 'streamDisconnected' , ( eventData ) => { this . ngZone . run ( ( ) => { this . streamDisconnected ( ) ; } ) ; } ) ; this . _spotify . auth . events . on ( 'authLoginChange' , ( eventData ) => { this . ngZone . run ( ( ) => { this . updateLogin ( eventData . data . status ) ; } ) ; } ) ; this . _spotify . auth . events . on ( 'authLoginSuccess' , ( eventData ) => { this . ngZone . run ( ( ) => { this . _throttledLoginSuccess ( ) ; } ) ; } ) ; this . _spotify . auth . events . on ( 'authLoginError' , ( eventData ) => { this . ngZone . run ( ( ) => { this . loginError ( eventData . data ) ; } ) ; } ) ; } } @ Injectable ( ) export class PlayerEffects { constructor ( private store , private logger , private actions$ , private player ) { } @ Effect ( { dispatch : false } ) listTogglePlay$ = this . actions$ . ofType ( PLAYER_ACTIONS . LIST_TOGGLE_PLAY ) . do ( ( action ) => { this . logger . debug ( `PlayerEffects.LIST_TOGGLE_PLAY` ) ; this . store . take ( 1 ) . subscribe ( ( state ) => { let result = ListViewHelper . update ( state , action . payload . trackId , action . payload . activeList , action . payload . activeShoutOutPath , action . payload . playlistId ) ; if ( result . ready ) { this . store . dispatch ( { type : PLAYER_ACTIONS . TOGGLE_PLAY , payload : { currentTrackId : result . trackId , playing : result . playing , activeShoutOutPath : result . activeShoutOutPath , activeList : action . payload . activeList } } ) ; this . store . dispatch ( { type : FIREBASE_ACTIONS . UPDATE , payload : { playlists : result . state . playlists , sharedlist : result . state . sharedlist } } ) ; } else { this . player . showAlert ( result . msg ) ; } } ) ; } ) ; }