So, 先看东西
UI丑点,不过咱也没发言权,产品说好看就好看。
需求:
点击一首歌的播放按钮开始播放
再次点击该按钮,暂停
再次点击该按钮,继续播放
无论暂停还是在播放,点击另外一首歌的播放按钮
播放另外的这首歌,停止上一首歌并清除progress信息
无论暂停还是在播放,点击“删除”
停止播放-播放队列移除-删除文件
实现
RecyclerView + Exoplayer
- 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())
}
})
}
}
- 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载入流程是- 以Uri方式获取到文件(
Uri.fromFile(it))
) - 将文件送入媒体工厂得到可播放的媒体资源(
pFactory.createMediaSource(uri)
) - 将可播放的媒体资源添加到队列(
concatenatingMediaSource.addMediaSource(it)
) - 开始载入(
exoplayer.prepare(concatenatingMediaSource)
)
- 以Uri方式获取到文件(
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)
- 时长转化为可阅读的方式显示,类似
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。如果您只想检测播放列表项的更改,则必须与最近一次已知的窗口索引或标记进行比较,因为提到的事件可能由于其他原因而触发。
尝试使用后发现
window还是会跳到下一个,并且已经播放了17帧,这就...
此ISSUE提出可以发送消息,但还是不能保证100%
换方案。
-
ConcatenaingMediaSource
点击才add-prepare并播放,切歌就remove再add-prepare。 -
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
- 快速拖动进度条到末尾,发现进度监听和播放监听失效
解决方法,播放状态改变的监听中通过判断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)
}
}
}