Skip to content

创建一个 compose 组件,并引入 AndroidView

kotlin

@OptIn(UnstableApi::class)
@Composable
fun VideoView(player: Player, onDispose: (Player) -> Unit) {
    val context = LocalContext.current
    AndroidView(factory = {
        PlayerView(context).apply {
            useController = true
            controllerShowTimeoutMs = 1000
            setBackgroundColor(Color.BLACK)
            setShowBuffering(SHOW_BUFFERING_ALWAYS)
        }
    }, modifier = Modifier.fillMaxSize(),

        update = {
            it.player = player
        }, onRelease = {
            it.player = null
        })

    //compose 帮助你去释放实例
    DisposableEffect(player) {
        onDispose {
            onDispose(player)
        }
    }
}

在另外一个 compose 组件中引用,

我习惯于在 viewmodel 中去添加和获取数据源,因为可以更好的结合 hilt

kotlin

 https://juejin.cn/post/7124893188812668964#heading-7 感谢这篇文章
 https://github.com/llwdslal/WanAndroid/tree/07-banner%E5%AE%9E%E7%8E%B0
 
 增加isAutoLoop的判断,撇去pagerState.isScrollInProgress
var isAutoLoop by remember { mutableStateOf(autoLoop) }

.pointerInput(items) {
                detectTapGestures(
                    onPress = {
                        isAutoLoop = false
                        val pressStartTime = System.currentTimeMillis()
                        //只监听 release
                        if (tryAwaitRelease()) {
                            isAutoLoop = true
                            val pressDuration = System.currentTimeMillis() - pressStartTime
                            //长按后 release 不触发 onClick 事件
                            if (pressDuration < viewConfiguration.longPressTimeoutMillis) { //400 ms
                                onItemClick?.invoke(items[pagerState.currentPage])
                            }
                        }
                    },
                )
            }

作者:给大佬们点赞
链接:https://juejin.cn/post/7124893188812668964
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

@Composable
fun ComposeView(videoViewModel: VideoViewModel = viewModel()){

 val exoPlayer = bannerViewModel.getMultipleExoPlayer(banner.data, context, )

 VideoView(exoPlayer) {
  videoViewModel.clearPlayer(it)
  }

}

添加数据源和缓存,这里我使用了 hilt 的单例模式

注意点:生命周期范围应在 viewmodel 中

kotlin

@OptIn(UnstableApi::class)
@InstallIn(ViewModelComponent::class)
@Module
class VideoModule {
    @ViewModelScoped
    @Provides
    fun provideSimpleCache(@ApplicationContext context: Context): SimpleCache {
        return VideoCache.getInstance(context)
    }

    @ViewModelScoped
    @Provides
    fun provideProgressiveMediaSource(
        @ApplicationContext context: Context, simpleCache: SimpleCache
    ): ProgressiveMediaSource.Factory {
        val httpDataSource = DefaultHttpDataSource.Factory().setAllowCrossProtocolRedirects(true)
        val upstreamFactory = DefaultDataSource.Factory(context, httpDataSource)
        val cacheDataSourceFactory = CacheDataSource.Factory().setCache(simpleCache)
            .setUpstreamDataSourceFactory(upstreamFactory).setCacheWriteDataSinkFactory(null)
            .setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR)

        return ProgressiveMediaSource.Factory(cacheDataSourceFactory)
    }
}

viewmodel

kotlin

