当硬件平台从高通8155 + ToF 相机 换成其他芯片+其他相机时,获取视频流的架构需要分层设计,只需替换底层适配层,上层业务代码可以保持不变。
分层架构设计
scss
复制代码
┌─────────────────────────────────────────────────────────────────────────────────────┐
│ 跨平台相机适配架构 │
├─────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────────────┐ │
│ │ 业务层(与硬件无关) │ │
│ │ • ToFPipelineManager(解算、队列、统计) │ │
│ │ • DisplayController(本地显示) │ │
│ │ • LiveStreamController(推流) │ │
│ └─────────────────────────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────────────────────────────────────────┐ │
│ │ 抽象接口层(ICameraWrapper) │ │
│ │ • open() / close() │ │
│ │ • startStream() / stopStream() │ │
│ │ • getFrame() / returnFrame() │ │
│ │ • setFrameCallback() │ │
│ └─────────────────────────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────────────────────────────────────────┐ │
│ │ 平台适配层(替换部分) │ │
│ ├─────────────────┬─────────────────┬─────────────────┬─────────────────────┤ │
│ │ 高通8155+ToF │ 瑞芯微+RGB │ NVIDIA+深度相机 │ 树莓派+USB相机 │ │
│ │ (EVS HAL) │ (V4L2) │ (GStreamer) │ (libcamera) │ │
│ └─────────────────┴─────────────────┴─────────────────┴─────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────────────────────────────────────────┐ │
│ │ 硬件层 │ │
│ │ ToF相机 │ USB摄像头 │ MIPI CSI │ 网络相机 │ │
│ └─────────────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────────────┘
不同平台的适配实现
1. 高通8155 + ToF相机(EVS HAL)
arduino
复制代码
// ============================================================================
// EvsCameraWrapper.cpp - 高通 EVS 适配
// ============================================================================
class EvsCameraWrapper : public ICameraWrapper {
public:
bool open(const std::string& deviceId) override {
// 使用你的 StreamHandler
mStreamHandler = new StreamHandler();
return mStreamHandler->start(deviceId.c_str(), 1, HAL_PIXEL_FORMAT_RAW16);
}
bool startStream() override {
// 注册回调
mStreamHandler->setFrameCallback([this](const CopiedFrame& frame) {
FrameData data;
data.data = frame.data.data();
data.size = frame.data.size();
data.width = frame.width;
data.height = frame.height;
data.format = frame.format;
data.timestamp = frame.timestamp;
if (mCallback) mCallback(data);
});
return true;
}
// 需要配合 deliverFrame_1_1 的 doneWithFrame
void returnFrame(const FrameData& frame) override {
// EVS 需要归还缓冲区
if (frame.nativeHandle) {
// 调用 doneWithFrame
}
}
private:
sp<StreamHandler> mStreamHandler;
FrameCallback mCallback;
};
2. 瑞芯微 + USB摄像头(V4L2)
ini
复制代码
// ============================================================================
// V4l2CameraWrapper.cpp - V4L2 适配(Linux 标准接口)
// ============================================================================
class V4l2CameraWrapper : public ICameraWrapper {
public:
bool open(const std::string& deviceId) override {
// /dev/video0
mFd = open(deviceId.c_str(), O_RDWR);
if (mFd < 0) return false;
// 配置格式
struct v4l2_format fmt = {};
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
fmt.fmt.pix.width = 640;
fmt.fmt.pix.height = 480;
ioctl(mFd, VIDIOC_S_FMT, &fmt);
// 请求缓冲区
struct v4l2_requestbuffers req = {};
req.count = 4;
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_MMAP;
ioctl(mFd, VIDIOC_REQBUFS, &req);
// 映射缓冲区
for (int i = 0; i < 4; i++) {
struct v4l2_buffer buf = {};
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = i;
ioctl(mFd, VIDIOC_QUERYBUF, &buf);
mBuffers[i].data = mmap(NULL, buf.length, PROT_READ | PROT_WRITE,
MAP_SHARED, mFd, buf.m.offset);
mBuffers[i].size = buf.length;
}
return true;
}
bool startStream() override {
// 入队所有缓冲区
for (int i = 0; i < 4; i++) {
struct v4l2_buffer buf = {};
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = i;
ioctl(mFd, VIDIOC_QBUF, &buf);
}
// 启动流
ioctl(mFd, VIDIOC_STREAMON, &type);
// 启动捕获线程
mCaptureThread = std::thread([this]() { captureLoop(); });
return true;
}
void captureLoop() {
while (mRunning) {
struct v4l2_buffer buf = {};
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
// 阻塞等待帧
ioctl(mFd, VIDIOC_DQBUF, &buf);
// 转换为统一格式
FrameData frame;
frame.data = (uint8_t*)mBuffers[buf.index].data;
frame.size = buf.bytesused;
frame.timestamp = getTimestamp();
if (mCallback) mCallback(frame);
// 重新入队
ioctl(mFd, VIDIOC_QBUF, &buf);
}
}
private:
int mFd = -1;
struct Buffer {
void* data;
size_t size;
} mBuffers[4];
std::thread mCaptureThread;
FrameCallback mCallback;
};
3. NVIDIA Jetson + 深度相机(GStreamer)
arduino
复制代码
// ============================================================================
// GstCameraWrapper.cpp - GStreamer 适配
// ============================================================================
class GstCameraWrapper : public ICameraWrapper {
public:
bool open(const std::string& deviceId) override {
// 构建 pipeline
// nvarguscamerasrc ! video/x-raw(memory:NVMM) ! nvvidconv ! appsink
std::string pipeline =
"nvarguscamerasrc sensor-id=" + deviceId + " ! "
"video/x-raw,width=640,height=480 ! "
"appsink name=sink emit-signals=true max-buffers=2 drop=true";
mPipeline = gst_parse_launch(pipeline.c_str(), NULL);
mSink = gst_bin_get_by_name(GST_BIN(mPipeline), "sink");
// 设置回调
gst_app_sink_set_callbacks(GST_APP_SINK(mSink), &sCallbacks, this, NULL);
return true;
}
bool startStream() override {
gst_element_set_state(mPipeline, GST_STATE_PLAYING);
// 启动处理线程
mProcessThread = std::thread([this]() { processLoop(); });
return true;
}
static GstFlowReturn onNewSample(GstAppSink* sink, gpointer user_data) {
auto* self = static_cast<GstCameraWrapper*>(user_data);
GstSample* sample = gst_app_sink_pull_sample(sink);
GstBuffer* buffer = gst_sample_get_buffer(sample);
GstMapInfo map;
gst_buffer_map(buffer, &map, GST_MAP_READ);
FrameData frame;
frame.data = map.data;
frame.size = map.size;
if (self->mCallback) self->mCallback(frame);
gst_buffer_unmap(buffer, &map);
gst_sample_unref(sample);
return GST_FLOW_OK;
}
private:
GstElement* mPipeline = nullptr;
GstAppSink* mSink = nullptr;
static GstAppSinkCallbacks sCallbacks;
};
4. 树莓派 + 相机模块(libcamera)
scss
复制代码
// ============================================================================
// LibcameraWrapper.cpp - libcamera 适配
// ============================================================================
class LibcameraWrapper : public ICameraWrapper {
public:
bool open(const std::string& deviceId) override {
// 创建相机管理器
mCameraManager = std::make_unique<CameraManager>();
mCameraManager->start();
// 获取相机
auto cameras = mCameraManager->cameras();
for (auto& cam : cameras) {
if (cam->id() == deviceId) {
mCamera = cam;
break;
}
}
// 配置
mCamera->acquire();
std::unique_ptr<CameraConfiguration> config = mCamera->generateConfiguration(
{ StreamRole::Viewfinder });
config->at(0).size = Size(640, 480);
config->at(0).pixelFormat = formats::YUYV;
mCamera->configure(config.get());
// 分配缓冲区
mAllocator = std::make_unique<FrameBufferAllocator>(mCamera);
mAllocator->allocate(mCamera->streams()[0]);
return true;
}
bool startStream() override {
mCamera->requestStarted.connect(this, &LibcameraWrapper::requestComplete);
mCamera->start();
// 提交请求
for (int i = 0; i < 3; i++) {
std::unique_ptr<Request> request = mCamera->createRequest();
request->addBuffer(mCamera->streams()[0],
mAllocator->allocate(mCamera->streams()[0]));
mCamera->queueRequest(request.get());
}
return true;
}
void requestComplete(Request* request) {
FrameBuffer* buffer = request->buffers().begin()->second;
FrameData frame;
frame.data = (uint8_t*)buffer->planes()[0].fd.get();
frame.size = buffer->planes()[0].length;
if (mCallback) mCallback(frame);
request->reuse();
mCamera->queueRequest(request);
}
private:
std::unique_ptr<CameraManager> mCameraManager;
std::shared_ptr<Camera> mCamera;
std::unique_ptr<FrameBufferAllocator> mAllocator;
};
业务层使用方式(无需修改)
arduino
复制代码
// ============================================================================
// ToFPipelineManager.cpp - 业务层代码(平台无关)
// ============================================================================
class ToFPipelineManager {
public:
void initialize(std::shared_ptr<ICameraWrapper> camera) {
mCamera = camera; // 注入相机适配器
}
bool start() {
// 打开相机
if (!mCamera->open("camera0")) {
return false;
}
// 设置回调
mCamera->setFrameCallback([this](const FrameData& frame) {
onFrameReceived(frame);
});
// 启动流
return mCamera->startStream();
}
void onFrameReceived(const FrameData& frame) {
// 这里统一处理,不管相机是什么平台
// 1. 解算深度
// 2. 推流
// 3. 本地显示
// ... 代码完全不变
}
private:
std::shared_ptr<ICameraWrapper> mCamera;
};
// ============================================================================
// main.cpp - 根据平台选择具体实现
// ============================================================================
int main() {
// 平台特定的相机适配器
std::shared_ptr<ICameraWrapper> camera;
#if defined(PLATFORM_QCOM_8155)
camera = std::make_shared<EvsCameraWrapper>();
#elif defined(PLATFORM_ROCKCHIP)
camera = std::make_shared<V4l2CameraWrapper>();
#elif defined(PLATFORM_NVIDIA_JETSON)
camera = std::make_shared<GstCameraWrapper>();
#elif defined(PLATFORM_RASPBERRY_PI)
camera = std::make_shared<LibcameraWrapper>();
#endif
// 业务层代码完全不变
ToFPipelineManager pipeline;
pipeline.initialize(camera);
pipeline.start();
return 0;
}
不同平台对比总结
| 平台 |
接口类型 |
适配难度 |
关键点 |
| 高通8155/MTK8678 |
EVS HAL |
中 |
需要处理 buffer 生命周期(doneWithFrame) |
| 瑞芯微/全志 |
V4L2 |
低 |
Linux 标准接口,代码通用 |
| NVIDIA Jetson |
GStreamer |
中 |
硬件加速编码,pipeline 灵活 |
| 树莓派 |
libcamera |
中 |
新标准,支持 CSI 相机 |
| PC调试 |
OpenCV |
低 |
快速原型开发 |
| 网络相机 |
RTSP |
中 |
网络传输,延迟较高 |
| 迁移类型 |
需修改的代码 |
工作量 |
| 高通8155 → 瑞芯微 |
仅适配层(约500行) |
1周 |
| 高通8155 → Jetson |
适配层 + 编码推流 |
2周 |
| ToF → RGB相机 |
解算层全部重写 |
1-3个月 |
| 同一平台不同型号 |
几乎无修改 |
1天 |
高通8155平台下换相机的分析
高通8155支持的相机接口
| 接口类型 |
说明 |
EVS是否支持 |
代码改动 |
| MIPI CSI |
车载相机主流接口 |
✅ 支持 |
无需改代码,改设备节点 |
| USB (UVC) |
USB摄像头 |
⚠️ 通过V4L2 |
需写V4L2适配层 |
| RGB (RAW/ YUV) |
传感器原始输出 |
✅ 支持 |
无需改代码 |
| ToF |
深度相机 |
✅ 支持 |
当前已实现 |
| GMSL/FPD-Link |
长距离传输 |
✅ 支持 |
无需改代码 |
换不同类型相机的改动分析
场景1:高通8155 + 同接口RGB相机(不改代码)
arduino
复制代码
// 只需修改 cameraID 和 format
// 你的代码已经支持,无需修改
// 之前(ToF)
const char* cameraID = "tof";
int32_t format = HAL_PIXEL_FORMAT_RAW16; // 32
// 改成(RGB)
const char* cameraID = "camera0";
int32_t format = HAL_PIXEL_FORMAT_YUYV; // 或 YUV_420_888
// 其他代码完全不变!
场景2:高通8155 + USB相机(需要适配层)
arduino
复制代码
// USB相机走的是 V4L2,不走 EVS HAL
// 需要写一个适配层,但上层代码不变
// 新增适配层
class UsbCameraAdapter : public ICameraSource {
public:
bool open(const char* device) override {
// V4L2 操作
fd = open("/dev/video0", O_RDWR);
ioctl(fd, VIDIOC_S_FMT, &fmt);
// ...
}
bool startStream() override {
// 启动 V4L2 采集线程
}
void setFrameCallback(FrameCallback cb) override {
// 转换为统一格式后回调
}
};
// 上层 ToFPipelineManager 不需要改!
// 只是注入不同的相机源
场景3:高通8155 + MIPI CSI Raw相机(不改代码)
ini
复制代码
// MIPI CSI Raw 相机同样走 EVS HAL
// 只需修改 cameraId
mEvsCamera = mEvsEnumerator->openCamera_1_1("mipi_raw", targetCfg);
// RAW 数据格式可能不同,需要配置正确的格式
targetCfg.format = HAL_PIXEL_FORMAT_RAW10; // 或其他
各平台开发环境完整对比
完整对比表
| 平台 |
构建系统 |
操作系统 |
构建文件 |
开发环境 |
编译器 |
调试方法 |
工具链文件 |
| 高通8155 |
Soong |
Android |
Android.bp |
AOSP源码/NDK |
Clang (预置) |
adb gdb / lldb |
build/make/core/combo/*.mk |
| MTK8678 |
Soong |
Android |
Android.bp |
AOSP源码/NDK |
Clang (预置) |
adb gdb / lldb |
同上(Android标准) |
| 瑞芯微 |
CMake/Make |
Linux |
CMakeLists.txt |
Linux (Buildroot/Yocto) |
ARM GCC |
gdb 远程调试 |
arm-linux-gnueabihf.toolchain.cmake |
| 全志 |
CMake/Make |
Linux |
CMakeLists.txt |
Linux (Tina SDK) |
ARM GCC |
gdb 远程调试 |
arm-openwrt-linux.cmake |
| NVIDIA Jetson |
CMake |
Linux |
CMakeLists.txt |
Ubuntu (JetPack) |
GCC / NVCC |
gdb + nsight-sysroot |
原生(无需交叉编译) |
| 树莓派 |
CMake |
Linux |
CMakeLists.txt |
Raspberry Pi OS |
GCC |
gdb |
原生或交叉编译 |
| PC调试 |
CMake |
Ubuntu |
CMakeLists.txt |
Ubuntu / Windows |
GCC / MSVC |
gdb / Visual Studio |
原生(无需工具链) |
NDK vs AOSP 源码开发对比
| 方面 |
NDK 开发 |
AOSP 源码开发 |
| 使用场景 |
独立应用/库开发 |
系统级集成、HAL 开发 |
| 构建系统 |
CMake / ndk-build |
Soong (Android.bp) |
| 访问系统接口 |
有限(通过 JNI/系统API) |
完整(可调用任何内部接口) |
| EVS HAL 访问 |
❌ 无法直接访问 |
✅ 可以直接调用 |
| 编译环境 |
独立 NDK + CMake |
完整 AOSP 源码 |
| 输出位置 |
/data/local/tmp/ 或 APK |
/vendor/lib/、/system/lib/ |
| 对比项 |
NDK 独立开发 |
AOSP 源码开发 |
| 适用场景 |
独立应用、普通库、算法库 |
系统服务、HAL 层、需要系统权限 |
| EVS HAL 访问 |
❌ 无法直接访问 |
✅ 完整访问 |
| HIDL 接口 |
❌ 无法直接调用 |
✅ 可直接调用 |
| 系统私有头文件 |
❌ 无 |
✅ 完整 |
| 编译速度 |
✅ 快(只编译你的代码) |
❌ 慢(需编译整个系统或模块) |
| 环境配置 |
✅ 简单(下载 NDK 即可) |
❌ 复杂(需下载 AOSP 源码) |
| 调试难度 |
✅ 简单 |
⚠️ 较复杂 |
| 输出位置 |
/data/local/tmp/ |
/vendor/lib/、/system/lib/ |
| 权限 |
普通应用权限 |
系统权限 |
| 依赖管理 |
手动管理 |
Android.bp 自动管理 |
不能用NDK的场景(必须AOSP)
高通8155和MTK 的 EVS HAL 开发必须用 AOSP ,普通库可用 NDK ,但有明确的边界:只能编译不依赖系统私有接口的代码。
| 场景 |
是否可用 |
原因 |
| EVS HAL调用 |
❌ 不可用 |
头文件不在NDK中 |
| HIDL接口 |
❌ 不可用 |
需要系统源码 |
| Binder IPC |
❌ 不可用 |
需要系统私有API |
| GraphicBuffer |
❌ 不可用 |
需要ui库头文件 |
| SurfaceFlinger |
❌ 不可用 |
系统服务 |
| Camera HAL |
❌ 不可用 |
不在NDK标准中 |
| Vendor分区写入 |
⚠️ 需要root |
普通NDK无权限 |
| 系统属性 |
⚠️ 有限支持 |
只能读,部分可写 |
- 算法、计算、工具类 → 用NDK
- EVS HAL、系统服务 → 用AOSP
- 两者结合 → NDK编译算法库,AOSP中链接使用