作品

第一阶段:摄像头采集与X264编码

Jul 12, 2026 作品

第一阶段:摄像头采集与 X264 编码

本阶段的目标是验证视频采集链路:从 USB 摄像头获取 YUV 原始图像,经由 X264 软件编码器压缩为 H.264 数据,最后在终端输出编码帧大小及 H.264 NALU 起始码。本阶段不涉及任何网络功能,仅聚焦采集与编码的正确性。所有类名、结构体名、宏名统一以 HZP 为前缀,采用现代 C++ 风格,使用 spdlog 异步日志替代传统宏打印。


一、项目目录结构

orangepi@orangepi5:~/HZPStreamServer$ tree
.
├── CMakeLists.txt
├── include
│   ├── HZPCameraCapture.h
│   ├── HZPCommonDef.h
│   ├── HZPEncoder.h
│   ├── HZPLogger.h
│   ├── HZPServerController.h
│   └── HZPStreamServer.h
└── src
    ├── HZPCameraCapture.cpp
    ├── HZPEncoder.cpp
    ├── HZPLogger.cpp
    ├── HZPServerController.cpp
    ├── HZPStreamServer.cpp
    └── main.cpp

3 directories, 12 files

文件职责一览:

文件 功能
HZPCommonDef.h 项目常量、公共头文件
HZPLogger.h/cpp 基于 spdlog 的异步日志 RAII 守卫
HZPCameraCapture.h/cpp V4L2 摄像头采集类
HZPEncoder.h/cpp X264 软件编码类
HZPStreamServer.h/cpp 流媒体服务器类(本阶段仅为骨架)
HZPServerController.h/cpp 服务器控制器,调度摄像头与服务器
main.cpp 程序入口,创建控制器启动

二、CMake 构建配置

# ============================================================
# CMake 最低版本要求,3.10 兼具现代特性与广泛支持
# ============================================================
cmake_minimum_required(VERSION 3.10)

# ============================================================
# 项目定义:名称、版本、语言(仅 C++)
# ============================================================
project(HZPStreamServer VERSION 1.0 LANGUAGES CXX)

# ============================================================
# 强制使用 C++14 标准,且不可降级(REQUIRED)
# ============================================================
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# ============================================================
# 源文件列表(集中管理,方便增减)
# ============================================================
set(SOURCE_FILES
    src/main.cpp
    src/HZPStreamServer.cpp
    src/HZPCameraCapture.cpp
    src/HZPEncoder.cpp
    src/HZPLogger.cpp
    src/HZPServerController.cpp
)

# ============================================================
# 生成可执行文件
# ============================================================
add_executable(HZPStreamServer ${SOURCE_FILES})

# ============================================================
# 头文件搜索路径:仅对本目标有效(PRIVATE),避免污染全局
# ============================================================
target_include_directories(HZPStreamServer PRIVATE
    ${PROJECT_SOURCE_DIR}/include
)

# ============================================================
# 查找必需库:x264(必须找到,否则报错退出)
# ============================================================
find_library(X264_LIBRARY NAMES x264 REQUIRED)
if(NOT X264_LIBRARY)
    message(FATAL_ERROR "libx264 not found!")
endif()

# ============================================================
# 查找可选库:v4l2(本项目直接使用 ioctl,若调用 v4l2_open 等则需要)
# ============================================================
find_library(V4L2_LIBRARY NAMES v4l2)

# ============================================================
# 查找 spdlog 库(必需,用于异步日志)
# ============================================================
find_package(spdlog REQUIRED)

# ============================================================
# 链接所有依赖库到目标
# ============================================================
target_link_libraries(HZPStreamServer PRIVATE
    ${X264_LIBRARY}       # libx264
    ${V4L2_LIBRARY}       # libv4l2(可能为空)
    spdlog::spdlog        # spdlog 异步日志
    pthread               # spdlog 异步线程池需要
)

关键说明
- target_include_directories 使用 PRIVATE 限定,防止此目标的包含路径泄漏给其他目标。
- find_library 查找系统库,返回绝对路径,避免 link_directories 的全局污染。
- spdlog 通过 find_package 导入,使用现代 CMake 的目标链接方式。
- 显式链接 pthread 是因为 spdlog 的异步模式需要线程库(尽管某些编译器会自动添加,显式写更安全)。


三、公共头文件 include/HZPCommonDef.h

#ifndef HZPCOMMONDEF_H
#define HZPCOMMONDEF_H

// ==================== 标准 C/C++ 头文件 ====================
#include <cstdio>           // printf, fprintf
#include <cstdlib>          // malloc, free
#include <cstring>          // memset, memcpy
#include <cstdint>          // uint8_t, uint32_t
#include <cerrno>           // errno
#include <unistd.h>         // close, usleep
#include <fcntl.h>          // open, O_RDWR
#include <sys/ioctl.h>      // ioctl
#include <sys/mman.h>       // mmap, munmap

// ==================== V4L2 相关头文件 ====================
#include <linux/videodev2.h>

// ==================== X264 头文件 ====================
#include <x264.h>

