视频播放 (二) 自定义 MediaPlayer

1. 说明

  1.1 使用Mediaplayer和surfaceView进行视频播放,并实现:感应生命周期、支持无缝续播、宽高比适配以及全屏模式

  1.2 创建一个播放控制View,并以ViewModel驱动

2. 配置信息

  2.1 AndroidManifest.xml 添加网络权限

 <uses-permission android:name="android.permission.INTERNET" />

  2.2 http 明文请求设置

 android:usesCleartextTraffic="true"

  2.3 引用 lifecycle 库

    def lifecycle_version = "2.6.0-alpha03"
    // ViewModel
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
    // ViewModel utilities for Compose
    implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version"
    // LiveData
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
    // Lifecycles only (without ViewModel or LiveData)
    implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
    // Saved state module for ViewModel
    implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version"

2.4 矢量图标,添加系统自带矢量图

    ic_baseline_play_arrow_24.xml,
    ic_baseline_replay_24.xml,
    ic_baseline_pause_24.xml

3. 布局文件

  3.1 控制View,controller_layout.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/controllerFrame"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#55000000">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="40dp"
        android:layout_gravity="bottom"
        android:layout_margin="4dp"
        android:orientation="horizontal">

        <ImageView
            android:id="@+id/buttonControl"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_weight="1"
            app:srcCompat="@drawable/ic_baseline_play_arrow_24" />

        <SeekBar
            android:id="@+id/seekBar"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_weight="12"
            android:progressBackgroundTint="#FFFFFF" />
    </LinearLayout>
</FrameLayout>

  3.2 竖屏布局,activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <FrameLayout
        android:id="@+id/playerFrame"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:background="#000000"
        app:layout_constraintDimensionRatio="16:9"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <SurfaceView
            android:id="@+id/surfaceView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center" />

        <ProgressBar
            android:id="@+id/progressBar"
            style="?android:attr/progressBarStyle"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center" />
    </FrameLayout>

    <include
        layout="@layout/controller_layout"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintDimensionRatio="16:9"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

  3.3 横屏布局, activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <FrameLayout
        android:id="@+id/playerFrame"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:background="#000000"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <SurfaceView
            android:id="@+id/surfaceView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center" />

        <ProgressBar
            android:id="@+id/progressBar"
            style="?android:attr/progressBarStyle"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center" />
    </FrameLayout>

    <include
        layout="@layout/controller_layout"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="1.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

4. VM 层实现

  4.1 自定义 MediaPlayer, MyMediaPlayer.kt

//LifecycleObserver
class MyMediaPlayer:MediaPlayer(), DefaultLifecycleObserver{
    
//   @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
//    fun pausePlayer(){
//        pause()
//    }
    
    override fun onPause(owner: LifecycleOwner) {
        super.onPause(owner)
        Log.e("MyTag","onPause");
        pause()
    }

    override fun onResume(owner: LifecycleOwner) {
        super.onResume(owner)
        Log.e("MyTag","onResume");
        start()
    }
}

  4.2 实现 ViewModel 控制,PlayerViewModel.kt

//播放状态
enum class PlayerStatus{
    Playing,Paused,Completed,NotReady
}


class PlayerViewModel(application: Application) : AndroidViewModel(application) {
    private var controllerShowTime = 0L
    val mediaPlayer = MyMediaPlayer()
    private val _playerStatus = MutableLiveData(PlayerStatus.NotReady)
    val playerStatus:LiveData<PlayerStatus> = _playerStatus
    private var _bufferPercent = MutableLiveData(0)
    val bufferPercent: LiveData<Int> = _bufferPercent
    private val _controllerFrameVisibility = MutableLiveData(View.INVISIBLE)
    val controllerFrameVisibility: LiveData<Int> = _controllerFrameVisibility;
    private val _progressBarVisibility = MutableLiveData(View.VISIBLE)
    val progressBarVisibility:LiveData<Int> = _progressBarVisibility
    private val _videoResolution = MutableLiveData(Pair(0,0))
    val videoResolution: LiveData<Pair<Int,Int>> = _videoResolution

