diff --git a/app/build.gradle b/app/build.gradle index 0194a1e..6e7f451 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -91,6 +91,8 @@ static def VersionName() { dependencies { def media3_version = "1.1.1" + implementation "androidx.media3:media3-ui:$media3_version" + // For media playback using ExoPlayer implementation "androidx.media3:media3-exoplayer:$media3_version" diff --git a/app/src/main/cpp/armeabi-v7a/libnative.so b/app/src/main/cpp/armeabi-v7a/libnative.so index 21148df..b7a7579 100755 Binary files a/app/src/main/cpp/armeabi-v7a/libnative.so and b/app/src/main/cpp/armeabi-v7a/libnative.so differ diff --git a/app/src/main/java/com/lizongying/mytv/ExoPlayerAdapter.kt b/app/src/main/java/com/lizongying/mytv/ExoPlayerAdapter.kt deleted file mode 100644 index 433b1ad..0000000 --- a/app/src/main/java/com/lizongying/mytv/ExoPlayerAdapter.kt +++ /dev/null @@ -1,290 +0,0 @@ -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.PlaybackException -import androidx.media3.common.Player -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 ExoPlayerAdapter(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@ExoPlayerAdapter) - 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 - - - private var mMinimumLoadableRetryCount = 3 - - - private var mPlayerErrorListener: PlayerErrorListener? = null - - init { - mPlayer?.playWhenReady = true - - if (mPlayerErrorListener == null) { - mPlayerErrorListener = PlayerErrorListener() - mPlayer?.addListener(mPlayerErrorListener!!) - } - } - - - open fun notifyBufferingStartEnd() { - callback.onBufferingStateChanged( - this@ExoPlayerAdapter, - 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@ExoPlayerAdapter) - } - } - } - - /** - * 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@ExoPlayerAdapter) - } - } else { - if (mInitialized) { - callback.onPreparedStateChanged(this@ExoPlayerAdapter) - } - } - } - - 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@ExoPlayerAdapter) - callback.onCurrentPositionChanged(this@ExoPlayerAdapter) - } - - override fun pause() { - if (isPlaying) { - mPlayer?.pause() - callback.onPlayStateChanged(this@ExoPlayerAdapter) - } - } - - override fun seekTo(newPosition: Long) { - if (!mInitialized) { - return - } - mPlayer?.seekTo(newPosition.toInt().toLong()) - } - - override fun getBufferedPosition(): Long { - return mBufferedProgress - } - - private inner class PlayerErrorListener : Player.Listener { - override fun onPlayerError(error: PlaybackException) { - callback.onError(this@ExoPlayerAdapter, error.errorCode, error.message) - } - } - - - private var mHeaders: Map? = mapOf() - - fun setHeaders(headers: Map) { - mHeaders = headers - } - - /** - * Sets the media source of the player witha given URI. - * - * @return Returns `true` if uri represents a new media; `false` - * otherwise. - * @see MediaPlayer.setDataSource - */ - - @OptIn(UnstableApi::class) - open fun setDataSource(uri: Uri?): Boolean { - if (if (mMediaSourceUri != null) mMediaSourceUri == uri else uri == null) { - return false - } - mMediaSourceUri = uri - - val httpDataSource = DefaultHttpDataSource.Factory() - mHeaders?.let { httpDataSource.setDefaultRequestProperties(it) } - - val hlsMediaSource = - HlsMediaSource.Factory(httpDataSource).setLoadErrorHandlingPolicy( - CustomLoadErrorHandlingPolicy(mMinimumLoadableRetryCount) - ).createMediaSource( - MediaItem.fromUri( - mMediaSourceUri!! - ) - ) - prepareMediaForPlaying(hlsMediaSource) - return true - } - - @OptIn(UnstableApi::class) - open fun setDataSource(hlsMediaSource: HlsMediaSource): Boolean { - prepareMediaForPlaying(hlsMediaSource) - return true - } - - fun setMinimumLoadableRetryCount(minimumLoadableRetryCount: Int) { - mMinimumLoadableRetryCount = minimumLoadableRetryCount - } - - @OptIn(UnstableApi::class) - private fun prepareMediaForPlaying(hlsMediaSource: HlsMediaSource) { - try { - mPlayer?.setMediaSource(hlsMediaSource) - } catch (e: IOException) { - e.printStackTrace() - throw RuntimeException(e) - } - mPlayer?.prepare() - - callback.onPlayStateChanged(this@ExoPlayerAdapter) - } - - /** - * @return True if MediaPlayer OnPreparedListener is invoked and got a SurfaceHolder if - * [PlaybackGlueHost] provides SurfaceHolder. - */ - override fun isPrepared(): Boolean { - return mInitialized && (mSurfaceHolderGlueHost == null || mHasDisplay) - } - - companion object { - private const val TAG = "ExoPlayerAdapter" - } -} - -internal class VideoPlayerSurfaceHolderCallback(private val playerAdapter: ExoPlayerAdapter) : - 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/MainActivity.kt b/app/src/main/java/com/lizongying/mytv/MainActivity.kt index a7c6d39..ea2f4ba 100644 --- a/app/src/main/java/com/lizongying/mytv/MainActivity.kt +++ b/app/src/main/java/com/lizongying/mytv/MainActivity.kt @@ -134,35 +134,7 @@ class MainActivity : FragmentActivity() { private inner class GestureListener : GestureDetector.SimpleOnGestureListener() { override fun onSingleTapConfirmed(e: MotionEvent): Boolean { - // 处理单击事件 - val versionName = getPackageInfo().versionName - - val textView = TextView(this@MainActivity) - textView.text = - "当前版本: $versionName\n获取最新: https://github.com/lizongying/my-tv/releases/" - - val imageView = ImageView(this@MainActivity) - val drawable = ContextCompat.getDrawable(this@MainActivity, R.drawable.appreciate) - imageView.setImageDrawable(drawable) - - val linearLayout = LinearLayout(this@MainActivity) - linearLayout.orientation = LinearLayout.VERTICAL - linearLayout.addView(textView) - linearLayout.addView(imageView) - - val layoutParams = LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, - LinearLayout.LayoutParams.WRAP_CONTENT - ) - imageView.layoutParams = layoutParams - textView.layoutParams = layoutParams - - val builder: AlertDialog.Builder = AlertDialog.Builder(this@MainActivity) - builder - .setView(linearLayout) - - val dialog: AlertDialog = builder.create() - dialog.show() + switchMainFragment() return true } diff --git a/app/src/main/java/com/lizongying/mytv/MainFragment.kt b/app/src/main/java/com/lizongying/mytv/MainFragment.kt index db074f1..4687d72 100644 --- a/app/src/main/java/com/lizongying/mytv/MainFragment.kt +++ b/app/src/main/java/com/lizongying/mytv/MainFragment.kt @@ -51,7 +51,6 @@ class MainFragment : BrowseSupportFragment() { // request?.fetchPage() // tvListViewModel.getTVViewModel(0)?.let { request?.fetchProgram(it) } } - tvListViewModel.getTVListViewModel().value?.forEach { tvViewModel -> tvViewModel.ready.observe(viewLifecycleOwner) { _ -> if (tvViewModel.ready.value != null) { diff --git a/app/src/main/java/com/lizongying/mytv/PlaybackControlGlue.kt b/app/src/main/java/com/lizongying/mytv/PlaybackControlGlue.kt deleted file mode 100644 index ba85436..0000000 --- a/app/src/main/java/com/lizongying/mytv/PlaybackControlGlue.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.lizongying.mytv - -import android.content.Context -import android.util.Log -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: PlayerAdapter?, -) : - PlaybackTransportControlGlue(context, playerAdapter) { - - override fun onKey(v: View?, keyCode: Int, event: KeyEvent?): Boolean { - if (event!!.action == KeyEvent.ACTION_DOWN) { - when (keyCode) { - KeyEvent.KEYCODE_DPAD_CENTER -> { - Log.i(TAG, "KEYCODE_DPAD_CENTER") - (context as? MainActivity)?.switchMainFragment() - } - - KeyEvent.KEYCODE_DPAD_UP -> { - if ((context as? MainActivity)?.mainFragmentIsHidden() == true) { - (context as? MainActivity)?.prev() - } - } - - KeyEvent.KEYCODE_DPAD_DOWN -> { - if ((context as? MainActivity)?.mainFragmentIsHidden() == true) { - (context as? MainActivity)?.next() - } - } - - KeyEvent.KEYCODE_DPAD_LEFT -> { - if ((context as? MainActivity)?.mainFragmentIsHidden() == true) { - (context as? MainActivity)?.prevSource() - } - } - - KeyEvent.KEYCODE_DPAD_RIGHT -> { - if ((context as? MainActivity)?.mainFragmentIsHidden() == true) { - (context as? MainActivity)?.nextSource() - } - } - } - } - - return super.onKey(v, keyCode, event) - } - - companion object { - private const val TAG = "PlaybackControlGlue" - } -} diff --git a/app/src/main/java/com/lizongying/mytv/PlaybackFragment.kt b/app/src/main/java/com/lizongying/mytv/PlaybackFragment.kt index 3f98c42..449ece3 100644 --- a/app/src/main/java/com/lizongying/mytv/PlaybackFragment.kt +++ b/app/src/main/java/com/lizongying/mytv/PlaybackFragment.kt @@ -1,40 +1,37 @@ package com.lizongying.mytv -import android.net.Uri import android.os.Bundle import android.util.Log -import androidx.leanback.app.VideoSupportFragment -import androidx.leanback.app.VideoSupportFragmentGlueHost -import androidx.leanback.media.PlaybackTransportControlGlue -import androidx.leanback.media.PlayerAdapter -import androidx.leanback.widget.PlaybackControlsRow +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.annotation.OptIn +import androidx.fragment.app.Fragment +import androidx.media3.common.MediaItem +import androidx.media3.common.util.UnstableApi +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.ui.PlayerView +import com.lizongying.mytv.databinding.PlayerBinding import com.lizongying.mytv.models.TVViewModel -import java.io.IOException -class PlaybackFragment : VideoSupportFragment() { - private lateinit var mTransportControlGlue: PlaybackTransportControlGlue - private var playerAdapter: ExoPlayerAdapter? = null - private var lastVideoUrl: String = "" - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - playerAdapter = ExoPlayerAdapter(context) - playerAdapter?.setRepeatAction(PlaybackControlsRow.RepeatAction.INDEX_NONE) +class PlaybackFragment : Fragment() { - view?.isFocusable = false - view?.isFocusableInTouchMode = false + private var lastVideoUrl: String = "" - val glueHost = VideoSupportFragmentGlueHost(this@PlaybackFragment) - mTransportControlGlue = PlaybackControlGlue(activity, playerAdapter) - mTransportControlGlue.host = glueHost - mTransportControlGlue.playWhenPrepared() - } + private var _binding: PlayerBinding? = null + private var playerView: PlayerView? = null - override fun showControlsOverlay(runAnimation: Boolean) { + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = PlayerBinding.inflate(inflater, container, false) + playerView = _binding!!.playerView + return _binding!!.root } + @OptIn(UnstableApi::class) fun play(tvModel: TVViewModel) { val videoUrl = tvModel.videoIndex.value?.let { tvModel.videoUrl.value?.get(it) } if (videoUrl == null || videoUrl == "") { @@ -49,26 +46,17 @@ class PlaybackFragment : VideoSupportFragment() { lastVideoUrl = videoUrl - playerAdapter?.callback = PlayerCallback(tvModel) - if (tvModel.ysp() != null) { - playerAdapter?.setMinimumLoadableRetryCount(0) - } - try { - playerAdapter?.setDataSource(Uri.parse(videoUrl)) - } catch (e: IOException) { - Log.e(TAG, "error $e") - return + if (playerView!!.player == null) { + playerView!!.player = activity?.let { + ExoPlayer.Builder(it) + .build() + } + playerView!!.player?.playWhenReady = true } - hideControlsOverlay(false) - } - private inner class PlayerCallback(private var tvModel: TVViewModel) : - PlayerAdapter.Callback() { - override fun onError(adapter: PlayerAdapter?, errorCode: Int, errorMessage: String?) { - Log.e(TAG, "on error: $errorMessage") - if (tvModel.ysp() != null && tvModel.videoIndex.value!! > 0 && errorMessage == "Source error") { - tvModel.changed() - } + playerView!!.player?.run { + setMediaItem(MediaItem.fromUri(videoUrl)) + prepare() } } diff --git a/app/src/main/res/layout/player.xml b/app/src/main/res/layout/player.xml new file mode 100644 index 0000000..3c8dbe6 --- /dev/null +++ b/app/src/main/res/layout/player.xml @@ -0,0 +1,14 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml new file mode 100644 index 0000000..1ff9bfe --- /dev/null +++ b/app/src/main/res/values/attrs.xml @@ -0,0 +1,202 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file