美文网首页
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