安卓MediaPlayer重置导致UI冻结

10

在更改播放器的 dataSource 时,我遇到了 Android MediaPlayer 的问题。根据 MediaPlayer 的说明书 (http://developer.android.com/reference/android/media/MediaPlayer.html),当更改 dataSource 时必须重置播放器。这个方法很有效,但是如果快速连续调用 channelChanged 方法两次,则 MediaPlayer.reset 将会冻结用户界面。我在此处对代码进行了分析:

public void channelChanged(String streamingUrl)
{
    long m1 = System.currentTimeMillis();
    mMediaPlayer.reset();
    long m2 = System.currentTimeMillis();
    try
    {
        mMediaPlayer.setDataSource(streamingUrl);
    }
    catch (IOException e)
    {
        e.printStackTrace();
    }
    long m3 = System.currentTimeMillis();
    mMediaPlayer.prepareAsync();
    long m4 = System.currentTimeMillis();
    Log.d("MEDIAPLAYER", "reset: " + (m2 - m1));
    Log.d("MEDIAPLAYER", "setDataSource: " + (m3 - m2));
    Log.d("MEDIAPLAYER", "preparing: " + (m4 - m3));
}

重置: 3

设置数据源: 1

准备中: 0

重置: 3119

设置数据源: 2

准备中: 1

显然,第一次调用的异步准备方法阻塞了重置方法(当我等待第一个流开始再调用channelChanged()时,一切正常)。

有什么解决问题的想法吗?我应该在单独的线程中执行整个方法吗?基本上,我想避免这种方式,因为它似乎不是很好的编码风格,可能会导致一些其他问题,例如,当用户尝试重新启动播放器但播放器仍在重置方法中时,重置方法似乎又在等待asyncPrepare方法。不清楚播放器会如何表现...

是否还有其他好的解决方案?


如果在调用reset之前立即插入对release的调用,会出现什么行为?它应该会取消准备工作(我原以为reset会做同样的事情...)。 - Dave
1
抱歉回复晚了。我无法在release之后调用reset,也无法在release之后调用任何东西而不会出现IllegalStateException异常。即使我尝试重新初始化MediaPlayer,在调用release方法时UI界面也会冻结:( 但是这个尝试还是值得的。 - Flixer
啊,当然你说得对,不应该在release之后调用任何东西。抱歉,我没有考虑到这一点...虽然我从来没有遇到过这两种方法阻塞的问题。你能确定是版本还是设备特定的吗?另外,作为一个通则,启动一个AsyncTask来处理这类任务并将其从UI线程中移出是没有问题的。任何可能会阻塞的调用都应该在不同的线程上执行。 - Dave
1
实际上,在异步任务中使用MediaPlayer可能会很棘手,因为MP(用本地C/C++编写)处理自己的内部线程...所以它变得棘手和有缺陷。顺序是mp.reset,mp.release。 - Martin Marconcini
1
我最近遇到了这个问题,并注意到在reset()内部的MediaPlayer的本地_reset()调用中存在显着的阻塞。因此,我开始在单独的线程中运行reset(),目前看起来效果不错。 - Michael
2个回答

13

MediaPlayer是一个棘手的家伙。我建议你看一下示例应用程序,通过查看你必须编写的混乱代码来实现一致的媒体播放体验,从而明确了MediaPlayer的糟糕设计。

如果有什么不同,看完示例后,你会发现当他们想要跳过一个曲目时,实际上是重置并释放...

    mPlayer.reset();
    mPlayer.release();

...然后当他们准备加载新的音轨时...

    try {
          mPlayer.reset();
          mPlayer.setDataSource(someUrl);
          mPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
             @Override
              public void onPrepared(MediaPlayer mediaPlayer) {
                   //bam!
              }
          });
          mPlayer.prepareAsync();
    } catch (IllegalStateException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } catch (IllegalArgumentException e) {
        e.printStackTrace();
    }

由于一些设备/操作系统版本上的MediaPlayer比其他设备更糟糕,有时它会出现奇怪的问题,因此我已经添加了try/catch。 您应该拥有一个能够对这些情况做出反应的接口/监听器。

更新:

这是我停止(或暂停)音乐播放时使用的方法(大多数来自示例应用程序,这在服务中运行,并已修改以适合我的应用程序,但仍然)。

第一个方法由stop和pause都使用,前者传递true,后者传递false。

