MediaPlayer and Media Playback

The music starts playing as soon as the app is launched. Press the volume up and volume down buttons of the emulator or device. When the Activity object is destroyed, playback stops. When the Activity object is recreated, playback starts again at the beginning of the audio file.

The onPause and onStop methods set the mediaPlayer variable to null to allow the MediaPlayer object to be garbage collected. I’m doing this in onStop because it says to do this in Releasing the Media Player. I’m also doing this in onPause because onStop is not always called.

This app is a stripped-down, barely legal example of a MediaPlayer object. We should have plugged at least two listeners into it, a MediaPlayer.OnCompletionListener and a MediaPlayer.OnErrorListener. See Handling asynchronous errors. We also committed a major breach of etiquette by not running the MediaPlayer under the control of an AudioManager. The AudioManager asks this app to be quiet while other apps are playing, and asks the other apps for silence while this one is playing. It allows only one app to play at any given time. See Handling audio focus and Managing Audio Focus.

Source code in MediaPlayer.zip

  1. MainActivity.java. We set the mediaPlayer to null to make it liable to Java garbage collection.
  2. activity_main.xml
  3. strings.xml
  4. musette.mid. Bach Musette in D major, BWV Anh. 126. The MIDI audio format is on Android’s list of Supported Media Formats.
  5. AndroidManifest.xml. See Manifest Declarations.
  6. build.gradle (Module: app)

Create the project

Create the raw folder

In the Android Studio project view, select the app/res folder.
File → New… → Android resource directory
New Resource Directory
Resource type: raw
OK

For the raw folder, see Grouping Resource Types and Using Media Player.

Copy the audio file to the raw folder

MIDI is on Android’s list of Supported Media Formats. I put the file musette.mid on my Desktop. Make sure you can play it before you add it to the project.

file musette.mid
musette.mid: Standard MIDI data (format 1) using 3 tracks at 1/384

Control-click on musette.mid and select Copy “musette.mid”. The control-click on the raw folder and select Paste.
Copy file /Users/myname/Desktop/musette.mid
New name: musette.mid
To directory: /Users/myname/AndroidStudioProjects/MediaPlayer/app/src/main/res/raw
OK

Add methods to the Activity

In MainActivity.java, click on the word MainActivity.
Code → Override Methods…
Select onPause and onStop. You can just type each name and press OK. We have to call release in these two methods. See Releasing the Media Player.

MediaPlayer in ApiDemos

Media → MediaPlayer → Play Audio from Local File or from Resources

  1. platform_development/samples/ApiDemos/src/com/example/android/apis/media/MediaPlayerDemo_Audio.java
    contains an Activity.
  2. platform_development/samples/ApiDemos/res/raw/test_cbr.mp3
  3. platform_development/samples/ApiDemos/AndroidManifest.xml
    Lines 3024–3028 declare the Activity.

To get the above “Play Audio from Local File” to work on the Genymotion Galaxy S5, I copied (“pushed”) an audio file (musette.mid) into the S5’s Music folder.

adb devices

adb -s 192.168.57.101:5555 shell find / -type d -name Music
/mnt/shell/emulated/0/Music
/data/media/0/Music

adb -s 192.168.57.101:5555 push musette.mid /mnt/shell/emulated/0/Music
822 KB/s (1765 bytes in 0.002s)

adb -s 192.168.57.101:5555 shell find / -type f -name musette.mid
/mnt/shell/emulated/0/Music/musette.mid
/data/media/0/Music/musette.mid

adb -s 192.168.57.101:5555 shell ls -l /storage/emulated
lrwxrwxrwx root     root              2015-08-25 16:51 legacy -> /mnt/shell/emulated/0
Launch the Fie Manager app to make sure musette.mid was copied to the Music folder. As I compiled ApiDemos using these instructions, I changed
                    path="";
to
                    path="file:///storage/emulated/0/Music/musette.mid";
in the playAudio method of class MediaPlayerDemo_Audio in MediaPlayerDemo_Audio.java.