// ==================== 项目常量(现代 C++ constexpr) ====================
namespace HZP {
    constexpr const char*   CAMERA_DEVICE     = "/dev/video0";   // 摄像头设备节点
    constexpr int           VIDEO_WIDTH       = 640;             // 采集宽度
    constexpr int           VIDEO_HEIGHT      = 480;             // 采集高度
    constexpr uint32_t      PIXEL_FORMAT      = V4L2_PIX_FMT_YUYV; // 原始像素格式(YUV422)
    constexpr int           BUFFER_COUNT      = 4;               // mmap 缓冲区数量
    constexpr int           H264_BUF_MAX_SIZE = 1024 * 1024;     // H.264 输出缓冲区 1MB
    constexpr int           SERVER_PORT       = 30000;           // 服务器监听端口(后续使用)
}

#endif // HZPCOMMONDEF_H

说明:使用 namespace HZP 替代宏定义,保证常量类型安全且作用域清晰。所有摄像头及编码相关常量集中于此,方便日后修改。因为已有 spdlog 日志系统,故移除了 HZP_DEBUGHZP_LOG 调试宏。


四、异步日志模块

4.1 头文件 include/HZPLogger.h

#ifndef HZPLOGGER_H
#define HZPLOGGER_H

namespace HZP {

/**
 * @brief 日志系统 RAII 守卫
 *
 * 构造时自动初始化异步 spdlog(控制台 + 滚动文件),
 * 析构时自动调用 shutdown 安全清理。
 * 使用方式:在 main 函数开头创建对象即可。
 */
class LoggerGuard {
public:
    LoggerGuard();      // 初始化异步日志
    ~LoggerGuard();     // 清理日志资源
};

} // namespace HZP

#endif // HZPLOGGER_H

4.2 实现 src/HZPLogger.cpp

#include "HZPLogger.h"
#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>          // 彩色控制台 sink
#include <spdlog/sinks/rotating_file_sink.h>          // 滚动文件 sink
#include <spdlog/async.h>                             // 异步日志支持

namespace HZP {

LoggerGuard::LoggerGuard() {
    // 1. 初始化全局异步线程池(队列大小 8192,1 个后台刷盘线程)
    spdlog::init_thread_pool(8192, 1);

    // 2. 创建控制台 sink(带色彩)
    auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();

    // 3. 创建滚动文件 sink(单个文件最大 5MB,最多保留 3 个滚动文件)
    auto file_sink = std::make_shared<spdlog::sinks::rotating_file_sink_mt>(
        "logs/hzp_stream_server.log", 1024 * 1024 * 5, 3);

    // 4. 创建异步 logger,挂接两个 sink
    auto async_logger = std::make_shared<spdlog::async_logger>(
        "HZP_Logger",
        spdlog::sinks_init_list{console_sink, file_sink},
        spdlog::thread_pool(),                               // 使用全局线程池
        spdlog::async_overflow_policy::block                 // 队列满时阻塞等待
    );

    // 5. 设置日志格式:[日期 时间.毫秒] [级别] [线程ID] 内容
    async_logger->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%^%l%$] [tid:%t] %v");
    // 6. 设置日志级别(开发阶段 debug,生产环境可改为 info)
    async_logger->set_level(spdlog::level::debug);

    // 7. 将该 logger 注册为全局默认,之后可直接使用 spdlog::info(...) 等
    spdlog::set_default_logger(async_logger);
}

LoggerGuard::~LoggerGuard() {
    // 析构时安全关闭日志系统,确保所有日志落盘
    spdlog::shutdown();
}

} // namespace HZP

异步模式优势:日志写入由后台线程处理,不阻塞主线程,适合对性能敏感的流媒体服务器。


五、摄像头采集模块

5.1 头文件 include/HZPCameraCapture.h

#ifndef HZPCAMERACAPTURE_H
#define HZPCAMERACAPTURE_H

#include "HZPCommonDef.h"
#include "HZPEncoder.h"
#include <string>
#include <vector>
#include <memory>

namespace HZP {

/**
 * @brief V4L2 摄像头采集类(RAII 封装)
 *
 * 负责打开设备、设置格式、mmap 内存映射、开启流、读取帧。
 * 内部持有一个 X264 编码器实例,可直接完成采集→编码流水线。
 */
class CameraCapture {
public:
    explicit CameraCapture(const std::string& device_path = CAMERA_DEVICE,
                           int width  = VIDEO_WIDTH,
                           int height = VIDEO_HEIGHT);
    ~CameraCapture();

    // 禁止拷贝,设备句柄唯一
    CameraCapture(const CameraCapture&) = delete;
    CameraCapture& operator=(const CameraCapture&) = delete;

    bool    open();                  // 打开设备并完成 V4L2 初始化
    bool    start();                 // 开始视频流采集
    int     readAndEncodeFrame();    // 读取一帧并编码(0 成功,-1 失败)
    bool    stop();                  // 停止视频流
    void    close();                 // 关闭设备,释放 mmap

    // 获取内部编码器引用,供外部访问编码结果
    Encoder& getEncoder() { return encoder_; }

private:
    // mmap 缓冲区描述符
    struct MmapBuffer {
        void*   start;
        size_t  length;
    };