    init {
        loadVideo()
    }

    private fun loadVideo(){
        mediaPlayer.apply {
            //https://media.w3.org/2010/05/sintel/trailer.mp4
            //http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4
            //$packageName
            //val videoPath = "android.resource://com.example.myplayer/${R.raw.redes}"
            //android.resource://com.example.myplayer/2131623936
            val videoPath = "https://media.w3.org/2010/05/sintel/trailer.mp4"
            reset()
            _progressBarVisibility.value = View.VISIBLE
            _playerStatus.value = PlayerStatus.NotReady
            setDataSource(videoPath)
            //val fd = getApplication<Application>().getAssets().openFd("red.mp4");
            //setDataSource(fd.getFileDescriptor(), fd.getStartOffset(), fd.getLength());
            setOnPreparedListener {
                _progressBarVisibility.value = View.INVISIBLE;
                //isLooping = true
                it.start()
                _playerStatus.value = PlayerStatus.Playing
                Log.e("MyTag", "setOnPreparedListener")
            }
            //宽高
            setOnVideoSizeChangedListener { _, width, height ->
                _videoResolution.value = Pair(width, height)
            }
            //缓冲
            setOnBufferingUpdateListener { _, percent ->
                _bufferPercent.value = percent
            }
            //播放完成
            setOnCompletionListener {
                _playerStatus.value = PlayerStatus.Completed
            }
            //进度完成
            setOnSeekCompleteListener {
                mediaPlayer.start()
                _playerStatus.value = PlayerStatus.Playing
                _progressBarVisibility.value = View.INVISIBLE
            }
            prepareAsync()
        }
    }

    //播放状态
    fun togglePlayerStatus(){
        when(_playerStatus.value){
           PlayerStatus.Playing ->{
               mediaPlayer.pause()
               _playerStatus.value = PlayerStatus.Paused
           }
            PlayerStatus.Paused ->{
                mediaPlayer.start()
                _playerStatus.value = PlayerStatus.Playing
            }
            PlayerStatus.Completed ->{
                mediaPlayer.start()
                _playerStatus.value = PlayerStatus.Playing
            }
            else -> return
        }
    }

    // 显示/隐藏 控制条
    fun toggleControllerFrame(){
       if(_controllerFrameVisibility.value == View.INVISIBLE){
           _controllerFrameVisibility.value = View.VISIBLE
           controllerShowTime = System.currentTimeMillis()
          viewModelScope.launch {
              delay(3000)
              if(System.currentTimeMillis() - controllerShowTime > 3000){
                  _controllerFrameVisibility.value = View.INVISIBLE
              }
          }
       }else{
           _controllerFrameVisibility.value = View.INVISIBLE
       }
    }

    //重新赋值
    fun emmitVideoResolution(){
        _videoResolution.value = _videoResolution.value
    }

    //设置 MediaPlayer 进度
    fun playerSeekToProgress(progress: Int){
        _progressBarVisibility.value = View.VISIBLE
        mediaPlayer.seekTo(progress)
    }

    override fun onCleared() {
        super.onCleared()
        mediaPlayer.release()
        Log.e("MyTag","mediaPlayer release");
    }
}

  4.3 调用view层, 使用ViewModel,MainActivity.kt

