Reputation: 335
I have been trying to implement a simple MediaPlayer (audio) using the prepareAsync() method along with the onPrepared() method. These are are called from within an IntentService. The related activity only sends start and stop intent actions. I have verified the events are being sent from the activity. Here is the code in the IntentService:
package com.javacodegeeks.androidmusic_intentservice;
import android.app.IntentService;
import android.content.Intent;
import android.content.Context;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.widget.Toast;
import java.io.IOException;
import android.net.Uri;
/**
* An {@link IntentService} subclass for handling asynchronous task requests in
* a service on a separate handler thread.
* <p/>
* TODO: Customize class - update intent actions, extra parameters and static
* helper methods.
*/
public class MusicIntentService extends IntentService implements MediaPlayer.OnErrorListener, MediaPlayer.OnPreparedListener {
// TODO: Rename actions, choose action names that describe tasks that this
private static final String ACTION_PLAY = "com.example.action.PLAY";
private static final String ACTION_STOP = "com.example.action.STOP";
static MediaPlayer mMediaPlayer = null;
public MusicIntentService() {
super("MusicIntentService");
}
@Override
public void onHandleIntent(Intent intent) {
if (intent != null) if (intent.getAction().equals(ACTION_PLAY)) {
if (mMediaPlayer == null) {
mMediaPlayer = new MediaPlayer();
String fileName = "android.resource://" + getPackageName() + "/" + R.raw.georgeharrisonlivinginthematerialworld;
try {
mMediaPlayer.setDataSource(this, Uri.parse(fileName));
} catch (IllegalArgumentException e) {
Toast.makeText(getApplicationContext(), "IllegalArgumentException", Toast.LENGTH_LONG).show();
} catch (SecurityException e) {
Toast.makeText(getApplicationContext(), "SecurityException", Toast.LENGTH_LONG).show();
} catch (IllegalStateException e) {
Toast.makeText(getApplicationContext(), "IllegalStateException", Toast.LENGTH_LONG).show();
} catch (IOException e) {
e.printStackTrace();
}
}
mMediaPlayer.setOnErrorListener(this);
mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mMediaPlayer.setOnPreparedListener(this);
try {
mMediaPlayer.prepareAsync();
} catch (IllegalStateException e) {
Toast.makeText(getApplicationContext(), "IllegalStateException", Toast.LENGTH_LONG).show();
}
/*calling prepare() and start() works ok*/
/* it is better to have the prepareAsync() and onPrepared() pattern implemented*/
/* try {
mMediaPlayer.prepare();
} catch (IllegalStateException e) {
Toast.makeText(getApplicationContext(), "IllegalStateException prepare()", Toast.LENGTH_LONG).show();
} catch (IOException e) {
Toast.makeText(getApplicationContext(), "IOException prepare()", Toast.LENGTH_LONG).show();
}*/
/* try {
mMediaPlayer.start();
} catch (IllegalStateException e) {
Toast.makeText(getApplicationContext(), "IllegalStateException start()", Toast.LENGTH_LONG).show();
}*/
} else if (intent.getAction().equals(ACTION_STOP)) {
if (mMediaPlayer.isPlaying()) {
mMediaPlayer.stop();
}
}
}
/** Called when MediaPlayer is ready */
public void onPrepared(MediaPlayer player) {
try {
mMediaPlayer.start();
} catch (IllegalStateException e) {
Toast.makeText(getApplicationContext(), "IllegalStateException in onPrepared", Toast.LENGTH_LONG).show();
}
}
public boolean onError(MediaPlayer mp, int what, int extra) {
/*
... react appropriately ...
The MediaPlayer has moved to the Error state, must be reset!
*/
return true;
}
}
I know the control logic isn't bullet proof but I cannot even reach onPrepared on the first Play command. When I execute the code via either run or debug (the emulator is Nexus 5 API 21 x86, running on a MacBook Pro, compiled with API21 using Android Studio) the onPrepared() function is never reached. The same goes for trying to run on a Samsung SM-G900T (API 19).
I receive the following in the Logcat window with a Log level of Debug and filtering out everything not related to this package:
01-07 02:29:04.203 2208-2208/com.javacodegeeks.androidmusic_intentservice I/System.out﹕ waiting for debugger to settle...
01-07 02:29:04.414 2208-2208/com.javacodegeeks.androidmusic_intentservice I/System.out﹕ debugger has settled (1484)
01-07 02:29:04.521 2208-2235/com.javacodegeeks.androidmusic_intentservice D/OpenGLRenderer﹕ Render dirty regions requested: true
01-07 02:29:04.522 2208-2208/com.javacodegeeks.androidmusic_intentservice D/﹕ HostConnection::get() New Host Connection established 0xa6cafee0, tid 2208
01-07 02:29:04.532 2208-2208/com.javacodegeeks.androidmusic_intentservice D/Atlas﹕ Validating map...
01-07 02:29:04.597 2208-2235/com.javacodegeeks.androidmusic_intentservice D/﹕ HostConnection::get() New Host Connection established 0xa6caf960, tid 2235
01-07 02:29:04.602 2208-2235/com.javacodegeeks.androidmusic_intentservice I/OpenGLRenderer﹕ Initialized EGL, version 1.4
01-07 02:29:04.612 2208-2235/com.javacodegeeks.androidmusic_intentservice D/OpenGLRenderer﹕ Enabling debug mode 0
01-07 02:29:04.622 2208-2235/com.javacodegeeks.androidmusic_intentservice W/EGL_emulation﹕ eglSurfaceAttrib not implemented
01-07 02:29:04.622 2208-2235/com.javacodegeeks.androidmusic_intentservice W/OpenGLRenderer﹕ Failed to set EGL_SWAP_BEHAVIOR on surface 0xa6cb0700, error=EGL_SUCCESS
01-07 02:29:18.595 2208-2245/com.javacodegeeks.androidmusic_intentservice W/MessageQueue﹕ Handler (android.media.MediaPlayer$EventHandler) {27bce352} sending message to a Handler on a dead thread
java.lang.IllegalStateException: Handler (android.media.MediaPlayer$EventHandler) {27bce352} sending message to a Handler on a dead thread
at android.os.MessageQueue.enqueueMessage(MessageQueue.java:325)
at android.os.Handler.enqueueMessage(Handler.java:631)
at android.os.Handler.sendMessageAtTime(Handler.java:600)
at android.os.Handler.sendMessageDelayed(Handler.java:570)
at android.os.Handler.sendMessage(Handler.java:507)
at android.media.MediaPlayer.postEventFromNative(MediaPlayer.java:2660)
01-07 02:29:18.597 2208-2248/com.javacodegeeks.androidmusic_intentservice W/MessageQueue﹕ Handler (android.media.MediaPlayer$EventHandler) {27bce352} sending message to a Handler on a dead thread
java.lang.IllegalStateException: Handler (android.media.MediaPlayer$EventHandler) {27bce352} sending message to a Handler on a dead thread
at android.os.MessageQueue.enqueueMessage(MessageQueue.java:325)
at android.os.Handler.enqueueMessage(Handler.java:631)
at android.os.Handler.sendMessageAtTime(Handler.java:600)
at android.os.Handler.sendMessageDelayed(Handler.java:570)
at android.os.Handler.sendMessage(Handler.java:507)
at android.media.MediaPlayer.postEventFromNative(MediaPlayer.java:2660)
However when I debug and set break points on the following lines:
mMediaPlayer.setOnErrorListener(this);
mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mMediaPlayer.setOnPreparedListener(this);
try {
mMediaPlayer.prepareAsync();
and step through these statements and then select Resume after stepping over the prepareAsync() statement, the code does reach the onPrepared() method with with audio output being ok.
When I uncomment the prepare() and play() statements and comment out the prepareAsynch() statement I get audio output when I run or debug with or without breakpoints set. (That's why I left in the commented out code, to show what worked.) I understand it's just luck the prepare is completed before the play() method is reached.
The onError() is never reached and I understand there is work to be done there.
The other thing is, I did try:
mMediaPlayer.setOnPreparedListener(MusicIntentService.this);
And it didn't seem to make a difference. In fact when I opened a watch window with for "MusicIntentService.this" I saw the same elements and values as I did for "this".
I'd really appreciate it if someone could point me in the right direction. My next step was going to try and implement the listener at the point of the call to setOnpreparedListener(....), but I'd like to understand why this current implementation isn't working consistently. Thanks in advance. Jim
Upvotes: 1
Views: 2899
Reputation: 335
Here is what I did.
My original objective was to implement a simple media player utilizing the prepareAsynch() method. This would allow for a more robust design in that one would not have to just hope that the media player reaches the prepared state before the start() method was called.
As noted above, pskink set me straight about the IntentService not being appropriate for handling the media player control.
I then assumed I would use a Service, I started looking at handling button presses and keeping the media player states consistent with the UI.
Based on some reading I did regarding features included in Lollipop, I went back to a project I had downloaded from:
The activity starts a service which handles the button inputs as well as media player control:
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Intent intent = new Intent( getApplicationContext(), MediaPlayerService.class );
intent.setAction( MediaPlayerService.ACTION_PLAY );
startService( intent );
}
}
The MediaPlayerService was as follows:
/**
* Created by paulruiz on 10/28/14.
*/
public class MediaPlayerService extends Service {
public static final String ACTION_PLAY = "action_play";
public static final String ACTION_PAUSE = "action_pause";
public static final String ACTION_REWIND = "action_rewind";
public static final String ACTION_FAST_FORWARD = "action_fast_foward";
public static final String ACTION_NEXT = "action_next";
public static final String ACTION_PREVIOUS = "action_previous";
public static final String ACTION_STOP = "action_stop";
private MediaPlayer mMediaPlayer;
private MediaSessionManager mManager;
private MediaSession mSession;
private MediaController mController;
@Override
public IBinder onBind(Intent intent) {
return null;
}
private void handleIntent( Intent intent ) {
if( intent == null || intent.getAction() == null )
return;
String action = intent.getAction();
if( action.equalsIgnoreCase( ACTION_PLAY ) ) {
mController.getTransportControls().play();
} else if( action.equalsIgnoreCase( ACTION_PAUSE ) ) {
mController.getTransportControls().pause();
} else if( action.equalsIgnoreCase( ACTION_FAST_FORWARD ) ) {
mController.getTransportControls().fastForward();
} else if( action.equalsIgnoreCase( ACTION_REWIND ) ) {
mController.getTransportControls().rewind();
} else if( action.equalsIgnoreCase( ACTION_PREVIOUS ) ) {
mController.getTransportControls().skipToPrevious();
} else if( action.equalsIgnoreCase( ACTION_NEXT ) ) {
mController.getTransportControls().skipToNext();
} else if( action.equalsIgnoreCase( ACTION_STOP ) ) {
mController.getTransportControls().stop();
}
}
private Notification.Action generateAction( int icon, String title, String intentAction ) {
Intent intent = new Intent( getApplicationContext(), MediaPlayerService.class );
intent.setAction( intentAction );
PendingIntent pendingIntent = PendingIntent.getService(getApplicationContext(), 1, intent, 0);
return new Notification.Action.Builder( icon, title, pendingIntent ).build();
}
private void buildNotification( Notification.Action action ) {
Notification.MediaStyle style = new Notification.MediaStyle();
Intent intent = new Intent( getApplicationContext(), MediaPlayerService.class );
intent.setAction( ACTION_STOP );
PendingIntent pendingIntent = PendingIntent.getService(getApplicationContext(), 1, intent, 0);
Notification.Builder builder = new Notification.Builder( this )
.setSmallIcon(R.drawable.ic_launcher)
.setContentTitle( "Media Title" )
.setContentText( "Media Artist" )
.setDeleteIntent( pendingIntent )
.setStyle(style);
builder.addAction( generateAction( android.R.drawable.ic_media_previous, "Previous", ACTION_PREVIOUS ) );
builder.addAction( generateAction( android.R.drawable.ic_media_rew, "Rewind", ACTION_REWIND ) );
builder.addAction( action );
builder.addAction( generateAction( android.R.drawable.ic_media_ff, "Fast Foward", ACTION_FAST_FORWARD ) );
builder.addAction( generateAction( android.R.drawable.ic_media_next, "Next", ACTION_NEXT ) );
style.setShowActionsInCompactView(0,1,2,3,4);
NotificationManager notificationManager = (NotificationManager) getSystemService( Context.NOTIFICATION_SERVICE );
notificationManager.notify( 1, builder.build() );
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if( mManager == null ) {
initMediaSessions();
}
handleIntent( intent );
return super.onStartCommand(intent, flags, startId);
}
private void initMediaSessions() {
mMediaPlayer = new MediaPlayer();
mSession = new MediaSession(getApplicationContext(), "simple player session");
mController =new MediaController(getApplicationContext(), mSession.getSessionToken());
mSession.setCallback(new MediaSession.Callback(){
@Override
public void onPlay() {
super.onPlay();
Log.e( "MediaPlayerService", "onPlay");
buildNotification( generateAction( android.R.drawable.ic_media_pause, "Pause", ACTION_PAUSE ) );
}
@Override
public void onPause() {
super.onPause();
Log.e( "MediaPlayerService", "onPause");
buildNotification(generateAction(android.R.drawable.ic_media_play, "Play", ACTION_PLAY));
}
@Override
public void onSkipToNext() {
super.onSkipToNext();
Log.e( "MediaPlayerService", "onSkipToNext");
//Change media here
buildNotification( generateAction( android.R.drawable.ic_media_pause, "Pause", ACTION_PAUSE ) );
}
@Override
public void onSkipToPrevious() {
super.onSkipToPrevious();
Log.e( "MediaPlayerService", "onSkipToPrevious");
//Change media here
buildNotification( generateAction( android.R.drawable.ic_media_pause, "Pause", ACTION_PAUSE ) );
}
@Override
public void onFastForward() {
super.onFastForward();
Log.e( "MediaPlayerService", "onFastForward");
//Manipulate current media here
}
@Override
public void onRewind() {
super.onRewind();
Log.e( "MediaPlayerService", "onRewind");
//Manipulate current media here
}
@Override
public void onStop() {
super.onStop();
Log.e( "MediaPlayerService", "onStop");
//Stop media player here
NotificationManager notificationManager = (NotificationManager) getApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel( 1 );
Intent intent = new Intent( getApplicationContext(), MediaPlayerService.class );
stopService( intent );
}
@Override
public void onSeekTo(long pos) {
super.onSeekTo(pos);
}
@Override
public void onSetRating(Rating rating) {
super.onSetRating(rating);
}
}
);
}
As the author notes, the actual media player instantiation and implementation is not present, however: "And with that we now have a fully working MediaStyle notification on our lock screen and in the notification drawer that takes advantage of MediaSession for playback control. Enjoy!"
I made several modifications.
The primary changes were to the method initMediaSessions(). Code was added to instantiate and initialize the media player. This is where the prepareAsync() and setOnPreparedListener() calls were added.
Changes were also made to the onPlay() and onPause() callbacks to include media player control methods.
public void initMediaSessions () {
Uri uri = Uri.parse("http://devimages.apple.com/iphone/samples/bipbop/bipbopall.m3u8" );
//String fileName = "android.resource://" + getPackageName() + "/" + R.raw.georgeharrisonlivinginthematerialworld;
//Uri uri = Uri.parse(fileName);
try {
mMediaPlayer = new MediaPlayer();
} catch (IllegalArgumentException e) {
Toast.makeText(getApplicationContext(), "IllegalArgumentException", Toast.LENGTH_LONG).show();
} catch (SecurityException e) {
Toast.makeText(getApplicationContext(), "SecurityException", Toast.LENGTH_LONG).show();
} catch (IllegalStateException e) {
Toast.makeText(getApplicationContext(), "IllegalStateException", Toast.LENGTH_LONG).show();
}
try {
mMediaPlayer.setDataSource(this, uri);
} catch (IllegalArgumentException e) {
Toast.makeText(getApplicationContext(), "IllegalArgumentException", Toast.LENGTH_LONG).show();
} catch (SecurityException e) {
Toast.makeText(getApplicationContext(), "SecurityException", Toast.LENGTH_LONG).show();
} catch (IllegalStateException e) {
Toast.makeText(getApplicationContext(), "IllegalStateException", Toast.LENGTH_LONG).show();
} catch (IOException e) {
e.printStackTrace();
}
mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mMediaPlayer.setOnPreparedListener(this);
try {
mMediaPlayer.prepareAsync();
} catch (IllegalArgumentException e) {
Toast.makeText(getApplicationContext(), "IllegalArgumentException", Toast.LENGTH_LONG).show();
} catch (SecurityException e) {
Toast.makeText(getApplicationContext(), "SecurityException", Toast.LENGTH_LONG).show();
} catch (IllegalStateException e) {
Toast.makeText(getApplicationContext(), "IllegalStateException", Toast.LENGTH_LONG).show();
}
mManager= (MediaSessionManager)getSystemService(Context.MEDIA_SESSION_SERVICE);
mSession = new MediaSession(getApplicationContext(), "simple player session");
mController =new MediaController(getApplicationContext(), mSession.getSessionToken());
mSession.setCallback(new MediaSession.Callback(){
@Override
public void onPlay() {
super.onPlay();
Log.e("MediaPlayerService", "onPlay");
if(first_time_through == true)
{
first_time_through = false;
}
else {
try {
mMediaPlayer.start();
} catch (IllegalArgumentException e) {
Toast.makeText(getApplicationContext(), "IllegalArgumentException", Toast.LENGTH_LONG).show();
} catch (SecurityException e) {
Toast.makeText(getApplicationContext(), "SecurityException", Toast.LENGTH_LONG).show();
} catch (IllegalStateException e) {
Toast.makeText(getApplicationContext(), "IllegalStateException", Toast.LENGTH_LONG).show();
}
}
buildNotification(generateAction(android.R.drawable.ic_media_pause, "Pause", ACTION_PAUSE));
}
@Override
public void onPause() {
super.onPause();
mMediaPlayer.pause();
Log.e( "MediaPlayerService", "onPause");
buildNotification(generateAction(android.R.drawable.ic_media_play, "Play", ACTION_PLAY));
}
@Override
public void onSkipToNext() {
super.onSkipToNext();
Log.e( "MediaPlayerService", "onSkipToNext");
//Change media here
buildNotification( generateAction( android.R.drawable.ic_media_pause, "Pause", ACTION_PAUSE ) );
}
@Override
public void onSkipToPrevious() {
super.onSkipToPrevious();
Log.e( "MediaPlayerService", "onSkipToPrevious");
//Change media here
buildNotification( generateAction( android.R.drawable.ic_media_pause, "Pause", ACTION_PAUSE ) );
}
@Override
public void onFastForward() {
super.onFastForward();
Log.e( "MediaPlayerService", "onFastForward");
//Manipulate current media here
}
@Override
public void onRewind() {
super.onRewind();
Log.e( "MediaPlayerService", "onRewind");
//Manipulate current media here
}
@Override
public void onStop() {
super.onStop();
Log.e( "MediaPlayerService", "onStop");
//Stop media player here
NotificationManager notificationManager = (NotificationManager) getApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel( 1 );
Intent intent = new Intent( getApplicationContext(), MediaPlayerService.class );
stopService( intent );
}
@Override
public void onSeekTo(long pos) {
super.onSeekTo(pos);
}
@Override
public void onSetRating(Rating rating) {
super.onSetRating(rating);
}
}
);
}
The activity starts the MediaPlayerService with the ACTION_PLAY intent action:
intent.setAction( MediaPlayerService.ACTION_PLAY );
The first_time_through logic was added to the onPlay() callback to prevent the mMediaPlayer.start() method from being called before the onPrepared() call back was executed. The following was added to the MediaPlayerService class:
public static boolean first_time_through = true;
The onPrepared() callback was also added to the MediaPlayerService.
/** Called when MediaPlayer is ready */
public void onPrepared(MediaPlayer player) {
try {
mMediaPlayer.start();
} catch (IllegalStateException e) {
Toast.makeText(getApplicationContext(), "IllegalStateException in onPrepared", Toast.LENGTH_LONG).show();
}
}
Since I tried this with an HLS source the following line was added to the AndroidManifest.xml file:
<uses-permission android:name="android.permission.INTERNET" />
In order to allow the lock screen notifications to control the media the following was also added to the AndroidManifest.xml file:
<permission android:name="android.permission.MEDIA_CONTENT_CONTROL" />
As it currently exists the app only allows two dynamic media player states, namely: Playing and Paused. Pressing the other buttons can lead to unexpected behavior. Furthermore, not all the media control methods are applicable to both .mp3 and an HLS sources. I will attempt to clean this up.
The first_time_through logic could probably be replaced by modifying the initialization but I'm not sure they too wouldn't have possible race conditions.
I tested this on a MacBook Pro using the "Nexus 5 21 API x86" emulator. The .mp3 file seems to play just fine. When I use the HLS source, audio is generally lost in about 20 seconds or so. The HLS source is actually a video file. When audio is lost, the following appear in the logcat:
01-10 01:25:22.074 1219-1237/system_process I/ActivityManager﹕ Waited long enough for: ServiceRecord{3ca21a0b u0 com.google.android.gms/.wearable.service.WearableService}
01-10 01:25:26.204 931-1216/? I/AudioFlinger﹕ BUFFER TIMEOUT: remove(4096) from active list on thread 0xb62b8000
01-10 01:25:34.675 2076-2100/com.android.calendar D/InitAlarmsService﹕ Clearing and rescheduling alarms.
01-10 01:25:59.696 1219-1232/system_process I/MediaFocusControl﹕ AudioFocus abandonAudioFocus() from android.media.AudioManager@1c78d923com.android.music.MediaPlaybackService$3@5af4e20
When I use this same audio source on my Samsung Galaxy S5, using a simplified (API 4.4.2 version of the app) the audio plays just fine. Furthermore, as expected, the same HLS source plays ok when started from Safari. I don't think there is too much to gain by looking into the HLS playback by the emulator.
Thanks again pskink.
Jim
Upvotes: 1