Android communication between MediaPlayer and SeekBar in MediaSession Architecuture

1.5k views Asked by At

I'm developing an audio streaming app. I've designed my app in the way android describes here. In my app, I have one activity, MainActivity, which loads fragments according to selected functions. In one of these fragments, I provide a ReplayPlayer where I would like to let users seek through the streamed audio, play/pause the stream, etc. I found this and have designed my app such that my StreamService controls the MediaPlayer, thus the MediaPlayer for my app resides in StreamService.

The problem is that I'm trying to associate my SeekBar in ReplayPlayer with the Media in MediaPlayer in StreamService. As to my understanding, MediaBrowserService cannot be bound, unlike Service, so I cannot access the currentPosition of my MediaPlayer in StreamService. So, I'm stuck with how I could access this MediaPlayer's currentPosition from my ReplayPlayer fragment.

As other music player apps clearly show the current position in songs, I sense that there is a way to achieve what I'm struggling with right now. How can I do so?

Thanks in advance for the help.

AndroidManifest.xml

<application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:theme="@style/TransparentTheme"
    android:usesCleartextTraffic="true">
    <activity android:name=".MainActivity" android:windowSoftInputMode="stateVisible|adjustResize"/>

    <service android:name=".StreamService">
        <intent-filter>
            <action android:name="android.media.browse.MediaBrowserService" />
            <action android:name="android.intent.action.MEDIA_BUTTON" />
            <action android:name="android.media.browse.AUDIO_BECOMING_NOISY" />
        </intent-filter>
    </service>

    <receiver android:name="androidx.media.session.MediaButtonReceiver">
        <intent-filter>
            <action android:name="android.intent.action.MEDIA_BUTTON" />
            <action android:name="android.media.AUDIO_BECOMING_NOISY" />
        </intent-filter>
    </receiver>

    <activity android:name=".SplashActivity" android:noHistory="true">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
</application>

StreamService.java

public class StreamService extends MediaBrowserServiceCompat implements MediaPlayer.OnPreparedListener, MediaPlayer.OnErrorListener, MediaPlayer.OnCompletionListener, AudioManager.OnAudioFocusChangeListener{

    private static final String TAG = "StreamService";

