Android 解读开源项目UniversalMusicPlayer(播放控制层)

版权声明:本文为博主原创文章,未经博主容许不得转载
源码:AnliaLee/android-UniversalMusicPlayer
你们要是看到有错误的地方或者有啥好的建议,欢迎留言评论java

前言

因为工做的缘由,很久没更新博客了,以前说要写UniversalMusicPlayer(后面统一简称UAMP)的源码分析,虽然代码中许多关键的地方都已经写好了注释,同时为了方便你们阅读也把Google原有的一些注释翻译了,但一直抽不出太多时间去写博客,只能是像挤牙膏似的天天抽一个小模块出来分析的样子_(:з」∠)_。因此若是有急需这个项目资料的童鞋能够关注一下我fork那个项目,通常我都会先在那写好注释而后再整理成博客,你们经过注释应该也能够将项目理清,就不须要再等个人龟速更新了~android

回到项目中来,我打算按照UAMP项目各个大模块的划分来写,所以可能会写好几篇博客凑成一个系列。这几篇博客没有特定的顺序,你们按需选择某个模块来看就行。另外UAMP播放器是基于MediaSession框架的,相关资料可参考Android 媒体播放框架MediaSession分析与实践,下面就开始正文吧git

参考资料
googlesamples/android-UniversalMusicPlayergithub


项目简介

UAMP播放器做为Google的官方demo展现了如何去开发一款音频媒体应用,该应用可跨多种外接设备使用,并为Android手机,平板电脑,Android Auto,Android Wear,Android TV和Google Cast设备提供一致的用户体验安全

项目按照标准的MVC架构管理各个模块,模块结构以下图所示bash

其中modeluiplayback模块分别表明MVC架构中的model层、view层以及controller层。此外,UAMP项目中深度使用了MediaSession框架实现了数据管理、播放控制、UI更新等功能,本系列博客将从各个模块入手,分析其源码及重要功能的实现逻辑,这期主要讲的是播放控制这块的内容微信


播放控制模块

在分析MediaSession框架的博客中咱们讲到在客户端使用MediaController发送指令,而后调用MediaBrowserService中重写的回调接口控制播放器进行播放的工做,这样就实现了从用户操做界面到控制音频播放的过程。分析这个过程咱们能够得知播放器是运行在Service层的,而为了将Service层和控制层进行解耦,UAMP项目中将播放器的控制逻辑放到了Playback的实例中,而后使用PlaybackManager做为中间者管理ServiceMediaSession以及Playback之间的交互。它们之间的关联与交互主要是经过各个回调方法来完成的:session

MediaBrowserService与PlaybackManager的关联

  • PlaybackManager中定义回调接口PlaybackServiceCallbackMusicService(继承自MediaBrowserService)实现了接口中的方法,同时也持有PlaybackManager的实例
//PlaybackManager.java
public interface PlaybackServiceCallback {
  void onPlaybackStart();
  void onNotificationRequired();
  void onPlaybackStop();
  void onPlaybackStateUpdated(PlaybackStateCompat newState);
}
复制代码
//MusicService.java
public class MusicService extends MediaBrowserServiceCompat implements PlaybackManager.PlaybackServiceCallback 复制代码
  • PlaybackManager的构造方法中须要传入实现了PlaybackServiceCallback的实例,所以在MusicService中会将自身做为参数构造PlaybackManager实例,此时MusicServicePlaybackManager之间完成了关联,能够相互调用回调方法用以传达指令状态
//PlaybackManager.java
public PlaybackManager(PlaybackServiceCallback serviceCallback, Resources resources, MusicProvider musicProvider, QueueManager queueManager, Playback playback) {
    ...
    mServiceCallback = serviceCallback;
}
复制代码
//MusicService.java
private PlaybackManager mPlaybackManager;

@Override
public void onCreate() {
    ...
    mPlaybackManager = new PlaybackManager(this, getResources(), mMusicProvider, queueManager,playback);
}
复制代码

