Android单ExoPlayer实例多视图播放同一视频方案咨询
基于ExoPlayer单实例实现多视图同步播放方案
核心思路
利用SurfaceTexture的纹理共享特性:让单个ExoPlayer实例将视频帧渲染到一个全局SurfaceTexture,多个播放视图各自绑定这个SurfaceTexture,通过监听帧更新事件触发所有视图重绘,实现单实例多视图播放。轻微不同步来自视图各自的重绘时机,符合需求。
基于Android Views的实现
1. 全局SurfaceTexture初始化
创建全局的SurfaceTexture并设置帧可用监听,确保新帧到来时通知所有视图更新:
private lateinit var sharedSurfaceTexture: SurfaceTexture private val videoViews = mutableListOf<SharedTextureView>() // 在Activity/Fragment初始化时调用 private fun initSharedTexture() { sharedSurfaceTexture = SurfaceTexture(0).apply { setOnFrameAvailableListener { st -> st.updateTexImage() // 更新纹理到最新帧 videoViews.forEach { it.invalidate() } // 触发所有视图重绘 } } }
2. 自定义共享纹理的TextureView
重写TextureView,替换默认的SurfaceTexture为全局共享实例:
class SharedTextureView(context: Context, attrs: AttributeSet? = null) : TextureView(context, attrs) { lateinit var globalSurfaceTexture: SurfaceTexture override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) { // 释放默认创建的SurfaceTexture,绑定全局共享实例 surface.release() setSurfaceTexture(globalSurfaceTexture) } }
3. 关联ExoPlayer与SurfaceTexture
创建Surface从全局SurfaceTexture,并绑定到ExoPlayer:
private fun initPlayer(videoUrl: String) { val player = ExoPlayer.Builder(context).build() val videoSurface = Surface(sharedSurfaceTexture) player.setVideoSurface(videoSurface) // 设置媒体源并启动播放 player.setMediaItem(MediaItem.fromUri(videoUrl)) player.prepare() player.play() // 记得在销毁时释放资源 lifecycle.addObserver(object : DefaultLifecycleObserver { override fun onDestroy(owner: LifecycleOwner) { player.release() sharedSurfaceTexture.release() } }) }
4. 绑定视图到全局纹理
在布局中添加四个SharedTextureView,并将它们加入视图列表、绑定全局纹理:
// 假设布局中有四个sharedTextureView1-4 videoViews.addAll(listOf(sharedTextureView1, sharedTextureView2, sharedTextureView3, sharedTextureView4)) videoViews.forEach { it.globalSurfaceTexture = sharedSurfaceTexture }
基于Jetpack Compose的实现
1. 全局状态与ExoPlayer初始化
使用remember持有全局SurfaceTexture和ExoPlayer实例,通过状态变量触发视图重绘:
@Composable fun MultiVideoPlayerScreen(videoUrl: String) { // 全局共享的SurfaceTexture val sharedSurfaceTexture = remember { SurfaceTexture(0).apply { setOnFrameAvailableListener { st -> st.updateTexImage() // 更新状态变量触发所有视图重组重绘 (LocalContext.current as? Activity)?.runOnUiThread { currentFrameTimestamp = st.timestamp } } } } var currentFrameTimestamp by remember { mutableStateOf(0L) } // 初始化单实例ExoPlayer val player = remember { ExoPlayer.Builder(LocalContext.current).build().apply { setVideoSurface(Surface(sharedSurfaceTexture)) setMediaItem(MediaItem.fromUri(videoUrl)) prepare() play() } } // 自动释放资源 DisposableEffect(Unit) { onDispose { player.release() sharedSurfaceTexture.release() } } // 布局四个播放视图 Column(modifier = Modifier.fillMaxSize()) { Row(modifier = Modifier.weight(1f)) { SharedVideoView(sharedSurfaceTexture, currentFrameTimestamp, Modifier.weight(1f)) SharedVideoView(sharedSurfaceTexture, currentFrameTimestamp, Modifier.weight(1f)) } Row(modifier = Modifier.weight(1f)) { SharedVideoView(sharedSurfaceTexture, currentFrameTimestamp, Modifier.weight(1f)) SharedVideoView(sharedSurfaceTexture, currentFrameTimestamp, Modifier.weight(1f)) } } }
2. 共享纹理的Compose视图
用AndroidView包裹TextureView,绑定全局SurfaceTexture:
@Composable fun SharedVideoView( surfaceTexture: SurfaceTexture, triggerRedraw: Long, modifier: Modifier = Modifier ) { AndroidView( factory = { context -> TextureView(context).apply { setSurfaceTexture(surfaceTexture) } }, modifier = modifier, update = { view -> // 每次帧时间戳变化时触发重绘 view.invalidate() } ) }
关键注意事项
- 所有视图共享同一个
SurfaceTexture,无需额外同步逻辑,轻微帧差属于正常现象。 - 必须确保
SurfaceTexture和ExoPlayer的生命周期一致,在页面销毁时及时释放,避免内存泄漏。 - 如果需要适配视频比例,可在
TextureView的onMeasure方法中自定义缩放逻辑,或在Compose中通过modifier调整。
内容的提问来源于stack exchange,提问作者Ziad Halabi
相关产品推荐
相关产品推荐

