基于ExoPlayer的音频播放列表

    最近公司有个项目需要做一个音频的播放列表,类似应用市场的下载列表,每一个item都是一个音乐播放器能单独播放,单独的进度。拿到需求的时候脑子回忆当前有使用的音乐播放app,里面的实现也没有这么的场景里过了一遍怎么实现。脑海里下意识的就想到2种实现方案。

    1:每个item都是一个独立的音乐播放控件,自定义一个view。 view里面包含了一个mediaplay,和播放,暂停,进度条。每个item都由对应的mediaplay独立控制,实现时就可以不关心对应item的数据实时刷新是否正确问题。但是疑问点是:android里可以同时存在多个mediaplay么?如果可以,每个item都持有一个mediaplay,当recyclerview列表上下滚动时,由于控件的复用时,当第一个正在播放的item还在刷新渲染UI,移动翻滚出显示区域后,最新出现的item调用onBindViewHolder时,重新对item初始时设置的初始数据,又会有什么影响?会不会影响到该item所持有的mediaplay。比如当前mediaplay线程是正在进行的,虽然onBindViewHolder调用了设置了值,但是没有触发到重新启动mediaplay播放,而mediaplay所在的item又是复用的View内所持有的mediaplay,而它正在播放,所以它会刷新渲染所持有它的View的UI。那这时候新出现的item未播放,但是它的ui却是在进行运动的,那就出问题了。 又或者onBindViewHolder 重新调用服用时,由于复用的item重新设值,当前正在播放的音乐停止的这样场景。

    2:全局只有一个mediaplay,而列表里只是有UI,当所有的按钮操作只是向mediaplay发送 操作事件,而全局mediaplay 接收到对应的事件后,对事件做相应的响应,并且实时的监听mediaplay的状态。不断的向列表adapter发送事件重新绘制对应的item的ui重绘请求,这样比较容易保证数据的统一。而且通过观察市面上的音乐播放主流app,估计他们也是使用这样实现。所有我也使用这样的方案来实现。

    音乐播放器需要使用的到mediaplay,很久以前写过一个简易的播放器。不过都过了好久忘记了。所有需要重新回炉下就BD下相关的资料。去github上搜索了些开源的demo,直接根据星星数来排列筛选。排名第一的就是这个Exoplayer。好奇去了解了下。EXOPlayer是Google官方开源的一种播放器官方介绍 ,能够支持DASH, SmoothStreaming 和 HLS,不能支持Adobe的rtsp、rtmp。功能还停强大,视频,音频都可以。我这里需要处理的是列表的音频播放。具体的单个简单播放,导入包之类我就不提了,我这里给个连接大家自己去了解下:https://www.jianshu.com/p/cb1e79267319。

添加依赖

    先在应用app模块的build.gradle文件中添加ExoPlayer库的依赖:


音频创建与播放

 ```

controlDispatcher =new com.google.android.exoplayer2.DefaultControlDispatcher();

//获取player的一个实例,大多数情况可以直接使用 DefaultTrackSelector

// DefaultTrackSelector 该类可以对当前播放的音视频进行操作,比如设置音轨,设置约束曲目选择,禁用渲染器

simpleExoPlayer = ExoPlayerFactory.newSimpleInstance(context,new DefaultTrackSelector());

defaultDataSourceFactory =new DefaultDataSourceFactory(context,"audio/mpeg");

//创建一个媒体连接源

ConcatenatingMediaSource concatenatingMediaSource =new ConcatenatingMediaSource();

concatenatingMediaSource.addMediaSources(sourceList);

simpleExoPlayer.setPlayWhenReady(false);

final ExtractorMediaSource mediaSource =new ExtractorMediaSource.Factory(defaultDataSourceFactory)

//创建一个播放数据源

.createMediaSource(Uri.parse("http://5.595818.com/2015/ring/000/140/6731c71dfb5c4c09a80901b65528168b.mp3"));

simpleExoPlayer.prepare(mediaSource);

 ```


改造开始

在熟悉了简单的音频播放后。我们需要对demo进行改造,AudioControl是核心控制类,并且一个Activity只有一个AudioControl对象,保证Activity开启关闭时对其进行初始化和资源的释放将SimpleExoPlayer对象放入AudioControl内,首先构造函数进行初始化,主要是生成Player实例。

Player对象实例