@OptIn(UnstableApi::class)
@HiltViewModel
class videoViewModel @Inject constructor(
    private val progressiveMediaSource: ProgressiveMediaSource.Factory
) : ViewModel() {
    private val players = hashMapOf<String, Player?>()
    private var playerListener: Player.Listener? = null

    private val _isPlayingEnd = MutableStateFlow(false)
    val isPlayingEnd = _isPlayingEnd.asStateFlow()
    fun setPlayingEnd(isEnd: Boolean) = viewModelScope.launch {
        _isPlayingEnd.emit(isEnd)
    }

    private val _isPlayingError = MutableStateFlow(false)
    val isPlayingError = _isPlayingError.asStateFlow()
    fun setPlayingError(isError: Boolean) = viewModelScope.launch {
        _isPlayingError.emit(isError)
    }

    // 去Mtime时光网借几个测试视频
    private val videoList = listOf(
        VideoBean(
            "https://vfx.mtime.cn/Video/2019/01/15/mp4/190115161611510728_480.mp4"
        ),
        VideoBean(
            "https://vfx.mtime.cn/Video/2024/03/29/mp4/240329104949956164.mp4"
        ),

        VideoBean(
            "https://vfx.mtime.cn/Video/2024/03/04/mp4/240304102705890107.mp4"
        ),

        VideoBean(
            "https://vfx.mtime.cn/Video/2024/03/28/mp4/240328135334006153.mp4"
        ),
        VideoBean(
            "https://vfx.mtime.cn/Video/2024/03/01/mp4/240301143025556186.mp4"
        ),

        )

    //数据源,可以从接口去获取,我这里直接写在本地了
    fun getVideoList(): List<VideoBean> {
        return videoList
    }

    //exoplayer实例 在这里感谢 https://mp.weixin.qq.com/s/Z5ocXx6kZHN6wd4CNrDZ2w,利用池的概念
    fun getMultipleExoPlayer(url: String, context: Context,isPlayWhenReady:Boolean): Player {
        if (playerListener == null) {
            playerListener = object : Player.Listener {
                override fun onPlayerError(error: PlaybackException) {
                    super.onPlayerError(error)
                    setPlayingError(true)
                }

                override fun onPlaybackStateChanged(playbackState: Int) {
                    super.onPlaybackStateChanged(playbackState)
                    if (playbackState == Player.STATE_ENDED) {
                        setPlayingEnd(true)
                    }
                }

            }
        }

        return players[url] ?: createExoPlayer(url, context,isPlayWhenReady).also {
            players[url] = it
        }
    }

    private fun createExoPlayer(url: String, context: Context,isPlayWhenReady:Boolean): ExoPlayer {
         val trackSelectionFactory = AdaptiveTrackSelection.Factory()
        val trackSelector = DefaultTrackSelector(context, trackSelectionFactory)
        val mediaSource = progressiveMediaSource.createMediaSource(MediaItem.fromUri(url))
        val MIN_BUFFER_MS = 1_000
        val MAX_BUFFER_MS = 50_000
        val BUFFER_FOR_PLAYBACK_MS = 500
        val BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS = 1_000
        val loadControl = DefaultLoadControl.Builder().setBufferDurationsMs(
            DefaultLoadControl.DEFAULT_MIN_BUFFER_MS, // 最小预加载时间
            DefaultLoadControl.DEFAULT_MAX_BUFFER_MS, // 最大预加载时间
            DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS, // 播放时的缓冲时间
            DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS // 重新缓冲后的播放缓冲时间
        )

            .setBufferDurationsMs(
                MIN_BUFFER_MS,
                MAX_BUFFER_MS,
                BUFFER_FOR_PLAYBACK_MS,
                BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS

            ).build()

        return ExoPlayer.Builder(context).setTrackSelector(trackSelector)
            .setLoadControl(loadControl).build().apply {
                addListener(playerListener!!)
                setMediaSource(mediaSource)
                repeatMode = if (bannerList.size<=1) REPEAT_MODE_ONE else REPEAT_MODE_OFF
                prepare()
                playWhenReady = isPlayWhenReady
            }
    }

    fun clearPlayer(player: Player){
         if (playerListener != null) {
            player.removeListener(playerListener!!)
        }
        player.release()
        players.remove(player.currentMediaItem?.localConfiguration?.uri.toString())
    }

    override fun onCleared() {
        super.onCleared()
        players.forEach {
            if (playerListener != null) {
                it.value?.removeListener(playerListener!!)
            }
            it.value?.release()
        }
        players.clear()
    }

}

缓存一定要用单例模式

kotlin

@UnstableApi
class VideoCache {
    companion object {
        @Volatile
        private var instance: SimpleCache? = null
        fun getInstance(context: Context): SimpleCache {
            val databaseProvider: DatabaseProvider = StandaloneDatabaseProvider(context)
            return instance ?: synchronized(this) {
                instance ?: SimpleCache(
                    File(context.cacheDir, "media"),
                    LeastRecentlyUsedCacheEvictor((100 * 1024 * 1024).toLong()),
                    databaseProvider
                ).also { instance = it }
            }
        }
    }
}

总结

经结合 HorizontalPager 测试,exoplayer 占用内存大小不到 90m(根据视频大小),每次切换都会释放 exoplayer 实例。

还需要补充的点

  • 如果想仿抖音那种,还需要,完善动画效果、无缝播放效果、实例缓存等等
kotlin
enum class ExoPlayerPool {
    INSTANCE;

    companion object {
        private const val MIN_POOL_SIZE = 2
        private const val MAX_POOL_SIZE = 4
    }

    private val playersPool: Deque<PlayerWrapper> = LinkedList()
    private val lock = ReentrantLock()

    fun get(context: Context): ExoPlayer {
        lock.withLock {
            val playerWrapper = playersPool.pollFirst()
            return playerWrapper?.player ?: createNewPlayer(context)
        }
    }

    private fun createNewPlayer(context: Context): ExoPlayer {
        return ExoPlayer.Builder(context).build()
    }

    // 释放播放器
    fun release(context: Context, player: ExoPlayer) {
        lock.withLock {
            playersPool.offerFirst(PlayerWrapper(player.apply {
                stop()
                clearMediaItems()
            }))
            trimToSize(getMaxPoolSize(context))
        }
    }

    // 缓存策略
    private fun trimToSize(maxSize: Int) {
        lock.withLock {
            Log.e("", "ExoPlayerImpl playersPool.size--->${playersPool.size}")
            while (playersPool.size > maxSize) {
                val playerWrapper = playersPool.pollLast()
                playerWrapper?.player?.release()
            }
        }
    }

    // 清空池
    fun clear() {
        lock.withLock {
            while (playersPool.isNotEmpty()) {
                val playerWrapper = playersPool.pollFirst()
                playerWrapper?.player?.release()
            }
        }
    }

    private fun getMaxPoolSize(context: Context): Int {
        val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
        val memoryInfo = ActivityManager.MemoryInfo()
        activityManager.getMemoryInfo(memoryInfo)
        val availableMemory = memoryInfo.availMem / 1024 / 1024 // 转换为MB

        Log.e("", "ExoPlayerImpl availableMemory--->${availableMemory}")

        // 根据可用内存设定播放器个数,这里是一个简单的例子
        // 可能需要根据具体情况调整内存占用限制和比例
        return when {
            availableMemory > 2000 -> 4 // 如果可用内存大于2GB,允许最多4个播放器实例
            availableMemory > 1000 -> 3 // 如果可用内存大于1GB,允许最多3个播放器实例
            else -> 2 // 否则,允许最多2个播放器实例
        }.coerceIn(MIN_POOL_SIZE, MAX_POOL_SIZE) // 确保值在最小和最大值范围内
    }

    private data class PlayerWrapper(
        val player: ExoPlayer
    )

}

欢迎