    bool initV4l2();      // 设置格式、申请并映射缓冲区
    void uninitV4l2();    // 解除映射并清空缓冲区

    std::string device_path_;               // 设备路径
    int         fd_;                        // 设备文件描述符
    int         width_;                     // 实际采集宽度
    int         height_;                    // 实际采集高度
    std::vector<MmapBuffer> buffers_;       // mmap 缓冲区数组
    Encoder     encoder_;                   // X264 编码器实例
    std::unique_ptr<uint8_t[]> h264_buf_;   // 编码输出缓冲区(智能指针管理)
    unsigned int encoded_length_;           // 当前帧编码字节数
    bool        is_capturing_;              // 是否正在采集标志
};

} // namespace HZP

#endif // HZPCAMERACAPTURE_H

5.2 实现 src/HZPCameraCapture.cpp

#include "HZPCameraCapture.h"
#include <spdlog/spdlog.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <cerrno>
#include <cstring>

namespace HZP {

// -----------------------------------------------------------------
// 局部辅助函数:带信号中断重试的 ioctl
// -----------------------------------------------------------------
static bool xioctl(int fd, int request, void *arg) {
    int r;
    do {
        r = ioctl(fd, request, arg);
    } while (r == -1 && errno == EINTR);       // 被信号中断时重试
    return r != -1;
}

// -----------------------------------------------------------------
// 构造函数
// -----------------------------------------------------------------
CameraCapture::CameraCapture(const std::string& device_path, int width, int height)
    : device_path_(device_path), width_(width), height_(height),
      fd_(-1), encoded_length_(0), is_capturing_(false) {}

// -----------------------------------------------------------------
// 析构函数:自动释放所有资源
// -----------------------------------------------------------------
CameraCapture::~CameraCapture() {
    close();
}

// -----------------------------------------------------------------
// 打开设备并初始化
// -----------------------------------------------------------------
bool CameraCapture::open() {
    // 1. 打开设备文件(非阻塞读写)
    fd_ = ::open(device_path_.c_str(), O_RDWR | O_NONBLOCK);
    if (fd_ < 0) {
        spdlog::error("Failed to open camera device: {}", device_path_);
        return false;
    }
    spdlog::info("Camera opened: {}", device_path_);

    // 2. 初始化 V4L2(查询能力、设置格式、mmap)
    if (!initV4l2()) {
        ::close(fd_);
        fd_ = -1;
        return false;
    }

    // 3. 分配编码输出缓冲区
    h264_buf_ = std::make_unique<uint8_t[]>(H264_BUF_MAX_SIZE);
    return true;
}

// -----------------------------------------------------------------
// 开始视频流采集
// -----------------------------------------------------------------
bool CameraCapture::start() {
    // 将所有缓冲区入队
    for (size_t i = 0; i < buffers_.size(); ++i) {
        struct v4l2_buffer buf{};
        buf.type   = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        buf.memory = V4L2_MEMORY_MMAP;
        buf.index  = static_cast<unsigned int>(i);
        if (!xioctl(fd_, VIDIOC_QBUF, &buf)) {
            spdlog::error("VIDIOC_QBUF failed for buffer {}", i);
            return false;
        }
    }

    // 开启视频流
    enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    if (!xioctl(fd_, VIDIOC_STREAMON, &type)) {
        spdlog::error("VIDIOC_STREAMON failed");
        return false;
    }

    is_capturing_ = true;
    spdlog::info("Camera streaming started");
    return true;
}

// -----------------------------------------------------------------
// 读取一帧并编码(核心循环)
// -----------------------------------------------------------------
int CameraCapture::readAndEncodeFrame() {
    // 1. 从驱动出队一帧(注意:设备以 O_NONBLOCK 打开,无数据时会返回 EAGAIN)
    struct v4l2_buffer buf{};
    buf.type   = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    buf.memory = V4L2_MEMORY_MMAP;

    // 直接使用 ioctl,不通过 xioctl,以便区分 EAGAIN
    if (ioctl(fd_, VIDIOC_DQBUF, &buf) == -1) {
        if (errno == EAGAIN) {
            // 非阻塞模式下无新帧,属于正常情况,返回 -2 告知上层等待
            return -2;
        }
        // 其他错误才是真正的失败
        spdlog::error("VIDIOC_DQBUF failed: {}", strerror(errno));
        return -1;
    }

    // 2. 取原始 YUYV 数据
    uint8_t *yuv422_frame = static_cast<uint8_t*>(buffers_[buf.index].start);

    // 3. 编码
    int enc_len = encoder_.compressFrame(-1, yuv422_frame, h264_buf_.get());
    encoded_length_ = static_cast<unsigned int>(enc_len);

    // 4. 缓冲区重新入队(入队通常不会 EAGAIN,继续用 xioctl 安全)
    if (!xioctl(fd_, VIDIOC_QBUF, &buf)) {
        spdlog::error("VIDIOC_QBUF failed");
        return -1;
    }

    return (enc_len > 0) ? 0 : -1;
}

// -----------------------------------------------------------------
// 停止视频流
// -----------------------------------------------------------------
bool CameraCapture::stop() {
    if (!is_capturing_) return true;         // 避免重复停止

    enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    if (!xioctl(fd_, VIDIOC_STREAMOFF, &type)) {
        spdlog::error("VIDIOC_STREAMOFF failed");
        return false;
    }

    is_capturing_ = false;
    spdlog::info("Camera streaming stopped");
    return true;
}

// -----------------------------------------------------------------
// 关闭设备,释放所有 V4L2 资源
// -----------------------------------------------------------------
void CameraCapture::close() {
    stop();           // 先停止流
    uninitV4l2();     // 解除 mmap
    if (fd_ >= 0) {
        ::close(fd_);
        fd_ = -1;
        spdlog::info("Camera closed");
    }
}

// -----------------------------------------------------------------
// 初始化 V4L2 格式与 mmap 映射(私有)
// -----------------------------------------------------------------
bool CameraCapture::initV4l2() {
    // --- 查询设备能力 ---
    struct v4l2_capability cap{};
    if (!xioctl(fd_, VIDIOC_QUERYCAP, &cap)) {
        spdlog::error("VIDIOC_QUERYCAP failed");
        return false;
    }
    if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE) ||
        !(cap.capabilities & V4L2_CAP_STREAMING)) {
        spdlog::error("Device does not support capture/streaming");
        return false;
    }

    // --- 设置图像格式 ---
    struct v4l2_format fmt{};
    fmt.type                = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    fmt.fmt.pix.width       = width_;
    fmt.fmt.pix.height      = height_;
    fmt.fmt.pix.pixelformat = PIXEL_FORMAT;             // YUYV
    fmt.fmt.pix.field       = V4L2_FIELD_ANY;           // 由驱动决定
    if (!xioctl(fd_, VIDIOC_S_FMT, &fmt)) {
        spdlog::error("VIDIOC_S_FMT failed");
        return false;
    }
    // 驱动可能修正参数,回读实际值
    width_  = fmt.fmt.pix.width;
    height_ = fmt.fmt.pix.height;
    spdlog::info("Set format: {}x{} YUYV", width_, height_);

    // --- 申请缓冲区 ---
    struct v4l2_requestbuffers req{};
    req.count  = BUFFER_COUNT;
    req.type   = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    req.memory = V4L2_MEMORY_MMAP;
    if (!xioctl(fd_, VIDIOC_REQBUFS, &req)) {
        spdlog::error("VIDIOC_REQBUFS failed");
        return false;
    }
    spdlog::info("Requested {} buffers, got {}", BUFFER_COUNT, req.count);

    // --- 映射每个缓冲区到用户空间 ---
    buffers_.resize(req.count);
    for (unsigned int i = 0; i < req.count; ++i) {
        struct v4l2_buffer buf{};
        buf.type   = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        buf.memory = V4L2_MEMORY_MMAP;
        buf.index  = i;
        if (!xioctl(fd_, VIDIOC_QUERYBUF, &buf)) {
            spdlog::error("VIDIOC_QUERYBUF failed for buffer {}", i);
            return false;
        }
        buffers_[i].length = buf.length;
        buffers_[i].start = mmap(nullptr, buf.length,
                                 PROT_READ | PROT_WRITE,
                                 MAP_SHARED,
                                 fd_, buf.m.offset);
        if (buffers_[i].start == MAP_FAILED) {
            spdlog::error("mmap failed for buffer {}", i);
            return false;
        }
    }
    spdlog::info("All buffers mapped successfully");
    return true;
}

