
本阶段的目标是验证视频采集链路:从 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 最低版本要求,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_DEBUG和HZP_LOG调试宏。
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
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
异步模式优势:日志写入由后台线程处理,不阻塞主线程,适合对性能敏感的流媒体服务器。
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
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
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
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
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
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 简洁,引入控制器类统一管理摄像头和服务器生命周期。
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
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 析构 → 安全关闭日志
}
# 安装编译工具及依赖库
sudo apt-get update
sudo apt-get install -y build-essential cmake pkg-config libv4l-dev libx264-dev libspdlog-dev
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_picture_clean 或类似函数未定义,可改为 free 并省略 x264_picture_clean 调用(因为内存是 x264_picture_alloc 分配的,释放时直接 free 平面数据可能不够安全,但通常也可行)。为彻底避免,可以用 x264_picture_clean 来释放内部数据,而我们用 free(pic) 释放结构体本身,这是正确的。./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$
sudo apt-get install valgrind -y
valgrind --leak-check=full --show-leak-kinds=all ./HZPStreamServer
运行约 10 秒后按 Ctrl+C,确认 definitely lost 和 indirectly lost 均为 0 字节。
--leak-check=full: 进行详尽的内存泄漏检查。--show-leak-kinds=all: 显示所有类型的内存泄漏(确定的、可能的、可达的)。程序运行结束后,Valgrind 会生成一份报告。只需要关注最后两部分:
ERROR SUMMARY)0 errors。new/delete 不匹配等。这是需要优先修复的严重问题。HEAP SUMMARY)in use at exit: 0 bytes in 0 blocks。free 或 delete。| 问题 | 原因 | 解决 |
|---|---|---|
无法打开 /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 异步模式,不再使用任何宏调试开关,代码整洁、可维护性极高。
此阶段的代码框架将直接作为第二阶段网络功能的插入点——控制器将负责把编码数据推送至所有在线客户端。所有后续开发都将在此基础上增量进行,无需重写现有模块。