From aef165504fbe1b582c89e5e1015f349803de2d65 Mon Sep 17 00:00:00 2001 From: Li ZongYing Date: Fri, 15 Dec 2023 15:41:59 +0800 Subject: [PATCH] change to exoplayer --- app/build.gradle | 8 + .../mytv/Custom2MediaPlayerAdapter.kt | 251 ++++++++++++++++++ .../lizongying/mytv/PlaybackControlGlue.kt | 5 +- .../com/lizongying/mytv/PlaybackFragment.kt | 17 +- 4 files changed, 266 insertions(+), 15 deletions(-) create mode 100644 app/src/main/java/com/lizongying/mytv/Custom2MediaPlayerAdapter.kt diff --git a/app/build.gradle b/app/build.gradle index a2ca148..77f8e1d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -90,6 +90,14 @@ static def VersionName() { } dependencies { + def media3_version = "1.1.1" + + // For media playback using ExoPlayer + implementation "androidx.media3:media3-exoplayer:$media3_version" + + // For HLS playback support with ExoPlayer + implementation "androidx.media3:media3-exoplayer-hls:$media3_version" + implementation 'com.google.protobuf:protobuf-kotlin:3.25.1' implementation 'com.squareup.retrofit2:converter-gson:2.9.0' implementation 'com.squareup.retrofit2:converter-protobuf:2.9.0' diff --git a/app/src/main/java/com/lizongying/mytv/Custom2MediaPlayerAdapter.kt b/app/src/main/java/com/lizongying/mytv/Custom2MediaPlayerAdapter.kt new file mode 100644 index 0000000..a7b5a17 --- /dev/null +++ b/app/src/main/java/com/lizongying/mytv/Custom2MediaPlayerAdapter.kt @@ -0,0 +1,251 @@ +package com.lizongying.mytv + +import android.content.Context +import android.media.MediaPlayer +import android.net.Uri +import android.os.Handler +import android.view.SurfaceHolder +import androidx.annotation.OptIn +import androidx.leanback.media.PlaybackGlueHost +import androidx.leanback.media.PlayerAdapter +import androidx.leanback.media.SurfaceHolderGlueHost +import androidx.media3.common.MediaItem +import androidx.media3.common.util.UnstableApi +import androidx.media3.datasource.DefaultHttpDataSource +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.exoplayer.hls.HlsMediaSource +import java.io.IOException + + +open class Custom2MediaPlayerAdapter(private var mContext: Context?) : PlayerAdapter() { + + val mPlayer = mContext?.let { ExoPlayer.Builder(it).build() } + var mSurfaceHolderGlueHost: SurfaceHolderGlueHost? = null + val mRunnable: Runnable = object : Runnable { + override fun run() { + callback.onCurrentPositionChanged(this@Custom2MediaPlayerAdapter) + mHandler.postDelayed(this, getProgressUpdatingInterval().toLong()) + } + }; + val mHandler = Handler() + var mInitialized = false // true when the MediaPlayer is prepared/initialized + + var mMediaSourceUri: Uri? = null + var mHasDisplay = false + var mBufferedProgress: Long = 0 + + var mBufferingStart = false + + open fun notifyBufferingStartEnd() { + callback.onBufferingStateChanged( + this@Custom2MediaPlayerAdapter, + mBufferingStart || !mInitialized + ) + } + + override fun onAttachedToHost(host: PlaybackGlueHost?) { + if (host is SurfaceHolderGlueHost) { + mSurfaceHolderGlueHost = host + mSurfaceHolderGlueHost!!.setSurfaceHolderCallback(VideoPlayerSurfaceHolderCallback(this)) + } + } + + /** + * Will reset the [MediaPlayer] and the glue such that a new file can be played. You are + * not required to call this method before playing the first file. However you have to call it + * before playing a second one. + */ + open fun reset() { + changeToUnitialized() +// mPlayer.reset() + } + + open fun changeToUnitialized() { + if (mInitialized) { + mInitialized = false + notifyBufferingStartEnd() + if (mHasDisplay) { + callback.onPreparedStateChanged(this@Custom2MediaPlayerAdapter) + } + } + } + + /** + * Release internal MediaPlayer. Should not use the object after call release(). + */ + open fun release() { + changeToUnitialized() + mHasDisplay = false + mPlayer?.release() + } + + override fun onDetachedFromHost() { + if (mSurfaceHolderGlueHost != null) { + mSurfaceHolderGlueHost!!.setSurfaceHolderCallback(null) + mSurfaceHolderGlueHost = null + } + reset() + release() + } + + /** + * @see MediaPlayer.setDisplay + */ + fun setDisplay(surfaceHolder: SurfaceHolder?) { + val hadDisplay = mHasDisplay + mHasDisplay = surfaceHolder != null + if (hadDisplay == mHasDisplay) { + return + } + mPlayer?.setVideoSurfaceHolder(surfaceHolder) + if (mHasDisplay) { + if (mInitialized) { + callback.onPreparedStateChanged(this@Custom2MediaPlayerAdapter) + } + } else { + if (mInitialized) { + callback.onPreparedStateChanged(this@Custom2MediaPlayerAdapter) + } + } + } + + override fun setProgressUpdatingEnabled(enabled: Boolean) { + mHandler.removeCallbacks(mRunnable) + if (!enabled) { + return + } + mHandler.postDelayed(mRunnable, getProgressUpdatingInterval().toLong()) + } + + /** + * Return updating interval of progress UI in milliseconds. Subclass may override. + * @return Update interval of progress UI in milliseconds. + */ + open fun getProgressUpdatingInterval(): Int { + return 16 + } + + override fun isPlaying(): Boolean { + return mInitialized && mPlayer?.isPlaying ?: false + } + + override fun getDuration(): Long { + if (mInitialized) { + val duration = mPlayer?.duration + if (duration != null) { + return duration.toLong() + } + } + return -1 + } + + override fun getCurrentPosition(): Long { + if (mInitialized) { + val currentPosition = mPlayer?.currentPosition + if (currentPosition != null) { + return currentPosition.toLong() + } + } + return -1 + } + + override fun play() { + if (!mInitialized || mPlayer?.isPlaying == true) { + return + } + mPlayer?.play() + callback.onPlayStateChanged(this@Custom2MediaPlayerAdapter) + callback.onCurrentPositionChanged(this@Custom2MediaPlayerAdapter) + } + + override fun pause() { + if (isPlaying) { + mPlayer?.pause() + callback.onPlayStateChanged(this@Custom2MediaPlayerAdapter) + } + } + + override fun seekTo(newPosition: Long) { + if (!mInitialized) { + return + } + mPlayer?.seekTo(newPosition.toInt().toLong()) + } + + override fun getBufferedPosition(): Long { + return mBufferedProgress + } + + /** + * Sets the media source of the player witha given URI. + * + * @return Returns `true` if uri represents a new media; `false` + * otherwise. + * @see MediaPlayer.setDataSource + */ + open fun setDataSource(uri: Uri?): Boolean { + if (if (mMediaSourceUri != null) mMediaSourceUri == uri else uri == null) { + return false + } + mMediaSourceUri = uri + prepareMediaForPlaying() + return true + } + + private var mHeaders: Map? = mapOf() + + fun setHeaders(headers: Map) { + mHeaders = headers + } + + @OptIn(UnstableApi::class) + private fun prepareMediaForPlaying() { + reset() + try { + if (mMediaSourceUri != null) { + val httpDataSource = DefaultHttpDataSource.Factory() + mHeaders?.let { httpDataSource.setDefaultRequestProperties(it) } + + val hlsMediaSource = + HlsMediaSource.Factory(httpDataSource).createMediaSource( + MediaItem.fromUri( + mMediaSourceUri!! + ) + ) + mPlayer?.setMediaSource(hlsMediaSource) + } else { + return + } + } catch (e: IOException) { + e.printStackTrace() + throw RuntimeException(e) + } + mPlayer?.prepare() + mPlayer?.playWhenReady = true + callback.onPlayStateChanged(this@Custom2MediaPlayerAdapter) + } + + /** + * @return True if MediaPlayer OnPreparedListener is invoked and got a SurfaceHolder if + * [PlaybackGlueHost] provides SurfaceHolder. + */ + override fun isPrepared(): Boolean { + return mInitialized && (mSurfaceHolderGlueHost == null || mHasDisplay) + } +} + +internal class VideoPlayerSurfaceHolderCallback(private val playerAdapter: Custom2MediaPlayerAdapter) : + SurfaceHolder.Callback { + + override fun surfaceCreated(surfaceHolder: SurfaceHolder) { + playerAdapter.setDisplay(surfaceHolder) + } + + override fun surfaceChanged(surfaceHolder: SurfaceHolder, i: Int, i1: Int, i2: Int) { + // Handle surface changes if needed + } + + override fun surfaceDestroyed(surfaceHolder: SurfaceHolder) { + playerAdapter.setDisplay(null) + } +} diff --git a/app/src/main/java/com/lizongying/mytv/PlaybackControlGlue.kt b/app/src/main/java/com/lizongying/mytv/PlaybackControlGlue.kt index 751cc49..b329d50 100644 --- a/app/src/main/java/com/lizongying/mytv/PlaybackControlGlue.kt +++ b/app/src/main/java/com/lizongying/mytv/PlaybackControlGlue.kt @@ -5,12 +5,13 @@ import android.view.KeyEvent import android.view.View import androidx.leanback.media.MediaPlayerAdapter import androidx.leanback.media.PlaybackTransportControlGlue +import androidx.leanback.media.PlayerAdapter class PlaybackControlGlue( context: Context?, - playerAdapter: MediaPlayerAdapter?, + playerAdapter: PlayerAdapter?, ) : - PlaybackTransportControlGlue(context, playerAdapter) { + PlaybackTransportControlGlue(context, playerAdapter) { override fun onKey(v: View?, keyCode: Int, event: KeyEvent?): Boolean { if (event!!.action == KeyEvent.ACTION_DOWN) { diff --git a/app/src/main/java/com/lizongying/mytv/PlaybackFragment.kt b/app/src/main/java/com/lizongying/mytv/PlaybackFragment.kt index 75618a3..cac47bb 100644 --- a/app/src/main/java/com/lizongying/mytv/PlaybackFragment.kt +++ b/app/src/main/java/com/lizongying/mytv/PlaybackFragment.kt @@ -5,21 +5,21 @@ import android.os.Bundle import android.util.Log import androidx.leanback.app.VideoSupportFragment import androidx.leanback.app.VideoSupportFragmentGlueHost -import androidx.leanback.media.MediaPlayerAdapter +import androidx.leanback.media.PlayerAdapter import androidx.leanback.media.PlaybackTransportControlGlue import androidx.leanback.widget.PlaybackControlsRow import java.io.IOException class PlaybackFragment : VideoSupportFragment() { - private lateinit var mTransportControlGlue: PlaybackTransportControlGlue - private var playerAdapter: MediaPlayerAdapter? = null + private lateinit var mTransportControlGlue: PlaybackTransportControlGlue + private var playerAdapter: Custom2MediaPlayerAdapter? = null private var lastVideoUrl: String = "" override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - playerAdapter = MediaPlayerAdapter(context) + playerAdapter = Custom2MediaPlayerAdapter(context) playerAdapter?.setRepeatAction(PlaybackControlsRow.RepeatAction.INDEX_NONE) view?.isFocusable = false @@ -60,15 +60,6 @@ class PlaybackFragment : VideoSupportFragment() { hideControlsOverlay(false) } - override fun onDestroy() { - if (playerAdapter?.mediaPlayer != null) { - playerAdapter?.release() - Log.d(TAG, "playerAdapter released") - } - - super.onDestroy() - } - companion object { private const val TAG = "PlaybackVideoFragment" }