```

private void init() {

if(handler==null){

handler=new Handler();

}

formatBuilder =new StringBuilder();

formatter =new Formatter(formatBuilder, Locale.getDefault());

adGroupTimesMs =new long[0];

playedAdGroups =new boolean[0];

extraAdGroupTimesMs =new long[0];

extraPlayedAdGroups =new boolean[0];

period =new Timeline.Period();

window =new Timeline.Window();

controlDispatcher =new com.google.android.exoplayer2.DefaultControlDispatcher();

//获取player的一个实例,大多数情况可以直接使用 DefaultTrackSelector

// DefaultTrackSelector 该类可以对当前播放的音视频进行操作,比如设置音轨,设置约束曲目选择,禁用渲染器

    simpleExoPlayer = ExoPlayerFactory.newSimpleInstance(context,new DefaultTrackSelector());

defaultDataSourceFactory =new DefaultDataSourceFactory(context,"audio/mpeg");

//创建一个媒体连接源

    ConcatenatingMediaSource concatenatingMediaSource =new ConcatenatingMediaSource();

concatenatingMediaSource.addMediaSources(sourceList);

simpleExoPlayer.setPlayWhenReady(false);

initListener();

}

```


 Player.EventListener监听

    增加Player对象的事件监听,主要是要对Player启动监听,当Player启动后,Handler开启定时轮询线程loadStatusRunable,该线程主要是为了当每次轮询时,获得当前时刻的player的数据,比如:当前播放时间量,总时长,当前已经缓冲数据时间量,播放状态,需要注意的是,获得的时间单位均是毫秒数。 loadStatusRunable代码如下:

```

Runnable loadStatusRunable=new Runnable() {

@Override

        public void run() {

long durationUs =0;

int adGroupCount =0;

long currentWindowTimeBarOffsetMs =0;

Timeline currentTimeline =simpleExoPlayer.getCurrentTimeline();

if (!currentTimeline.isEmpty()) {

int currentWindowIndex =simpleExoPlayer.getCurrentWindowIndex();

int firstWindowIndex = currentWindowIndex;

int lastWindowIndex = currentWindowIndex;

for (int i = firstWindowIndex; i <= lastWindowIndex; i++) {

if (i == currentWindowIndex) {

currentWindowTimeBarOffsetMs = C.usToMs(durationUs);

}

currentTimeline.getWindow(i,window);

if (window.durationUs == C.TIME_UNSET) {

//                      /**/ Assertions.checkState(!multiWindowTimeBar);

                        break;

}

for (int j =window.firstPeriodIndex; j <=window.lastPeriodIndex; j++) {

currentTimeline.getPeriod(j,period);

int periodAdGroupCount =period.getAdGroupCount();

for (int adGroupIndex =0; adGroupIndex < periodAdGroupCount; adGroupIndex++) {

long adGroupTimeInPeriodUs =period.getAdGroupTimeUs(adGroupIndex);

if (adGroupTimeInPeriodUs == C.TIME_END_OF_SOURCE) {

if (period.durationUs == C.TIME_UNSET) {

// Don't show ad markers for postrolls in periods with unknown duration.

                                    continue;

}

adGroupTimeInPeriodUs =period.durationUs;

}

long adGroupTimeInWindowUs = adGroupTimeInPeriodUs +period.getPositionInWindowUs();

if (adGroupTimeInWindowUs >=0 && adGroupTimeInWindowUs <=window.durationUs) {

if (adGroupCount ==adGroupTimesMs.length) {

int newLength =adGroupTimesMs.length ==0 ?1 :adGroupTimesMs.length *2;

adGroupTimesMs = Arrays.copyOf(adGroupTimesMs, newLength);

playedAdGroups = Arrays.copyOf(playedAdGroups, newLength);

}

adGroupTimesMs[adGroupCount] = C.usToMs(durationUs + adGroupTimeInWindowUs);

playedAdGroups[adGroupCount] =period.hasPlayedAdGroup(adGroupIndex);

adGroupCount++;

}

}

}

durationUs +=window.durationUs;

}

}

durationUs = C.usToMs(window.durationUs);

long  curtime = currentWindowTimeBarOffsetMs +simpleExoPlayer.getContentPosition();

long  bufferedPosition = currentWindowTimeBarOffsetMs +simpleExoPlayer.getContentBufferedPosition();

if(listener!=null){

listener.setCurTimeString(position,""+Util.getStringForTime(formatBuilder,formatter, curtime));

listener.setDurationTimeString(position,""+Util.getStringForTime(formatBuilder,formatter, durationUs));

listener.setBufferedPositionTime(position,bufferedPosition);

listener.setCurPositionTime(position,curtime);

listener.setDurationTime(position,durationUs);

}

handler.removeCallbacks(loadStatusRunable);

int playbackState=simpleExoPlayer==null?Player.STATE_IDLE:simpleExoPlayer.getPlaybackState();

//播放器未开始播放后者播放器播放结束

            if(playbackState!=Player.STATE_IDLE&&playbackState!=Player.STATE_ENDED){

long delayMs=0;

//当正在播放状态时

                if(simpleExoPlayer.getPlayWhenReady()&&playbackState==Player.STATE_READY){

float playBackSpeed =simpleExoPlayer.getPlaybackParameters().speed;

if(playBackSpeed<=0.1f){

delayMs=1000;

}else if(playBackSpeed<=5f){

//中间更新周期时间

                        long mediaTimeUpdatePeriodMs=1000 / Math.max(1, Math.round(1 / playBackSpeed));

//当前进度时间与中间更新周期之间的多出的不足一个中间更新周期时长的时间

                        long surplusTimeMs = curtime % mediaTimeUpdatePeriodMs;

//播放延迟时间

                        long mediaTimeDelayMs=mediaTimeUpdatePeriodMs-surplusTimeMs;

if(mediaTimeDelayMs<(mediaTimeUpdatePeriodMs/5)){

mediaTimeDelayMs+=mediaTimeUpdatePeriodMs;

}

delayMs=playBackSpeed==1?mediaTimeDelayMs:(long) (mediaTimeDelayMs/playBackSpeed);

}else {

delayMs=200;

}

}else {

//当暂停状态时

                    delayMs=1000;

}

handler.postDelayed(this,delayMs);

}else {

if(listener!=null){

//播放完结

                    listener.isPlay(position,false);

}

}

}

};

```

