Search code examples
androidaudiocrashbufferandroid-mediaplayer

Android MediaPlayer Fails on API 21 (but NOT API 19 or 22!)


In my app, I would like to have a simple method that uses the MediaPlayer class to play a sound resource once (and again, if needed).

I have successfully achieved this (see code below) and successfully tested it on a real hardware device with API 23, and also on emulators with API levels from 19 all the way to P... EXCEPT FOR API 21, WHERE IT FAILS.

Isn't that weird? Why would something work on API 19... and 23, but not 21?

So what do I mean by fail? Well, on API 21, if you press the play button, no sound plays, and nothing appears to happen (although if you peek at the logcat, ye shall behold all manner of scary native library messages including many "deaths" and even "tombstone" or two).

If you continue to press play a second time, the same thing happens, with more internal error messages being generated in logcat... and the third time, you finally get a crash.

The only error I can see that could be related to anything I have control over is the Exception that's caught in Log.i:

"com.example.boober.stackqmediaplayer I/SFX: error:java.io.IOException: Prepare failed.: status=0x64"

All the other messages and final stack trace are all generated by internal native libraries.

I've googled these errors, read the logcat and MediaPlayer documentation. Hopefully I'm just doing something fundamentally stupid that someone can point out to me.

I was really hoping maybe someone out there can take a look.

I've whittled up a very minimal example of the problem that should be very easy to cut/paste/rebuild in Android Studio so you can reproduce the problem:

MainActivity:

public class MainActivity extends Activity {

    MediaPlayer ourPlayer;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ourPlayer = MediaPlayer.create(this, R.raw.soundeffect);
    }

    public void play(View v) {
        try {
            ourPlayer.stop();
            ourPlayer.prepare();
            ourPlayer.start();
        } catch (IOException e) {
            Log.i("SFX", "error:" + e.toString());
        }
    }
}

activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/button"
        android:onClick="play"
        android:clickable="true"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        android:text="PLAY"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>

Res Folder (use any audio file):

enter image description here

Build.gradle:

apply plugin: 'com.android.application'

android {
    compileSdkVersion 27
    defaultConfig {
        applicationId "com.example.boober.stackqmediaplayer"
        minSdkVersion 19
        targetSdkVersion 27
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support.constraint:constraint-layout:1.1.0'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}

Solution

  • According to MediaPlayer's documentation:

    1. ourPlayer starts in the initialized state, not in the idle state, because you used MediaPlayer.create().

    2. From the state graph, you can call stop() only when the player is playing or paused, but the first time your button click method runs, your player is initialized only, so your calling stop() puts your player in the error state, not the stopped state.

    3. Your call to prepare() in the error state, IOException or IllegalStateException or may be thrown. The docs don't say when the former is thrown, but it is caught in your try-catch. The latter makes your app crash the third time you click the play button.

    The solution is to call stop() only when the player is playing or paused, because these are the states that have a direct arrow to the stopped state.

    MediaPlayer has an isPlaying() method, but there's no isPaused(). Some workarounds to this problem may be found here

    So below is my proposed solution:

    public class MainActivity extends Activity {
        private MediaPlayer ourPlayer;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            ourPlayer = MediaPlayer.create(this, R.raw.beer03);
        }
    
        @Override
        protected void onDestroy() {
            super.onDestroy();
            ourPlayer.release(); // Free native resources
        }
    
        public void play(View ignored) {
            try {
                if (ourPlayer.isPlaying() || isPlaybackPaused()) {
                    ourPlayer.stop();
                    ourPlayer.prepare();
                }
                player.start();
            } catch (IOException e) {
                Log.i("SFX", "error:" + e.toString());
            }
        }
    
        private boolean isPlaybackPaused() {
            // TODO
        }
    }