    private MediaPlayer mediaPlayer;
    private MediaSessionCompat mediaSessionCompat;
    private BroadcastReceiver mNoisyReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            if(mediaPlayer != null && mediaPlayer.isPlaying()) mediaPlayer.pause();
        }
    };

    private WifiManager.WifiLock wifiLock;

    @Override
    public void onCreate() {
        super.onCreate();

        wifiLock =((WifiManager)getApplicationContext().getSystemService(Context.WIFI_SERVICE)).createWifiLock(WifiManager.WIFI_MODE_FULL, "myLock");

        initMediaSession();
        initNoisyReceiver();
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
        audioManager.abandonAudioFocus(this);
        unregisterReceiver(mNoisyReceiver);
        mediaSessionCompat.release();
        if(mediaPlayer!=null) mediaPlayer.release();
        stopSelf();
    }

    private void initMediaSession(){
        ComponentName mediaButtonReceiver = new ComponentName(getApplicationContext(), MediaButtonReceiver.class);
        mediaSessionCompat = new MediaSessionCompat(getApplicationContext(), "Tag", mediaButtonReceiver, null);

        mediaSessionCompat.setCallback(mediaSessionCallbacks);
        mediaSessionCompat.setFlags( MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS );

        Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON);
        mediaButtonIntent.setClass(this, MediaButtonReceiver.class);
        PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, mediaButtonIntent, 0);
        mediaSessionCompat.setMediaButtonReceiver(pendingIntent);

        setSessionToken(mediaSessionCompat.getSessionToken());
    }

    private void initNoisyReceiver(){
        //Handles headphones coming unplugged. cannot be done through a manifest receiver
        IntentFilter filter = new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
        registerReceiver(mNoisyReceiver, filter);
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        MediaButtonReceiver.handleIntent(mediaSessionCompat, intent);
        return super.onStartCommand(intent, flags, startId);
    }

    @Nullable
    @Override
    public BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid, @Nullable Bundle rootHints) {
        if(TextUtils.equals(clientPackageName, getPackageName())) {
            return new BrowserRoot(getString(R.string.app_name), null);
        }

        return null;
    }

    @Override
    public void onLoadChildren(@NonNull String parentId, @NonNull Result<List<MediaBrowserCompat.MediaItem>> result) {
        result.sendResult(null);
    }

    private MediaSessionCompat.Callback mediaSessionCallbacks = new MediaSessionCompat.Callback() {
        @Override
        public void onPlay() {
            super.onPlay();
            if( !successfullyRetrievedAudioFocus() ) {
                return;
            }
            mediaPlayer.start();
            setMediaPlaybackState(PlaybackStateCompat.STATE_PLAYING);
        }

        @Override
        public void onPause() {
            super.onPause();

            if( mediaPlayer.isPlaying() ) {
                mediaPlayer.pause();
                setMediaPlaybackState(PlaybackStateCompat.STATE_PAUSED);
            }
        }

        @Override
        public void onPlayFromUri(Uri uri, Bundle extras) {
            if(mediaPlayer != null) mediaPlayer.release();
            super.onPlayFromUri(uri, extras);

            try {
                mediaPlayer = new MediaPlayer();
                mediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);
                mediaPlayer.setDataSource(uri.toString());
                mediaPlayer.setVolume(1.0f, 1.0f);

                if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
                    mediaPlayer.setAudioAttributes(new AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_MUSIC).build());
                else // setAudioStreamType deprecated past Oreo
                    mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
                mediaPlayer.setOnPreparedListener(StreamService.this);
                setMediaSessionMetadata(extras);

                mediaPlayer.prepareAsync();
                wifiLock.acquire();
            } catch(IOException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onStop() {
            super.onStop();
            Log.d(TAG, "onStop called");
            if(mediaPlayer.isPlaying()) {
                mediaPlayer.release();
                setMediaPlaybackState(PlaybackStateCompat.STATE_STOPPED);
            }
        }
    };

    @Override
    public void onPrepared(MediaPlayer mp) {
        Log.d(TAG, "mediaPlayer prepared");
        mediaSessionCompat.setActive(true);
        mediaPlayer.start();
        setMediaPlaybackState(PlaybackStateCompat.STATE_PLAYING);
        successfullyRetrievedAudioFocus();
    }

    @Override
    public boolean onError(MediaPlayer mp, int what, int extra) {
        mp = null;
        wifiLock.release();
        return false;
    }

    @Override
    public void onCompletion(MediaPlayer mp) {
        if(mp != null) mp.release();
    }

    @Override
    public void onAudioFocusChange(int focusChange) {
        switch( focusChange ) {
            case AudioManager.AUDIOFOCUS_LOSS: {
                Log.d(TAG, "audio focus loss");
                if( mediaPlayer.isPlaying() ) {
                    mediaPlayer.stop();
                }
                break;
            }
            case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: {
                Log.d(TAG, "audio focus loss transient");
                mediaPlayer.pause();
                break;
            }
            case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: {
                Log.d(TAG, "audio focus loss transient can duck");
                if( mediaPlayer != null ) {
                    mediaPlayer.setVolume(0.3f, 0.3f);
                }
                break;
            }
            case AudioManager.AUDIOFOCUS_GAIN: {
                Log.d(TAG, "audio focus gain");
                if( mediaPlayer != null ) {
                    if( !mediaPlayer.isPlaying() ) {
                        mediaPlayer.start();
                    }
                    mediaPlayer.setVolume(1.0f, 1.0f);
                }
                break;
            }
        }
    }

    private boolean successfullyRetrievedAudioFocus() {
        AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
        AudioAttributes attr = new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA).setContentType(AudioAttributes.CONTENT_TY    PE_MUSIC).build();
        int result = -1;
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            result = audioManager.requestAudioFocus(new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN).setAudioAttributes(attr).setAcceptsDelayedFocusGain(true).setOnAudioFocusChangeListener(this).build());
            synchronized(this) {
                return result==AudioManager.AUDIOFOCUS_GAIN;
            }
        } else {
            result = audioManager.requestAudioFocus(this, AudioAttributes.CONTENT_TYPE_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
            return result == AudioManager.AUDIOFOCUS_GAIN;
        }
    }

    private void setMediaPlaybackState(int state) {
        PlaybackStateCompat.Builder playbackstateBuilder = new PlaybackStateCompat.Builder();
        if( state == PlaybackStateCompat.STATE_PLAYING ) {
            playbackstateBuilder.setActions(PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_PAUSE);
        } else {
            playbackstateBuilder.setActions(PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_PLAY);
        }
        playbackstateBuilder.setState(state, PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, 0);
        mediaSessionCompat.setPlaybackState(playbackstateBuilder.build());
    }

    private void setMediaSessionMetadata(Bundle extras) {
        MediaMetadataCompat.Builder metadataBuilder = new MediaMetadataCompat.Builder();

        if(extras.getParcelable("Track")!=null) {
            Track track = extras.getParcelable("Track");
            Log.d(TAG, String.valueOf(track.getID()));
            metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, String.valueOf(track.getID()));
            metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_ART, track.getArtworkURL());
            metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, track.getTitle());
            metadataBuilder.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI, track.getStreamURL());
            metadataBuilder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, track.getDuration());
        }

        mediaSessionCompat.setMetadata(metadataBuilder.build());
    }
}
1

