赞
踩
通过上一篇的学习实践,我们了解了ExoPlayer的优缺点以及基本用法,今天我们进入ExoPlayer的音频播放实践,我们来一起实现一个简单的音频播放器。
音频播放器并不总是需要使其UI可见。一旦开始播放音频,播放器就可以作为后台任务运行。用户可以切换到另一个应用程序,并继续听。
要在Android中实现这一设计,您可以使用两个组件构建一个音频应用程序: activity(展示所用) 和播放器service。如果用户切换到另一个应用程序,则该service可以在后台运行。通过将音频应用程序的两个部分分解为单独的组件,每个组件可以独立运行。与播放器相比,UI通常是短暂的,可能会在没有UI的情况下运行很长时间。
在设计音乐播放器APP架构时,有几种常用的做法
方案一
方案二
Android5.0时推出的MediaSession框架(Supprot包中MediaSessionCompat也对低版本做了支持),专门用来解决媒体播放时界面和Service通信的问题,在结构低耦合方面的设计做的比较好
支持库提供了两个类来实现此客户端/服务器方法:MediaBrowserService和MediaBrowser。该服务组件被实现为包含媒体会话及其播放器的MediaBrowserService的子类。使用UI和媒体控制器的活动应包括与MediaBrowserService进行通信的MediaBrowser。
使用MediaBrowserService可以让随身设备(如Android Auto and Wear)轻松发现您的应用,连接到它,浏览内容和控制播放,而无需访问您的Activity
我们今天的学习实践是基于方案二的MediaSession的框架
图片来自 媒体应用架构概览
MediaBrowser
用来连接MediaBrowserService和订阅数据,通过他的回调可以获取和Service的连接状态以及获取在Service中异步获取的音乐数据(这个一般不在Service中进行获取,因为涉及到的是具体的业务逻辑)
MediaBrowserService
是一个Service,封装了媒体相关的一些功能,通过onGetRoot的返回值决定是否允许客户端连接。onLoadChildren回调在Sercive中异步获取的数据给到MediaBrowser。也包含媒体播放器实例(比如我们本篇实践的ExoPlayer)
MediaSession
一般在MediaBrowserService的onCreate中创建,通过MediaSession.CallBack回调接收MediaController发来的指令,触发对应的播放器相关的操作
MediaController
MediaContoller的创建需要MediaSession的配对令牌,在MediaBrowser连接服务成功之后创建。MediaController可以主动的发送指令或者被动的接收MediaController.Callback回调来改变播放状态和界面刷新。
更详细的介绍请参考官方文档或者Android 媒体播放框架MediaSession分析与实践
下面我们看下如何使用MediaSession框架实现简单的音频播放
首先我们继承MediaBrowserServiceCompat实现和注册Service
- public class MusicService extends MediaBrowserServiceCompat {
-
- private static final String TAG = "MusicService";
- private SimpleExoPlayer exoPlayer;
- private MediaSessionCompat mediaSession;
-
- /**
- * 当服务收到onCreate()生命周期回调方法时,它应该执行以下步骤:
- * 1. 创建并初始化media session
- * 2. 设置media session回调
- * 3. 设置media session token
- */
- @Override
- public void onCreate() {
- Log.i(TAG, "onCreate: ");
- super.onCreate();
- //1. 创建并初始化MediaSession
- mediaSession = new MediaSessionCompat(getApplicationContext(), TAG);
-
- mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS
- | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
-
- PlaybackStateCompat playbackState = new PlaybackStateCompat.Builder()
- .setActions(PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PLAY_PAUSE
- | PlaybackStateCompat.ACTION_STOP | PlaybackStateCompat.ACTION_PLAY_PAUSE |
- PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID |
- PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH |
- PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS |
- PlaybackStateCompat.ACTION_SKIP_TO_NEXT | PlaybackStateCompat.ACTION_SEEK_TO)
- .build();
- mediaSession.setPlaybackState(playbackState);
-
- //2. 设置mediaSession回调
- mediaSession.setCallback(new MyMediaSessionCallBack());
-
- //3. 设置mediaSessionToken
- setSessionToken(mediaSession.getSessionToken());
-
- //创建播放器实例
- exoPlayer = new SimpleExoPlayer.Builder(getApplicationContext()).build();
- }
- }
MediaSessionCompat.Callback的回调用于接收业务成通过mediaController.getTransportControls进行播放相关操作(播放、暂停、seek、倍速等等)的回调
- /**
- * 用于接收由MediaControl触发的改变,内部封装实现播放器和播放状态的改变
- */
- private class MyMediaSessionCallBack extends MediaSessionCompat.Callback {
-
-
- @Override
- public void onPlay() {
- super.onPlay();
-
- Log.i(TAG, "onPlay: ");
- exoPlayer.play();
- }
-
- @Override
- public void onPause() {
- super.onPause();
-
- Log.i(TAG, "onPause: ");
- exoPlayer.pause();
- }
-
- @Override
- public void onSeekTo(long pos) {
- super.onSeekTo(pos);
- Log.i(TAG, "onSeekTo: pos=" + pos);
-
- exoPlayer.seekTo(pos);
- }
-
- ...
- }
MediaBrowserServiceCompat有两个回调方法onGetRoot和onLoadChildren。其中onGetRoot用于告诉MediaBrowser是否连接连接成功;onLoadChildren则是加载音视频数据。
具体使用如下:
- @Nullable
- @Override
- public BrowserRoot onGetRoot(@NonNull String clientPackageName, int clientUid, @Nullable Bundle rootHints) {
- Log.i(TAG, "onGetRoot: clientPackageName=" + clientPackageName + " clientUid=" + clientUid + " pid=" + Binder.getCallingPid()
- + " uid=" + Binder.getCallingUid());
- //返回非空,表示连接成功
- return new BrowserRoot("media_root_id", null);
- }
-
- //获取音视频信息(这个更应该是在业务层处理事情)
- @Override
- public void onLoadChildren(@NonNull String parentId, @NonNull Result<List<MediaBrowserCompat.MediaItem>> result) {
- Log.i(TAG, "onLoadChildren: parentId=" + parentId);
- List<MediaBrowserCompat.MediaItem> mediaItems = new ArrayList<>();
- if (TextUtils.equals("media_root_id", parentId)) {
-
- }
- ArrayList<MusicEntity> musicEntityList = getMusicEntityList();
-
- for (int i = 0; i < musicEntityList.size(); i++) {
- MusicEntity musicEntity = musicEntityList.get(i);
-
- MediaMetadataCompat metadataCompat = buildMediaMetadata(musicEntity);
-
- if (i == 0) {
- mediaSession.setMetadata(metadataCompat);
- }
-
- mediaItems.add(new MediaBrowserCompat.MediaItem(metadataCompat.getDescription(), MediaBrowserCompat.MediaItem.FLAG_BROWSABLE));
-
- exoPlayer.addMediaItem(MediaItem.fromUri(musicEntity.source));
- }
- //当设置多首歌曲组成队列时报错
- // IllegalStateException: sendResult() called when either sendResult() or sendError() had already been called for: media_root_id
- //原因,之前在for处理了,应该在设置好mediaItems列表后,统一设置result
- result.sendResult(mediaItems);
- Log.i(TAG, "onLoadChildren: addMediaItem");
-
- initExoPlayerListener();
-
- exoPlayer.prepare();
- Log.i(TAG, "onLoadChildren: prepare");
- }
-
- private void initExoPlayerListener() {
- exoPlayer.addListener(new Player.EventListener() {
- @Override
- public void onPlaybackStateChanged(int state) {
- long currentPosition = exoPlayer.getCurrentPosition();
- long duration = exoPlayer.getDuration();
-
- //状态改变(播放器内部发生状态变化的回调,
- // 包括
- // 1. 用户触发的 比如: 手动切歌曲、暂停、播放、seek等;
- // 2. 播放器内部触发 比如: 播放结束、自动切歌曲等)
-
- //该如何通知给ui业务层呐??好些只能通过回调
- //那有该如何 --》查看源码得知通过setPlaybackState设置
- Log.i(TAG, "onPlaybackStateChanged: currentPosition=" + currentPosition + " duration=" + duration + " state=" + state);
-
- int playbackState;
- switch (state) {
- default:
- case Player.STATE_IDLE:
- playbackState = PlaybackStateCompat.STATE_NONE;
- break;
- case Player.STATE_BUFFERING:
- playbackState = PlaybackStateCompat.STATE_BUFFERING;
- break;
- case Player.STATE_READY:
- if(exoPlayer.getPlayWhenReady()){
- playbackState = PlaybackStateCompat.STATE_PLAYING;
- }else {
- playbackState = PlaybackStateCompat.STATE_PAUSED;
- }
- break;
- case Player.STATE_ENDED:
- playbackState = PlaybackStateCompat.STATE_STOPPED;
- break;
- }
- //播放器的状态变化,通过mediasession告诉在ui业务层注册的MediaControllerCompat.Callback进行回调
-
- setPlaybackState(playbackState);
- }
-
-
-
- private void setPlaybackState(int playbackState) {
- float speed = exoPlayer.getPlaybackParameters() == null ? 1f : exoPlayer.getPlaybackParameters().speed;
-
- mediaSession.setPlaybackState(new PlaybackStateCompat.Builder().setState(playbackState, exoPlayer.getCurrentPosition(), speed).build());
- }
-
- @NotNull
- private ArrayList<MusicEntity> getMusicEntityList() {
- ArrayList<MusicEntity> list = new ArrayList<MusicEntity>();
- ...
-
- MusicEntity musicEntity2 = new MusicEntity();
- musicEntity2.id = "wake_up_02";
- musicEntity2.title = "Geisha";
- musicEntity2.album = "Wake Up";
- musicEntity2.artist = "Media Right Productions";
- musicEntity2.genre = "Electronic";
- musicEntity2.source = "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/02_-_Geisha.mp3";
- musicEntity2.image = "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/art.jpg";
- musicEntity2.trackNumber = 2;
- musicEntity2.totalTrackCount = 13;
- musicEntity2.duration = 267;
- musicEntity2.site = "http://freemusicarchive.org/music/The_Kyoto_Connection/Wake_Up_1957/";
-
- list.add(musicEntity2);
-
- return list;
- }
下面我们再来看下Client端的实现
- public class ExoSimpleAudioPlayerActivity extends Activity implements View.OnClickListener {
- private MediaBrowserCompat mediaBrowser;
- private MediaBrowserCompat.ConnectionCallback mConnectionCallbacks = new MyConnectionCallback();
- private MediaControllerCompat.Callback mMediaControllerCallback;
- private MediaBrowserCompat.SubscriptionCallback mSubscriptionCallback;
-
- @Override
- protected void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_simple_audio);
- ...
- //mConnectionCallbacks 是C-S连接的callback
- mediaBrowser = new MediaBrowserCompat(this, new ComponentName(this, MusicService.class),
- mConnectionCallbacks, null);
- }
-
- @Override
- protected void onStart() {
- super.onStart();
- Log.i(TAG, "onStart: ");
- //发出C-S连接请求 创建MusicService,收到onGetRoot回调值不为空说明建立连接成功--》然后触发MyConnectionCallback的回调onConnected
- mediaBrowser.connect();
- // subscribe();
- }
-
- @Override
- protected void onStop() {
- super.onStop();
- Log.i(TAG, "onStop: ");
- mediaBrowser.disconnect();
- }
- }
MediaBrowserCompat.ConnectionCallback用于接收与Server端连接的状态回调
- public class MyConnectionCallback extends MediaBrowserCompat.ConnectionCallback {
- @Override
- public void onConnected() {
- super.onConnected();
- Log.i(TAG, "onConnected: MyConnectionCallback");
-
- //MediaBrowser和MediaBrowerService建立连接之后会回调该方法
- MediaSessionCompat.Token sessionToken = mediaBrowser.getSessionToken();
-
- //建立连接之后再创建MediaController
- mediaController = new MediaControllerCompat(ExoSimpleAudioPlayerActivity.this, sessionToken);
-
- MediaControllerCompat.setMediaController(ExoSimpleAudioPlayerActivity.this, mediaController);
-
- subscribe();
- //MediaController发送命令
- buildTransportControls();
- if (mMediaControllerCallback == null) {
- //这个callback 是Controller的callback,即用户触发了播放、暂停,后发生状态变化的回调。
- //像播放结束、自动切歌,则无法收到该回调(那该如何处理呐?)
-
- mMediaControllerCallback = new MediaControllerCompat.Callback() {
-
- //这里的回调,只有用户触发的才会有相应的回调。
- //播放结束 这里没有
- //ExoPlayer getDuration : https://stackoverflow.com/questions/35298125/exoplayer-getduration
- // Override
- public void onPlaybackStateChanged(PlaybackStateCompat state) {
- super.onPlaybackStateChanged(state);
- Log.i(TAG, "onPlaybackStateChanged: state=" + state.getState());
- if (PlaybackStateCompat.STATE_PLAYING == state.getState()) {
- playButton.setText("暂停");
- } else {
- playButton.setText("播放");
- }
- updatePlaybackState(state);
-
- MediaMetadataCompat metadata = mediaController.getMetadata();
- updateDuration(metadata);
- }
-
- @Override
- public void onMetadataChanged(MediaMetadataCompat metadata) {
- super.onMetadataChanged(metadata);
- durationSet = false;
- Log.i(TAG, "onMetadataChanged: metadata=" + metadata.toString());
- updateDuration(metadata);
-
- }
- }
- mediaController.registerCallback(mMediaControllerCallback);
- PlaybackStateCompat state = mediaController.getPlaybackState();
- updatePlaybackState(state);
- updateProgress();
- if (state != null && (state.getState() == PlaybackStateCompat.STATE_PLAYING ||
- state.getState() == PlaybackStateCompat.STATE_BUFFERING)) {
- scheduleSeekbarUpdate();
- }
-
- //通过mediaController获取MediaMetadataCompat
- MediaMetadataCompat metadata = mediaController.getMetadata();
- updateDuration(metadata);
- }
-
-
- @Override
- public void onConnectionFailed() {
- super.onConnectionFailed();
- }
- }
歌曲播放播放暂停
当用户点击了播放/暂停按钮后,获取当前的播放状态,通过mediaController.getTransportControls给到通过Binder给到mediaSession,在service中MediaSessionCompat.Callback改变Exoplayer的播放状态,exoplayer的onPlaybackStateChanged收到播放状态改变的通知后触发,给mediasession设置mediaSession.setPlaybackState
对应关键代码如下:
- client端用户点击事件处理
-
- //ExoSimpleAudioPlayerActivity.java
-
- PlaybackStateCompat playbackState = mediaController.getPlaybackState();
- int state = playbackState.getState();
- Log.i(TAG, "onClick: state=" + state);
- //通过 mediaController.getTransportControls 触发MediaSessionCompat.Callback回调--》进行播放控制
- if (state == PlaybackStateCompat.STATE_PLAYING) {
- mediaController.getTransportControls().pause();
- } else {
- mediaController.getTransportControls().play();
- }
//Server端MediasessionCallback实现,接收mediaController.getTransportControls()的事件
- //com.example.myplayer.audio.MusicService.MyMediaSessionCallBack
-
- @Override
- public void onPlay() {
- super.onPlay();
-
- Log.i(TAG, "onPlay: ");
- exoPlayer.play();
- }
-
- @Override
- public void onPause() {
- super.onPause();
-
- Log.i(TAG, "onPause: ");
- exoPlayer.pause();
- }
//server端 exoplayer状态变化监听
- //com.example.myplayer.audio.MusicService#initExoPlayerListener
-
- exoPlayer.addListener(new Player.EventListener() {
- @Override
- public void onPlaybackStateChanged(int state) {
- long currentPosition = exoPlayer.getCurrentPosition();
- long duration = exoPlayer.getDuration();
-
- //状态改变(播放器内部发生状态变化的回调,
- // 包括
- // 1. 用户触发的 比如: 手动切歌曲、暂停、播放、seek等;
- // 2. 播放器内部触发 比如: 播放结束、自动切歌曲等)
-
- //该如何通知给ui业务层呐??好些只能通过回调
- //那有该如何 --》查看源码得知通过setPlaybackState设置
- Log.i(TAG, "onPlaybackStateChanged: currentPosition=" + currentPosition + " duration=" + duration + " state=" + state);
-
- int playbackState;
- switch (state) {
- default:
- case Player.STATE_IDLE:
- playbackState = PlaybackStateCompat.STATE_NONE;
- break;
- case Player.STATE_BUFFERING:
- playbackState = PlaybackStateCompat.STATE_BUFFERING;
- break;
- case Player.STATE_READY:
- if(exoPlayer.getPlayWhenReady()){
- playbackState = PlaybackStateCompat.STATE_PLAYING;
- }else {
- playbackState = PlaybackStateCompat.STATE_PAUSED;
- }
- break;
- case Player.STATE_ENDED:
- playbackState = PlaybackStateCompat.STATE_STOPPED;
- break;
- }
- //播放器的状态变化,通过mediasession告诉在ui业务层注册的MediaControllerCompat.Callback进行回调
-
- setPlaybackState(playbackState);
- }
- }
-
- private void setPlaybackState(int playbackState) {
- float speed = exoPlayer.getPlaybackParameters() == null ? 1f : exoPlayer.getPlaybackParameters().speed;
-
- mediaSession.setPlaybackState(new PlaybackStateCompat.Builder().setState(playbackState, exoPlayer.getCurrentPosition(), speed).build());
- }
虽然知道了怎么使用,但是整个流程是怎样的呐?
其中用到了Handler和Binder的线程和进程通信相关的知识,后续我们专题单独深入学习实践下,这里我们先顺着流程画下播放/暂停的流程图,从用户按下按钮到播放器开始播放以及页面更新的整个流程是怎样的。
上一首下一首切换
歌曲切换流程个上面的播放流程基本上一致,
- //com.example.myplayer.audio.ExoSimpleAudioPlayerActivity#onClick
-
- if (id == R.id.prev) {
- if (mediaController != null) {
- mediaController.getTransportControls().skipToPrevious();
- }
- } else if (id == R.id.next) {
- if (mediaController != null) {
- mediaController.getTransportControls().skipToNext();
- }
- }
区别在于 没有触发ExoPlayer的播放回调,需要再sessionCallback中调用exoplayer的next/prev进行歌曲切换,并且设置新的playstate状态给到mession
- //com.example.myplayer.audio.MusicService.MyMediaSessionCallBack
-
- @Override
- public void onSkipToNext() {
- super.onSkipToNext();
- Log.i(TAG, "onSkipToNext: ");
- exoPlayer.next();
- exoPlayer.setPlayWhenReady(true);
- setPlaybackState(PlaybackStateCompat.STATE_SKIPPING_TO_NEXT);
- mediaSession.setMetadata(getMediaMetadata(1));
- }
-
- @Override
- public void onSkipToPrevious() {
- super.onSkipToPrevious();
- Log.i(TAG, "onSkipToPrevious: ");
- exoPlayer.previous();
- exoPlayer.setPlayWhenReady(true);
- setPlaybackState(PlaybackStateCompat.STATE_SKIPPING_TO_PREVIOUS);
- mediaSession.setMetadata(getMediaMetadata(0));
-
- }
最终MediaControllerCallback的onPlaybackStateChanged收到回调,根据状态进行
- public void onPlaybackStateChanged(PlaybackStateCompat state) {
- super.onPlaybackStateChanged(state);
- ...
- if (state.getState() == PlaybackStateCompat.STATE_SKIPPING_TO_PREVIOUS || state.getState() == PlaybackStateCompat.STATE_SKIPPING_TO_NEXT) {
- updateShowMediaInfo(description);
- }
- }
-
- private void updateShowMediaInfo(MediaDescriptionCompat description) {
- if (description == null) return;
-
- titleView.setText(description.getTitle());
- artistView.setText(description.getSubtitle());
-
- Glide.with(ExoSimpleAudioPlayerActivity.this).load(description.getIconUri().toString()).into(iconView);
- Uri mediaUri = description.getMediaUri();
- Uri iconUri = description.getIconUri();
- Log.i(TAG, "onChildrenLoaded: title=" + description.getTitle() + " subtitle=" + description.getSubtitle()
- + " mediaUri=" + mediaUri + " iconUri=" + iconUri);
- }
倍速
- //com.example.myplayer.audio.ExoSimpleAudioPlayerActivity#onClick
- if (id == R.id.speed) {
- if (mediaController != null) {
- float speed = getSpeed();
- speedView.setText("倍速 " + speed);
- mediaController.getTransportControls().setPlaybackSpeed(speed);
- }
- }
-
- float[] speedArray = new float[]{0.5f, 1f, 1.5f, 2f};
- int curSpeedIndex = 1;
-
- private float getSpeed() {
- if (curSpeedIndex > 3) {
- curSpeedIndex = 0;
- }
- return speedArray[curSpeedIndex++];
- }
然后再MediaSessionCallBack中实现onSetPlaybackSpeed回调,进行播放倍速设置以及mession的设置
- //com.example.myplayer.audio.MusicService.MyMediaSessionCallBack
-
- @Override
- public void onSetPlaybackSpeed(float speed) {
- super.onSetPlaybackSpeed(speed);
- Log.i(TAG, "onSetPlaybackSpeed: speed=" + speed);
- PlaybackParameters playParams = new PlaybackParameters(speed);
- exoPlayer.setPlaybackParameters(playParams);
- //重新设置mediaSession.setPlaybackState 告知 监听者 speed变化
- setPlaybackState(exoPlayer.getPlaybackState());
- }
-
- private void setPlaybackState(int playbackState) {
- float speed = exoPlayer.getPlaybackParameters() == null ? 1f : exoPlayer.getPlaybackParameters().speed;
-
- mediaSession.setPlaybackState(new PlaybackStateCompat.Builder().setState(playbackState, exoPlayer.getCurrentPosition(), speed).build());
- }
需要注意
播放状态 MediaSession框架和ExoPlayer的不同与联系
- //android.support.v4.media.session.PlaybackStateCompat
- TATE_NONE, STATE_STOPPED, STATE_PAUSED, STATE_PLAYING, STATE_FAST_FORWARDING,
- STATE_REWINDING, STATE_BUFFERING, STATE_ERROR, STATE_CONNECTING,
- STATE_SKIPPING_TO_PREVIOUS, STATE_SKIPPING_TO_NEXT, STATE_SKIPPING_TO_QUEUE_ITEM
//com.google.android.exoplayer2.Player.State
STATE_IDLE, STATE_BUFFERING, STATE_READY, STATE_ENDED
上面的实践中存在一些问题,比如数据如何交互,我们看到Activity直接和Service通过MediaSession框架中的各种回调进行通信,播放器ExoPlayer封装在Service内,数据的获取也在Service中。这明显和真实的场景有差异。
另外播放管理相关的没有分离,播放队列的维护,播放状态的管理等等没有统一的管理,不利于扩展扩展更换播放器等。
下一篇我们来分析umap的实现,它是如何进行架构的,如何解决上面的问题的。
完整代码已上传至 github https://github.com/ayyb1988/mediajourney
ExoPlayer
UAMP相关
音频播放器相关开源项目
其他
网络接口以及歌曲来源
- 来自google官方的uamp开源项目
-
- http://storage.googleapis.com/automotive-media/music.json
- https://storage.googleapis.com/uamp/catalog.json
-
-
- Music provided by the [Free Music Archive](http://freemusicarchive.org/).
-
- - [Irsen's Tale](http://freemusicarchive.org/music/Kai_Engel/Irsens_Tale/) by
- [Kai Engel](http://freemusicarchive.org/music/Kai_Engel/).
- - [Wake Up](http://freemusicarchive.org/music/The_Kyoto_Connection/Wake_Up_1957/) by
- [The Kyoto Connection](http://freemusicarchive.org/music/The_Kyoto_Connection/).
-
-
- 长音频:https://v.typlog.com/oohomechat/8385162738_706123.mp3
通过本篇的学习实践,
感谢你的阅读
下一篇我们继续学习实践ExoPlayer,关注公众号“音视频开发之旅”,回复“源码”,获取音视频开发相关代码,一起学习成长。
欢迎交流
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。