// -----------------------------------------------------------------
// 解除 mmap 映射并清空缓冲区数组(私有)
// -----------------------------------------------------------------
void CameraCapture::uninitV4l2() {
    for (auto& buf : buffers_) {
        if (buf.start && buf.start != MAP_FAILED) {
            munmap(buf.start, buf.length);
        }
    }
    buffers_.clear();
}

} // namespace HZP

六、X264 编码模块

6.1 头文件 include/HZPEncoder.h

#ifndef HZPENCODER_H
#define HZPENCODER_H

#include "HZPCommonDef.h"
#include <memory>

namespace HZP {

/**
 * @brief X264 软件编码器 RAII 封装
 *
 * 负责将 YUYV 原始帧转换为 H.264 AnnexB 字节流。
 * 使用智能指针管理底层 C 结构体生命周期。
 */
class Encoder {
public:
    explicit Encoder(int width  = VIDEO_WIDTH, int height = VIDEO_HEIGHT);
    ~Encoder();

    Encoder(const Encoder&) = delete;
    Encoder& operator=(const Encoder&) = delete;

    bool init();    // 初始化参数并打开编码器
    int  compressFrame(int type, uint8_t *in, uint8_t *out); // 压缩一帧
    unsigned int getEncodedLength() const { return encoded_length_; }

private:
    // 自定义释放函数,用于智能指针
    // 注意:结构体本身通过 new 分配,必须用 delete 释放
    static void freeParam(x264_param_t* p)       { delete p; }
    static void cleanPicture(x264_picture_t* p)  { x264_picture_clean(p); delete p; }

