ExoPlayer 切换视频,缓存视频,预加载
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.FrameLayout
import androidx.annotation.OptIn
import androidx.media3.common.MediaItem
import androidx.media3.common.PlaybackException
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.ProgressiveMediaSource
import androidx.media3.ui.PlayerView
class DualExoPlayer @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
private val playerViewA = PlayerView(context).apply {
useController = false
}
private val playerViewB = PlayerView(context).apply {
useController = false
}
private val players = arrayOf(
ExoPlayer.Builder(context).build(),
ExoPlayer.Builder(context).build()
)
private var currentPlayerIndex = 0 // 0 = A, 1 = B
init {
addView(playerViewA)
addView(playerViewB)
playerViewB.visibility = View.GONE
}
@OptIn(UnstableApi::class)
fun playVideo(url: String) {
val nextPlayerIndex = (currentPlayerIndex + 1) % 2
val nextPlayer = players[nextPlayerIndex]
preparePlayer(nextPlayer, url)
nextPlayer.repeatMode = ExoPlayer.REPEAT_MODE_ONE
// 添加监听器,等待准备完成
nextPlayer.addListener(object : Player.Listener {
private var isReady = false
override fun onPlaybackStateChanged(playbackState: Int) {
when (playbackState) {
ExoPlayer.STATE_READY -> {
if (!isReady) {
isReady = true
// 切换 UI 显示
switchToPlayer(nextPlayerIndex)
nextPlayer.play()
}
}
ExoPlayer.STATE_ENDED -> {
// 视频结束
}
else -> Unit
}
}
override fun onPlayerError(error: PlaybackException) {
// 错误处理
error.printStackTrace()
}
})
}
@OptIn(UnstableApi::class)
private fun preparePlayer(player: ExoPlayer, url: String) {
val dataSourceFactory = VideoCacheManager.getCacheDataSourceFactory(context)
val mediaItem = MediaItem.fromUri(url)
val source = ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(mediaItem)
player.setMediaSource(source, 0)
player.prepare()
player.playWhenReady = false // 不自动播放
}
private fun switchToPlayer(targetIndex: Int) {
if (targetIndex == currentPlayerIndex) return
val currentView = if (currentPlayerIndex == 0) playerViewA else playerViewB
val targetView = if (targetIndex == 0) playerViewA else playerViewB
targetView.player = players[targetIndex]
targetView.visibility = VISIBLE
targetView.animate()
.alpha(1f)
.setDuration(600)
.withEndAction {
currentView.visibility = GONE
currentView.player?.pause()
currentView.player = null
}
.start()
currentPlayerIndex = targetIndex
}
@OptIn(UnstableApi::class)
fun preloadNextVideo(url: String) {
val nextPlayerIndex = (currentPlayerIndex + 1) % 2
val nextPlayer = players[nextPlayerIndex]
preparePlayer(nextPlayer, url)
}
fun resume() {
players.forEach {
it.playWhenReady = true
}
}
fun pause() {
players.forEach {
it.pause()
}
}
fun stop() {
players.forEach {
it.stop()
}
}
fun release() {
players.forEach {
it.release()
}
}
}
import android.content.Context
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.*
import androidx.media3.datasource.cache.Cache
import androidx.media3.datasource.cache.CacheDataSource
import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor
import androidx.media3.datasource.cache.SimpleCache
import androidx.media3.datasource.cache.SimpleCache.delete
import java.io.File
@UnstableApi
object VideoCacheManager {
private var cache: Cache? = null
fun initialize(context: Context) {
val cacheDir = File(context.cacheDir, "video_cache")
cache = SimpleCache(cacheDir, LeastRecentlyUsedCacheEvictor(1024 * 1024 * 1024))
}
fun getCacheDataSourceFactory(context: Context): CacheDataSource.Factory {
// 确保 cache 不为空后再使用
val currentCache = checkNotNull(cache) { "Cache not initialized" }
return CacheDataSource.Factory()
.setCache(currentCache) // Cache 类型匹配
.setUpstreamDataSourceFactory(DefaultDataSource.Factory(context))
}
fun clearCache() {
cache?.apply {
release()
}
cache = null
}
}
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.sound.candy.riddle.box.stmh.view.DualExoPlayer
android:id="@+id/playView"
app:use_controller="false"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<TextView
android:id="@+id/tv_down"
android:layout_width="@dimen/dp_200"
android:layout_height="@dimen/dp_50"
android:layout_marginBottom="@dimen/dp_100"
android:background="@drawable/shape_black_30"
android:gravity="center"
android:text="下一首"
android:textColor="@color/white"
android:textSize="@dimen/sp_20"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import com.sound.candy.riddle.box.stmh.databinding.FragmentWhiteVideoBinding
import com.sound.candy.riddle.box.stmh.utils.setDebounceClickListener
import com.sound.candy.riddle.box.stmh.view.VideoCacheManager
class WhiteVideoFragment : WhiteBaseFragment<FragmentWhiteVideoBinding>() {
override fun provideViewBinding(
inflater: LayoutInflater, container: ViewGroup?
): FragmentWhiteVideoBinding {
return FragmentWhiteVideoBinding.inflate(inflater, container, false)
}
override fun onKeyCodeBack(): Boolean {
return true
}
var list = mutableListOf<String>()
var index = 0
@OptIn(UnstableApi::class)
override fun initUI() {
binding.tvDown.setDebounceClickListener {
playNext()
}
}
@OptIn(UnstableApi::class)
override fun initData() {
list.add("https://project.xiability.cn/bigger_and_stronger/create_brilliance_again/v4/476466.mp4")
list.add("https://project.xiability.cn/bigger_and_stronger/create_brilliance_again/v4/608007.mp4")
list.add("https://project.xiability.cn/bigger_and_stronger/create_brilliance_again/v4/851687.mp4")
binding.playView.playVideo(list[index])
preloadVideo()
}
fun playNext() {
index++
if (index > list.lastIndex) {
index = 0
}
binding.playView.playVideo(list[index])
preloadVideo()
}
fun preloadVideo(){
var nextIndex = index + 1
if (nextIndex > list.lastIndex) {
nextIndex = 0
}
binding.playView.preloadNextVideo(list[nextIndex])
}
override fun onResume() {
super.onResume()
binding.playView.resume()
}
override fun onPause() {
super.onPause()
binding.playView.pause()
}
override fun onStop() {
super.onStop()
binding.playView.stop()
}
override fun onDestroy() {
super.onDestroy()
binding.playView.release()
}
}