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

331 lines
11 KiB
Kotlin

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.AppDatabase
import com.videoapp.tv.data.ConfigRepository
import com.videoapp.tv.data.PlayHistory
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 val playHistoryDao by lazy { AppDatabase.getInstance(this).playHistoryDao() }
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
// 用于保存播放历史的信息
private var videoTitle: String = ""
private var videoCategory: String? = null
private var coverUrl: String? = null
private var detailUrl: String = ""
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)
detailUrl = intent.getStringExtra("detail_url") ?: ""
videoTitle = intent.getStringExtra("title") ?: ""
videoCategory = intent.getStringExtra("category")
coverUrl = intent.getStringExtra("cover_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()
// 保存播放历史
savePlayHistory(ep.title)
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 savePlayHistory(episodeName: String?) {
lifecycleScope.launch {
try {
// 检查是否已存在相同的播放记录
val existing = playHistoryDao.findByDetailUrl(detailUrl)
if (existing != null) {
// 更新现有记录的播放时间和剧集
playHistoryDao.updatePlayTime(existing.id, System.currentTimeMillis(), episodeName)
} else {
// 插入新记录
val playHistory = PlayHistory(
title = videoTitle,
episodeName = episodeName,
detailUrl = detailUrl,
coverUrl = coverUrl,
category = videoCategory,
playTime = System.currentTimeMillis()
)
playHistoryDao.insert(playHistory)
}
} catch (e: Exception) {
// 保存播放历史失败不影响播放功能
e.printStackTrace()
}
}
}
private fun tryPlayDirectly(detailUrl: String, config: com.videoapp.tv.data.SiteConfig) {
// 保存播放历史(直接播放时使用标题作为剧集名)
savePlayHistory(videoTitle)
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
}
}