    int width_;
    int height_;
    int pts_;                           // 时间戳计数器
    unsigned int encoded_length_;       // 当前帧编码长度

    // 智能指针管理 X264 结构体
    std::unique_ptr<x264_param_t, decltype(&freeParam)> param_;
    std::unique_ptr<x264_picture_t, decltype(&cleanPicture)> pic_in_;
    x264_picture_t pic_out_;
    x264_t*        handle_;             // 编码器句柄
    x264_nal_t*    nals_;               // NAL 单元数组
    int            num_nals_;           // NAL 数量
};

} // namespace HZP

#endif // HZPENCODER_H

6.2 实现 src/HZPEncoder.cpp

#include "HZPEncoder.h"
#include <spdlog/spdlog.h>
#include <cstring>

namespace HZP {

// -----------------------------------------------------------------
// 构造函数:仅初始化基础变量,不分配重资源
// -----------------------------------------------------------------
Encoder::Encoder(int width, int height)
    : width_(width), height_(height), pts_(0), encoded_length_(0),
      param_(nullptr, freeParam),            // 自定义删除器
      pic_in_(nullptr, cleanPicture),        // 自定义删除器
      handle_(nullptr), nals_(nullptr), num_nals_(0) {}

// -----------------------------------------------------------------
// 析构函数:安全关闭编码器
// -----------------------------------------------------------------
Encoder::~Encoder() {
    if (handle_) {
        x264_encoder_close(handle_);
        handle_ = nullptr;
    }
    // param_ 和 pic_in_ 由各自智能指针自动调用删除器释放
    spdlog::debug("X264 Encoder destroyed");
}

// -----------------------------------------------------------------
// 初始化编码器参数并打开
// -----------------------------------------------------------------
bool Encoder::init() {
    // 1. 分配参数与图像结构体
    param_.reset(new x264_param_t);
    pic_in_.reset(new x264_picture_t);

    // 2. 设置默认值,应用预设和调优
    x264_param_default(param_.get());
    x264_param_default_preset(param_.get(), "veryfast", "zerolatency");

    // 3. 基本编码参数
    param_->i_width          = width_;
    param_->i_height         = height_;
    param_->i_csp            = X264_CSP_I420;      // 内部色彩空间 YUV420P
    param_->i_fps_num        = 30;
    param_->i_fps_den        = 1;
    param_->rc.i_lookahead   = 0;                   // 零延迟
    param_->b_annexb         = 1;                   // AnnexB 格式(带起始码)
    param_->i_keyint_max     = 30;                  // 最大关键帧间隔
    param_->i_keyint_min     = 15;
    param_->i_bframe         = 0;                   // 不使用 B 帧
    param_->b_repeat_headers = 1;                   // 每 IDR 前重复 SPS/PPS
    param_->i_threads        = 1;
    param_->i_slice_count    = 1;

    // 4. 应用 baseline profile
    x264_param_apply_profile(param_.get(), "baseline");

    // 5. 打开编码器
    handle_ = x264_encoder_open(param_.get());
    if (!handle_) {
        spdlog::error("x264_encoder_open failed");
        return false;
    }

    // 6. 为输入图像分配内存
    if (x264_picture_alloc(pic_in_.get(), X264_CSP_I420, width_, height_) < 0) {
        spdlog::error("x264_picture_alloc failed");
        return false;
    }
    pic_in_->img.i_csp   = X264_CSP_I420;
    pic_in_->img.i_plane = 3;                     // Y、U、V 三个平面

    spdlog::info("X264 Encoder initialized: {}x{} @ 30fps", width_, height_);
    return true;
}

// -----------------------------------------------------------------
// 压缩一帧:YUYV → I420 → H.264
// -----------------------------------------------------------------
int Encoder::compressFrame(int type, uint8_t *in, uint8_t *out) {
    // ---------- 1. YUYV (YUV422) → I420 (YUV420P) ----------
    uint8_t *y_plane = pic_in_->img.plane[0];
    uint8_t *u_plane = pic_in_->img.plane[1];
    uint8_t *v_plane = pic_in_->img.plane[2];

    // 提取 Y 分量
    for (int i = 0; i < width_ * height_ * 2; i += 2) {
        y_plane[i / 2] = in[i];
    }

    // 提取 U、V 并下采样
    int u_idx = 0, v_idx = 0;
    for (int row = 0; row < height_; row += 2) {
        for (int col = 0; col < width_ * 2; col += 4) {
            int base = row * width_ * 2 + col;
            u_plane[u_idx++] = in[base + 1];
            v_plane[v_idx++] = in[base + 3];
        }
    }

    // ---------- 2. 设置帧类型与时间戳 ----------
    switch (type) {
        case 0:  pic_in_->i_type = X264_TYPE_P;   break;
        case 1:  pic_in_->i_type = X264_TYPE_IDR; break;
        case 2:  pic_in_->i_type = X264_TYPE_I;   break;
        default: pic_in_->i_type = X264_TYPE_AUTO; // 自动决定
    }
    pic_in_->i_pts = pts_++;

    // ---------- 3. 执行编码 ----------
    int nals = 0;
    x264_nal_t *nals_out = nullptr;
    int frame_size = x264_encoder_encode(handle_, &nals_out, &nals,
                                         pic_in_.get(), &pic_out_);
    if (frame_size < 0) {
        spdlog::error("x264_encoder_encode failed");
        return -1;
    }

    // ---------- 4. 拷贝 NAL 单元到输出缓冲区 ----------
    uint8_t *p = out;
    int total = 0;
    for (int i = 0; i < nals; i++) {
        memcpy(p, nals_out[i].p_payload, nals_out[i].i_payload);
        p     += nals_out[i].i_payload;
        total += nals_out[i].i_payload;
    }

    nals_            = nals_out;
    num_nals_        = nals;
    encoded_length_  = total;

    return total;   // 返回实际编码字节数
}

} // namespace HZP

