Android MediaPlayer 在 API 21 时失败(但不是 API 19 或 22!)

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

在我的应用程序中,我希望有一个简单的方法使用 MediaPlayer class 播放一次声音资源(如果需要,再播放一次)。

我已经成功地实现了这一点(见下面的代码)并在 API 23 的真实硬件设备上成功地测试了它,还在 API 等级从 19 一直到 P 的模拟器上进行了测试... 除了 API 21,失败的地方。

是不是很奇怪?为什么某些东西适用于 API 19... 和 23,而不是 21?

所以失败是什么意思?那么,在 API 21 上,如果您按下 播放 按钮, 没有声音播放 ,而且似乎什么也没有发生(尽管如果您窥视 logcat,您将看到各种可怕的本机库消息,包括许多 "deaths" 甚至 "tombstone" 或两个)。

如果您继续按 play 第二次,同样的事情会发生,在 logcat... 和第三次中会生成更多内部错误消息,你终于崩溃了。

我能看到的唯一可能与我控制的任何事情相关的错误是 Log.i:

中捕获的异常

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

所有其他消息和最终堆栈跟踪均由内部本机库生成。

我用谷歌搜索了这些错误,阅读了 logcat 和 MediaPlayer 文档。希望我只是在做一些根本上愚蠢的事情,有人可以向我指出。

我真的希望有人能看一看。

我已经编写了一个非常简单的问题示例,在 Android Studio 中 cut/paste/rebuild 应该很容易,因此您可以重现该问题:

主要活动:

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 文件夹(使用任何音频文件):

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'
}

根据MediaPlayer's documentation

  1. ourPlayer启动时处于initialized状态,而不是idle状态,因为你使用MediaPlayer.create().

  2. 从状态图中,您可以仅在播放器正在播放或暂停时调用stop(),但是您的按钮单击方法第一次运行时,您的播放器仅被初始化,因此您的调用 stop() 会使您的播放器处于错误状态,而不是停止状态。

  3. 您对prepare()的调用处于错误状态,IOExceptionIllegalStateException或可能被抛出。文档没有说明前者何时被抛出,但它在您的 try-catch 中被捕获。后者会使您的应用程序在您第三次单击播放按钮时崩溃。

解决方案是仅在播放器处于播放暂停时调用stop(),因为这些状态有指向停止状态的直接箭头。

MediaPlayer 有一个 isPlaying() 方法,但是没有 isPaused()。可能会找到此问题的一些解决方法 here

下面是我提出的解决方案:

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
    }
}

万一有人和我有完全相同的用例...

只要按下按钮(或触发事件)时,您只需要从头开始播放声音...

我找到了另一个似乎有效的简单 solution/workaround:

@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) {

        if (ourPlayer != null) {
        ourPlayer.release(); }
        ourPlayer = MediaPlayer.create(this, R.raw.soundeffect);
        ourPlayer.start();

    }

如果出于某种原因这是个坏主意,请任何人告诉我!谢谢!