上一篇算过 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>。同一帧可以被最新帧缓存、文件输出和订阅者共同持有。

flowchart TB V["视频 Pipeline"] --> P["FramePacket / FramePtr"] I["IMU Pipeline"] --> P P --> S["CaptureSession"] S --> D["接收诊断"] S --> L["各通道最新帧缓存"] S --> F["FrameDispatcher"] F --> Q1["文件输出有界队列"] F --> Q2["订阅有界队列"] Q1 --> W1["文件 worker"] Q2 --> W2["订阅 worker"] W1 --> O1["FileSink"] W2 --> O2["用户回调"]

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,避免在设备状态不确定时继续开流。

flowchart TB A["读取设备支持的档位"] --> B["按格式和参数筛选"] B --> C["限制组合数量并排序"] C --> D["取一个候选并启动 Pipeline"] D --> E{"启动成功?"} E -->|是| F["预热 2 秒"] F --> G["独立窗口统计帧数与字节数"] G --> H["停止并清理 Pipeline"] E -->|否| H H --> J{"清理成功?"} J -->|否| K["记录 cleanup_error<br/>其余候选标记 not_tested"] J -->|是| L["汇总启动、帧率和回调状态"] L --> M{"还有候选?"} M -->|是| D M -->|否| N["输出探测结果"]

图中描述的是 --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()。

sequenceDiagram participant C as 控制线程 participant S as CaptureSession participant O as OrbbecSource participant D as FrameDispatcher participant W as 输出 worker participant F as FileSink C->>S: stop() S->>S: 停止接收新回调并唤醒等待者 S->>O: stop() O-->>S: 数据源停止 S->>D: stop() D->>W: 通知停止,处理已入队数据 loop 队列尚有帧 W->>F: write(frame) F-->>W: 返回写入结果 end W-->>D: 工作线程退出 D->>D: join() D->>F: close() F-->>D: 刷写结果或关闭错误 D-->>S: 完成输出端收尾 S-->>C: 返回

图中展示正常停止路径。排空队列的前提是输出端仍能写入;若 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,让报告能显示统计过程是否漏掉样本。