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):
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'
}
According to MediaPlayer's documentation:
ourPlayer
starts in the initialized state, not in the idle state, because you used MediaPlayer.create()
.
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.
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
}
}