上一篇算过 Gemini 338Le 的多流带宽。落实到程序里,还要回答几个问题:设备列出了某个档位,两路能否同时启动?SDK 回调返回以后,写盘线程还能不能访问这一帧?停止采集时,队列里剩下的数据怎样处理?
ObCapture 把这些工作组织成一个 C++20 采集库,再用命令行程序完成设备枚举、组合探测、录制和诊断。这篇沿着一帧数据从相机到磁盘的路径,说明当前实现怎样分配职责、管理内存和报告失败。格式与带宽的基础说明可以结合系列第二篇阅读。Gemini 338Le 的多流带宽怎么算
用一套帧接口连接采集与输出
公共接口放在 include/obcapture/,核心库处理会话、分发、存储和诊断,Orbbec 后端负责设备发现与 SDK 调用。CMake 分别生成 obcapture_core 和 obcapture_orbbec,命令行入口再组合两者。关闭 OBCAPTURE_WITH_ORBBEC 后,核心库可以单独构建。
| 组件 | 当前承担的工作 |
|---|---|
OrbbecSource | 发现 Gemini 338Le,选择设备,启动视频与 IMU 管线,将 SDK 帧转换为公共帧对象 |
ProfileSelector / OrbbecProfileNegotiator | 筛选设备支持的档位,生成候选组合,实际开流探测 |
CaptureSession | 组织启动与停止,统计接收情况,缓存各通道最新帧,把帧交给分发器 |
FrameDispatcher | 为每个输出端维护队列和工作线程,记录写入、丢弃与错误 |
FileSink | 保存图像、RAW、IMU CSV 和逐帧索引,检查写入及刷写结果 |
对外的数据源接口是 IFrameSource,输出接口是 IFrameSink。帧在两者之间以 FramePtr 传递,也就是 std::shared_ptr<const FramePacket>。同一帧可以被最新帧缓存、文件输出和订阅者共同持有。
latestFrame() 适合只关心最新状态的消费者,waitNext() 等待最新帧指针发生变化。需要异步处理到达的帧时,可以使用 subscribe();订阅回调由分发器工作线程执行。视频和 IMU 在后端各用一条 Pipeline,适应它们不同的采样节奏。Orbbec 官方多流示例也采用独立 IMU 管线。SDK v2.10.6 多流示例
先生成候选,再用实际开流回答能否工作
OrbbecProfileNegotiator 从传感器列表读取视频档位,把宽、高、FPS 和格式交给 ProfileSelector。未明确指定参数时,当前实现偏向 1280 × 800、15 FPS,彩色优先 MJPEG,深度优先 Y16。用户显式指定格式后,候选筛选会要求格式一致。
开启 --auto-profile 后,候选可选用不高于请求值的宽、高和 FPS。排序会考虑分辨率与帧率的偏离、格式偏好、各路 FPS 是否一致,以及可计算的图像载荷。MJPEG 的码率保留为未知,等收到压缩帧再统计。代码对已知载荷超过 850 Mbps 的候选增加排序惩罚,这个值只是工程上的排序参数,具体组合是否可用仍由探测结果决定。
每条流最多保留 8 个可选档位,组合展开最多 32768 个,再按调用者要求截取候选数量。这样能限制多路枚举的计算量。生成 SDK 配置时,程序取回完全匹配的 profile 对象,逐条调用 enableStream()。
--probe 会为每个候选真正创建 Pipeline。启动后先预热 2 秒,再开启统计窗口,逐路累加帧数与字节数。观测时长使用主机单调时钟计算,窗口结束后停止管线。当前 healthy 的判定要求各请求流都收到帧、实测 FPS 至少达到目标的 90%,并且回调和停止过程没有错误。
预热与统计窗口分开,是为了让启动耗时和持续收帧速率各自表达清楚。若把设备启动时间计入一个很短的采样窗口,即使之后按目标帧率送帧,平均值也会偏低。程序用单调时钟计算观测时长,避免主机系统时间调整影响 FPS 计算。
./build/obcapture --sn YOUR_SN --streams color,depth \
--profile color=1280x800@15:mjpg \
--profile depth=1280x800@15:y16 \
--probe --probe-ms 5000 --net-if eth1 \
--probe-json ./diagnostics/probe15.json命令中的 YOUR_SN 和 eth1 分别替换为相机序列号与实际网卡名,报告路径使用新文件。想比较备选组合时,加入 --auto-profile --probe-count 8。
正式采集的自动回退只在 SDK 启动异常且回滚成功后尝试下一个候选。启动成功后,运行中的低 FPS 会进入诊断报告。探测过程中若停止失败,报告保留独立的 cleanup_error,后续候选标记为 not_tested,避免在设备状态不确定时继续开流。
图中描述的是 --probe 的候选探测流程。正式采集使用 --auto-profile 时,仅在启动失败且回滚成功后尝试后续候选;运行中的低帧率不会触发自动换档。
让帧内存活到消费者使用结束
SDK 回调收到 FrameSet 后,emitFrames() 按通道取出帧,填写 FramePacket。视频帧保留宽高、公共格式、原生格式代码与名称、帧序号、三种时间戳和字节数,深度帧另外读取 getValueScale()。IMU 的三轴值和温度直接复制到 ImuSample。
视频数据的所有权由下面三行代码连接起来。这是 OrbbecSource.cpp 中的实际做法。
packet->bufferOwner = std::static_pointer_cast<const void>(frame);
packet->data = frame->getData();
packet->dataSize = frame->getDataSize();data 提供访问入口,bufferOwner 保留 SDK Frame 的共享所有权。回调返回后,只要某个消费者仍持有这份 FramePacket,对应的 SDK 帧就继续存活。公共头文件用 const void 隐藏具体后端类型,写盘模块因此可以只依赖本项目的帧模型。这段适配过程没有主动复制整幅视频载荷,转换和压缩阶段则按各自需要分配输出缓冲区。SDK Frame 的所有权与数据访问接口
三个时间戳分别保存在独立字段中,单位均为微秒。设备时间戳描述设备时钟域中的采集时间,系统时间戳描述主机时钟域中的接收时间,全局时间戳对应转换到主机时钟域的采集时间。当前公共模型用全局时间戳 0 表示不可用。保留这些区别,后续做帧间隔统计和时间关联时才能选对字段。SDK 时间戳接口说明
CaptureSession 将每条流独立缓存和分发。取出最新 Color 和最新 Depth 时,需要分别查看时间戳。当前这条数据通路提供单帧及时间信息,后续严格配对和 D2C 处理可以在此基础上增加。
给每个输出端一条有上限的队列
PNG 压缩和磁盘写入耗时会波动。如果直接在相机回调里完成这些工作,回调就会随着磁盘变慢。当前回调完成帧封装、统计、更新最新帧后,把 FramePtr 放进队列。每个 sink 拥有自己的 worker,工作线程取出帧后释放队列锁,再执行 write()。
队列容量按帧对象数计算。CLI 文件输出默认容量为 32,Color 和 Depth 一起送入同一个 FileSink 时,共用这 32 个位置。假设两路各 30 FPS,总到达率约 60 帧/秒,32 个位置相当于约 0.53 秒的到达量;这是容量换算,实际积压还取决于工作线程的处理速度。队列上限也决定有多少 SDK 帧可能被暂时保留。
| 溢出策略 | 队列满时的行为 | 当前使用位置 |
|---|---|---|
DropOldest | 丢弃队头旧帧,接收新帧,增加丢弃计数 | subscribe() 默认使用,队列容量为 8 |
Fail | 记录队列溢出错误,拒收该帧及后续帧,继续处理已排队数据 | CLI 文件输出使用,主循环检测错误后结束采集 |
每个 worker 提供 received、written、dropped、queued 和错误文本。written 只在 write() 返回后增加。发生写入异常时,分发器记录当前处理帧及剩余队列的损失,并停止该输出端。停止会话后,CLI 再检查源错误、缺流和输出端丢帧,使用退出码报告本次采集是否完整。
Dispatcher 的 start() 会重置 worker 的停止标志、计数和错误状态,再进入新一轮处理。停止标志属于单次运行的状态,不能直接沿用到下一次启动;否则工作线程可能刚启动就退出,新帧也会被当作停止后的数据拒收。
按停止顺序完成收尾
启动会话时,先打开各个 sink 并创建工作线程,再启动数据源。这样首帧到达时已经有输出端可用。如果数据源启动抛出异常,会话停止数据源并回收分发器。
正常停止时,CaptureSession 先停止接受新回调并唤醒等待者,然后停止 SDK 数据源,最后让 Dispatcher 排空已经入队的帧、回收线程、调用各个 sink 的 close()。FileSink 会在 close() 中检查 CSV 和逐帧索引的刷写结果。关闭错误同样进入 worker 的统计结果。
这些生命周期操作由同一个控制线程串行执行。用户订阅回调运行在 worker 上,若在回调内部调用停止,便会涉及线程等待自身的问题。业务侧需要结束采集时,可以通知控制线程来执行 stop()。
图中展示正常停止路径。排空队列的前提是输出端仍能写入;若 write() 抛出异常,分发器会记录损失并停止该输出端,控制线程再收集错误状态。
文件保存时,把原始值与解释它的信息一起留下
FileSink 按通道建立目录,视频文件名包含本通道保存序号和设备时间戳。MJPEG 直接将 SDK 给出的压缩字节写成 JPEG;Y16 写成 16 位灰度 PNG;YUYV、UYVY 先转为 RGB,再写入 PNG。RAW 模式保存原始 SDK 字节,both 同时保留可支持的图像文件与 RAW,便于检查转换是否改变数据。
Y16 导出需要处理字节序。当前 PNG 写入器按小端输入读取两个字节,交换成 PNG 要求的高字节在前,保留 16 位原始深度值。实际距离由逐帧索引中的 depth_scale_mm 与原始值共同确定。PNG 写入器按紧密排列的行计算偏移,接入带额外行步长的数据源时,帧模型与写入器需要一起扩展。
每个视频文件先写到 .part 路径,成功后重命名,再追加一条 frames.jsonl。索引记录通道、SDK 帧号、时间戳、宽高、像素格式、载荷字节数、深度比例和对应文件。CLI 还保存 negotiated_profiles.json,记录最终选中的档位,方便将请求参数与实际结果对照。
原生 Bag 录制通过数据源的 startNativeRecording() 接口调用 SDK 录制器,和 FileSink 的逐帧导出走各自路径。目录导出的每一条索引可以用于定位文件,会话级完成状态和中断恢复仍需要单独设计。
./build/obcapture --sn YOUR_SN --streams color,depth \
--profile color=1280x800@15:mjpg \
--profile depth=1280x800@15:y16 \
--seconds 10 --save both --out ./captures/session01 \
--report-jsonl ./diagnostics/session01.jsonl --net-if eth1用三处计数定位数据在哪里变少
采集程序同时记录网卡、SDK 接收和文件输出端的数据。三处计数有各自的范围,排查时需要放在同一个观测时段内对照。
| 观测位置 | 当前记录的内容 | 怎样解读 |
|---|---|---|
| 主机网卡 | 协商速率、接收字节、接收丢弃及错误计数 | 覆盖该接口的全部流量,可观察采集期间的增量 |
| SDK 回调 | 逐流 FPS、载荷速率、帧索引间断估计、时间戳回退 | 描述应用收到的帧,载荷速率按 dataSize 统计 |
| 输出端 worker | 接收、写入、丢弃、排队数量及错误 | 描述帧从分发器进入具体消费者后的处理结果 |
例如,SDK 接收稳定而输出队列持续增长,可以沿图像转换、压缩和磁盘写入继续查。若 SDK 收帧偏低,则结合开流配置、帧索引变化与网卡增量再定位。帧索引缺口记录为估计值,网络丢包原因需要额外证据。诊断自身处理失败也会增加 missedDiagnosticSamples,让报告能显示统计过程是否漏掉样本。
Orbbec SDK 使用笔记(三):ObCapture 多流采集与异步录制的实现思路
本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
评论交流
欢迎留下你的想法