/**
 * Releases resources used by the service for playback. This includes the "foreground service"
 * status and notification, the wake locks and possibly the MediaPlayer.
 *
 * @param releaseMediaPlayer Indicates whether the Media Player should also be released or not
 */
void relaxResources(boolean releaseMediaPlayer) {
    stopForeground(true);
    stopMonitoringPlaybackProgress();
    // stop and release the Media Player, if it's available
    if (releaseMediaPlayer && mPlayer != null) {
        mPlayer.reset();
        mPlayer.release();
        mPlayer = null;
    }
    // we can also release the Wifi lock, if we're holding it
    if (mWifiLock.isHeld()) {
        mWifiLock.release();
    }
}

这是processPauseRequest()的一部分:

if (mState == State.Playing) {
        // Pause media player and cancel the 'foreground service' state.
        mState = State.Paused;
        mPlayer.pause();
        dispatchBroadcastEvent(ServiceConstants.EVENT_AUDIO_PAUSE);//notify broadcast receivers
        relaxResources(false); // while paused, we always retain the mp and notification

这是processStopRequest()的一部分(简化版):

void processStopRequest(boolean force, final boolean stopSelf) {
    if (mState == State.Playing || mState == State.Paused || force) {
        mState = State.Stopped;
        // let go of all resources...
        relaxResources(true);
        currentTrackNotification = null;
        giveUpAudioFocus();         

    }
}

现在核心部分是下一步/跳过...

这是我所做的...

void processNextRequest(final boolean isSkipping) {
    processStopRequest(true, false); // THIS IS IMPORTANT, WE RELEASE THE MP HERE
    mState = State.Retrieving;
    dispatchBroadcastEvent(ServiceConstants.EVENT_TRACK_INFO_LOAD_START);
    // snipped but here you retrieve your next track and when it's ready…
    // you just processPlayRequest() and "start from scratch"

这是MediaPlayer示例的实现方式(在样例文件夹中找到),我使用过并没有问题。

话虽如此,当你说整个东西被阻塞时,我知道你的意思,我也见过它,那是由于MediaPlayer的漏洞。如果你遇到ANR错误,我想看看相关日志。

记录一下,以下是我“开始播放”的代码(省略了许多自定义代码,但你能看到有关MediaPlayer的部分):

/**
 * Starts playing the next song.
 */
void beginPlaying(Track track) {
    mState = State.Stopped;
    relaxResources(false); // release everything except MediaPlayer
    try {
        if (track != null) {
            createMediaPlayerIfNeeded();
            mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
            mPlayer.setDataSource(track.audioUrl);
        } else {
            processStopRequest(true, false); // stop everything! 
            return;
        }
        mState = State.Preparing;
        setUpAsForeground(); //service

        /* STRIPPED ALL CODE FROM REMOTECONTROLCLIENT, AS IT ADDS A LOT OF NOISE :) */

        // 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!
        mPlayer.prepareAsync();
        // 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.
        if (!mWifiLock.isHeld()) {
            mWifiLock.acquire();
        }

    } catch (IOException ex) {
        Log.e("MusicService", "IOException playing next song: " + ex.getMessage());
        ex.printStackTrace();
    }
}
作为最后的说明,我注意到当音频流或来源不可用或不可靠时,“媒体播放器阻止一切”的情况会发生。
祝你好运!让我知道你想要看到什么特定的内容。

2
感谢您的回答(对于我的迟到回答很抱歉)。不幸的是,这并不能帮助我,因为当MediaPlayer在同时执行AsyncPrepare时重置时,UI会冻结(我还尝试在线程中调用正常的“prepare”方法来实现自己的“prepareAsync”,但也没有帮助)。而且,无论是调用“reset”还是“release”,UI都会冻结。 - Flixer
你尝试过调用reset然后创建一个新的MP实例吗? - Martin Marconcini
1
我曾经制作过一个应用程序,唯一的目的是将MediaPlayer方法放置在不同的顺序中,并尝试不使其崩溃。 - Léon Pelletier

0

最新的手机和Android API工作得更加流畅,当快速切换歌曲(下一首或上一首)时,reset方法仅需要5-20毫秒。

因此,对于旧手机来说,没有解决方案,这就是它的工作方式。


对于更高级别的API,有“ExoPlayer 2”。我不知道为什么Android仍然拥有两者... - Martin Marconcini

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接