Android MediaPlayer多次Seek产生杂音优化

前言

MediaPlayer 作为Android自带的Player目前还是存在很多不好使用问题,但实际开发中,还是有不少使用场景,本文针对多次seek产生杂音的问题进行分析讨论,自己遇到了进行记录,目前底层也不好解决和轻易改动原生代码,只能通过应用层兼容

1.为什么会产生杂音

Android MediaPlayer多次Seek产生杂音优化_第1张图片
从图中可以看出,是因为产生的两次start,那我们就得研究这两次start如何产生的

2.单次Seek场景

pause - seek - resume(恢复pause前记录的状态)
resume即代表了你之前播放就是继续播放,之前暂停则会继续暂停

3.多次seek的场景

setOnCompletionListener

Android MediaPlayer多次Seek产生杂音优化_第2张图片
这是我实现视频播放器多次seek的自定义日志
日志TAG解释

**pauseByEvent 主动暂停**
startTouch 开始按住(手势滑动 或者seekbar)
seek   拖动进度,或者手按住的时候抖动
stopTouch 松开 (手势滑动 或者seekbar)
**resumeByEvetn 主动**
seekComplete 监听seek结束
  • 这里得解释下为什么会有pauseByEvent 主动暂停和resumeByEvetn 主动恢复,根据【2.单次Seek场景】,单次seek是自动会记录,暂停和恢复的,但是实际场景中,你在拖动过程中是不能有声音的,它单次seek完就出声了,显然是不满足需求的

问题最后一次就出现了回调了两个seekComplete,根据【2.单次Seek场景】,自然就出现了多次带间隔的pause和start杂音就产生了

4.溯本求源

/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

5.记录seek缓存及其执行次数(已废弃)

根据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
    }

}

6.将工具类用于seek事件

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

7.监听seekComplete

[mediaPlayer.setOnSeekCompleteListener ]
1.判断seek彻底结束
2.手已经松开
3.恢复播放

  setOnSeekCompleteListener {
                    Log.d(TAG, "seekComplete:${player?.currentPosition}")
                    HandlerSeekNum.reduce()
                    if (HandlerSeekNum.isFinish() && !HandlerSeekNum.isOnStartTrackingTouch) {
                        PlayerManager.getInstance().resumeByEvent()
                    }
                }

8.结果LOG验证,此时再验证波形已没有seek产生的杂音

Android MediaPlayer多次Seek产生杂音优化_第3张图片

9.方案优化(最终方案)

在实际验证的一段时间发现了极端情况下仍然会有bug
Android MediaPlayer多次Seek产生杂音优化_第4张图片
也就是在一个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()
                }
            }

10.补充解释

pauseByEvent和resumeByEvent为事件引起的暂停和恢复
而非用户暂停按键的恢复属于自己记录的方法

   fun resumeByEventTrue() {
        Log.i(TAG, "resumeByEventTrue $isPauseByEvent")
        if (isPauseByEvent) {
            SLog.i(TAG, "resumeByEventTrue play")
            *yourPlayer.play()
        }
        isPauseByEvent = false
    }

你可能感兴趣的:(多媒体,android,mediaplayer,杂音,播放器爆音,多次seek)