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

本文详细介绍了如何使用Exoplayer实现音乐播放的控制功能,包括播放、暂停、切换歌曲和删除操作。同时,针对RecyclerView中的SeekBar进度监听进行了优化,避免了快速拖动进度条导致的监听失效问题。还探讨了ConcatenatingMediaSource在无缝播放和监听上的限制,并提出了按需加载的解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

So, 先看东西

fb20e0e62655

效果

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

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。如果您只想检测播放列表项的更改,则必须与最近一次已知的窗口索引或标记进行比较,因为提到的事件可能由于其他原因而触发。

尝试使用后发现

fb20e0e62655

df

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

}

}

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值