19、视频布局:网格布局、演讲者模式、画中画(PiP)模式、自定义布局引擎
视频布局这件事,说实话,是视频会议APP里最容易被低估的模块。很多人觉得不就是把几个视频流往屏幕上一摆嘛,有什么难的?我刚开始做的时候也这么想,结果被产品经理追着改了五版——嗯,从那以后我再也不敢小看布局了。
今天咱们就把这块彻底聊透。网格布局、演讲者模式、画中画,还有自定义布局引擎,一个一个来。
19.1 网格布局:最基础的“公平模式”
网格布局,说白了就是所有参会者平分屏幕。每个人占一个格子,大小一样,谁也不特殊。这种模式适合人数不多的会议,比如4到6个人。
实现上其实不复杂,核心就是计算行列数。我习惯用 GridLayoutManager 配合 RecyclerView 来做,但要注意一点:视频流是动态的,有人进有人出,布局要实时刷新。
// 计算网格行列数
fun calculateGridSpan(count: Int): Pair<Int, Int> {
return when {
count <= 1 -> Pair(1, 1)
count <= 4 -> Pair(2, 2)
count <= 6 -> Pair(3, 2)
count <= 9 -> Pair(3, 3)
else -> Pair(4, 3) // 超过9人,用4列
}
}
这里有个坑:视频渲染的SurfaceView不能复用。我在项目中遇到过,RecyclerView复用item时,SurfaceView的绑定会乱掉,画面串流。解决方案是给每个item绑定唯一的渲染器,或者用TextureView替代。
19.2 演讲者模式:谁说话谁C位
演讲者模式,就是当前发言的人占大屏,其他人缩成小窗排在旁边。这个模式在WebRTC里实现起来有个关键点:音频检测。
WebRTC提供了 RTCAudioLevelDelegate 回调,可以拿到每个音频轨道的音量。我一般会做一个音量阈值判断,超过阈值的就认为是“活跃发言者”。
// 音频检测回调
audioTrack.setVolume(0) // 本地不播放
audioTrack.setAudioLevelDelegate { level ->
if (level > THRESHOLD) {
// 标记为活跃发言者
activeSpeakerDetector.onAudioLevel(peerId, level)
}
}
布局上,大屏占屏幕的70%,小窗排成一行或一列。小窗的宽高比建议固定为16:9,不然画面拉伸很难看。
你想想看,如果两个人同时说话怎么办?嗯,这里我一般用“最近活跃”策略——谁最后说话,谁占大屏。也可以加个手动切换按钮,让用户自己选。
19.3 画中画(PiP)模式:Android原生的优势
画中画模式,就是用户切到后台时,视频会议窗口缩成一个小浮窗。Android从8.0开始支持PiP,WebRTC天然兼容。
实现起来其实不复杂,核心就三步:
- 在Manifest里声明Activity支持PiP
- 调用
enterPictureInPictureMode() - 在
onPictureInPictureModeChanged里处理UI切换
// 进入PiP模式
val params = PictureInPictureParams.Builder()
.setAspectRatio(Rational(16, 9))
.build()
enterPictureInPictureMode(params)
但有个细节很多人会忽略:PiP模式下要关闭摄像头预览。为什么?因为PiP窗口很小,开摄像头纯属浪费性能。我一般会在进入PiP时停止本地视频采集,只保留音频。
19.4 自定义布局引擎:让产品经理随便改
网格、演讲者、PiP,这三种是标配。但产品经理总会有新想法——比如“能不能让主持人固定在大屏,其他人自动排列?”或者“能不能拖拽调整位置?”
这时候就需要一个自定义布局引擎了。我设计的方案是这样的:
- 定义一套布局规则(JSON格式)
- 解析规则后生成布局参数
- 渲染器根据参数调整位置和大小
// 布局规则示例
{
"mode": "custom",
"zones": [
{"id": "main", "x": 0, "y": 0, "width": 0.7, "height": 1.0},
{"id": "sidebar", "x": 0.7, "y": 0, "width": 0.3, "height": 1.0}
],
"rules": {
"main": {"type": "fixed", "peer": "host"},
"sidebar": {"type": "auto", "direction": "vertical"}
}
}
解析引擎拿到这个JSON后,动态计算每个视频流的位置。这样做的好处是,产品经理改布局只需要改JSON,不用动代码。
我个人习惯把布局引擎做成一个独立的模块,用接口隔离。这样换UI框架(比如从SurfaceView换到TextureView)时,布局逻辑完全不受影响。
19.5 知识体系总览
下面这张图是我自己整理的视频布局知识体系,你可以对照着看,心里有个谱。
19.6 性能优化:别让布局拖垮帧率
视频会议最怕卡顿。布局切换时如果处理不好,画面会掉帧。我总结了几条经验:
- 减少布局嵌套:能用RelativeLayout就别用LinearLayout套LinearLayout
- 预计算位置:布局变化时,先算好所有位置,再一次性更新,别一个一个改
- 复用渲染器:视频流的SurfaceView或TextureView尽量复用,别频繁创建销毁
我记得有一次,演讲者模式切换时,因为布局计算和视频渲染在同一个线程,导致画面卡了半秒。后来把布局计算放到子线程,渲染只负责应用结果,问题就解决了。
Choreographer 监听帧率,如果掉帧超过阈值,自动降低布局刷新频率。用户体验比“实时但卡顿”好得多。
好了,视频布局这块就聊到这儿。四种模式各有适用场景,核心是理解布局引擎的设计思路——规则与渲染分离。这样不管产品经理怎么改,你都能从容应对。
公众号:蓝海资料掘金营,微信deep3321