class MainActivity : AppCompatActivity() {
    private lateinit var playerViewModel: PlayerViewModel
    private lateinit var surfaceView: SurfaceView
    private lateinit var playerFrameLayout: FrameLayout
    private lateinit var seekBar: SeekBar

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
//        object :OrientationEventListener(this){
//            override fun onOrientationChanged(orientation: Int) {
//            }
//        }
        setContentView(R.layout.activity_main)
        val progressBar: ProgressBar = findViewById(R.id.progressBar)
        seekBar = findViewById(R.id.seekBar)
        val controllerFrameLayout: FrameLayout = findViewById(R.id.controllerFrame)
        val buttonControl: ImageView = findViewById(R.id.buttonControl)
        playerFrameLayout = findViewById(R.id.playerFrame)
        updatePlayerProgress()
        playerViewModel = ViewModelProvider(this)[PlayerViewModel::class.java].apply {
            progressBarVisibility.observe(this@MainActivity) {
                progressBar.visibility = it
            }
            videoResolution.observe(this@MainActivity) {
                seekBar.max = mediaPlayer.duration
                //Log.e("MyTag","---- ${mediaPlayer.duration}");
                playerFrameLayout.post {
                    reSizePlayer(it.first, it.second)
                }
            }
            controllerFrameVisibility.observe(this@MainActivity) {
                controllerFrameLayout.visibility = it
            }

            bufferPercent.observe(this@MainActivity, Observer {
                //Log.e("MyTag","---- $it");
                seekBar.secondaryProgress = seekBar.max * it / 100;
            })

            playerStatus.observe(this@MainActivity) {
                buttonControl.isClickable = true
                when (it) {
                    PlayerStatus.Paused -> buttonControl.setImageResource(R.drawable.ic_baseline_play_arrow_24)
                    PlayerStatus.Completed -> buttonControl.setImageResource(R.drawable.ic_baseline_replay_24)
                    PlayerStatus.NotReady -> buttonControl.isClickable = false
                    else -> buttonControl.setImageResource(R.drawable.ic_baseline_pause_24)
                }
            }
        }

        lifecycle.addObserver(playerViewModel.mediaPlayer)

        buttonControl.setOnClickListener {
            playerViewModel.togglePlayerStatus()
        }

        playerFrameLayout.setOnClickListener {
            playerViewModel.toggleControllerFrame()
        }

        seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
            override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
                if (fromUser) {
                    playerViewModel.playerSeekToProgress(progress)
                }
            }

            override fun onStartTrackingTouch(seekBar: SeekBar?) {}

            override fun onStopTrackingTouch(seekBar: SeekBar?) {}
        })
        surfaceView = findViewById(R.id.surfaceView)
        surfaceView.holder.addCallback(object : SurfaceHolder.Callback {
            override fun surfaceCreated(holder: SurfaceHolder) {}

            override fun surfaceChanged(
                holder: SurfaceHolder, format: Int, width: Int, height: Int
            ) {
                playerViewModel.mediaPlayer.setDisplay(holder)
                playerViewModel.mediaPlayer.setScreenOnWhilePlaying(true)
            }

            override fun surfaceDestroyed(holder: SurfaceHolder) {}
        })
    }

    override fun onWindowFocusChanged(hasFocus: Boolean) {
        super.onWindowFocusChanged(hasFocus)
        if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
            hideSystemUI()
            playerViewModel.emmitVideoResolution()
        }
    }

    private fun reSizePlayer(width: Int, height: Int) {
        if (width == 0 || height == 0) return
        surfaceView.layoutParams = FrameLayout.LayoutParams(
            playerFrameLayout.height * width / height,
            FrameLayout.LayoutParams.MATCH_PARENT,
            Gravity.CENTER
        )
        //1674 1908
        //Log.e("MyTag","Size width:  ${playerFrameLayout.height * width / height}")
    }

    private fun updatePlayerProgress() {
        lifecycleScope.launch {
            while (true) {
                delay(500)
                seekBar.progress = playerViewModel.mediaPlayer.currentPosition
            }
        }
    }

    private fun hideSystemUI() {
        val decorView: View = window.decorView
        // Set the content to appear under the system bars so that the
        decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
                // content doesn't resize when the system bars hide and show.
                or View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN //Hide the nav bar and status bar
                or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_FULLSCREEN)
    }
}

5. 效果图

猜你喜欢

转载自blog.csdn.net/u011193452/article/details/128137432