七、流媒体服务器骨架

7.1 头文件 include/HZPStreamServer.h

#ifndef HZPSTREAMSERVER_H
#define HZPSTREAMSERVER_H

namespace HZP {

/**
 * @brief 流媒体服务器类(第一阶段占位)
 *
 * 后续阶段将添加 TCP 监听、客户端管理等完整功能。
 */
class StreamServer {
public:
    StreamServer();
    ~StreamServer();

    StreamServer(const StreamServer&) = delete;
    StreamServer& operator=(const StreamServer&) = delete;

private:
    // 后续添加监听 socket、客户端列表等
};

} // namespace HZP

#endif // HZPSTREAMSERVER_H

7.2 实现 src/HZPStreamServer.cpp

#include "HZPStreamServer.h"
#include <spdlog/spdlog.h>

namespace HZP {

StreamServer::StreamServer() {
    spdlog::info("StreamServer created (placeholder)");
}

StreamServer::~StreamServer() {
    spdlog::info("StreamServer destroyed");
}

} // namespace HZP

八、服务器控制器

为保持 main.cpp 简洁,引入控制器类统一管理摄像头和服务器生命周期。

8.1 头文件 include/HZPServerController.h

#ifndef HZPSERVERCONTROLLER_H
#define HZPSERVERCONTROLLER_H

#include "HZPCameraCapture.h"
#include "HZPStreamServer.h"
#include <memory>

namespace HZP {

/**
 * @brief 服务器控制器,协调摄像头采集与流媒体服务器
 *
 * 职责:
 * - 创建并持有摄像头和服务器实例
 * - 运行主循环:采集 → 编码 → (后续转发)
 * - 信号处理与优雅退出
 */
class ServerController {
public:
    ServerController();
    ~ServerController();

    ServerController(const ServerController&) = delete;
    ServerController& operator=(const ServerController&) = delete;

    /**
     * @brief 启动控制器(进入采集-编码循环)
     * @return 退出状态码(0 正常)
     */
    int run();

    // 请求停止运行(供信号处理调用)
    void requestStop();

private:
    static ServerController* s_instance;    // 用于信号处理的单例

    static void signalHandler(int sig);     // 静态信号处理函数

    std::unique_ptr<CameraCapture> camera_;   // 摄像头对象
    std::unique_ptr<StreamServer>  server_;   // 服务器骨架
    bool running_;                            // 运行标志
};

} // namespace HZP

#endif // HZPSERVERCONTROLLER_H

8.2 实现 src/HZPServerController.cpp

#include "HZPServerController.h"
#include <spdlog/spdlog.h>
#include <csignal>
#include <thread>
#include <chrono>

namespace HZP {

// 单例指针定义
ServerController* ServerController::s_instance = nullptr;

// -----------------------------------------------------------------
// 静态信号处理函数:转发到单例对象
// -----------------------------------------------------------------
void ServerController::signalHandler(int /*sig*/) {
    if (s_instance) {
        s_instance->requestStop();
    }
}

// -----------------------------------------------------------------
// 构造函数:创建对象并注册信号
// -----------------------------------------------------------------
ServerController::ServerController()
    : running_(true)
{
    s_instance = this;   // 设置单例指针,供信号处理使用

    // 注册信号处理
    std::signal(SIGINT,  signalHandler);    // Ctrl+C
    std::signal(SIGTERM, signalHandler);    // kill 命令

    // 创建摄像头和服务器实例
    camera_ = std::make_unique<CameraCapture>(CAMERA_DEVICE, VIDEO_WIDTH, VIDEO_HEIGHT);
    server_ = std::make_unique<StreamServer>();   // 第一阶段仅占位
}

// -----------------------------------------------------------------
// 析构函数
// -----------------------------------------------------------------
ServerController::~ServerController() {
    s_instance = nullptr;
}

// -----------------------------------------------------------------
// 启动控制器:初始化并进入主循环
// -----------------------------------------------------------------
int ServerController::run() {
    // 1. 打开摄像头并开始采集
    if (!camera_->open()) {
        spdlog::critical("Failed to open camera");
        return 1;
    }
    if (!camera_->start()) {
        spdlog::critical("Failed to start streaming");
        return 1;
    }

    // 2. 初始化编码器
    Encoder& encoder = camera_->getEncoder();
    if (!encoder.init()) {
        spdlog::critical("Failed to initialize encoder");
        return 1;
    }

    // 3. 分配 H.264 输出缓冲区(智能指针)
    auto h264_buf = std::make_unique<uint8_t[]>(H264_BUF_MAX_SIZE);

    spdlog::info("Controller started. Entering main loop...");

    // 4. 主循环
    while (running_) {
        int ret = camera_->readAndEncodeFrame();
        if (ret == 0) {
            unsigned int len = encoder.getEncodedLength();
            spdlog::debug("Frame encoded: {} bytes", len);
            // 后续阶段将在此处调用 server_ 进行数据转发
        } else if (ret == -2) {
            // 非阻塞模式下暂无数据,短暂休眠避免 CPU 空转
            std::this_thread::sleep_for(std::chrono::milliseconds(10));
        } else {
            // 真正的采集错误
            spdlog::error("Camera read error, exiting loop");
            break;
        }
    }

    // 5. 优雅退出
    spdlog::info("Shutting down...");
    camera_->close();   // 停止流并释放资源
    return 0;
}

// -----------------------------------------------------------------
// 请求停止主循环
// -----------------------------------------------------------------
void ServerController::requestStop() {
    running_ = false;
}

} // namespace HZP

