change to exoplayer
This commit is contained in:
parent
dc5f988baa
commit
aef165504f
|
|
@ -90,6 +90,14 @@ static def VersionName() {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
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.google.protobuf:protobuf-kotlin:3.25.1'
|
||||||
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
|
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
|
||||||
implementation 'com.squareup.retrofit2:converter-protobuf:2.9.0'
|
implementation 'com.squareup.retrofit2:converter-protobuf:2.9.0'
|
||||||
|
|
|
||||||
|
|
@ -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<String, String>? = mapOf()
|
||||||
|
|
||||||
|
fun setHeaders(headers: Map<String, String>) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,12 +5,13 @@ import android.view.KeyEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.leanback.media.MediaPlayerAdapter
|
import androidx.leanback.media.MediaPlayerAdapter
|
||||||
import androidx.leanback.media.PlaybackTransportControlGlue
|
import androidx.leanback.media.PlaybackTransportControlGlue
|
||||||
|
import androidx.leanback.media.PlayerAdapter
|
||||||
|
|
||||||
class PlaybackControlGlue(
|
class PlaybackControlGlue(
|
||||||
context: Context?,
|
context: Context?,
|
||||||
playerAdapter: MediaPlayerAdapter?,
|
playerAdapter: PlayerAdapter?,
|
||||||
) :
|
) :
|
||||||
PlaybackTransportControlGlue<MediaPlayerAdapter>(context, playerAdapter) {
|
PlaybackTransportControlGlue<PlayerAdapter>(context, playerAdapter) {
|
||||||
|
|
||||||
override fun onKey(v: View?, keyCode: Int, event: KeyEvent?): Boolean {
|
override fun onKey(v: View?, keyCode: Int, event: KeyEvent?): Boolean {
|
||||||
if (event!!.action == KeyEvent.ACTION_DOWN) {
|
if (event!!.action == KeyEvent.ACTION_DOWN) {
|
||||||
|
|
|
||||||
|
|
@ -5,21 +5,21 @@ import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.leanback.app.VideoSupportFragment
|
import androidx.leanback.app.VideoSupportFragment
|
||||||
import androidx.leanback.app.VideoSupportFragmentGlueHost
|
import androidx.leanback.app.VideoSupportFragmentGlueHost
|
||||||
import androidx.leanback.media.MediaPlayerAdapter
|
import androidx.leanback.media.PlayerAdapter
|
||||||
import androidx.leanback.media.PlaybackTransportControlGlue
|
import androidx.leanback.media.PlaybackTransportControlGlue
|
||||||
import androidx.leanback.widget.PlaybackControlsRow
|
import androidx.leanback.widget.PlaybackControlsRow
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
class PlaybackFragment : VideoSupportFragment() {
|
class PlaybackFragment : VideoSupportFragment() {
|
||||||
|
|
||||||
private lateinit var mTransportControlGlue: PlaybackTransportControlGlue<MediaPlayerAdapter>
|
private lateinit var mTransportControlGlue: PlaybackTransportControlGlue<PlayerAdapter>
|
||||||
private var playerAdapter: MediaPlayerAdapter? = null
|
private var playerAdapter: Custom2MediaPlayerAdapter? = null
|
||||||
private var lastVideoUrl: String = ""
|
private var lastVideoUrl: String = ""
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
playerAdapter = MediaPlayerAdapter(context)
|
playerAdapter = Custom2MediaPlayerAdapter(context)
|
||||||
playerAdapter?.setRepeatAction(PlaybackControlsRow.RepeatAction.INDEX_NONE)
|
playerAdapter?.setRepeatAction(PlaybackControlsRow.RepeatAction.INDEX_NONE)
|
||||||
|
|
||||||
view?.isFocusable = false
|
view?.isFocusable = false
|
||||||
|
|
@ -60,15 +60,6 @@ class PlaybackFragment : VideoSupportFragment() {
|
||||||
hideControlsOverlay(false)
|
hideControlsOverlay(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
if (playerAdapter?.mediaPlayer != null) {
|
|
||||||
playerAdapter?.release()
|
|
||||||
Log.d(TAG, "playerAdapter released")
|
|
||||||
}
|
|
||||||
|
|
||||||
super.onDestroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "PlaybackVideoFragment"
|
private const val TAG = "PlaybackVideoFragment"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue