Android列表音频播放与状态控制

So, 先看东西

效果

UI丑点,不过咱也没发言权,产品说好看就好看。

需求:

点击一首歌的播放按钮开始播放
再次点击该按钮,暂停
再次点击该按钮,继续播放

无论暂停还是在播放,点击另外一首歌的播放按钮
播放另外的这首歌,停止上一首歌并清除progress信息

无论暂停还是在播放,点击“删除”
停止播放-播放队列移除-删除文件

实现

RecyclerView + Exoplayer

  1. seekbar进度监听实现
  • 监听
    源头在exoplayer的播放状态改变的监听事件中,当状态变为“播放”后,开始一个handler任务,不断获取exoplayer当前进度设置给seekbar,为了避免休眠操作,我们可以递归执行这个handler,同时防止调用过于频繁,可以使用postDelay方法。
fun initView(){
changeSeekBarRunnable = Runnable {
  startWatchProgress()
}
}

fun startWatchProgress() {
  if (exoPlayer.playWhenReady && !isDragSeekBar) {
      myRecordProgress[exoPlayer.currentWindowIndex] = exoPlayer.currentPosition
      recycler_view_record.findViewHolderForAdapterPosition(exoPlayer.currentWindowIndex)?.itemView?.seek_bar_item_record?.progress =
          myRecordProgress[exoPlayer.currentWindowIndex].toInt()
  }
  handler.postDelayed(changeSeekBarRunnable, 500)
}
  • 停止
    使用handler.removeCallbacks()方法移除runnable,其实如果handler没有其余任务使用handler.removeCallbacksAndMessages(null)就可以了,少用一个runnable变量。
fun endWatchProgress() {
    handler.removeCallbacks(changeSeekBarRunnable)
}
  • seekbar拖动时防干扰
    通过变量实现,这里是isDragSeekBar,很简单。
fun initView(){
xxxAdapter.onBindViewHolder{
seek_bar_item_record.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
        override fun onStartTrackingTouch(seekBar: SeekBar?) {
             isDragSeekBar = true
         }

        override fun onStopTrackingTouch(seekBar: SeekBar?) {
              isDragSeekBar = false
               // 在播放才播放
               if (exoPlayer.playWhenReady) exoPlayer.seekTo(position, seek_bar_item_record.progress.toLong())
         }

        override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
            // 文字改变
            myRecordProgress[position] = progress.toLong()
            progress_item_record.text = longToStringTime(progress.toLong())
        }
})
}
}

  1. exoplayer初始化与文件载入
  • 初始化,简单粗暴
exoPlayer = SimpleExoPlayer.Builder(requireActivity()).build()
exoPlayer.addListener(object : Player.EventListener {
    // 播放状态监听
    override fun onIsPlayingChanged(isPlaying: Boolean) {
        super.onIsPlayingChanged(isPlaying)
        if (isPlaying) startWatchProgress()
        else endWatchProgress()
    }
})
  • 文件载入与prepare
    Exoplayer载入流程是
    1. 以Uri方式获取到文件(Uri.fromFile(it)))
    2. 将文件送入媒体工厂得到可播放的媒体资源(pFactory.createMediaSource(uri))
    3. 将可播放的媒体资源添加到队列(concatenatingMediaSource.addMediaSource(it))
    4. 开始载入(exoplayer.prepare(concatenatingMediaSource))
val dFactory = DefaultDataSourceFactory(
            requireActivity(), Util.getUserAgent(requireActivity(), BuildConfig.APPLICATION_ID)
        )
val pFactory = ProgressiveMediaSource.Factory(dFactory)
concatenatingMediaSource.clear()
concatenatingMediaSource.addMediaSource(pFactory.createMediaSource(Uri.fromFile(it)))

exoplayer.prepare(concatenatingMediaSource)
  1. 时长转化为可阅读的方式显示,类似 90s <==> 01:30
fun longToStringTime(timeLong: Long): String {
    var seconds = timeLong / 1000
    val res = StringBuilder()
    val h = seconds / 3600
    if (h in 1..9)  res.append(0)
    if (h > 0) res.append(h).append(":")
    seconds %= 3600

    val m = seconds / 60
    if (m < 10) res.append(0)
    res.append(m).append(":")
    seconds %= 60

    if (seconds < 10) res.append(0)
    res.append(seconds)
    return res.toString()
}

差一点完美的方案

使用ConcatenaingMediaSource,一次读入所有mp3文件然后全部添加到播放队列并prepare,发现默认是无缝播放(1-10这么多首歌看作一首播放),而且也不能监听到windowIndex也就是当前的播放位置的改变。

追求效率先看看博客、文章有没有前辈遇到类似问题,发现没有...
Github issue肯定有,是的,但是官方说他们不打算做这个需求,给了几个解决方案,并在官方文档做了总结

当前播放项目更改时,可以调用三种类型的事件:

EventListener.onPositionDiscontinuity与reason = Player.DISCONTINUITY_REASON_PERIOD_TRANSITION
当播放自动从一项过渡到另一项时,会发生这种情况。
EventListener.onPositionDiscontinuity与reason = Player.DISCONTINUITY_REASON_SEEK
当当前播放项目作为查找操作的一部分而发生更改时(例如在调用时),会发生这种情况 Player.next。
EventListener.onTimelineChanged与reason = Player.TIMELINE_CHANGE_REASON_DYNAMIC
当播放列表发生更改(例如,添加,移动或删除项目)时,就会发生这种情况。
在所有情况下,当您的应用程序代码接收到该事件时,您都可以查询播放器以确定正在播放播放列表中的哪个项目。可以使用诸如Player.getCurrentWindowIndex和的 方法来完成Player.getCurrentTag。如果您只想检测播放列表项的更改,则必须与最近一次已知的窗口索引或标记进行比较,因为提到的事件可能由于其他原因而触发。

尝试使用后发现

df

window还是会跳到下一个,并且已经播放了17帧,这就...
ISSUE提出可以发送消息,但还是不能保证100%

换方案。

  1. ConcatenaingMediaSource点击才add-prepare并播放,切歌就remove再add-prepare。

  2. ProgressiveMediaSource 全局仅一个变量,同一时间只有一首歌在准备。

但是初次进入界面如何获取所有歌曲的时长呢??
显然不管用什么方案,让exoplayer prepare后获取mediaSource的length都是最蠢的。
这里我们用metadataRetriever获取,当然,我的File在/sdcard/android/data目录下,没有任何权限问题。

    private suspend fun handleFiles(tempList: MutableList<File>) = withContext(Dispatchers.IO) {
        myRecordFiles.clear()
        myRecordDuration.clear()
        val metadataRetriever = MediaMetadataRetriever()
        // 进度更新
        tempList.forEach {
            var duration = 0L
            // 获取文件名
            try {
                metadataRetriever.setDataSource(it.absolutePath)
                // 获取播放时长
                duration = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION).toLong()
            } catch (e: Exception) {
                MobclickAgent.reportError(requireActivity(), "metadataRetriever gain duration error userid>>${MuseSpUtil.get().uId} ")
            }
            // 记录每首歌的时长
            myRecordDuration.add(duration)
        }
        // 播放器文件更新
        myRecordFiles.addAll(tempList)
    }

BUG

  1. 快速拖动进度条到末尾,发现进度监听和播放监听失效

解决方法,播放状态改变的监听中通过判断currentPosition是否大于总的播放时长(实测是有可能的)

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

推荐阅读更多精彩内容