美文网首页
Android列表音频播放与状态控制

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

作者: 小强开学前 | 来源:发表于2020-04-23 11:21 被阅读0次

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)
        }
    }
}

相关文章

网友评论

      本文标题:Android列表音频播放与状态控制

      本文链接:https://www.haomeiwen.com/subject/dnznihtx.html