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());
}
}
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:
You can set the total duration of Media file using MediaSessionCompat.setMetadata(Metadata).
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.