2026-05-24 21:09:05 +08:00
|
|
|
package com.videoapp.tv
|
|
|
|
|
|
|
|
|
|
import android.os.Bundle
|
|
|
|
|
import android.view.View
|
|
|
|
|
import android.webkit.WebView
|
|
|
|
|
import android.webkit.WebViewClient
|
|
|
|
|
import android.widget.Button
|
2026-05-24 21:19:34 +08:00
|
|
|
import android.widget.ImageButton
|
2026-05-24 21:09:05 +08:00
|
|
|
import androidx.appcompat.app.AppCompatActivity
|
2026-05-24 21:14:19 +08:00
|
|
|
import androidx.core.content.ContextCompat
|
2026-05-24 21:09:05 +08:00
|
|
|
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.VideoExtractor
|
|
|
|
|
import kotlinx.coroutines.launch
|
|
|
|
|
|
|
|
|
|
class PlayerActivity : AppCompatActivity() {
|
|
|
|
|
|
|
|
|
|
private lateinit var playerView: PlayerView
|
|
|
|
|
private lateinit var playerWebView: WebView
|
|
|
|
|
private lateinit var episodePanel: View
|
|
|
|
|
private lateinit var episodeList: android.widget.LinearLayout
|
|
|
|
|
private lateinit var loadingIndicator: View
|
|
|
|
|
private lateinit var errorText: android.widget.TextView
|
2026-05-24 21:19:34 +08:00
|
|
|
private lateinit var btnClose: ImageButton
|
2026-05-24 21:09:05 +08:00
|
|
|
|
|
|
|
|
private var exoPlayer: ExoPlayer? = null
|
|
|
|
|
private val videoExtractor = VideoExtractor()
|
|
|
|
|
private val configRepo by lazy { ConfigRepository(this) }
|
|
|
|
|
|
|
|
|
|
private var episodes: List<Episode> = emptyList()
|
|
|
|
|
private var currentEpisodeIndex = 0
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
episodePanel = findViewById(R.id.episode_panel)
|
|
|
|
|
episodeList = findViewById(R.id.episode_list)
|
|
|
|
|
loadingIndicator = findViewById(R.id.loading_indicator)
|
|
|
|
|
errorText = findViewById(R.id.error_text)
|
2026-05-24 21:19:34 +08:00
|
|
|
btnClose = findViewById(R.id.btn_close)
|
2026-05-24 21:09:05 +08:00
|
|
|
|
|
|
|
|
val detailUrl = intent.getStringExtra("detail_url") ?: ""
|
|
|
|
|
val title = intent.getStringExtra("title") ?: ""
|
|
|
|
|
|
2026-05-24 21:19:34 +08:00
|
|
|
btnClose.setOnClickListener { finish() }
|
|
|
|
|
|
2026-05-24 21:09:05 +08:00
|
|
|
initExoPlayer()
|
|
|
|
|
loadVideos(detailUrl, title)
|
2026-05-24 21:19:34 +08:00
|
|
|
setupTouchListeners()
|
2026-05-24 21:09:05 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun initExoPlayer() {
|
|
|
|
|
exoPlayer = ExoPlayer.Builder(this).build().also { player ->
|
|
|
|
|
playerView.player = player
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-24 21:19:34 +08:00
|
|
|
private fun loadVideos(detailUrl: String, @Suppress("UNUSED_PARAMETER") title: String) {
|
2026-05-24 21:09:05 +08:00
|
|
|
showLoading(true)
|
|
|
|
|
val config = configRepo.getConfig()
|
|
|
|
|
|
|
|
|
|
lifecycleScope.launch {
|
|
|
|
|
try {
|
|
|
|
|
val videoInfo = videoExtractor.extractVideos(detailUrl, config)
|
|
|
|
|
episodes = videoInfo.episodes
|
|
|
|
|
|
|
|
|
|
if (videoInfo.episodes.isNotEmpty()) {
|
|
|
|
|
buildEpisodeUI(videoInfo.episodes)
|
|
|
|
|
playEpisode(videoInfo.episodes.first())
|
|
|
|
|
} else {
|
|
|
|
|
episodePanel.visibility = View.GONE
|
|
|
|
|
tryPlayDirectly(detailUrl, config)
|
|
|
|
|
}
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
showError("加载视频失败: ${e.message}")
|
|
|
|
|
showLoading(false)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun buildEpisodeUI(eps: List<Episode>) {
|
|
|
|
|
episodeList.removeAllViews()
|
|
|
|
|
episodePanel.visibility = View.VISIBLE
|
|
|
|
|
|
|
|
|
|
eps.forEachIndexed { index, ep ->
|
|
|
|
|
val btn = Button(this).apply {
|
|
|
|
|
text = ep.title
|
|
|
|
|
setBackgroundResource(R.drawable.episode_selector)
|
2026-05-24 21:14:19 +08:00
|
|
|
setTextColor(ContextCompat.getColor(this@PlayerActivity, R.color.text_primary))
|
2026-05-24 21:09:05 +08:00
|
|
|
textSize = 13f
|
|
|
|
|
minWidth = 0
|
|
|
|
|
setPadding(16, 8, 16, 8)
|
|
|
|
|
isFocusable = true
|
|
|
|
|
isFocusableInTouchMode = true
|
|
|
|
|
setOnClickListener {
|
|
|
|
|
currentEpisodeIndex = index
|
|
|
|
|
highlightEpisode(it)
|
|
|
|
|
playEpisode(ep)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
episodeList.addView(btn)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun playEpisode(ep: Episode) {
|
|
|
|
|
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) {
|
|
|
|
|
playWithExoPlayer(directUrl)
|
|
|
|
|
} else if (iframeUrl != null) {
|
|
|
|
|
playWithWebView(iframeUrl)
|
|
|
|
|
} else {
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-24 21:19:34 +08:00
|
|
|
private fun setupTouchListeners() {
|
2026-05-24 21:09:05 +08:00
|
|
|
var panelVisible = true
|
|
|
|
|
playerView.setOnClickListener {
|
|
|
|
|
panelVisible = !panelVisible
|
|
|
|
|
episodePanel.visibility = if (panelVisible) View.VISIBLE else View.GONE
|
|
|
|
|
}
|
2026-05-24 21:19:34 +08:00
|
|
|
playerWebView.setOnClickListener {
|
|
|
|
|
panelVisible = !panelVisible
|
|
|
|
|
episodePanel.visibility = if (panelVisible) View.VISIBLE else View.GONE
|
2026-05-24 21:09:05 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun onPause() {
|
|
|
|
|
super.onPause()
|
|
|
|
|
exoPlayer?.playWhenReady = false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun onDestroy() {
|
|
|
|
|
super.onDestroy()
|
|
|
|
|
exoPlayer?.release()
|
|
|
|
|
exoPlayer = null
|
|
|
|
|
}
|
|
|
|
|
}
|