ToF 工程说明
1. 工程概览
本工程面向 高通 8155 + ToF 相机 场景,当前主程序为 tof_stream,实现了一条完整的实时数据链路:
- 通过 EVS 打开相机并持续接收图像流。
- 将相机原始数据拷贝为用户态连续内存。
- 调用厂商
DepthProcessor完成 ToF 解算。 - 得到
depth和ir两路结果。 - 将结果回调给上层。
- 通过 AVS 推送到客户端会话。
- 可选在设备本地通过
EGL + OpenGL ES显示深度伪彩和 IR 灰度预览。 - 可选异步保存
raw/depth/ir到本地文件。
当前代码已经从早期版本演进为:
- 支持 EVS 事件热插拔恢复。
- 支持
binder died后自动重新openCamera + startVideoStream。 - 支持本地显示模块
DisplayController。 - 支持通过配置开关启用或关闭本地显示。
- 推流控制器已升级为“缓冲 + 限流 + 分发 + 编码协调 + 重连管理”模式。
- 已移除流控制器中的命令处理和手动保存职责,相关功能不再由
LiveStreamController承担。
2. 当前构建产物
按当前 Android.bp,实际注册的主要构建目标是:
tof_stream- 完整应用,包含采集、解算、推流、可选本地显示、可选文件保存。
源码目录中仍保留以下封装/示例代码:
samples/streamcapture.hsamples/streamcapture.cppsamples/streamcapture_demo.cppsamples/README_streamcapture.md
但这些模块当前没有在 Android.bp 中注册为独立产物,因此不会随当前配置自动构建。
3. 目录与模块
3.1 入口
srcs/main.cpp- 程序入口。
- 负责组装
ToFPipelineManager、LiveStreamController、DisplayController。 - 负责读取命令行参数并决定是否启用本地显示。
3.2 采集层
EvsStream/StreamHandler.hsrcs/StreamHandler.cpp- 对接 EVS。
- 打开相机并启动视频流。
- 接收
deliverFrame_1_1()回调。 - 将 HAL buffer 拷贝成紧凑连续内存。
- 处理 EVS 热插拔事件和 binder 死亡恢复。
3.3 流水线核心
include/ToFPipelineManager.hsrcs/ToFPipelineManager.cpp- 管理采集、解算、统计线程。
- 管理原始帧队列、显示帧队列。
- 调用 SDK 解算。
- 可选提交异步保存任务。
- 通过回调把
ProcessedFrameData交给上层。
3.4 保存层
include/AsyncFileWriter.hsrcs/AsyncFileWriter.cpp- 独立文件写线程。
- 当
saveRawData / saveDepthData / saveIRData任一开启时才初始化并启动。
3.5 推流控制层
EvsStream/LiveStreamController.hsrcs/LiveStreamController.cpp- 接收
ProcessedFrameData。 - 使用有上限的帧缓冲做平滑处理。
- 按目标 FPS 限流。
- 向预览/录像/AI 回调分发数据。
- 负责推流后端的连接检测和重连调度。
- 接收
3.6 推流执行层
EvsStream/StreamPusher.hsrcs/StreamPusher.cpp- 动态加载
libalgorithm_visual_service.so。 - 注册 AVS session。
- 维护内部推流队列。
- 进行图像编码等待、元数据序列化等待。
- 将图像和元数据真正推送到 session。
- 动态加载
3.7 本地显示层
display/DisplayController.hdisplay/DisplayController.cppdisplay/SimpleRenderer.hdisplay/SimpleRenderer.cppdisplay/createNativeWindow.cppDisplayController负责显示线程、深度伪彩转换和渲染调度。SimpleRenderer负责EGL + OpenGL ES 2.0渲染。- 当前支持本地双窗口显示:
- 深度图伪彩显示
- IR 灰度图显示
4. 核心数据结构
文件:include/FrameDataTypes.h
4.1 原始帧
struct RawFrameData {
uint64_t frameId;
uint64_t timestampNs;
uint32_t width;
uint32_t height;
uint32_t stride;
std::vector<uint8_t> data;
};
data保存从 EVS 拷贝出的原始连续内存。- 当前主路径使用
RAW16。
4.2 解算后帧
struct ProcessedFrameData {
uint64_t frameId;
uint64_t timestampNs;
uint32_t depthWidth;
uint32_t depthHeight;
uint32_t irWidth;
uint32_t irHeight;
float exposureTime;
float temperature;
std::vector<uint8_t> depthData;
std::vector<uint8_t> irData;
std::vector<uint8_t> rawData;
};
depthData按uint16_t存储。irData按uint8_t存储。rawData作为原始帧副本,用于保存或调试。
4.3 EVS 拷贝帧
文件:EvsStream/StreamHandler.h
struct CopiedFrame {
std::vector<uint8_t> data;
uint32_t width;
uint32_t height;
uint32_t stride;
uint32_t format;
uint64_t timestamp;
};
CopiedFrame是StreamHandler从 EVS/HAL buffer 拷贝到用户态后的中间帧结构。- 它和原始
BufferDesc的区别是:- 生命周期完全由应用控制
- 可以在归还 HAL buffer 后继续使用
- 便于后续采集线程和解算线程异步处理
4.4 文件写任务
文件:include/AsyncFileWriter.h
struct WriteTask {
std::string filename;
std::vector<uint8_t> data;
uint64_t frameId;
bool isCritical;
};
WriteTask是文件写线程真正消费的任务结构。data保存待写入磁盘的二进制内容。isCritical用于在退出时区分关键任务,必要时等待其落盘完成。
4.5 AVS 推流帧
文件:EvsStream/StreamPusher.h
struct FrameData {
unsigned char* depth_data;
unsigned char* ir_data;
size_t depth_size;
size_t ir_size;
int width;
int height;
int channels;
uint64_t frame_id;
uint64_t timestamp;
};
- 这是
StreamPusher内部排队等待推送的帧结构。 depth_data和ir_data是独立申请并复制的堆内存。- 这样上游提交后即可返回,不需要等待真正推流完成。
4.6 无锁队列 LockFreeQueue<T>
文件:include/LockFreeQueue.h
template<typename T>
class LockFreeQueue {
public:
bool tryPush(T&& item);
bool forcePush(T&& item);
bool tryPop(T& item);
size_t tryPopBatch(T* items, size_t maxCount);
};
当前实现特点:
- 基于环形缓冲区。
- 使用原子变量
head_和tail_管理读写位置。 - 适合高频、简单的单生产者/单消费者流式搬运场景。
tryPush()队列满时返回false。forcePush()队列满时覆盖最老元素。tryPopBatch()支持批量取帧,减少频繁调用开销。
4.7 工程里的队列与缓存结构
4.7.1 EVS 拷贝帧队列
- 类型:
std::queue<CopiedFrame> - 名称:
StreamHandler::mFrameDataQueue - 保护方式:
mFrameBufferMutex + mFrameBufferCV - 作用:
- 保存从 EVS 回调中拷贝出的最新帧
- 供
captureLoop()消费 - 热插拔或断线时支持清队列
4.7.2 原始帧无锁队列
- 类型:
LockFreeQueue<RawFrameData> - 名称:
ToFPipelineManager::rawQueue_ - 生产者:
captureLoop() - 消费者:
processLoop() - 作用:
- 采集线程和解算线程解耦
- 避免高频路径上的锁竞争
4.7.3 显示帧无锁队列
- 类型:
LockFreeQueue<ProcessedFrameData> - 名称:
ToFPipelineManager::procQueue_ - 生产者:
processLoop() - 消费者:
DisplayController::displayThreadFunc() - 作用:
- 把最新解算结果提供给本地显示
- 避免显示线程阻塞主链路
4.7.4 文件写任务队列
- 类型:
std::queue<WriteTask> - 名称:
AsyncFileWriter::taskQueue_ - 保护方式:
queueMutex_ + queueCV_ - 作用:
- 后台写线程异步消费保存任务
- 避免解算线程直接做磁盘 IO
4.7.5 流控制器缓冲队列
- 类型:
std::deque<tof::ProcessedFrameData> - 名称:
LiveStreamController::frameBuffer_ - 保护方式:
frameMutex_ + frameCV_ - 作用:
- 作为推流控制层的环形缓冲
- 配合限流、断线缓冲和多路分发
4.7.6 AVS 推流队列
- 类型:
std::queue<StreamPusher::FrameData> - 名称:
StreamPusher::frame_queue_ - 保护方式:
queue_mutex_ + queue_cv_ - 作用:
- 存放真正等待送往 AVS 的帧
- 由
StreamPusher::pushLoop()后台消费
4.8 为什么有些地方用无锁队列,有些地方不用
本工程没有强行把所有队列都改成无锁队列,而是按用途选型:
- 高频数据搬运
- 用
LockFreeQueue - 例如
rawQueue_、procQueue_
- 用
- 需要等待、唤醒、清空、重连协调的控制链路
- 用
std::queue / std::deque + mutex + condition_variable - 例如
mFrameDataQueue、taskQueue_、frameBuffer_、frame_queue_
- 用
这样做的原因是:
- 无锁队列适合简单高速搬运
- 加锁队列更适合复杂控制逻辑
- 两者混用更符合当前工程实际需求
5. 启动命令速查表
5.1 命令格式
tof_stream <cameraId> <targetFps> <pixelFormat> <enableLocalDisplay>
参数说明:
cameraId- EVS 相机 ID,例如
1
- EVS 相机 ID,例如
targetFps- 目标帧率,例如
30
- 目标帧率,例如
pixelFormat- EVS 像素格式,当前支持:
raw16rgba
- EVS 像素格式,当前支持:
enableLocalDisplay- 是否启用设备本地显示,支持:
1 / 0true / falseon / offyes / no
- 是否启用设备本地显示,支持:
5.2 常用命令
只做采集 + 解算 + 推流
tof_stream 1 30 raw16 0
适用场景:
- 设备侧只负责采集、解算、推流
- 不需要本地屏幕显示
- 不需要额外显示线程和 OpenGL 渲染
采集 + 解算 + 推流 + 本地显示
tof_stream 1 30 raw16 1
适用场景:
- 需要在车机屏幕上确认相机正在取流
- 需要同时看深度伪彩图和 IR 图
使用 RGBA 格式调试
tof_stream 1 30 rgba 1
适用场景:
- EVS/HAL 侧需要切到
RGBA_8888做联调 - 用于验证非
RAW16模式下的链路
5.3 当前默认运行习惯
如果你当前项目目标是:
- ToF 相机取流
- SDK 解算
- AVS 推流到客户端
- 本地屏幕只作为辅助预览
推荐优先使用:
tof_stream 1 30 raw16 1
如果你只关心设备侧后台服务,不需要本地预览:
tof_stream 1 30 raw16 0
6. 当前主流程
6.1 启动流程
main() 当前执行顺序:
- 配置 HIDL 线程池。
- 构造
ToFPipelineManager::Config。 - 读取命令行参数:
argv[1]:cameraIdargv[2]:targetFpsargv[3]:EVS 像素格式,支持raw16/rgbaargv[4]:本地显示开关,支持1/0、true/false、on/off
- 初始化
ToFPipelineManager。 - 根据
enableLocalDisplay决定是否启动DisplayController。 - 启动
ToFPipelineManager。 - 初始化并启动
LiveStreamController。 - 进入主循环,周期性打印统计信息。
6.2 主数据流
EVS Camera
-> StreamHandler::deliverFrame_1_1()
-> CopiedFrame
-> ToFPipelineManager::captureLoop()
-> RawFrameData
-> rawQueue_
-> ToFPipelineManager::processLoop()
-> DepthProcessor
-> ProcessedFrameData
-> submitToWriter() -> AsyncFileWriter
-> frameCallback_(out)
-> main.cpp::onFrameReady()
-> LiveStreamController::submitFrame()
-> LiveStreamController::pushThreadFunc()
-> StreamPusher::pushFrame()
-> StreamPusher::pushLoop()
-> avs_push_to_session()
6.3 本地显示流
ProcessedFrameData
-> ToFPipelineManager::procQueue_
-> DisplayController::displayThreadFunc()
-> 深度图伪彩转换 / IR 灰度显示
-> SimpleRenderer
6.4 总链路图
+---------------------------+
| 相机 / EVS HAL |
+-------------+-------------+
|
v
+---------------------------+
| StreamHandler::start() |
| openCamera / startStream |
+-------------+-------------+
|
v
+---------------------------+
| deliverFrame_1_1() |
| 拷贝 HAL buffer |
| 异步 doneWithFrame |
+-------------+-------------+
|
v
+---------------------------+
| mFrameDataQueue |
+-------------+-------------+
|
v
+---------------------------+
| captureLoop() |
| CopiedFrame -> RawFrame |
+-------------+-------------+
|
v
+---------------------------+
| rawQueue_ |
+-------------+-------------+
|
v
+---------------------------+
| processLoop() |
| DepthProcessor 解算 |
+-------------+-------------+
|
v
+---------------------------+
| ProcessedFrameData |
+------+------+-------------+
| |
可选保存 | | 回调给上层
| v
| +------------------------+
| | onFrameReady() |
| | submitFrame() |
| +-----------+------------+
| |
| v
| +------------------------+
| | LiveStreamController |
| | 缓冲 / 限流 / 分发 |
| | 重连管理 |
| +-----------+------------+
| |
| v
| +------------------------+
| | StreamPusher |
| | 编码等待 / 序列化等待 |
| | avs_push_to_session |
| +-----------+------------+
| |
| v
| +------------------------+
| | 客户端 / AVS Session |
| +------------------------+
|
v
+---------------------------+
| submitToWriter() |
| AsyncFileWriter |
+---------------------------+
ProcessedFrameData
-> procQueue_
-> DisplayController
-> SimpleRenderer
-> 设备本地显示
热插拔 / 异常恢复:
STREAM_STOPPED -> 清队列 / 标记离线 / 暂停采集
STREAM_STARTED -> 恢复在线 / resumeVideoStream
binder died -> reopenCamera / startVideoStream
7. 热插拔与断线恢复
当前 StreamHandler 已具备热插拔和服务级恢复能力。
7.1 EVS 事件热插拔
StreamHandler 监听 EVS 事件:
STREAM_STOPPED- 认为相机拔掉或掉线。
- 清空采集队列。
- 标记相机离线。
- 尝试暂停视频流。
STREAM_STARTED- 认为相机重新上线。
- 恢复在线状态。
- 优先尝试
resumeVideoStream()恢复流。
7.2 Binder 死亡恢复
当相机服务或 binder 直接失效时:
onCameraBinderDied()标记待重连。- 清空内部帧队列并标记离线。
- 采集线程检测到重连请求后,按退避间隔尝试恢复。
- 重新执行:
- 获取 enumerator
openCamerasetMaxFramesInFlightstartVideoStream
- 成功后继续采集。
7.3 熔断机制
ToFPipelineManager 保留了连续错误计数和熔断恢复逻辑,用于解算异常时的容错,不替代热插拔恢复,但与其配合工作。
8. 保存功能
保存仍然保留在 pipeline 中,但默认关闭。
8.1 默认配置
在 main.cpp 中默认:
config.saveRawData = false;
config.saveDepthData = false;
config.saveIRData = false;
8.2 当前行为
- 三个保存开关都为
false时:- 不会创建
AsyncFileWriter - 不会启动文件 IO 线程
- 不会写磁盘
- 不会创建
- 任一开关为
true时:- 初始化
AsyncFileWriter - 解算成功后按配置异步写入文件
- 初始化
9. 本地显示功能
本地显示当前已经接通,不再是保留代码。
9.1 当前能力
- 支持通过
DisplayController启停。 - 支持从
ToFPipelineManager::getLatestFrame()读取最新处理帧。 - 支持深度图伪彩显示。
- 支持 IR 灰度显示。
- 支持通过配置或命令行关闭。
9.2 开关控制
ToFPipelineManager::Config 中新增:
bool enableLocalDisplay = true;
main.cpp 中第 4 个命令行参数可覆盖该值:
tof_stream 1 30 raw16 1
tof_stream 1 30 raw16 0
分别表示:
1:启用本地显示0:禁用本地显示
10. 推流控制器现状
LiveStreamController 已不再负责远程命令处理或手动保存,而是聚焦流控制本身。
10.1 已实现职责
- 帧缓存
- 使用有容量限制的
deque作为缓冲区。
- 使用有容量限制的
- 帧率控制
- 按
targetPushFps控制送往 AVS 的节奏。
- 按
- 数据分发
- 提供
preview / record / ai三类回调入口。
- 提供
- 推流管理
- 检查连接数。
- 连接断开时保留缓冲并周期性重连。
- 编码协调
- 推流前等待图像编码完成。
- 推流前等待元数据序列化完成。
10.2 StreamPusher 当前职责
dlopen动态加载libalgorithm_visual_service.so- 注册 session
- 获取连接数
- 接收帧入队
- 队列满时丢弃最旧帧
- 在内部线程中调用:
avs_set_image_data()avs_wait_image_encode()avs_set_serialize_data()avs_wait_serialize()avs_push_to_session()
10.3 当前不再承担的职责
以下能力已从流控制器代码路径中去掉或不再启用:
- AVS 命令处理
start_capture / stop_capture / save_frame / get_status- 流控制器内的手动保存文件
如果客户端需要这些功能,建议在客户端界面或更上层业务模块实现。
10.4 AVS 是什么
这里的 AVS,对应当前工程里动态加载的 libalgorithm_visual_service.so,可以理解成项目现有的一套“算法数据分发 / 会话推送服务”。
在本工程里,它主要承担这些职责:
- 注册会话
session - 维护客户端连接数
- 接收图像和元数据
- 把深度图、IR 图和 JSON 元信息推送给客户端
当前 StreamPusher 实际依赖它提供的接口包括:
avs_register_session()avs_get_session_connections()avs_set_image_data()avs_wait_image_encode()avs_set_serialize_data()avs_wait_serialize()avs_push_to_session()
也就是说,当前代码里的“推流”本质上不是 RTSP/RTMP 标准推流,而是“通过 AVS 会话把算法结果发给客户端”。
10.5 如果没有 libalgorithm_visual_service.so,怎么实现推流
如果没有这套库,核心思路是:
- 保留
LiveStreamController - 替换
StreamPusher - 改成你们自己的传输协议和客户端接收方式
可选实现方案:
方案一:自定义 TCP / Unix Socket 推送原始结果
适合场景:
- 设备端和客户端都由自己控制
- 只想快速把
depth/ir/meta发出去
实现方式:
- 新写一个
SocketStreamPusher - 用 socket 发送:
- 帧头
- 深度图二进制
- IR 图二进制
- JSON 元数据
优点:
- 实现直接
- 不依赖第三方推流库
缺点:
- 需要自己定义协议
- 需要自己处理断线重连、粘包拆包、客户端解析
方案二:WebSocket 推送
适合场景:
- 客户端是网页或 H5
- 希望直接在浏览器端接收
实现方式:
- 设备端起一个 WebSocket 服务或客户端
- 深度图、IR 图和元数据按二进制帧或 JSON + 二进制方式发送
优点:
- 和前端联调方便
- 适合你现在“推到客户端界面显示”的需求
缺点:
- 需要自己处理大帧传输效率
- 需要前端自己解析和显示深度/IR 数据
方案三:RTSP / RTP / WebRTC 推视频流
适合场景:
- 客户端更习惯接标准视频流
- 只关心“看画面”,不强依赖原始深度值
实现方式:
- 把深度图先转换成伪彩 RGB
- 把 IR 图转成灰度视频
- 用
MediaCodec、GStreamer、FFmpeg或平台编码器编码为 H264/H265 - 通过 RTSP / RTP / WebRTC 输出
优点:
- 标准协议
- 客户端播放器兼容性好
缺点:
- 原始深度数值会丢失或需要另发
- 要增加编码与视频传输链路
方案四:gRPC / HTTP 上传结构化帧
适合场景:
- 客户端更偏算法服务或后端服务
- 不一定要实时视频显示
实现方式:
- 定义
protobuf或 HTTP 二进制接口 - 每帧上传
depth/ir/meta
优点:
- 接口规范
- 后端服务接入方便
缺点:
- 更适合“数据服务”,不如视频协议适合实时预览
10.6 当前工程里最推荐的替代方式
如果没有 libalgorithm_visual_service.so,结合你这个项目现状,优先建议:
- 如果目标是“客户端界面显示”
- 用
WebSocket或RTSP/WebRTC
- 用
- 如果目标是“把深度/IR 原始结果给另一个程序或算法服务”
- 用自定义
TCP或gRPC
- 用自定义
对应代码结构建议:
LiveStreamController保留StreamPusher抽象成接口- 新增:
AvsStreamPusherWebSocketStreamPusherRtspStreamPusherSocketStreamPusher
这样主流程不变:
ProcessedFrameData
-> LiveStreamController
-> XxxStreamPusher
-> 客户端
也就是说,真正可替换的是“推流执行层”,不是整个 pipeline。
11. 线程模型
当前主要线程如下:
- HIDL / EVS 回调线程
- 接收 EVS buffer 并做用户态拷贝。
- 采集线程
captureLoop()- 从
StreamHandler取拷贝帧并送入rawQueue_。
- 从
- 解算线程
processLoop()- 调用 SDK 解算并产出
ProcessedFrameData。
- 调用 SDK 解算并产出
- 统计线程
statsLoop()- 计算 FPS 与统计指标。
- 文件写线程
AsyncFileWriter::writerLoop()- 仅在保存功能开启时存在。
- 推流控制线程
LiveStreamController::pushThreadFunc()- 做限流、分发、重连控制。
- AVS 推流线程
StreamPusher::pushLoop()- 真正调用 AVS 接口出流。
- 显示线程
DisplayController::displayThreadFunc()- 仅在本地显示开启时存在。
12. 默认配置与运行建议
12.1 当前默认运行形态
默认启用:
- EVS 采集
- SDK 解算
- 热插拔恢复
- binder died 自动重连
- AVS 推流
- 本地显示
默认关闭:
- 原始帧保存
- 深度图保存
- IR 图保存
12.2 推荐命令
tof_stream 1 30 raw16 1
含义:
cameraId = 1targetFps = 30- EVS 像素格式为
RAW16 - 启用本地显示
如果只需要采集、解算、推流,不需要设备本地显示:
tof_stream 1 30 raw16 0
13. 依赖
当前 Android.bp 中 tof_stream 依赖以下关键库:
libdepth_processorlibspectrelibimagerwrapperlibalgorithm_visual_serviceandroid.hardware.automotive.evs@1.0android.hardware.automotive.evs@1.1libuilibguilibEGLlibGLESv2
其中:
libalgorithm_visual_service用于 AVS 推流。libEGL/libGLESv2用于本地显示。
14. 建议阅读顺序
建议按以下顺序理解当前工程:
srcs/main.cppinclude/ToFPipelineManager.hsrcs/ToFPipelineManager.cppEvsStream/StreamHandler.hsrcs/StreamHandler.cppdisplay/DisplayController.hdisplay/DisplayController.cppdisplay/SimpleRenderer.hdisplay/SimpleRenderer.cppEvsStream/LiveStreamController.hsrcs/LiveStreamController.cppEvsStream/StreamPusher.hsrcs/StreamPusher.cpp
15. 一句话总结
当前工程已经从“采集 + 解算 + 可选保存”的基础版本,演进为一套面向设备常驻运行的 ToF 服务:
EVS 采集
-> 热插拔 / binder died 自动恢复
-> SDK 解算
-> 可选本地显示
-> 可选异步保存
-> 流控制器缓冲 / 限流 / 分发 / 重连
-> AVS 推流到客户端