PlaybackManager与MediaSession的关联

  • PlaybackManager中实现了MediaSession的回调MediaSessionCallback,在MusicService配置MediaSession时能够用PlaybackManager.getMediaSessionCallback拿到这个回调,而后调用MediaSession.setCallback传入回调。此时PlaybackManagerMediaSession之间完成了关联,后续使用MediaController发送指令时,指令经过上述回调最终会传达至PlaybackManager
//PlaybackManager.java
private MediaSessionCallback mMediaSessionCallback;
public PlaybackManager(PlaybackServiceCallback serviceCallback, Resources resources, MusicProvider musicProvider, QueueManager queueManager, Playback playback) {
    ...
    mMediaSessionCallback = new MediaSessionCallback();
}

public MediaSessionCompat.Callback getMediaSessionCallback() {
    return mMediaSessionCallback;
}

private class MediaSessionCallback extends MediaSessionCompat.Callback {
    ...
}
复制代码
//MusicService.java
@Override
public void onCreate() {
    mSession.setCallback(mPlaybackManager.getMediaSessionCallback());
}
复制代码

PlaybackManager与Playback的关联

  • Playback中定义了回调接口CallbackPlaybackManager实现了这个接口中的方法,同时持有Playback的实例(Playback自己也是接口,因此此处持有的是Playback的实例,默认为LocalPlayback,其做为参数在MusicService构造PlaybackManager实例时传入)
//Playback.java
public interface Playback {
    ...
    interface Callback {
        /** * 当前音乐播放完成时调用 */
        void onCompletion();
        /** * 在播放状态改变时调用 * 启用该回调方法能够更新MediaSession上的播放状态 */
        void onPlaybackStatusChanged(int state);

        /** * @param error to be added to the PlaybackState */
        void onError(String error);

        /** * @param mediaId being currently played */
        void setCurrentMediaId(String mediaId);
    }
}
复制代码
//PlaybackManager.java
public class PlaybackManager implements Playback.Callback 复制代码
//LocalPlayback.java
public final class LocalPlayback implements Playback 复制代码
//MusicService.java
@Override
public void onCreate() {
    ...
    LocalPlayback playback = new LocalPlayback(this, mMusicProvider);
    mPlaybackManager = new PlaybackManager(this, getResources(), mMusicProvider, queueManager, playback);
}
复制代码
  • PlaybackManager的构造方法中拿到Playback的实例后,调用Playback.setCallback将自身做为参数传入,此时PlaybackManagerPlayback之间完成了关联,能够相互调用回调方法用以传达指令状态
//Playback.java
public interface Playback {
    ...
    void setCallback(Callback callback);
}
复制代码
//PlaybackManager.java
public PlaybackManager(PlaybackServiceCallback serviceCallback, Resources resources, MusicProvider musicProvider, QueueManager queueManager, Playback playback) {
    ...
    mPlayback.setCallback(this);
}
复制代码

简单总结一下,UAMP播放控制流程能够分为指令下发状态回传两个过程:架构

  • 指令下发能够理解为从客户端UI层Playback层每一层经过调用下一层的实例的方法控制指令一直传达到播放器,从而达到UI组件控制播放器播放音乐的功能
  • 状态回传则是指下层经过上层实现的回调播放状态一路回传到UI层中,用以更新UI组件的显示

了解播放控制流程的设计思路以后,下面咱们开始分析一些具体功能的实现框架


与播放器的交互

前面咱们提到播放器的具体实现是放在Playback层的,那么就先看看Playback类提供了哪些接口

public interface Playback {
    /** * Start/setup the playback. * Resources/listeners would be allocated by implementations. */
    void start();

    /** * Stop the playback. All resources can be de-allocated by implementations here. * @param notifyListeners if true and a callback has been set by setCallback, * callback.onPlaybackStatusChanged will be called after changing * the state. */
    void stop(boolean notifyListeners);

    /** * Set the latest playback state as determined by the caller. */
    void setState(int state);

    /** * Get the current {@link android.media.session.PlaybackState#getState()} */
    int getState();

