从拉流、叠加到国标多平台分发:SmartMediaKit 多模态融合推流方案设计

0 阅读21分钟

前言:这篇文章到底想解决什么问题?

在工业巡检、环境监测、应急通信、移动作业、特种设备监管等场景里,现场设备往往不只是“采集一路视频并推上平台”这么简单。

很多项目真正需要的是一套完整的边缘侧融合系统:

  • 既要接入主视频画面,也要接入本地辅助摄像头画面;
  • 既要看到实时画面,也要把关键状态信息叠加到画面上;
  • 既要通过 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 验证走向工程交付。