Sapan Diwakar

Software developer

Follow me on Twitter Check out my code on GitHub View some of my designs on Dribbble Take a look at my Linked In profile

Media Player basics for Android

As my previous post about creating Android Music Player was getting a lot of traction, I figured it's about time to do a rewrite as the previous one is ages old and there have been a lot of improvements and better APIs on Android for supporting music since then. This is going to be the first of a series of post talking about building a full fledged music player app with streaming, custom playlists, offline tracks (basically a boiled down Spotify clone).

Media Player Basics

First, let's talk about playing music on the device. The Android multimedia framework includes support for playing variety of common media types, so that you can easily integrate audio, video and images into your applications. You can play audio or video from media files stored in your application's resources (raw resources), from standalone files in the filesystem, or from a data stream arriving over a network connection, all using MediaPlayer APIs.

Creating a MediaPlayer

One of the most important components of the media framework is the MediaPlayer class. An object of this class can fetch, decode, and play both audio and video with minimal setup. It supports several different media sources such as:

  • Local resources
  • Internal URIs, such as one you might obtain from a Content Resolver
  • External URLs (streaming)

Here's how you can create a media player

private void createMediaPlayerIfNeeded() {  
    if (mMediaPlayer == null) {
        mMediaPlayer = new MediaPlayer();

        // Make sure the media player will acquire a wake-lock while
        // playing. If we don't do that, the CPU might go to sleep while the
        // song is playing, causing playback to stop.
        mMediaPlayer.setWakeMode(mService.getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);

        // we want the media player to notify us when it's ready preparing,
        // and when it's done playing:
        mMediaPlayer.setOnPreparedListener(this);
        mMediaPlayer.setOnCompletionListener(this);
        mMediaPlayer.setOnErrorListener(this);
        mMediaPlayer.setOnSeekCompleteListener(this);
    } else {
        mMediaPlayer.reset();
    }
}

Handling MediaPlayer states

MediaPlayer manages the playback control as a state machine. That is, the MediaPlayer has an internal state that you must always be aware of when writing your code, because certain operations are only valid when then player is in specific states. For example, you should only try to play the media after it has finished preparing. This is why we set the OnPreparedListener to get notified when the media player finishes preparing so we can start playback.

/**
 * Called when media player is done preparing.
 */
@Override
public void onPrepared(MediaPlayer player) {  
    // The media player is done preparing. That means we can start playing if we
    // have audio focus.
    configMediaPlayerState();
}

Handling noisy intent and audio focus

In order to keep this blog post short and easy to understand, I will keep the more complex features out with a link on how to implement them. Well written apps need to do a bit more than just playing audio. For example, when the user disconnects headphones accidentally, the app should pause the music automatically to prevent playing it out through the speakers. You would need to handle the AUDIO_BECOMING_NOISY Intent to implement this in your app.

Another important thing to remember when building a music app is to correctly handle audio focus. Even though only one activity can run at any given time, Android is a multi-tasking environment. When a user is listening to music and another application needs to notify the user of something very important, the user might not hear the notification tone due to the loud music. When your application needs to output audio such as music or a notification, you should always request audio focus. Once it has focus, it can use the sound output freely, but it should always listen for focus changes. If it is notified that it has lost the audio focus, it should immediately either kill the audio or lower it to a quiet level (known as "ducking"—there is a flag that indicates which one is appropriate) and only resume loud playback after it receives focus again.

/**
 * Reconfigures MediaPlayer according to audio focus settings and
 * starts/restarts it. This method starts/restarts the MediaPlayer
 * respecting the current audio focus state. So if we have focus, it will
 * play normally; if we don't have focus, it will either leave the
 * MediaPlayer paused or set it to a low volume, depending on what is
 * allowed by the current focus settings. This method assumes mPlayer !=
 * null, so if you are calling it, you have to do so from a context where
 * you are sure this is the case.
 */