    /** * @return boolean that indicates that this is ready to be used. */
    boolean isConnected();

    /** * @return boolean indicating whether the player is playing or is supposed to be * playing when we gain audio focus. */
    boolean isPlaying();

    /** * @return pos if currently playing an item */
    long getCurrentStreamPosition();

    /** * Queries the underlying stream and update the internal last known stream position. */
    void updateLastKnownStreamPosition();
    void play(QueueItem item);
    void pause();
    void seekTo(long position);
    void setCurrentMediaId(String mediaId);
    String getCurrentMediaId();
    void setCallback(Callback callback);
}
复制代码

UAMP经过指令下发的流程将用户点击UI控件所发送的指令一路传递到Playback的方法中,以点击播放按钮为例,播放指令传递过程当中调用的方法顺序大体以下:

OnClickListener.onClick → MediaController.getTransportControls().play
→ MediaSession.Callback.onPlay → Playback.play
复制代码

Playback类的具体实现是LocalPlayback,那么咱们看下LocalPlayback.play方法都作了些什么

//LocalPlayback.java
@Override
public void play(QueueItem item) {
    mPlayOnFocusGain = true;
    ...
    if (mExoPlayer == null) {
        mExoPlayer =
                ExoPlayerFactory.newSimpleInstance(
                        mContext, new DefaultTrackSelector(), new DefaultLoadControl());
        mExoPlayer.addListener(mEventListener);
    }
    ...
    mExoPlayer.prepare(mediaSource);
    ...
    configurePlayerState();
}

private void configurePlayerState() {
    ...
    if (mPlayOnFocusGain) {
        mExoPlayer.setPlayWhenReady(true);
        mPlayOnFocusGain = false;
    }
}
复制代码

能够看到这里初始化了ExoPlayer播放器,并调用ExoPlayer相应的方法播放音频

那么和播放器交互的分析就到这,至于ExoPlayer的操做就不细说了,你们能够对照着源码中的注释以及ExoPlayer的文档理解其中的实现逻辑便可


耳机插拔的处理逻辑

当咱们插着线控耳机或者连着蓝牙耳机听歌时,有时可能会忽然发生意外的情况,形成耳机与设备断连了(线被拔掉或者蓝牙中断了),为了在公共场合下避免没必要要的尴尬,此时播放程序通常都会自动暂停音乐的播放。这个功能不是系统帮咱们实现的,这须要咱们本身完成相应逻辑的开发

Android系统中有着音频输出通道的概念,例如当咱们使用线控耳机收听音乐时,音乐是从Headset通道出来的,拔掉耳机后,音频输出的通道则会切换至Speaker通道,此时系统会发出AudioManager.ACTION_AUDIO_BECOMING_NOISY这一广播告知咱们耳机被拔掉了。UAMP中正是经过监听此广播实现了耳机插拔的逻辑处理,以前咱们也提到了这功能是在LocalPlayback中实现的:

public final class LocalPlayback implements Playback {
    ...
    private boolean mAudioNoisyReceiverRegistered;
    private final IntentFilter mAudioNoisyIntentFilter =
            new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY);

    private final BroadcastReceiver mAudioNoisyReceiver =
            new BroadcastReceiver() {
                @Override
                public void onReceive(Context context, Intent intent) {
                    if (AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(intent.getAction())) {
                        LogHelper.d(TAG, "Headphones disconnected.");
                        //当音乐正在播放中,通知Service暂停播放音乐(在Service.onStartCommand中处理此命令)
                        if (isPlaying()) {
                            Intent i = new Intent(context, MusicService.class);
                            i.setAction(MusicService.ACTION_CMD);
                            i.putExtra(MusicService.CMD_NAME, MusicService.CMD_PAUSE);
                            mContext.startService(i);
                        }
                    }
                }
            };

    private void registerAudioNoisyReceiver() {
        //注销耳机插拔、蓝牙耳机断连的广播接收者
        if (!mAudioNoisyReceiverRegistered) {
            mContext.registerReceiver(mAudioNoisyReceiver, mAudioNoisyIntentFilter);
            mAudioNoisyReceiverRegistered = true;
        }
    }

    private void unregisterAudioNoisyReceiver() {
        //注销耳机插拔的广播接收者
        if (mAudioNoisyReceiverRegistered) {
            mContext.unregisterReceiver(mAudioNoisyReceiver);
            mAudioNoisyReceiverRegistered = false;
        }
    }
}
复制代码