九、入口程序 src/main.cpp

/**
 * @file main.cpp
 * @brief 第一阶段程序入口
 *
 * 仅负责创建日志系统、控制器,然后启动。
 * 业务逻辑完全封装在 ServerController 中,main 函数极其简洁。
 */
#include "HZPLogger.h"
#include "HZPServerController.h"

int main() {
    // 1. 初始化异步日志(RAII,离开作用域自动清理)
    HZP::LoggerGuard log_guard;

    // 2. 创建控制器并运行
    HZP::ServerController controller;
    return controller.run();

    // 离开 main 时:
    // - controller 析构 → 释放摄像头和服务器
    // - log_guard 析构 → 安全关闭日志
}

十、编译与验证

10.1 环境准备

# 安装编译工具及依赖库
sudo apt-get update
sudo apt-get install -y build-essential cmake pkg-config libv4l-dev libx264-dev libspdlog-dev

10.2 编译

cd ~/HZPStreamServer/build
cmake ..
make -j$(nproc)

无错误生成 HZPStreamServer 可执行文件。nproc可以 查看进程数,如果全用了,可能会造成崩溃。这里使用make -j4

orangepi@orangepi5:~/HZPStreamServer/build$ nproc
8
orangepi@orangepi5:~$ cd ~/HZPStreamServer/build
orangepi@orangepi5:~/HZPStreamServer/build$ cmake ..
-- Configuring done
-- Generating done
-- Build files have been written to: /home/orangepi/HZPStreamServer/build
orangepi@orangepi5:~/HZPStreamServer/build$ make -j4
Consolidate compiler generated dependencies of target HZPStreamServer
[ 42%] Building CXX object CMakeFiles/HZPStreamServer.dir/src/HZPEncoder.cpp.o
[ 42%] Building CXX object CMakeFiles/HZPStreamServer.dir/src/main.cpp.o
[ 42%] Building CXX object CMakeFiles/HZPStreamServer.dir/src/HZPStreamServer.cpp.o
[ 57%] Building CXX object CMakeFiles/HZPStreamServer.dir/src/HZPCameraCapture.cpp.o
[ 71%] Building CXX object CMakeFiles/HZPStreamServer.dir/src/HZPLogger.cpp.o
[ 85%] Building CXX object CMakeFiles/HZPStreamServer.dir/src/HZPServerController.cpp.o
[100%] Linking CXX executable HZPStreamServer
[100%] Built target HZPStreamServer
orangepi@orangepi5:~/HZPStreamServer/build$

注意事项

  • x264 版本:若仍遇到 x264_picture_clean 或类似函数未定义,可改为 free 并省略 x264_picture_clean 调用(因为内存是 x264_picture_alloc 分配的,释放时直接 free 平面数据可能不够安全,但通常也可行)。为彻底避免,可以用 x264_picture_clean 来释放内部数据,而我们用 free(pic) 释放结构体本身,这是正确的。

10.3 运行测试

./HZPStreamServer

预期看到带时间戳和线程 ID 的彩色日志,持续输出类似:

orangepi@orangepi5:~/HZPStreamServer/build$ ./HZPStreamServer
[2026-07-12 23:13:54.585] [info] [tid:3280] StreamServer created (placeholder)
[2026-07-12 23:13:54.666] [info] [tid:3280] Camera opened: /dev/video0
[2026-07-12 23:13:54.667] [info] [tid:3280] Set format: 640x480 YUYV
[2026-07-12 23:13:54.670] [info] [tid:3280] Requested 4 buffers, got 4
[2026-07-12 23:13:54.671] [info] [tid:3280] All buffers mapped successfully
[2026-07-12 23:13:54.695] [info] [tid:3280] Camera streaming started
x264 [info]: using cpu capabilities: ARMv8 NEON
x264 [info]: profile Constrained Baseline, level 3.0, 4:2:0, 8-bit
[2026-07-12 23:13:54.744] [info] [tid:3280] X264 Encoder initialized: 640x480 @ 30fps
[2026-07-12 23:13:54.747] [info] [tid:3280] Controller started. Entering main loop...
[2026-07-12 23:13:55.367] [debug] [tid:3280] Frame encoded: 18268 bytes
[2026-07-12 23:13:55.430] [debug] [tid:3280] Frame encoded: 7466 bytes
[2026-07-12 23:13:55.444] [debug] [tid:3280] Frame encoded: 7506 bytes
[2026-07-12 23:13:55.477] [debug] [tid:3280] Frame encoded: 7543 bytes
[2026-07-12 23:13:55.528] [debug] [tid:3280] Frame encoded: 7785 bytes
[2026-07-12 23:13:55.562] [debug] [tid:3280] Frame encoded: 8362 bytes
[2026-07-12 23:13:55.604] [debug] [tid:3280] Frame encoded: 8847 bytes
[2026-07-12 23:13:55.639] [debug] [tid:3280] Frame encoded: 11949 bytes
[2026-07-12 23:13:55.686] [debug] [tid:3280] Frame encoded: 8799 bytes
^C[2026-07-12 23:13:55.721] [debug] [tid:3280] Frame encoded: 10171 bytes
[2026-07-12 23:13:55.721] [info] [tid:3280] Shutting down...
[2026-07-12 23:13:55.722] [info] [tid:3280] Camera streaming stopped
[2026-07-12 23:13:55.723] [info] [tid:3280] Camera closed
[2026-07-12 23:13:55.723] [info] [tid:3280] StreamServer destroyed
x264 [info]: frame I:1     Avg QP:16.40  size: 18268
x264 [info]: frame P:9     Avg QP:24.66  size:  8714
x264 [info]: mb I  I16..4: 31.2%  0.0% 68.8%
x264 [info]: mb P  I16..4: 51.0%  0.0% 14.3%  P16..4: 23.7%  8.0%  1.4%  0.0%  0.0%    skip: 1.5%
x264 [info]: coded y,uvDC,uvAC intra: 34.6% 55.6% 21.5% inter: 24.7% 72.6% 1.0%
x264 [info]: i16 v,h,dc,p: 29% 28%  9% 33%
x264 [info]: i4 v,h,dc,ddl,ddr,vr,hd,vl,hu: 28% 27% 23%  3%  4%  3%  4%  4%  5%
x264 [info]: i8c dc,h,v,p: 57% 22% 18%  4%
x264 [info]: kb/s:2320.70
[2026-07-12 23:13:55.724] [debug] [tid:3280] X264 Encoder destroyed
orangepi@orangepi5:~/HZPStreamServer/build$

10.4 内存泄漏检查

sudo apt-get install valgrind -y
valgrind --leak-check=full --show-leak-kinds=all ./HZPStreamServer

运行约 10 秒后按 Ctrl+C,确认 definitely lostindirectly lost 均为 0 字节。

  • --leak-check=full: 进行详尽的内存泄漏检查。
  • --show-leak-kinds=all: 显示所有类型的内存泄漏(确定的、可能的、可达的)。

程序运行结束后,Valgrind 会生成一份报告。只需要关注最后两部分:

  1. 看错误摘要 (ERROR SUMMARY)
    - 目标:这里必须是 0 errors
    - 解读:任何非零的数字都代表你的程序存在内存操作错误,比如使用未初始化的内存、数组越界、new/delete 不匹配等。这是需要优先修复的严重问题。
  2. 看内存摘要 (HEAP SUMMARY)
    - 目标in use at exit: 0 bytes in 0 blocks
    - 解读:这行字是“黄金标准”,意味着程序退出时,所有申请的内存都已正确释放,没有内存泄漏。如果这里的字节数不为 0,就说明有内存忘了 freedelete
  3. 看详细信息 (如果有错误)
    - 如果上面两步发现了问题,就需要往报告的上方看。Valgrind 会详细打印出错的类型、发生错误的代码文件和行号,以及相关的调用栈,帮助你快速定位问题。

10.5 常见问题

问题 原因 解决
无法打开 /dev/video0 权限不足 sudo usermod -aG video $USER 并重新登录
VIDIOC_S_FMT 失败 摄像头不支持当前分辨率/格式 v4l2-ctl --list-formats-ext 查看并修改 HZPCommonDef.h 中的常量
编码数据长度为 0 YUYV→I420 转换有误或镜头被遮挡 检查转换逻辑,确保摄像头对准有纹理的场景

十一、阶段总结

第一阶段成功构建了基于现代 C++ 和 RAII 机制的 H.264 实时采集编码程序。我们通过 CameraCapture 类封装了 V4L2 的所有细节,用 Encoder 类管理 X264 编码器资源,通过 ServerController 调度对象并处理信号,最终将主程序缩减至寥寥数行。日志系统采用 spdlog 异步模式,不再使用任何宏调试开关,代码整洁、可维护性极高。

此阶段的代码框架将直接作为第二阶段网络功能的插入点——控制器将负责把编码数据推送至所有在线客户端。所有后续开发都将在此基础上增量进行,无需重写现有模块。