从拉流、叠加到国标多平台分发: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 多平台等模块。

从代码结构可以看出几个重要设计点。

播放器侧由SmartPlayerJniV2LibPlayerWrapper封装,负责 RTSP/RTMP 等视频源的播放、拉流、录像、回调和资源释放。

推流侧由SmartPublisherJniV2LibPublisherWrapper封装,负责编码、图层投递、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 媒体回传的关键流程可以理解为:

  1. 平台发起 Invite;
  2. 设备侧解析 SDP;
  3. 创建 RTP Sender;
  4. 获取本地 RTP 端口;
  5. 回应 200 OK;
  6. 收到 ACK 后启动媒体流;
  7. 将编码输出送入 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。

LibPlayerWrapperLibPublisherWrapper对播放、推流、录像、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博客