private void configMediaPlayerState() {  
    if (mAudioFocus == AUDIO_NO_FOCUS_NO_DUCK) {
        // If we don't have audio focus and can't duck, we have to pause,
        if (mState == PlaybackState.STATE_PLAYING) {
            pause();
        }
    } else {  // we have audio focus:
        if (mAudioFocus == AUDIO_NO_FOCUS_CAN_DUCK) {
            mMediaPlayer.setVolume(VOLUME_DUCK, VOLUME_DUCK); // we'll be relatively quiet
        } else {
            if (mMediaPlayer != null) {
                mMediaPlayer.setVolume(VOLUME_NORMAL, VOLUME_NORMAL); // we can be loud again
            } // else do something for remote client.
        }
        // If we were playing when we lost focus, we need to resume playing.
        if (mPlayOnFocusGain) {
            if (mMediaPlayer != null && !mMediaPlayer.isPlaying()) {
                LogHelper.d(TAG, "configMediaPlayerState startMediaPlayer. seeking to ",
                        mCurrentPosition);
                if (mCurrentPosition == mMediaPlayer.getCurrentPosition()) {
                    mMediaPlayer.start();
                    mState = PlaybackState.STATE_PLAYING;
                } else {
                    mMediaPlayer.seekTo(mCurrentPosition);
                    mState = PlaybackState.STATE_BUFFERING;
                }
            }
            mPlayOnFocusGain = false;
        }
    }
    if (mCallback != null) {
        mCallback.onPlaybackStatusChanged(mState);
    }
}

Putting everything together

Putting everything together, we can create a basic play method that takes a QueueItem and plays it. Note that we keep track of the source of the media items separately. The custom music provider class handles the mapping from QueueItem ids to music source.

public void play(QueueItem item, boolean reset) {  
    mPlayOnFocusGain = true;

    // TODO Try to get audio focus
    // TODO Register for audio noisy receiver

    String mediaId = item.getDescription().getMediaId();
    boolean mediaHasChanged = !TextUtils.equals(mediaId, mCurrentMediaId) || reset;
    if (mediaHasChanged) {
        mCurrentPosition = 0;
        mCurrentMediaId = mediaId;
    }

    if (mState == PlaybackState.STATE_PAUSED && !mediaHasChanged && mMediaPlayer != null) {
        // Begin playing if we were paused before and the media hasn't changed
        configMediaPlayerState();
    } else {
        // Recreate the media player
        mState = PlaybackState.STATE_STOPPED;
        relaxResources(false); // release everything except MediaPlayer

        // Get the source for the music track
        // TODO Customize this as per your needs
        MediaMetadata track = mMusicProvider.getMusic(mediaId);
        String source = track.getString(MusicProvider.CUSTOM_METADATA_TRACK_SOURCE);

        try {
            createMediaPlayerIfNeeded();

            mState = PlaybackState.STATE_BUFFERING;

            mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
            mMediaPlayer.setDataSource(source);

            // Starts preparing the media player in the background. When
            // it's done, it will call our OnPreparedListener (that is,
            // the onPrepared() method on this class, since we set the
            // listener to 'this'). Until the media player is prepared,
            // we *cannot* call start() on it!
            mMediaPlayer.prepareAsync();

            // If we are streaming from the internet, we want to hold a
            // Wifi lock, which prevents the Wifi radio from going to
            // sleep while the song is playing.
            mWifiLock.acquire();

            if (mCallback != null) {
                mCallback.onPlaybackStatusChanged(mState);
            }

        } catch (IOException ex) {
            LogHelper.e(TAG, ex, "Exception playing song");
            if (mCallback != null) {
                mCallback.onError(ex.getMessage());
            }
        }
    }
}

Releasing the MediaPlayer

A MediaPlayer can consume valuable system resources. Therefore, you should always take extra precautions to make sure you are not hanging on to a MediaPlayer instance longer than necessary. When you are done with it, you should always call release() to make sure any system resources allocated to it are properly released.

/**
 * Releases resources used by the service for playback. This includes the
 * "foreground service" status, the wake locks and possibly the MediaPlayer.
 */
private void relaxResources(boolean releaseMediaPlayer) {  
    mService.stopForeground(true);

    // stop and release the Media Player, if it's available
    if (releaseMediaPlayer && mMediaPlayer != null) {
        mMediaPlayer.reset();
        mMediaPlayer.release();
        mMediaPlayer = null;
    }

    // we can also release the Wifi lock, if we're holding it
    if (mWifiLock.isHeld()) {
        mWifiLock.release();
    }
}