增加AutioControlListener供外部监听AutioControl的使用状态

   在设计AutioControl支在播放音频时,外部需要知道内部的运行情况用以实时的刷新对应的UI显示。这时候就需要增加一个监听器AutioControlListener 用以提供外部对AutioControl的监听。只需要在定时轮询的loadStatusRunable线程中获得到对应的数据后调用即可

···

public interface AutioControlListener{

//当前播放时长

void setCurPositionTime(int position,long curPositionTime);

//总时长

void setDurationTime(int position,long durationTime);

//当前缓冲时长

void setBufferedPositionTime(int position,long bufferedPosition);

//当前播放时长的字符串显示

void setCurTimeString(int position,String curTimeString);

//是否正在播放

void isPlay(int position,boolean isPlay);

//当前音频的总时长字符串显示

void setDurationTimeString(int position,String durationTimeString);

}

···

以上AudioControl的实现就已经基本完成大部分的工作了,剩下的就是在列表内如何处理这些监听的回调处理UI了,由于过往开发中RecyclerView使用到了 BaseRecyclerViewAdapterHelper框架,所以这次为了简化Adapter也是用到了这个

dependencies{

        implementation'com.github.CymChad:BaseRecyclerViewAdapterHelper:2.9.31'

}

创建MediaEntity实体


创建MyBaseAdapter


为什么要从外部注入AutioControl,因为实例AutioControl需要一个上下文对象,而在MyBaseAdapter由于BaseQuickAdapter框架的原因,虽然有上下文对象,但是在最初的构造方法里,上下文是不存在还没有生成或者注入的,为NULL.具体的源代码没有去深入了解。不做深究。所以在Activity里实例AutioControl对象,再将AutioControl对象在MyBaseAdapter实例时当做参数注入进去。


TimeBar的使用

    谷歌在Exoplayer内置一个进度条控件。可根据自定义属性来修改样式,比如进度条颜色,大小,当前进度颜色,剩余进度颜色,当前刻度圆大小颜色。或可制定自定义icon图片,缓冲进度颜色等。基本能满足当前所需要的所有的进度显示样式。TimeBar的设置刻度单位也是毫秒数,与Player相配套一起使用的


UI刷新

准备好上面的所有时候基本算是已经完成。剩下的就是在Adapter内实现AutioControlListener的抽象方法。在Player实例成功并且启动后启动无限定时轮询。在时间间隔内不断的获得当前Player的播放状态和播放进度。回调通知Adapter。adapter根据回调的方法参数做相应的Item UI刷新。

回调函数刷新UI

最主要的是数据UI的初始化方法。当我们点击一首歌的时候对应下标的UI在刷新显示。这时候我们点击别的歌的时候 原先播放的UI应该恢复到初始播放状态。新歌的条目的UI应该开始跑动

UI初始化

这个方法只在点击歌曲条目的播放按钮时候调用,上面的playIndex 是当前正在播放的音乐的下标,clickindex是点击新歌曲播放按钮时候新歌曲的音乐条目下标。需要注意的是,这个地方才用了播放和暂时是同一个位置大小相同,互斥显示的2个按钮。这样保证一个按钮做一件事。而且播放的音乐和暂停的音乐下标,回调函数都已经告诉我们了。省得只是一个按钮显示播放与暂停增加一个boolean变量做判断,在列表内的状态的保存。

点击事件处理


每次点击播放时候都是先初始化UI,再启动播放,往autioContrl内传入歌曲URL地址。启动时候,在AudioControl内保存当前播放的歌曲的下标。

最后别忘记了在Activity关闭时候release吧AudioControl内的PlayerRelease了

以上就是实现音频列表UI刷新的整个实现流畅。集体代码已经上传到Github上。有需要的朋友可以去下载集体了解下。完

地址:https://github.com/dingdingww/AudioList

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342

推荐阅读更多精彩内容