从拉流、叠加到国标多平台分发:SmartMediaKit 多模态融合推流方案设计
前言:这篇文章到底想解决什么问题?
在工业巡检、环境监测、应急通信、移动作业、特种设备监管等场景里,现场设备往往不只是“采集一路视频并推上平台”这么简单。
很多项目真正需要的是一套完整的边缘侧融合系统:
- 既要接入主视频画面,也要接入本地辅助摄像头画面;
- 既要看到实时画面,也要把关键状态信息叠加到画面上;
- 既要通过 GB28181 对接一个或多个平台,也要通过 MQTT 上报结构化数据;
- 既要实时推流,也要本地录像、快照、回放、导出;
- 既要支持报警联动,也要保证推流、存储、UI、数据上报之间互不影响。
所以,这类系统的难点不在于某一个功能点,而在于如何把“视频链路、数据链路、协议链路、本地业务链路”组织成一个稳定可运行的整体。
本文结合 RK3588 平台,以及大牛直播 SDK(SmartMediaKit)中的 SmartRelayDemoV3 工程,梳理一套多模态感知融合推流系统的设计思路。
这里重点不是讲某个硬件接口怎么接,也不是把所有项目需求逐条罗列,而是从 SmartMediaKit 的模块能力出发,说明这套系统应该怎么设计、Demo 中哪些代码已经支撑了核心链路、后续项目中如何扩展到真实行业场景。
整体设计思路:一条主链路,四类扩展能力
这套系统可以理解为一条主链路,加上四类扩展能力。
主链路是:
视频接入 → 解码/取帧 → 图层合成 → 编码输出 → 协议推送
在这条主链路之上,再扩展:
- PIP 画中画能力;
- OSD 状态叠加能力;
- GB28181 多平台并行推送能力;
- 本地录像、快照、回放、导出能力;
- 结构化数据上报与报警联动能力。
SmartRelayDemoV3 的价值就在于,它不是单独演示“播放”或“推流”,而是把播放器、推流器、图层、摄像头叠加、录像、GB28181 fanout 等模块放在一个工程里,形成了一套可继续裁剪和扩展的工程基础。
从工程结构上看,可以抽象成下面几层:
- 感知接入层:主视频源、辅助摄像头、设备状态、传感数据;
- 媒体处理层:拉流、解码、外部视频输入、图层合成、OSD、PIP、编码;
- 协议分发层:GB28181、RTMP、RTSP、MQTT;
- 本地业务层:实时预览、录像、快照、历史回放、文件管理、数据导出;
- 平台应用层:监管平台、业务平台、移动端、运维平台。
SmartMediaKit 在系统中承担的是“媒体核心引擎”的角色,负责把多路画面和业务信息合成为一路统一的视频输出,再通过不同协议送到平台或本地存储;业务层负责传感数据、设备状态、报警逻辑、UI交互和数据上报。
SmartRelayDemoV3 的工程基础
在 SmartRelayDemoV3 中,SmartPlayer.java是整体业务组织的核心 Activity。它同时维护了播放器、推流器、录像、RTSP 服务、摄像头叠加、状态水印、GB28181 多平台等模块。
从代码结构可以看出几个重要设计点。
播放器侧由SmartPlayerJniV2和LibPlayerWrapper封装,负责 RTSP/RTMP 等视频源的播放、拉流、录像、回调和资源释放。
推流侧由SmartPublisherJniV2和LibPublisherWrapper封装,负责编码、图层投递、RTMP 推送、RTSP 服务、GB28181 媒体流等能力。
CameraOverlay 由CameraOverlayHelper负责,把 Camera2 采集到的 YUV_420_888 数据作为图层投递给 publisher,实现 PIP 画中画。
动态水印由LayerPostThread负责,按固定图层编号向 publisher 投递文字、图片、矩形等图层数据。
GB28181 fanout 由SmartPlayer.java中的多平台上下文管理,每个平台有自己的 SIP 参数、注册状态、RTP Sender 和媒体流启动逻辑。
录像文件管理由RecorderManager负责,历史回放由RecorderPlayback负责,形成了本地录像列表、点击播放、删除、返回刷新等基本闭环。
这些代码说明,Demo 已经具备了这类项目最关键的基础骨架:
- 能接入主视频;
- 能二次编码;
- 能叠加图层;
- 能叠加本地摄像头画面;
- 能推送到多个 GB28181 平台;
- 能录像;
- 能管理和回放录像文件;
- 能在 Activity 生命周期中统一释放资源。
在此基础上,项目侧要做的不是从零搭建音视频链路,而是围绕业务数据、UI、报警、存储、平台参数配置做扩展。
主视频链路:从拉流到统一编码输出
系统中的主视频一般来自网络摄像头、设备侧编码器、NVR,或者其他标准视频源。SmartRelayDemoV3 中的主链路可以概括为:
播放器拉流 → 解码输出 → 外部视频帧进入 publisher → 图层合成 → 二次编码输出
如果只是做透明转发,系统可以使用透传模式,直接把编码后的音视频数据送到下游推流模块。这样资源消耗低,延迟更小。
但一旦项目需要叠加 OSD、PIP、报警提示、水印、状态信息,就必须进入二次编码模式。因为透传模式下码流不经过画面重绘,无法把业务信息真正写入视频画面。
这也是 Demo 中很关键的一个判断逻辑:CameraOverlay 在透传模式下不支持叠加,必须切换到二次编码模式。这个设计非常符合工程实际,因为 PIP、OSD、本地状态叠加都属于“画面级处理”,必须在编码前完成。
所以,在多模态感知系统中,推荐把主视频链路设计为:
- 主视频接入;
- 解码为原始画面;
- 进入 SmartMediaKit 图层合成管线;
- 叠加 PIP、状态 OSD、报警信息、固定水印;
- 统一编码输出;
- 同时用于平台推流、本地录像、快照和回放。
这样做的好处是,平台端看到的画面、本地录像保存的画面、后续历史回放看到的画面,都保持一致。
PIP 画中画:辅助视角如何进入编码画面
多模态场景下,单一路主画面往往不够。比如主画面展示远端设备或现场大视角,本地 MIPI/USB 摄像头可以展示操作者视角、设备细节、周边环境或补充画面。
SmartRelayDemoV3 中的CameraOverlayHelper就是为这个场景准备的。
它的职责非常清晰:
- 封装 Camera2 的启动、停止、切换摄像头;
- 监听 Camera2 的 YUV_420_888 图像回调;
- 计算 PIP 窗口位置、大小和旋转角度;
- 通过
PostLayerImageYUV420888ByteBuffer()把摄像头画面投递到指定图层; - 将该图层交给 publisher 参与统一合成编码。
在 Demo 的图层规划里,主视频占用 Layer 0,摄像头 PIP 占用 Layer 1。这样设计很清晰:主视频是底图,PIP 作为第二层叠加在主画面上方,后面的 OSD 和水印再继续叠加。
PIP 的工程细节比表面看起来复杂。因为 Camera2 返回的是 YUV_420_888,不同设备的 rowStride、pixelStride、UV 平面布局可能不同。如果直接按连续内存处理,很容易出现色彩错乱、画面偏移或花屏。
Demo 中的做法是把 Y、U、V 三个 plane 的 ByteBuffer、offset、rowStride、pixelStride 都传给 SDK,由 SDK 侧完成正确处理。这比简单转 NV21 或手动拷贝更适合长期稳定运行。
另外,PIP 还涉及旋转和缩放。摄像头竖屏采集时,rotation=90 或 270 的情况下,叠加区域的宽高语义需要适配,否则会出现子窗口比例不对、方向不对、位置不准的问题。Demo 中通过计算 rotation、effective width/height、overlay width/height,并在必要时对调 scale_width 和 scale_height,保证最终 PIP 子窗口正常显示。
从方案角度看,PIP 模块建议这样设计:
- 主画面固定为 Layer 0;
- 辅助摄像头固定为 Layer 1;
- PIP 默认放在右下角或右侧中部;
- PIP 尺寸按主画面宽高比例计算;
- 摄像头旋转角根据设备方向和 CameraCharacteristics 计算;
- 开启和关闭 PIP 时,要显式 EnableLayer / RemoveLayer,避免图层残留。
这样,PIP 既可以作为 Demo 功能演示,也可以直接扩展到项目中的近端摄像头、MIPI 摄像头、USB 摄像头等场景。
OSD 状态叠加:让视频画面携带业务信息
多模态系统的核心价值之一,是把业务状态直接叠加到视频画面上。
比如:
- 设备电量;
- 网络状态;
- GPS 定位;
- 传感器实时数据;
- 报警状态;
- 机器码;
- 公司名称;
- 时间戳;
- 平台侧需要识别的业务标识。
这些信息如果只显示在本地 UI 上,平台端和录像文件里是看不到的。真正有价值的做法,是通过 SmartMediaKit 的图层机制,把这些信息写入编码输出画面。
Demo 中LayerPostThread的设计就体现了这个思路。它把水印、文字、图片、矩形等内容放到独立线程中刷新,并且规划了清晰的图层编号:
- Layer 0:主视频;
- Layer 1:摄像头 PIP;
- Layer 2~6:动态水印、文字、图片、矩形等扩展图层;
- Layer 7:状态 OSD;
- Layer 8:固定公司/设备标识水印。
这种图层规划非常适合行业项目。因为业务越复杂,越不能把所有文字和图片混在一个图层里。比如传感器数据每秒刷新,电量可能几十秒才变化一次,公司名称基本不变,报警提示只有异常时显示。如果全部放在同一个 Bitmap 里频繁刷新,会浪费资源,也不利于维护。
更合理的方式是:
- 固定水印使用独立图层,初始化后投递一次;
- 状态 OSD 使用独立图层,按秒刷新;
- 报警 OSD 使用独立图层,异常时显示,恢复后移除;
- PIP 使用独立图层,按摄像头帧率刷新;
- 主视频使用独立图层,按主码流帧率刷新。
这样做的好处是:
- 图层互不干扰;
- 刷新频率可控;
- 关闭某个业务能力时,不影响其他图层;
- 平台推流和本地录像画面保持一致;
- 后续增加新的业务信息也更容易扩展。
对于实际项目中的传感器数据,比如四路气体数值,可以由业务层维护一份实时状态对象,然后 OSD 线程定时读取状态对象,生成 Bitmap 或文字图层投递给 SDK。
本地 UI 显示和视频 OSD 不建议互相依赖。正确方式是:它们共享同一份业务数据,但各自负责自己的呈现方式。
多平台 GB28181 并行推送:不是重复编码,而是统一输出后分发
行业项目中很常见的需求是:同一台设备需要同时接入多个国标平台。
如果每个平台都单独拉一路、解一路、编码一路,资源消耗会非常高,而且多个平台看到的画面可能不一致。
SmartRelayDemoV3 的 GB28181 fanout 设计提供了更合理的方式:
- 主视频、PIP、OSD 先统一进入图层合成;
- 合成后的画面只做一次编码;
- 编码输出再分发给多个 GB28181 平台;
- 每个平台维护独立的 SIP 注册、心跳、Invite/ACK、RTP Sender 和媒体流状态。
这样可以形成“一次合成编码,多平台独立分发”的架构。
在 Demo 中,每个平台都有自己的 SIP 参数,包括本地端口、服务器 ID、域、服务器地址、端口、用户名、密码、设备 ID 等。平台之间互相隔离,一个平台异常不应该影响另外的平台。
GB28181 媒体回传的关键流程可以理解为:
- 平台发起 Invite;
- 设备侧解析 SDP;
- 创建 RTP Sender;
- 获取本地 RTP 端口;
- 回应 200 OK;
- 收到 ACK 后启动媒体流;
- 将编码输出送入 GB28181 媒体发送链路。
Demo 中 ACK 后才真正启动媒体流,这是比较规范的处理方式。因为在国标流程中,Invite/200 OK/ACK 是媒体会话建立的关键过程,过早发送媒体可能导致平台侧未准备好接收。
多平台 GB28181 并行推送要重点关注:
- 不同平台本地端口要隔离;
- SIP 注册状态要独立维护;
- 心跳异常要能重连;
- 平台 BYE 后要正确释放 RTP Sender;
- ACK 后再启动媒体流;
- 异常平台不能影响主编码链路;
- 平台参数要配置化,而不是写死在代码里。
SmartMediaKit 支持以统一编码输出为中心,把一路融合后的业务画面同时送到多个国标平台,既节省资源,又保证多平台画面一致。
MQTT 数据通道:结构化数据不要塞进视频协议里
视频画面适合通过 GB28181、RTSP、RTMP 等媒体协议传输,但设备状态、传感器数据、报警事件、定位信息更适合通过 MQTT 这种轻量消息协议上报。
所以,这套系统应该明确区分两条通道:
- 视频通道:负责主画面、PIP、OSD、音视频编码、GB28181/RTSP/RTMP 推送、本地录像;
- 数据通道:负责设备状态、定位、电量、网络状态、传感器数据、报警事件、平台业务消息。
两者在业务上关联,在链路上解耦。
比如四路气体数据,既可以叠加到视频 OSD 上,也可以通过 MQTT 发送 JSON 给平台。视频 OSD 解决“平台值班人员直观看到异常”的问题,MQTT 解决“平台业务系统结构化处理、入库、告警、统计”的问题。
推荐的数据流是:
- 传感器/设备状态 → 业务解析层 → 状态缓存 → OSD 渲染线程;
- 传感器/设备状态 → 业务解析层 → MQTT 消息封装 → 发布队列 → 平台 Broker;
- 报警事件 → 报警状态机 → 本地 UI / OSD / 外设 / MQTT / 录像标记。
这样设计后,视频推流和数据上报互不阻塞。即使 MQTT Broker 临时不可用,也不影响视频继续推;即使视频网络弱,也不影响关键报警数据走独立队列补发。
文章中不一定需要展开具体 JSON 字段,但可以强调结构化数据建议包含:
- 设备 ID;
- 时间戳;
- 定位信息;
- 网络状态;
- 电池状态;
- 多路传感器数值;
- 报警状态;
- 事件编号;
- 平台需要的扩展字段。
本地录像、快照与历史回放:形成现场留存闭环
实时推流解决的是“现在看得到”,录像和回放解决的是“以后查得到”。
在 SmartRelayDemoV3 中,录像、文件管理、历史回放已经形成了一个基础闭环。
RecorderManager负责扫描录像目录,后台加载文件列表,读取文件大小、时长等信息,并显示到 ListView。点击文件后,跳转到RecorderPlayback进行播放。
这个设计虽然是 Demo 级 UI,但工程方向是对的:
- 文件扫描放到后台线程,避免阻塞 UI;
- 文件列表展示名称、大小、时长等关键元信息;
- 点击文件进入回放界面;
- 删除后返回列表刷新;
- 回放界面提供播放、暂停、进度条、删除等基础能力。
- 对于行业项目,可以在这个基础上扩展:
- 按日期筛选录像;
- 按报警事件筛选录像;
- 按设备 ID 筛选录像;
- 视频文件和传感器数据文件按时间戳关联;
- 支持 USB 导出;
- 支持循环覆盖;
- 支持异常断电后的文件修复或索引重建;
- 支持本地 UI 中查看历史数据曲线。
这里要强调一个设计原则:
推流和存储必须解耦。
实时推流是网络链路,录像是本地 IO 链路,二者不能互相阻塞。录像写入速度变慢时,不能影响平台实时预览;平台网络抖动时,也不能导致本地录像丢失。
SmartMediaKit 的优势在于,统一编码输出可以同时服务于推流和录像,避免重复编码。业务层只需要做好文件管理、空间策略、导出逻辑和回放 UI 即可。
UI 信息分层:本地 UI 和编码输出不能混为一谈
很多项目初期容易犯一个错误:以为本地 UI 上显示了电量、定位、传感器数据,平台端推流画面里就应该也有这些信息。
实际上不是。
本地 UI 是 Android View 体系,平台端视频画面是编码输出结果。它们不是同一层东西。
所以系统中要明确区分两类显示:
- 本地交互层:按钮、输入框、文件列表、回放控制、参数配置、调试日志;
- 编码输出层:状态 OSD、业务水印、PIP 画面、报警提示、设备编号、时间戳。
本地 UI 可以用 TextView、ImageView、ListView、TextureView 来实现;编码输出必须通过 SmartMediaKit 的图层投递接口写入视频画面。
比较好的做法是:
- 业务状态统一维护;
- 本地 UI 订阅这份状态并刷新 View;
- OSD 图层线程订阅这份状态并刷新视频图层;
- 报警状态机改变后,同时驱动 UI 提示、视频 OSD、MQTT 上报和本地留存。
这样才能保证:
- 本地看得到;
- 平台看得到;
- 录像里也看得到;
- 后续回放还能查得到。
这就是“多模态感知融合”的关键:不是简单把数据放到旁边显示,而是让视频、状态、事件和留存真正形成统一闭环。
报警联动:从数值超标到事件闭环
在这类系统里,报警不能只理解成“弹一个提示框”。
一个完整的报警事件,至少应该包括:
- 异常条件识别;
- 报警状态切换;
- 本地 UI 提示;
- 视频画面 OSD 报警;
- 本地声光或外设联动;
- MQTT 平台事件上报;
- 录像文件或日志中记录事件;
- 历史回放中可以按事件追溯。
推荐在业务层设计一个报警状态机,而不是让 UI、MQTT、OSD、外设模块各自判断。
比如某一路传感器数值超过阈值后,报警状态机生成一个报警事件,然后统一通知各模块:
- UI 模块显示红色提示;
- OSD 模块在画面中叠加报警文字;
- MQTT 模块上报报警消息;
- 录像模块记录事件时间点;
- 外设模块触发本地声光输出;
- 恢复正常后再生成恢复事件。
- 这样,报警链路才是闭环的。
SmartMediaKit 负责把报警结果可视化地写入视频画面,业务层负责报警规则、状态机、外设控制和平台数据上报。两者边界清晰,系统才好维护。
线程模型与资源释放:长期运行比功能演示更重要
这类系统通常不是运行几分钟就结束,而是要长时间运行。因此,线程模型和资源释放非常关键。
SmartRelayDemoV3 中有几个值得保留的工程思路。
第一,耗时操作尽量放后台线程。
比如 JNI 初始化、RTSP Server 初始化、释放播放/推流资源、GB28181 停止、录像文件扫描等,都不应该阻塞 UI 主线程。
第二,图层刷新独立线程执行。
LayerPostThread按固定周期刷新图层,不直接依赖 UI 主线程。这样动态水印、状态 OSD 不会拖慢界面交互。
第三,CameraOverlay 独立封装生命周期。
摄像头启动、停止、切换都收敛在CameraOverlayHelper中,Activity 只负责调用,不直接处理每一帧细节。
第四,Wrapper 管理 native handle。
LibPlayerWrapper和LibPublisherWrapper对播放、推流、录像、RTSP、GB28181 状态做了封装,释放时先停止运行状态,再关闭 native handle,避免资源泄漏。
第五,onDestroy 中要有完整释放顺序。
Demo 中先停止 GB28181 平台,再停止快照、音频、图层线程、摄像头叠加、状态水印,然后异步释放播放、推流、RTSP Server、publisher、player 等 JNI 资源,并清空 Handler 队列。这类释放顺序对长期运行非常重要。
UI 主线程只做交互和状态展示,媒体处理、协议操作、文件 IO、图层刷新、网络重连都必须后台化。
推荐的项目实施路径
为了降低联调复杂度,项目不要一开始就把所有功能同时打开。建议按下面路径推进。
第一阶段:打通主视频接入。
先验证 RTSP/GB28181 视频源能正常接入、播放、解码,确认画面稳定、延迟可接受。
第二阶段:打通二次编码输出。
在主视频基础上启用 publisher,确认解码后的画面可以进入 SmartMediaKit 的图层合成和编码输出链路。
第三阶段:加入 OSD 状态叠加。
先从固定文字、时间戳、水印做起,再扩展到电量、网络、定位、传感器数据、报警状态。
第四阶段:加入 PIP 画中画。
接入本地摄像头,确认 Camera2 采集、YUV 投递、旋转、缩放、图层叠加、关闭释放都正常。
第五阶段:加入多平台 GB28181 fanout。
先接一个平台,再扩展到两个、三个平台。重点测试注册、心跳、Invite、ACK、BYE、断线重连、平台异常隔离。
第六阶段:加入 MQTT 数据通道。
把设备状态、定位、传感器数据、报警事件通过 MQTT 上报,和视频链路解耦。
第七阶段:完善录像、快照、回放、导出。
把实时推流之外的本地留存能力补齐,形成现场数据可追溯闭环。
第八阶段:加入报警联动。
最后再整合阈值配置、报警状态机、UI提示、OSD报警、MQTT上报、外设联动、日志留存。
这个顺序的好处是,每一步都有明确验证目标,出问题也容易定位:到底是视频源问题、解码问题、图层问题、编码问题、协议问题、网络问题、存储问题,还是业务状态同步问题。
小结
基于 RK3588 与大牛直播 SDK(SmartMediaKit)的多模态感知融合推流系统,本质上不是一个简单的“拉流转推”项目,而是一套边缘侧音视频与业务数据融合系统。
SmartRelayDemoV3 已经把核心工程链路搭出来了:
- 主视频拉流与播放;
- 二次编码输出;
- Layer 图层合成;
- PIP 摄像头叠加;
- 动态 OSD 与水印;
- 多平台 GB28181 fanout;
- 本地录像与快照;
- 录像文件管理与历史回放;
- 生命周期和资源释放管理。
在真实项目中,业务侧只需要在这个基础上继续扩展传感器数据接入、MQTT 上报、报警状态机、UI配置、外设控制、文件导出和平台参数管理,就可以形成一套完整的行业化方案。
这套方案的核心价值可以概括为五点:
- 视频与数据融合;
- 本地画面、平台画面、录像画面一致;
- 一次合成编码,多平台分发;
- 推流、存储、数据上报互相解耦;
- Demo 工程具备可复用、可裁剪、可扩展的落地基础。
对于工业巡检、环境监测、应急通信、移动作业、特种设备监管等场景,这种以 SmartMediaKit 为核心的模块化架构,既能降低音视频链路开发难度,也能让项目更快从 Demo 验证走向工程交付。
📎 CSDN官方博客:音视频牛哥-CSDN博客