Files
android-tv/app/src/main/java/com/videoapp/tv/PlayerActivity.kt

286 lines
8.9 KiB
Kotlin
Raw Normal View History

package com.videoapp.tv
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.View
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.Button
import android.widget.ImageButton
import android.widget.LinearLayout
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.MediaItem
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.ui.PlayerView
import com.videoapp.tv.data.ConfigRepository
import com.videoapp.tv.engine.Episode
import com.videoapp.tv.engine.PlaySource
import com.videoapp.tv.engine.VideoExtractor
import kotlinx.coroutines.launch
class PlayerActivity : AppCompatActivity() {
private lateinit var playerView: PlayerView
private lateinit var playerWebView: WebView
private lateinit var controlPanel: View
private lateinit var sourceList: LinearLayout
private lateinit var episodeList: LinearLayout
private lateinit var loadingIndicator: View
private lateinit var errorText: android.widget.TextView
private lateinit var btnClose: ImageButton
private var exoPlayer: ExoPlayer? = null
private val videoExtractor = VideoExtractor()
private val configRepo by lazy { ConfigRepository(this) }
private var sources: List<PlaySource> = emptyList()
private var currentSourceIndex = 0
private var currentEpisode: Episode? = null
private val hideHandler = Handler(Looper.getMainLooper())
private val hideRunnable = Runnable { hideControls() }
private var controlsVisible = true
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_player)
playerView = findViewById(R.id.player_view)
playerWebView = findViewById(R.id.player_webview)
controlPanel = findViewById(R.id.control_panel)
sourceList = findViewById(R.id.source_list)
episodeList = findViewById(R.id.episode_list)
loadingIndicator = findViewById(R.id.loading_indicator)
errorText = findViewById(R.id.error_text)
btnClose = findViewById(R.id.btn_close)
val detailUrl = intent.getStringExtra("detail_url") ?: ""
btnClose.setOnClickListener { finish() }
initExoPlayer()
loadSources(detailUrl)
setupTouchListeners()
}
private fun initExoPlayer() {
exoPlayer = ExoPlayer.Builder(this).build().also { player ->
playerView.player = player
}
}
private fun loadSources(detailUrl: String) {
showLoading(true)
val config = configRepo.getConfig()
lifecycleScope.launch {
try {
sources = videoExtractor.extractVideos(detailUrl, config)
if (sources.isNotEmpty()) {
buildSourceUI()
selectSource(0)
resetAutoHide()
} else {
tryPlayDirectly(detailUrl, config)
}
} catch (e: Exception) {
showError("加载失败: ${e.message}")
showLoading(false)
}
}
}
private fun buildSourceUI() {
sourceList.removeAllViews()
sources.forEachIndexed { index, source ->
val btn = Button(this).apply {
text = source.name
setBackgroundResource(R.drawable.episode_selector)
setTextColor(ContextCompat.getColor(this@PlayerActivity, R.color.text_primary))
textSize = 13f
minWidth = 0
setPadding(16, 8, 16, 8)
isFocusable = true
isFocusableInTouchMode = true
setOnClickListener {
selectSource(index)
resetAutoHide()
}
}
sourceList.addView(btn)
}
}
private fun selectSource(index: Int) {
if (index !in sources.indices) return
currentSourceIndex = index
// Highlight selected source
for (i in 0 until sourceList.childCount) {
sourceList.getChildAt(i).isSelected = (i == index)
}
// Build episode list for this source
val source = sources[index]
buildEpisodeUI(source.episodes)
// Auto-play first episode
if (source.episodes.isNotEmpty()) {
playEpisode(source.episodes.first())
}
}
private fun buildEpisodeUI(eps: List<Episode>) {
episodeList.removeAllViews()
eps.forEach { ep ->
val btn = Button(this).apply {
text = ep.title
setBackgroundResource(R.drawable.episode_selector)
setTextColor(ContextCompat.getColor(this@PlayerActivity, R.color.text_primary))
textSize = 12f
minWidth = 0
setPadding(14, 6, 14, 6)
isFocusable = true
isFocusableInTouchMode = true
setOnClickListener {
highlightEpisode(it)
playEpisode(ep)
resetAutoHide()
}
}
episodeList.addView(btn)
}
}
private fun playEpisode(ep: Episode) {
currentEpisode = ep
showLoading(true)
val config = configRepo.getConfig()
lifecycleScope.launch {
val (directUrl, iframeUrl) = videoExtractor.extractFromPlayPage(ep.playUrl, config)
if (directUrl != null) {
playWithExoPlayer(directUrl)
} else if (iframeUrl != null) {
playWithWebView(iframeUrl)
} else {
playWithWebView(ep.playUrl)
}
}
}
private fun tryPlayDirectly(detailUrl: String, config: com.videoapp.tv.data.SiteConfig) {
lifecycleScope.launch {
val (directUrl, iframeUrl) = videoExtractor.extractFromPlayPage(detailUrl, config)
if (directUrl != null) {
controlPanel.visibility = View.GONE
playWithExoPlayer(directUrl)
} else if (iframeUrl != null) {
controlPanel.visibility = View.GONE
playWithWebView(iframeUrl)
} else {
controlPanel.visibility = View.GONE
playWithWebView(detailUrl)
}
}
}
private fun playWithExoPlayer(url: String) {
playerWebView.visibility = View.GONE
playerView.visibility = View.VISIBLE
showLoading(false)
val mediaItem = MediaItem.fromUri(url)
exoPlayer?.apply {
setMediaItem(mediaItem)
prepare()
playWhenReady = true
}
}
private fun playWithWebView(url: String) {
playerView.visibility = View.GONE
playerWebView.visibility = View.VISIBLE
showLoading(false)
playerWebView.settings.javaScriptEnabled = true
playerWebView.settings.domStorageEnabled = true
playerWebView.settings.mediaPlaybackRequiresUserGesture = false
playerWebView.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView, url: String) {
super.onPageFinished(view, url)
showLoading(false)
}
}
playerWebView.loadUrl(url)
}
private fun highlightEpisode(view: View) {
for (i in 0 until episodeList.childCount) {
episodeList.getChildAt(i).isSelected = (episodeList.getChildAt(i) == view)
}
}
private fun showLoading(show: Boolean) {
loadingIndicator.visibility = if (show) View.VISIBLE else View.GONE
}
private fun showError(msg: String) {
errorText.text = msg
errorText.visibility = View.VISIBLE
showLoading(false)
}
private fun setupTouchListeners() {
val listener = View.OnClickListener { toggleControls() }
playerView.setOnClickListener(listener)
playerWebView.setOnClickListener(listener)
}
private fun toggleControls() {
if (controlsVisible) {
hideControls()
} else {
showControls()
resetAutoHide()
}
}
private fun showControls() {
controlsVisible = true
btnClose.visibility = View.VISIBLE
controlPanel.visibility = View.VISIBLE
}
private fun hideControls() {
controlsVisible = false
btnClose.visibility = View.GONE
controlPanel.visibility = View.GONE
}
private fun resetAutoHide() {
hideHandler.removeCallbacks(hideRunnable)
if (!controlsVisible) {
showControls()
}
hideHandler.postDelayed(hideRunnable, 4000)
}
override fun onPause() {
super.onPause()
exoPlayer?.playWhenReady = false
}
override fun onDestroy() {
super.onDestroy()
hideHandler.removeCallbacks(hideRunnable)
exoPlayer?.release()
exoPlayer = null
}
}