有关接收到广播后的操做已经在代码的注释中说明,就很少赘述了。此外,为了防止内存泄漏,咱们须要在适当的时机注册和注销BroadcastReceiver,通常的逻辑就是开始播放音乐时注册,暂停或中止播放时注销,LocalPlayback中一样遵循着这一逻辑,具体的你们看下源码注册和注销两个方法何时被调用就能够了


有关音频焦点的控制

在分析源码以前,咱们先简单了解一下什么是音频焦点。在Android系统中,设备全部发出的声音统称为音频流,这其中包括应用播放的音乐按键声通知铃声电话的声音等等。因为Android是多任务系统,那么这些声音就存在同时播放的可能,咱们可能就会由于正在播放的音乐声而错过某些重要的提示音。系统虽然不会区分哪些声音对咱们来讲是更重要的,但它提供了一套机制让开发者能够本身处理多个音频流同时播放的问题

Android 2.2以后引入了音频焦点机制,各个应用能够经过这个机制协商各自音频输出的优先级。这套机制提供了请求和放弃音频焦点的方法,以及通知咱们音频焦点状态改变的监听器。当咱们须要播放音频时,就能够尝试请求获取音频焦点绑定状态监听器。如有其余应用的音频流忽然插手竞争音频焦点时,系统会根据这个插手的音频流的类型经过监听器通知咱们音频焦点状态的改变。这个改变后的状态其实也是系统对于如何处理当前播放音频的一种建议,状态类型以下:

  • AUDIOFOCUS_GAIN:获得音频焦点时触发的状态,请求获得的音频焦点通常会长期占有
  • AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:失去音频焦点时触发的状态,在该状态的时候不须要暂停音频,可是咱们须要下降音频的声音
  • AUDIOFOCUS_LOSS_TRANSIENT:失去音频焦点时触发的状态,可是该状态不会长时间保持,此时咱们应该暂停音频,且当从新获取音频焦点的时候继续播放
  • AUDIOFOCUS_LOSS:失去音频焦点时触发的状态,且这个状态有可能会长期保持,此时应当暂停音频并释放音频相关的资源

了解这些概念以后,咱们来看下在UAMP项目中官方给出的有关音频焦点的实现示例。有关音频焦点的实如今LocalPlayback类中,首先是定义须要用到的常量音频焦点状态监听器

public final class LocalPlayback implements Playback {
    ...
    //当音频失去焦点,且不须要中止播放,只须要减少音量时,咱们设置的媒体播放器音量大小
    //例如微信的提示音响起,咱们只须要减少当前音乐的播放音量便可
    public static final float VOLUME_DUCK = 0.2f;
    //当咱们获取音频焦点时设置的播放音量大小
    public static final float VOLUME_NORMAL = 1.0f;

    //没有获取到音频焦点,也不容许duck状态
    private static final int AUDIO_NO_FOCUS_NO_DUCK = 0;
    //没有获取到音频焦点,但容许duck状态
    private static final int AUDIO_NO_FOCUS_CAN_DUCK = 1;
    //彻底获取音频焦点
    private static final int AUDIO_FOCUSED = 2;
    private boolean mPlayOnFocusGain;
    //当前音频焦点的状态
    private int mCurrentAudioFocusState = AUDIO_NO_FOCUS_NO_DUCK;

