• 视频播放 (二) 自定义 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 库

    1. def lifecycle_version = "2.6.0-alpha03"
    2. // ViewModel
    3. implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
    4. // ViewModel utilities for Compose
    5. implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version"
    6. // LiveData
    7. implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
    8. // Lifecycles only (without ViewModel or LiveData)
    9. implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
    10. // Saved state module for ViewModel
    11. implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version"

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

    1. ic_baseline_play_arrow_24.xml,
    2. ic_baseline_replay_24.xml,
    3. ic_baseline_pause_24.xml

    3. 布局文件

      3.1 控制View,controller_layout.xml

    1. "1.0" encoding="utf-8"?>
    2. <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    3. xmlns:app="http://schemas.android.com/apk/res-auto"
    4. android:id="@+id/controllerFrame"
    5. android:layout_width="match_parent"
    6. android:layout_height="match_parent"
    7. android:background="#55000000">
    8. <LinearLayout
    9. android:layout_width="match_parent"
    10. android:layout_height="40dp"
    11. android:layout_gravity="bottom"
    12. android:layout_margin="4dp"
    13. android:orientation="horizontal">
    14. <ImageView
    15. android:id="@+id/buttonControl"
    16. android:layout_width="wrap_content"
    17. android:layout_height="match_parent"
    18. android:layout_weight="1"
    19. app:srcCompat="@drawable/ic_baseline_play_arrow_24" />
    20. <SeekBar
    21. android:id="@+id/seekBar"
    22. android:layout_width="wrap_content"
    23. android:layout_height="match_parent"
    24. android:layout_weight="12"
    25. android:progressBackgroundTint="#FFFFFF" />
    26. LinearLayout>
    27. FrameLayout>

      3.2 竖屏布局,activity_main.xml

    1. "1.0" encoding="utf-8"?>
    2. <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    3. xmlns:app="http://schemas.android.com/apk/res-auto"
    4. xmlns:tools="http://schemas.android.com/tools"
    5. android:layout_width="match_parent"
    6. android:layout_height="match_parent"
    7. tools:context=".MainActivity">
    8. <FrameLayout
    9. android:id="@+id/playerFrame"
    10. android:layout_width="0dp"
    11. android:layout_height="0dp"
    12. android:background="#000000"
    13. app:layout_constraintDimensionRatio="16:9"
    14. app:layout_constraintEnd_toEndOf="parent"
    15. app:layout_constraintHorizontal_bias="0.0"
    16. app:layout_constraintStart_toStartOf="parent"
    17. app:layout_constraintTop_toTopOf="parent">
    18. <SurfaceView
    19. android:id="@+id/surfaceView"
    20. android:layout_width="wrap_content"
    21. android:layout_height="wrap_content"
    22. android:layout_gravity="center" />
    23. <ProgressBar
    24. android:id="@+id/progressBar"
    25. style="?android:attr/progressBarStyle"
    26. android:layout_width="wrap_content"
    27. android:layout_height="wrap_content"
    28. android:layout_gravity="center" />
    29. FrameLayout>
    30. <include
    31. layout="@layout/controller_layout"
    32. android:layout_width="0dp"
    33. android:layout_height="0dp"
    34. app:layout_constraintDimensionRatio="16:9"
    35. app:layout_constraintEnd_toEndOf="parent"
    36. app:layout_constraintStart_toStartOf="parent"
    37. app:layout_constraintTop_toTopOf="parent" />
    38. androidx.constraintlayout.widget.ConstraintLayout>

      3.3 横屏布局, activity_main.xml

    1. "1.0" encoding="utf-8"?>
    2. <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    3. xmlns:app="http://schemas.android.com/apk/res-auto"
    4. xmlns:tools="http://schemas.android.com/tools"
    5. android:layout_width="match_parent"
    6. android:layout_height="match_parent"
    7. tools:context=".MainActivity">
    8. <FrameLayout
    9. android:id="@+id/playerFrame"
    10. android:layout_width="0dp"
    11. android:layout_height="0dp"
    12. android:background="#000000"
    13. app:layout_constraintBottom_toBottomOf="parent"
    14. app:layout_constraintEnd_toEndOf="parent"
    15. app:layout_constraintHorizontal_bias="0.0"
    16. app:layout_constraintStart_toStartOf="parent"
    17. app:layout_constraintTop_toTopOf="parent">
    18. <SurfaceView
    19. android:id="@+id/surfaceView"
    20. android:layout_width="wrap_content"
    21. android:layout_height="wrap_content"
    22. android:layout_gravity="center" />
    23. <ProgressBar
    24. android:id="@+id/progressBar"
    25. style="?android:attr/progressBarStyle"
    26. android:layout_width="wrap_content"
    27. android:layout_height="wrap_content"
    28. android:layout_gravity="center" />
    29. FrameLayout>
    30. <include
    31. layout="@layout/controller_layout"
    32. android:layout_width="0dp"
    33. android:layout_height="0dp"
    34. app:layout_constraintBottom_toBottomOf="parent"
    35. app:layout_constraintEnd_toEndOf="parent"
    36. app:layout_constraintHorizontal_bias="1.0"
    37. app:layout_constraintStart_toStartOf="parent"
    38. app:layout_constraintTop_toTopOf="parent" />
    39. androidx.constraintlayout.widget.ConstraintLayout>

    4. VM 层实现

      4.1 自定义 MediaPlayer, MyMediaPlayer.kt

    1. //LifecycleObserver
    2. class MyMediaPlayer:MediaPlayer(), DefaultLifecycleObserver{
    3. // @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
    4. // fun pausePlayer(){
    5. // pause()
    6. // }
    7. override fun onPause(owner: LifecycleOwner) {
    8. super.onPause(owner)
    9. Log.e("MyTag","onPause");
    10. pause()
    11. }
    12. override fun onResume(owner: LifecycleOwner) {
    13. super.onResume(owner)
    14. Log.e("MyTag","onResume");
    15. start()
    16. }
    17. }

      4.2 实现 ViewModel 控制,PlayerViewModel.kt

    1. //播放状态
    2. enum class PlayerStatus{
    3. Playing,Paused,Completed,NotReady
    4. }
    5. class PlayerViewModel(application: Application) : AndroidViewModel(application) {
    6. private var controllerShowTime = 0L
    7. val mediaPlayer = MyMediaPlayer()
    8. private val _playerStatus = MutableLiveData(PlayerStatus.NotReady)
    9. val playerStatus:LiveData = _playerStatus
    10. private var _bufferPercent = MutableLiveData(0)
    11. val bufferPercent: LiveData<Int> = _bufferPercent
    12. private val _controllerFrameVisibility = MutableLiveData(View.INVISIBLE)
    13. val controllerFrameVisibility: LiveData<Int> = _controllerFrameVisibility;
    14. private val _progressBarVisibility = MutableLiveData(View.VISIBLE)
    15. val progressBarVisibility:LiveData<Int> = _progressBarVisibility
    16. private val _videoResolution = MutableLiveData(Pair(0,0))
    17. val videoResolution: LiveDataInt,Int>> = _videoResolution
    18. init {
    19. loadVideo()
    20. }
    21. private fun loadVideo(){
    22. mediaPlayer.apply {
    23. //https://media.w3.org/2010/05/sintel/trailer.mp4
    24. //http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4
    25. //$packageName
    26. //val videoPath = "android.resource://com.example.myplayer/${R.raw.redes}"
    27. //android.resource://com.example.myplayer/2131623936
    28. val videoPath = "https://media.w3.org/2010/05/sintel/trailer.mp4"
    29. reset()
    30. _progressBarVisibility.value = View.VISIBLE
    31. _playerStatus.value = PlayerStatus.NotReady
    32. setDataSource(videoPath)
    33. //val fd = getApplication().getAssets().openFd("red.mp4");
    34. //setDataSource(fd.getFileDescriptor(), fd.getStartOffset(), fd.getLength());
    35. setOnPreparedListener {
    36. _progressBarVisibility.value = View.INVISIBLE;
    37. //isLooping = true
    38. it.start()
    39. _playerStatus.value = PlayerStatus.Playing
    40. Log.e("MyTag", "setOnPreparedListener")
    41. }
    42. //宽高
    43. setOnVideoSizeChangedListener { _, width, height ->
    44. _videoResolution.value = Pair(width, height)
    45. }
    46. //缓冲
    47. setOnBufferingUpdateListener { _, percent ->
    48. _bufferPercent.value = percent
    49. }
    50. //播放完成
    51. setOnCompletionListener {
    52. _playerStatus.value = PlayerStatus.Completed
    53. }
    54. //进度完成
    55. setOnSeekCompleteListener {
    56. mediaPlayer.start()
    57. _playerStatus.value = PlayerStatus.Playing
    58. _progressBarVisibility.value = View.INVISIBLE
    59. }
    60. prepareAsync()
    61. }
    62. }
    63. //播放状态
    64. fun togglePlayerStatus(){
    65. when(_playerStatus.value){
    66. PlayerStatus.Playing ->{
    67. mediaPlayer.pause()
    68. _playerStatus.value = PlayerStatus.Paused
    69. }
    70. PlayerStatus.Paused ->{
    71. mediaPlayer.start()
    72. _playerStatus.value = PlayerStatus.Playing
    73. }
    74. PlayerStatus.Completed ->{
    75. mediaPlayer.start()
    76. _playerStatus.value = PlayerStatus.Playing
    77. }
    78. else -> return
    79. }
    80. }
    81. // 显示/隐藏 控制条
    82. fun toggleControllerFrame(){
    83. if(_controllerFrameVisibility.value == View.INVISIBLE){
    84. _controllerFrameVisibility.value = View.VISIBLE
    85. controllerShowTime = System.currentTimeMillis()
    86. viewModelScope.launch {
    87. delay(3000)
    88. if(System.currentTimeMillis() - controllerShowTime > 3000){
    89. _controllerFrameVisibility.value = View.INVISIBLE
    90. }
    91. }
    92. }else{
    93. _controllerFrameVisibility.value = View.INVISIBLE
    94. }
    95. }
    96. //重新赋值
    97. fun emmitVideoResolution(){
    98. _videoResolution.value = _videoResolution.value
    99. }
    100. //设置 MediaPlayer 进度
    101. fun playerSeekToProgress(progress: Int){
    102. _progressBarVisibility.value = View.VISIBLE
    103. mediaPlayer.seekTo(progress)
    104. }
    105. override fun onCleared() {
    106. super.onCleared()
    107. mediaPlayer.release()
    108. Log.e("MyTag","mediaPlayer release");
    109. }
    110. }

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

    1. class MainActivity : AppCompatActivity() {
    2. private lateinit var playerViewModel: PlayerViewModel
    3. private lateinit var surfaceView: SurfaceView
    4. private lateinit var playerFrameLayout: FrameLayout
    5. private lateinit var seekBar: SeekBar
    6. override fun onCreate(savedInstanceState: Bundle?) {
    7. super.onCreate(savedInstanceState)
    8. // object :OrientationEventListener(this){
    9. // override fun onOrientationChanged(orientation: Int) {
    10. // }
    11. // }
    12. setContentView(R.layout.activity_main)
    13. val progressBar: ProgressBar = findViewById(R.id.progressBar)
    14. seekBar = findViewById(R.id.seekBar)
    15. val controllerFrameLayout: FrameLayout = findViewById(R.id.controllerFrame)
    16. val buttonControl: ImageView = findViewById(R.id.buttonControl)
    17. playerFrameLayout = findViewById(R.id.playerFrame)
    18. updatePlayerProgress()
    19. playerViewModel = ViewModelProvider(this)[PlayerViewModel::class.java].apply {
    20. progressBarVisibility.observe(this@MainActivity) {
    21. progressBar.visibility = it
    22. }
    23. videoResolution.observe(this@MainActivity) {
    24. seekBar.max = mediaPlayer.duration
    25. //Log.e("MyTag","---- ${mediaPlayer.duration}");
    26. playerFrameLayout.post {
    27. reSizePlayer(it.first, it.second)
    28. }
    29. }
    30. controllerFrameVisibility.observe(this@MainActivity) {
    31. controllerFrameLayout.visibility = it
    32. }
    33. bufferPercent.observe(this@MainActivity, Observer {
    34. //Log.e("MyTag","---- $it");
    35. seekBar.secondaryProgress = seekBar.max * it / 100;
    36. })
    37. playerStatus.observe(this@MainActivity) {
    38. buttonControl.isClickable = true
    39. when (it) {
    40. PlayerStatus.Paused -> buttonControl.setImageResource(R.drawable.ic_baseline_play_arrow_24)
    41. PlayerStatus.Completed -> buttonControl.setImageResource(R.drawable.ic_baseline_replay_24)
    42. PlayerStatus.NotReady -> buttonControl.isClickable = false
    43. else -> buttonControl.setImageResource(R.drawable.ic_baseline_pause_24)
    44. }
    45. }
    46. }
    47. lifecycle.addObserver(playerViewModel.mediaPlayer)
    48. buttonControl.setOnClickListener {
    49. playerViewModel.togglePlayerStatus()
    50. }
    51. playerFrameLayout.setOnClickListener {
    52. playerViewModel.toggleControllerFrame()
    53. }
    54. seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
    55. override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
    56. if (fromUser) {
    57. playerViewModel.playerSeekToProgress(progress)
    58. }
    59. }
    60. override fun onStartTrackingTouch(seekBar: SeekBar?) {}
    61. override fun onStopTrackingTouch(seekBar: SeekBar?) {}
    62. })
    63. surfaceView = findViewById(R.id.surfaceView)
    64. surfaceView.holder.addCallback(object : SurfaceHolder.Callback {
    65. override fun surfaceCreated(holder: SurfaceHolder) {}
    66. override fun surfaceChanged(
    67. holder: SurfaceHolder, format: Int, width: Int, height: Int
    68. ) {
    69. playerViewModel.mediaPlayer.setDisplay(holder)
    70. playerViewModel.mediaPlayer.setScreenOnWhilePlaying(true)
    71. }
    72. override fun surfaceDestroyed(holder: SurfaceHolder) {}
    73. })
    74. }
    75. override fun onWindowFocusChanged(hasFocus: Boolean) {
    76. super.onWindowFocusChanged(hasFocus)
    77. if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
    78. hideSystemUI()
    79. playerViewModel.emmitVideoResolution()
    80. }
    81. }
    82. private fun reSizePlayer(width: Int, height: Int) {
    83. if (width == 0 || height == 0) return
    84. surfaceView.layoutParams = FrameLayout.LayoutParams(
    85. playerFrameLayout.height * width / height,
    86. FrameLayout.LayoutParams.MATCH_PARENT,
    87. Gravity.CENTER
    88. )
    89. //1674 1908
    90. //Log.e("MyTag","Size width: ${playerFrameLayout.height * width / height}")
    91. }
    92. private fun updatePlayerProgress() {
    93. lifecycleScope.launch {
    94. while (true) {
    95. delay(500)
    96. seekBar.progress = playerViewModel.mediaPlayer.currentPosition
    97. }
    98. }
    99. }
    100. private fun hideSystemUI() {
    101. val decorView: View = window.decorView
    102. // Set the content to appear under the system bars so that the
    103. decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
    104. // content doesn't resize when the system bars hide and show.
    105. 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
    106. or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_FULLSCREEN)
    107. }
    108. }

    5. 效果图

     

  • 相关阅读:
    金融信创,软件规划需关注自主安全及生态建设
    商城免费搭建之java商城 开源java电子商务Spring Cloud+Spring Boot+mybatis+MQ+VR全景+b2b2c
    详解如何利用PHP实现RPC
    举例解释大数定律、中心极限定理及其在机器学习中的应用
    李一鹏:一份让我无法拒绝的offer | OneFlow U
    C# 超详细的WebService创建、发布与调用(VS2022)
    C语言实现扫雷游戏(分解代码,超级详细,无压力)
    Debian11安装Geoserver+矢量插件Vector Tiles
    PMP考生如何应对新考纲?看过来!
    【微软技术栈】C#.NET 正则表达式源生成器
  • 原文地址:https://blog.csdn.net/u011193452/article/details/128137432