MediaPlayer 作为Android自带的Player目前还是存在很多不好使用问题,但实际开发中,还是有不少使用场景,本文针对多次seek产生杂音的问题进行分析讨论,自己遇到了进行记录,目前底层也不好解决和轻易改动原生代码,只能通过应用层兼容
从图中可以看出,是因为产生的两次start,那我们就得研究这两次start如何产生的
pause - seek - resume(恢复pause前记录的状态)
resume即代表了你之前播放就是继续播放,之前暂停则会继续暂停
setOnCompletionListener
这是我实现视频播放器多次seek的自定义日志
日志TAG解释
**pauseByEvent 主动暂停**
startTouch 开始按住(手势滑动 或者seekbar)
seek 拖动进度,或者手按住的时候抖动
stopTouch 松开 (手势滑动 或者seekbar)
**resumeByEvetn 主动**
seekComplete 监听seek结束
问题最后一次就出现了回调了两个seekComplete,根据【2.单次Seek场景】,自然就出现了多次带间隔的pause和start杂音就产生了
/aosp13/frameworks/av/media/libmedia/mediaplayer.cpp
// cache duration
577 mCurrentPosition = msec;
578 mCurrentSeekMode = mode;
579 if (mSeekPosition < 0) {
580 mSeekPosition = msec;
581 mSeekMode = mode;
582 return mPlayer->seekTo(msec, mode);
---------------------------------------------------------
930 case MEDIA_SEEK_COMPLETE:
931 ALOGV("Received seek complete");
932 if (mSeekPosition != mCurrentPosition || (mSeekMode != mCurrentSeekMode)) {
933 ALOGV("Executing queued seekTo(%d, %d)", mCurrentPosition, mCurrentSeekMode);
934 mSeekPosition = -1;
935 mSeekMode = MediaPlayerSeekMode::SEEK_PREVIOUS_SYNC;
936 seekTo_l(mCurrentPosition, mCurrentSeekMode);
937 }
938 else {
939 ALOGV("All seeks complete - return to regularly scheduled program");
940 mCurrentPosition = mSeekPosition = -1;
941 mCurrentSeekMode = mSeekMode = MediaPlayerSeekMode::SEEK_PREVIOUS_SYNC;
942 }
943 break;
查看源码,我们可以看到无论多少次seek,cache最多有两个seek,mSeekPosition 小于0则不会执行seek,complete不相等则执行新的mSeekPosition 则不会为-1
根据log实际分析再结合源码,我们得到以下简单的工具类
/**
* @author rex
* @date 2023/6/9 17:13
* 解决多次seek杂音的问题
* seek 在mediaplayer中最多缓存两次seek(MAX_CACHE_SEEK),再松开seek后计数为0则适合resume-play
*/
object HandlerSeekNum {
const val TAG = "HandlerSeekNum"
const val MAX_CACHE_SEEK = 2
const val FINISH = 0
var isOnStartTrackingTouch = false
var seekNum = FINISH
fun reset() {
seekNum = FINISH
Log.d(TAG, "reset 0")
}
fun add() {
seekNum++
if (seekNum > MAX_CACHE_SEEK) {
seekNum = MAX_CACHE_SEEK
}
Log.d(TAG, "add $seekNum")
}
fun reduce() {
seekNum--
if (seekNum < FINISH) {
seekNum = FINISH
}
Log.d(TAG, "reduce $seekNum")
}
fun isFinish(): Boolean {
return seekNum == FINISH
}
}
[seekBar.setOnSeekBarChangeListener]
override fun onStartTrackingTouch(seekBar: SeekBar?) {
Log.e(TAG, "onStartTrackingTouch")
isOnStartTrackingTouch = true
HandlerSeekNum.reset()
mOnOverlayCallback?.pauseByEvent()
}
override fun onStopTrackingTouch(seekBar: SeekBar?) {
isOnStartTrackingTouch = false
Log.e(TAG, "onStopTrackingTouch")
mOnOverlayCallback?.seek(progress)
// 此处为seek完成的较快抬手回调马上结束了 此时也可以直接播放
if (HandlerSeekNum.isFinish()) {
mOnOverlayCallback?.resumeByEvent()
}
}
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
HandlerSeekNum.add()
Log.i(TAG, "seek position $position")
PlayerManager.getInstance().seekTo(position)
}
[mediaPlayer.setOnSeekCompleteListener ]
1.判断seek彻底结束
2.手已经松开
3.恢复播放
setOnSeekCompleteListener {
Log.d(TAG, "seekComplete:${player?.currentPosition}")
HandlerSeekNum.reduce()
if (HandlerSeekNum.isFinish() && !HandlerSeekNum.isOnStartTrackingTouch) {
PlayerManager.getInstance().resumeByEvent()
}
}
在实际验证的一段时间发现了极端情况下仍然会有bug
也就是在一个seek中,由于赋值的反复变化最终和第一个一致的时候,计数为2的,第二次回调就没有,所以又修正了新方案采用了记值而不是计数
object HandlerSeekNum {
const val TAG = "HandlerSeekNum"
const val SEEK_DEFAULT = -1
var isOnStartTrackingTouch = false
/**
* 头部seek 正在生效的seek
*/
private var mStartSeek = SEEK_DEFAULT
/**
* 最后一次想要的seek
*/
private var mEndSeek = SEEK_DEFAULT
/**
* 按住重置
*/
fun resetSeekCache() {
mStartSeek = SEEK_DEFAULT
mEndSeek = SEEK_DEFAULT
Log.d(TAG, "resetLastSeek $mStartSeek -> $mEndSeek")
}
fun isEndSeek(position: Int): Boolean {
return position == mEndSeek
}
fun addSeek(position: Int) {
if (mStartSeek == SEEK_DEFAULT) {
mStartSeek = position
mEndSeek = position
} else {
mEndSeek = position
}
Log.d(TAG, "addSeek: [new $position] [start -> end $mStartSeek -> $mEndSeek]")
}
/**
* 单次onPlayComplete结束回调
* @return 是否不会有下一次onPlayComplete,已经结束 保证是松开 则可以继续播放
*/
fun seekComplete(): Boolean {
Log.d(TAG, "onPlayComplete $mStartSeek -> $mEndSeek")
if (isFinish()) {
resetSeekCache()
return true
}
mStartSeek = mEndSeek
Log.d(TAG, "change $mStartSeek -> $mEndSeek")
return false
}
fun isFinish(): Boolean {
return mStartSeek == mEndSeek
}
}
setOnSeekCompleteListener {
Log.d(TAG, "HandlerSeekNum seekComplete:${player?.currentPosition}")
val isComplete = HandlerSeekNum.seekComplete()
val notTouch = !HandlerSeekNum.isOnStartTrackingTouch
Log.d(TAG, "HandlerSeekNum seekComplete isComplete:$isComplete notTouch:$notTouch")
if (isComplete && notTouch) {
Log.e(TAG, "HandlerSeekNum success resumeByEvent")
// 恢复播放 且需要区分是否是有事件引起的暂停
*yourPlayer.resumeByEvent()
}
}
seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
var newProgress = progress
val isNeedDisPatch = fromUser || isOnStartTrackingTouch
if (!isNeedDisPatch) {
return
}
seekBar?.run {
Log.d(
"onProgressChanged",
"isOnStartTrackingTouch $isOnStartTrackingTouch " +
"max $max progress $newProgress "
)
*yourPlayer.seek(newProgress)
}
}
override fun onStartTrackingTouch(seekBar: SeekBar?) {
Log.e(TAG, "HandlerSeekNum onStartTrackingTouch------------------->")
isOnStartTrackingTouch = true
HandlerSeekNum.resetSeekCache()
ivMetadataRetriever.visibility = View.GONE
*pauseByEvent()
}
override fun onStopTrackingTouch(seekBar: SeekBar?) {
isOnStartTrackingTouch = false
Log.e(TAG, "<-------------------HandlerSeekNum onStopTrackingTouch")
seekBar?.run {
val maxStr = Util.getStringForTime(formatBuilder, formatter, max.toLong())
val progressStr =
Util.getStringForTime(formatBuilder, formatter, progress.toLong())
SLog.d("onProgressChanged", "HandlerSeekNum:$progressStr -> $maxStr")
*yourPlayer?.seek(progress)
}
if (HandlerSeekNum.isFinish()) {
Log.e(TAG, "HandlerSeekNum success resumeByEvent2")
*resumeByEvent()
}
}
pauseByEvent和resumeByEvent为事件引起的暂停和恢复
而非用户暂停按键的恢复属于自己记录的方法
fun resumeByEventTrue() {
Log.i(TAG, "resumeByEventTrue $isPauseByEvent")
if (isPauseByEvent) {
SLog.i(TAG, "resumeByEventTrue play")
*yourPlayer.play()
}
isPauseByEvent = false
}