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

96
Anlia
0.3 2018.04.29 12:35 字数 3021

版权声明:本文为博主原创文章,未经博主允许不得转载
源码:AnliaLee/android-UniversalMusicPlayer
首发地址:Anlia_掘金
大家要是看到有错误的地方或者有啥好的建议,欢迎留言评论

前言

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

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

参考资料
googlesamples/android-UniversalMusicPlayer


项目简介

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

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

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


播放控制模块

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

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,之后就可以调用其提供的方法找到当前要播放的音频了,具体的大家可以参照博主在源码中的注释,这里就不一一拿出来细讲了

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

Android源码阅读
Web note ad 1