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替代。

避坑指南:我曾经在网格布局里直接用RecyclerView的item复用机制,结果画面错乱,调试了一整天。后来改用SparseArray维护每个视频流的独立渲染器,问题解决。

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天然兼容。

实现起来其实不复杂,核心就三步:

  1. 在Manifest里声明Activity支持PiP
  2. 调用 enterPictureInPictureMode()
  3. onPictureInPictureModeChanged 里处理UI切换
// 进入PiP模式
val params = PictureInPictureParams.Builder()
    .setAspectRatio(Rational(16, 9))
    .build()
enterPictureInPictureMode(params)

但有个细节很多人会忽略:PiP模式下要关闭摄像头预览。为什么?因为PiP窗口很小,开摄像头纯属浪费性能。我一般会在进入PiP时停止本地视频采集,只保留音频。

小技巧:PiP模式下可以加个“挂断”按钮,用户不用切回APP就能结束会议。用RemoteAction实现,代码量不大。

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 知识体系总览

下面这张图是我自己整理的视频布局知识体系,你可以对照着看,心里有个谱。

视频布局知识体系 网格布局 演讲者模式 画中画模式 自定义引擎 行列计算 SurfaceView复用 音频检测 大屏+小窗 PiP生命周期 RemoteAction JSON规则 位置计算 核心原则:解耦布局逻辑与渲染实现

19.6 性能优化:别让布局拖垮帧率

视频会议最怕卡顿。布局切换时如果处理不好,画面会掉帧。我总结了几条经验:

  • 减少布局嵌套:能用RelativeLayout就别用LinearLayout套LinearLayout
  • 预计算位置:布局变化时,先算好所有位置,再一次性更新,别一个一个改
  • 复用渲染器:视频流的SurfaceView或TextureView尽量复用,别频繁创建销毁

我记得有一次,演讲者模式切换时,因为布局计算和视频渲染在同一个线程,导致画面卡了半秒。后来把布局计算放到子线程,渲染只负责应用结果,问题就解决了。

建议:Choreographer 监听帧率,如果掉帧超过阈值,自动降低布局刷新频率。用户体验比“实时但卡顿”好得多。

好了,视频布局这块就聊到这儿。四种模式各有适用场景,核心是理解布局引擎的设计思路——规则与渲染分离。这样不管产品经理怎么改,你都能从容应对。


公众号:蓝海资料掘金营,微信deep3321