    /** * 根据音频焦点的设置从新配置播放器 以及 启动/从新启动 播放器。调用这个方法 启动/从新启动 播放器实例取决于当前音频焦点的状态。 * 所以若是咱们持有音频焦点,则正常播放音频;若是咱们失去音频焦点,播放器将暂停播放或者设置为低音量,这取决于当前焦点设置容许哪一种设置 */
    private void configurePlayerState() {
        LogHelper.d(TAG, "configurePlayerState. mCurrentAudioFocusState=", mCurrentAudioFocusState);
        if (mCurrentAudioFocusState == AUDIO_NO_FOCUS_NO_DUCK) {
            // We don't have audio focus and can't duck, so we have to pause
            pause();
        } else {
            registerAudioNoisyReceiver();

            if (mCurrentAudioFocusState == AUDIO_NO_FOCUS_CAN_DUCK) {
                // We're permitted to play, but only if we 'duck', ie: play softly
                mExoPlayer.setVolume(VOLUME_DUCK);
            } else {
                mExoPlayer.setVolume(VOLUME_NORMAL);
            }

            // If we were playing when we lost focus, we need to resume playing.
            if (mPlayOnFocusGain) {
                //播放的过程当中因失去焦点而暂停播放,短暂暂停以后仍须要继续播放时会进入这里执行相应的操做
                mExoPlayer.setPlayWhenReady(true);
                mPlayOnFocusGain = false;
            }
        }
    }

    /** * 请求音频焦点成功以后监听其状态的Listener */
    private final AudioManager.OnAudioFocusChangeListener mOnAudioFocusChangeListener =
            new AudioManager.OnAudioFocusChangeListener() {
                @Override
                public void onAudioFocusChange(int focusChange) {
                    LogHelper.d(TAG, "onAudioFocusChange. focusChange=", focusChange);
                    switch (focusChange) {
                        case AudioManager.AUDIOFOCUS_GAIN:
                            mCurrentAudioFocusState = AUDIO_FOCUSED;
                            break;
                        case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
                            // Audio focus was lost, but it's possible to duck (i.e.: play quietly)
                            mCurrentAudioFocusState = AUDIO_NO_FOCUS_CAN_DUCK;
                            break;
                        case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
                            // Lost audio focus, but will gain it back (shortly), so note whether
                            // playback should resume
                            mCurrentAudioFocusState = AUDIO_NO_FOCUS_NO_DUCK;
                            mPlayOnFocusGain = mExoPlayer != null && mExoPlayer.getPlayWhenReady();
                            break;
                        case AudioManager.AUDIOFOCUS_LOSS:
                            // Lost audio focus, probably "permanently"
                            mCurrentAudioFocusState = AUDIO_NO_FOCUS_NO_DUCK;
                            break;
                    }

                    if (mExoPlayer != null) {
                        // Update the player state based on the change
                        configurePlayerState();
                    }
                }
            };
}
复制代码

接着定义请求与放弃音频焦点的方法

public final class LocalPlayback implements Playback {
    ...
    /** * 尝试获取音频焦点 * requestAudioFocus(OnAudioFocusChangeListener l, int streamType, int durationHint) * OnAudioFocusChangeListener l:音频焦点状态监听器 * int streamType:请求焦点的音频类型 * int durationHint:请求焦点音频持续性的指示 * AUDIOFOCUS_GAIN:指示申请获得的音频焦点不知道会持续多久,通常是长期占有 * AUDIOFOCUS_GAIN_TRANSIENT:指示要申请的音频焦点是暂时性的,会很快用完释放的 * AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK:指示要申请的音频焦点是暂时性的,同时还指示当前正在使用焦点的音频能够继续播放,只是要“duck”一下(下降音量) */
    private void tryToGetAudioFocus() {
        LogHelper.d(TAG, "tryToGetAudioFocus");
        int result =
                mAudioManager.requestAudioFocus(
                        mOnAudioFocusChangeListener,//状态监听器
                        AudioManager.STREAM_MUSIC,//
                        AudioManager.AUDIOFOCUS_GAIN);
        if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
            mCurrentAudioFocusState = AUDIO_FOCUSED;
        } else {
            mCurrentAudioFocusState = AUDIO_NO_FOCUS_NO_DUCK;
        }
    }

    /** * 放弃音频焦点 */
    private void giveUpAudioFocus() {
        LogHelper.d(TAG, "giveUpAudioFocus");
        //申请放弃音频焦点
        if (mAudioManager.abandonAudioFocus(mOnAudioFocusChangeListener)
                == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
            //AudioManager.AUDIOFOCUS_REQUEST_GRANTED 申请成功
            mCurrentAudioFocusState = AUDIO_NO_FOCUS_NO_DUCK;
        }
    }
}
复制代码