Things to try

  1. Configure the MediaPlayer.
            mediaPlayer.setLooping(true);        //repeat forever
            mediaPlayer.setVolume(1.0f, 1.0f);   //left, right; 1.0f is maximum
    

  2. Display information about the MediaPlayer’s audio file. Give the TextView an id: android:id="@+id/textView". Print three digits to the right of the decimal point because the duration is in milliseconds.
            mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
                @Override
                public void onPrepared(MediaPlayer mp) {
                    int milliseconds = mp.getDuration();
                    int hours = milliseconds / (1000 * 60 * 60);
                    milliseconds -= 1000 * 60 * 60 * hours;
                    int minutes = milliseconds / (1000 * 60);
                    milliseconds -= 1000 * 60 * minutes;
                    float seconds = milliseconds / 1000f;
    
                    String s = String.format("Duration: %02d:%02d:%06.3f",
                            hours, minutes, seconds);
                    TextView textView = (TextView)findViewById(R.id.textView);
                    textView.append("\n" + s);
                }
            });
    
    Duration: 00:00:19.995
    

  3. Center a Play/Pause button in the RelativeLayout. Create two string resources in strings.xml.
        <string name="play">Play</string>
        <string name="pause">Pause</string>
    
    Create a Button in activity_main.xml.
        <Button
            android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:text="@string/play"/>
    
    Create a listener in the onCreate method of the Activity.
            Button button = (Button)findViewById(R.id.button);
    
            button.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    Button button = (Button)view;
                    int id;
                    if (mediaPlayer.isPlaying()) {
                        mediaPlayer.pause();
                        id = R.string.play;
                    } else {
                        mediaPlayer.start(); //or resume from where we left off
                        id = R.string.pause;
                    }
                    button.setText(id);
                }
            });
    
            //mediaPlayer.start();	//Remove this statement.
    

  4. At the end of the file, change the button back to Play. The button will have to be final.
            mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
                @Override
                public void onCompletion(MediaPlayer mp) {
                    mediaPlayer.pause();
                    mediaPlayer.seekTo(0);
                    button.setText(R.string.play);
                }
            });
    

  5. In activity_main.xml, add a second TextView to display our progress in milliseconds. Give the TextView a fixed width and the attributes android:gravity="right" and android:typeface="monospace". Define this class inside of class MainActivity. The isCancelled (together with the cancel below) ensures that the PositionerTask thread will not outlive the Activity that created it. This will allow a second Activity to create a second PositionerTask thread. (There can be only at most one PositionerTask thread at any given time.)
        private class PositionerTask extends AsyncTask<Void, Integer, Void> {
    
            //This method is executed by the second thread.
            //It gets its arguments from the execute method.
    
            @Override
            protected Void doInBackground(Void... v) {
                    while (!isCancelled()) {
    			int milliseconds = mediaPlayer.getCurrentPosition();
    			publishProgress(milliseconds);
    
                            //Sleep for 1/10 of a second.
                            try {
    				Thread.sleep(100L);   //milliseconds
    			} catch (InterruptedException interruptedException) {
                            }
    		}
            }
    
    	//This method is executed by the UI thread.
            //It gets its arguments from the publishProgress method.
    
            @Override
            protected void onProgressUpdate(Integer... position) {
                 int milliseconds = position[0].intValue();
                 Display milliseconds in the second TextView: setText(String.valueOf(milliseconds));
            }
        }
    
    Give class MainActivity this field:
        private PositionerTask positionerTask;
    
    In the onCreate method of the MainActivity, after creating the MediaPlayer,
            positionerTask = new PositionerTask();
            positionerTask.execute();
    
    Add the following code to the start of onPause and onStop. The cancel causes the isCancelled in doInBackground to return true.
            if (positionerTask != null) {
                positionerTask.cancel(true);
                positionerTask = null;
            }
    

  6. Add a SeekBar to show our progress. If the SeekBar is ever destroyed and re-created, the android:saveEnabled="false" will re-create it with its original progress value of zero.
        <SeekBar
            android:id="@+id/seekBar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:max="100"
            android:progress="0"
            android:saveEnabled="false"/>
    
    In onProgressUpdate,
                //assume that int milliseconds is where we are now.
                int duration = mediaPlayer.getDuration();
                float fraction = (float)milliseconds / duration;
                SeekBar seekBar = (SeekBar)MainActivity.this.findViewById(R.id.seekBar);
                seekBar.setProgress((int)(fraction * seekBar.getMax()));
    

  7. Make the SeekBar touch-sensitive so you can drag it to move to a different place in the sound file. In the onCreate method of class MainActivity,
            SeekBar seekBar = (SeekBar)MainActivity.this.findViewById(R.id.seekBar);
    
            seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
                @Override
                public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                    if (fromUser) {
                        float fraction = (float)progress / seekBar.getMax();
                        int duration = mediaPlayer.getDuration();
                        mediaPlayer.seekTo((int)(fraction * duration));
                    }
                }
    
                @Override
                public void onStartTrackingTouch(SeekBar seekBar) {
                }
    
                @Override
                public void onStopTrackingTouch(SeekBar seekBar) {
                }
            });
    

  8. MediaPlayer.create calls prepare, which does not return until the MediaPlayer is prepared to play. This can take several seconds—too long for the UI thread. Instead of creating the MediaPlayer with create, we should create it with new, setDataSource, and prepareAsync. prepareAsync. always returns immediately even if the MediaPlayer is not yet prepared to play. It does its work in a separate thread, and then calls the OnPreparedListener.
            mediaPlayer = new MediaPlayer();
            Uri uri = Uri.parse("android.resource://" + getPackageName() + "/" + R.raw.musette);
            try {
                mediaPlayer.setDataSource(this, uri);
            } catch (IOExceptionToast toast = Toast.makeText(MediaPlayerService.this, iOException.toString(), Toast.LENGTH_LONG);
                toast.show();
            }
    
            mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
                @Override
                public void onPrepared(MediaPlayer mp) {
                    mp.start();
                }
            });
    
            mediaPlayer.prepareAsync(); //will eventually call the onPrepared method of the OnPreparedListener
    

  9. Destroy and re-create the Activity object by changing the orientation of the device (portrait to landscape) while the app is running. You’ll have to put an
                        if (mediaPlayer != null) {
    
    around the body of the while loop in doInBackground. Note that the music cuts off.

    An easy way to keep the Activity alive and the music playing is by adding the following attribute to the <activity> element in AndroidManifest.xml.

    android:configChanges="orientation|screenSize"
    
    To demonstrate that the Activity still notices the change in orientation, even though Activity is not killed by it, add the following method to the Activity.
        @Override
        public void onConfigurationChanged(Configuration newConfig) {
            super.onConfigurationChanged(newConfig);
            String s;
            if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
                s = "portrait";
            } else if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
                s = "landscape";
            } else {
                s = "Orientation unknown.";
            }
            Toast toast = Toast.makeText(this, s, Toast.LENGTH_LONG);
            toast.show();
        }
    
    See Handling the Configuation Change Yourself.

  10. We have just kept the music playing by making the Activity object immortal. Let’s attempt to keep the music playing after the death of the Activity. Restore the mortality of the Activity by removing the android:configChanges attribute from the <activity> element. A change of orientation will now destroy the Activity object and the mediaPlayer variable inside it, leaving the MediaPlayer object vulnerable to garbage collection because there are no more references to it. Even if we remove the following code from onPause and onStop,
            if (mediaPlayer != null) {
                mediaPlayer.release();
                mediaPlayer = null;
            }
    
    there will still be no more references to the MediaPlayer object. If the MediaPlayer object gets garbage collected, the music will stop. If the MediaPlayer object doesn’t get garbage collected, the music will keep playing. There is no way to predict whether the garbage collector will actually collect the MediaPlayer. Even if we try to prod the garbage collector into activity (which is the last thing in the world we want to do if we want to keep the music playing) by saying
            System.gc();
    
    in the onDestroy method of the Activity, there is still no way to predict whether the MediaPlayer will be garbage collected. So we don’t know whether the music will keep playing. And there’s another problem. The next incarnation of the Activity object will start the sound file playing again, so we might have two copies playing simultaneously (unpleasantly out of sync with each other).

    So how can we “uncouple” the lifespan of the MediaPlayer from the lifespan of the Activity? See the next example, MediaPlayerService.