There are 1 answers

3
A-run On BEST ANSWER

You can post your MediaPlayer's position updates in MediaSessionCompat on service side and then in your fragment you will receive Playback state updates in MediaControllerCompat.Callback's onPlaybackStateChanged(PlaybackStateCompat state) method. Use a handler to post MediaPlayer update at regular intervals. Something like this:

public void onPlay() {
    if (!mMediaPlayer.isPlaying()) {
        mMediaPlayer.start();
        updateCurrentPosition();
    }
}

public void onPause() {
    super.onPause();
    if (mMediaPlayer.isPlaying()) {
        mMediaPlayer.pause();
        PlaybackStateCompat playbackState = new PlaybackStateCompat.Builder()
                .setActions(PAUSE_ACTIONS)
                .setState(PlaybackStateCompat.STATE_PAUSED, mMediaPlayer.getCurrentPosition()
                        , 0)
                .build();
        stopPlaybackStateUpdate();
    }
}


private void updateCurrentPosition() {
    if (mMediaPlayer == null) {
        return;
    }
    handler.postDelayed(new Runnable() {
        @Override
        public void run() {
            int currentPosition = mMediaPlayer.getCurrentPosition();
            PlaybackStateCompat playbackState = new PlaybackStateCompat.Builder()
                    .setActions(PLAYING_ACTIONS)
                    .setState(PlaybackStateCompat.STATE_PLAYING, currentPosition, 1)
                    .build();
            mMediaSession.setPlaybackState(playbackState);
            updateCurrentPosition();
        }
    }, 1000);
}

private void stopPlaybackStateUpdate() {
    if (handler != null) {
        handler.removeCallbacksAndMessages(null);
    }
}

You can set the total duration of Media file using MediaSessionCompat.setMetadata(Metadata).

MediaMetadataCompat mediaMetadata = new MediaMetadataCompat.Builder()
            .putString(MediaMetadata.METADATA_KEY_TITLE, "XYZ"))
            .putLong(MediaMetadata.METADATA_KEY_DURATION, mMediaPlayer.getDuration())
            .build();

In your your Fragment/Activity, you can then set the progress(i.e Seekbar.setProgress()) of current position of MediaPlayer using the Position update that are received in MediaController.Callback. Also set the Seekbar.setMax using the total duration of currently playing media.

    private MediaControllerCompat.Callback mMediaControllerCallback =

        new MediaControllerCompat.Callback() {
            @Override
            public void onMetadataChanged(MediaMetadataCompat metadata) {
                super.onMetadataChanged(metadata);
                int totalDuration = (int) metadata.getLong(MediaMetadataCompat.METADATA_KEY_DURATION);
                mSeekBar.setMax(totalDuration);
            }

            @Override
            public void onPlaybackStateChanged(PlaybackStateCompat state) {
                mSeekBar.setMax(state.getPosition()); //You will receive MediaPlayer's current position every 1 second here.
            }

        };