那么什么时候该请求(放弃)音频焦点呢?举个例子,播放器开始播放(中止)音乐时,就须要请求(放弃)焦点了

public final class LocalPlayback implements Playback {
    ...
    @Override
    public void stop(boolean notifyListeners) {
        giveUpAudioFocus();//放弃音频焦点
        ...
    }

    @Override
    public void play(QueueItem item) {
        mPlayOnFocusGain = true;
        tryToGetAudioFocus();
        ...
        configurePlayerState();
    }
}
复制代码

播放队列控制

以前咱们讲到了UAMP项目中数据层播放控制层是分离开来的,且正如使用PlaybackManager做为中间者管理播放器,这里一样使用了QueueManager这个类做为中间者连通数据层播放控制层Service层,并提供了队列形式的存储容器(这个队列是线程安全的)以及能够管理音乐层级关系的方法

那么首先来看看如何初始化QueueManagerQueueManager中提供了一个对外的回调接口,重写接口中的方法便可在QueueManager中操做外面的方法

public class QueueManager {
    ...
    /** * @param musicProvider 数据源提供者 * @param resources 系统资源 * @param listener 播放数据更新的回调接口 */
    public QueueManager(@NonNull MusicProvider musicProvider, @NonNull Resources resources, @NonNull MetadataUpdateListener listener) {
        ...
    }
    
    public interface MetadataUpdateListener {
        void onMetadataChanged(MediaMetadataCompat metadata);//媒体数据变动时调用
        void onMetadataRetrieveError();//媒体数据检索失败时调用
        void onCurrentQueueIndexUpdated(int queueIndex);//当前播放索引变动时调用
        void onQueueUpdated(String title, List<MediaSessionCompat.QueueItem> newQueue);//当前播放队列变动时调用
    }
}
复制代码

重写这些回调方法是在MusicService建立时完成的,细心的小伙伴应该发现了以前在Service中就有将建立好的QueueManager做为参数构造PlaybackManager类,咱们来看源码

public class MusicService extends MediaBrowserServiceCompat implements PlaybackManager.PlaybackServiceCallback {
   ...
   @Override
   public void onCreate() {
       ...          
       QueueManager queueManager = new QueueManager(mMusicProvider, getResources(),
               new QueueManager.MetadataUpdateListener() {
                   @Override
                   public void onMetadataChanged(MediaMetadataCompat metadata) {
                       mSession.setMetadata(metadata);
                   }

                   @Override
                   public void onMetadataRetrieveError() {
                       mPlaybackManager.updatePlaybackState(
                               getString(R.string.error_no_metadata));
                   }

                   @Override
                   public void onCurrentQueueIndexUpdated(int queueIndex) {
                       mPlaybackManager.handlePlayRequest();
                   }

                   @Override
                   public void onQueueUpdated(String title, List<MediaSessionCompat.QueueItem> newQueue) {
                       mSession.setQueue(newQueue);
                       mSession.setQueueTitle(title);
                   }
               });
       mPlaybackManager = new PlaybackManager(this, getResources(), mMusicProvider, queueManager, playback);
   }
}
复制代码

此时播放控制层就成功连上了QueueManager,以后就能够调用其提供的方法找到当前要播放的音频了,具体的你们能够参照博主在源码中的注释,这里就不一一拿出来细讲了

这篇博客就先到这了,若是这个模块还有什么须要补充的,我会直接在这进行更新。如有什么遗漏或者建议的欢迎留言评论,若是以为博主写得还不错麻烦点个赞,大家的支持是我最大的动力~