Wrapping everything up

In order to simplify the handling of MediaPlayer, let's wrap everything that we discussed above into an interface. This interface could then be used wherever we want to play music - be it in a Service to allow playing music in the background (more on this in the next post), or an Activity to play music only while the user is interacting with the application.

public interface Playback {  
    /**
     * Start/setup the playback.
     * Resources/listeners would be allocated by implementations.
     */
    void start();

    /**
     * Stop the playback. All resources can be de-allocated by implementations here.
     */
    void stop();

    /**
     * Set the latest playback state as determined by the caller.
     */
    void setState(int state);

    /**
     * Get the current [email protected] android.media.session.PlaybackState#getState()}
     */
    int getState();

    /**
     * @return boolean indicating whether the player is playing or is supposed to be
     * playing when we gain audio focus.
     */
    boolean isPlaying();

    /**
     * @return pos if currently playing an item
     */
    int getCurrentStreamPosition();

    /**
     * @return duration if currently playing an item
     */
    long getDuration();

    /**
     * Set the current position. Typically used when switching players that are in
     * paused state.
     *
     * @param pos position in the stream
     */
    void setCurrentStreamPosition(int pos);

    /**
     * @param item to play
     */
    void play(QueueItem item);

    /**
     * @param item  to play
     * @param reset true to always start from beginning
     */
    void play(QueueItem item, boolean reset);

    /**
     * Pause the current playing item
     */
    void pause();

    /**
     * Seek to the given position
     */
    void seekTo(int position);

    interface Callback {
        /**
         * On current music completed.
         */
        void onCompletion();

        /**
         * on Playback status changed
         * Implementations can use this callback to update
         * playback state on the media sessions.
         */
        void onPlaybackStatusChanged(int state);

        /**
         * @param error to be added to the PlaybackState
         */
        void onError(String error);
    }

    /**
     * @param callback to be called
     */
    void setCallback(Callback callback);
}

Bonus

Here's a sample implementation of the MusicProvider class to map the QueueItem to the actual music source url.

public class MusicProvider {

    public static final String CUSTOM_METADATA_TRACK_SOURCE = "__SOURCE__";
    private final Context mContext;
    private final Realm mRealm;

    public MusicProvider(Context context, Realm realm) {
        mContext = context;
        mRealm = realm;
    }

    public AlbumFile getMusicFile(String musicId) {
        return mRealm.where(Track.class).equalTo("id", Integer.parseInt(musicId)).findFirst();
    }

    public MediaMetadata getMusic(String musicId) {
        return getMusicFile(musicId).getMediaMetadata(mContext, mRealm);
    }
}

Our Track class contains all details about the music track and creates the MediaMetadata as follows:

public MediaMetadata getMediaMetadata(Context context, Realm realm) {  
    Album album = getAlbum(context, realm); // Get the album this track belongs to
    List<AlbumFile> allTracks = album.getTracks();
    return new MediaMetadata.Builder()
            .putString(MediaMetadata.METADATA_KEY_MEDIA_ID, Integer.toString(getId()))
            .putString(MusicProvider.CUSTOM_METADATA_TRACK_SOURCE, getUrl(context, realm).toString())
            .putString(MediaMetadata.METADATA_KEY_ALBUM, album.getTitle())
            .putString(MediaMetadata.METADATA_KEY_ARTIST, album.getArtist().getName())
            .putLong(MediaMetadata.METADATA_KEY_DURATION, Float.valueOf(getDuration()).longValue())
            .putString(MediaMetadata.METADATA_KEY_ALBUM_ART_URI, album.getImagePath())
            .putString(MediaMetadata.METADATA_KEY_TITLE, getTitle())
            .putLong(MediaMetadata.METADATA_KEY_TRACK_NUMBER, allTracks.indexOf(this))
            .putLong(MediaMetadata.METADATA_KEY_NUM_TRACKS, allTracks.size())
            .build();
}

This is the first tutorial in the series Building a Music Player App for Android. Stay tuned for more tutorials in this series! Take me to next tutorial