mirror of
https://github.com/lizongying/my-tv.git
synced 2026-11-02 17:46:35 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aef165504f | ||
|
|
dc5f988baa |
@@ -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'
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ class CardPresenter(
|
|||||||
cardView.titleText = tv.title
|
cardView.titleText = tv.title
|
||||||
cardView.setMainImageDimensions(CARD_WIDTH, CARD_HEIGHT)
|
cardView.setMainImageDimensions(CARD_WIDTH, CARD_HEIGHT)
|
||||||
cardView.tag = tv.videoUrl
|
cardView.tag = tv.videoUrl
|
||||||
Log.i(TAG, "bind ${tv.videoUrl}")
|
|
||||||
|
|
||||||
// lifecycleScope.launch(Dispatchers.IO) {
|
// lifecycleScope.launch(Dispatchers.IO) {
|
||||||
// val videoThumbnail = tv.videoUrl?.let { getVideoThumbnail(it) }
|
// val videoThumbnail = tv.videoUrl?.let { getVideoThumbnail(it) }
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -102,7 +102,6 @@ class MainFragment : BrowseSupportFragment() {
|
|||||||
)
|
)
|
||||||
listRowAdapter.add(v1)
|
listRowAdapter.add(v1)
|
||||||
idx2++
|
idx2++
|
||||||
|
|
||||||
tvListViewModel.addTV(v1)
|
tvListViewModel.addTV(v1)
|
||||||
}
|
}
|
||||||
val header = HeaderItem(idx, k)
|
val header = HeaderItem(idx, k)
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,12 +71,15 @@ class TVViewModel(private var tv: TV) : ViewModel() {
|
|||||||
val backgroundImage: LiveData<String>
|
val backgroundImage: LiveData<String>
|
||||||
get() = _backgroundImage
|
get() = _backgroundImage
|
||||||
|
|
||||||
// 方法用于更新背景图信息
|
fun getBackgroundImage(): String {
|
||||||
|
return tv.logo ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
fun updateBackgroundImage(url: String) {
|
fun updateBackgroundImage(url: String) {
|
||||||
_backgroundImage.value = url
|
_backgroundImage.value = url
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateVideoUrl(url: String) {
|
private fun updateVideoUrl(url: String) {
|
||||||
tv.videoUrl = listOf(url)
|
tv.videoUrl = listOf(url)
|
||||||
tv.videoIndex = 0
|
tv.videoIndex = 0
|
||||||
_videoUrl.value = listOf(url)
|
_videoUrl.value = listOf(url)
|
||||||
@@ -106,10 +109,9 @@ class TVViewModel(private var tv: TV) : ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun ysp(): String? {
|
fun ysp(): String? {
|
||||||
|
if (tv.pid == "") {
|
||||||
|
return null
|
||||||
|
}
|
||||||
return mapping[tv.title]
|
return mapping[tv.title]
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getBackgroundImage(): String {
|
|
||||||
return tv.logo ?: ""
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user