音视频修炼之前置工具链(一):C/C++
·
C/C++ 在音视频中的应用要点 —— 内存 / 指针 / RAII / 字节序
音视频底层 90% 是 C/C++(FFmpeg、x264、libvlc、WebRTC)。Java/Kotlin 工程师转 NDK 必须懂的 C/C++ 实战要点。这一篇讲那些在音视频开发中真正会用到的 C/C++ 知识。
0. 为啥音视频要懂 C/C++
FFmpeg 是 C
x264 / x265 是 C++
libvlc 核心是 C
WebRTC 是 C++
ijkplayer 核心是 C
Android NDK / iOS native 开发都绕不开。这一篇按"频率"列出关键概念。
1. 指针与内存管理
1.1 指针基础
int x = 10;
int* p = &x; // p 是 x 的地址
*p = 20; // 通过指针修改 x
printf("%d\n", x); // 20
1.2 音视频中的常见指针
// AVFrame 拿 YUV plane
AVFrame* frame = av_frame_alloc();
uint8_t* y_plane = frame->data[0]; // Y plane 起始地址
uint8_t* u_plane = frame->data[1];
uint8_t* v_plane = frame->data[2];
int y_stride = frame->linesize[0]; // Y plane 每行字节数
// 读第 i 行第 j 列的 Y 值
uint8_t y_value = y_plane[i * y_stride + j];
1.3 内存分配
// C 风格
uint8_t* buffer = (uint8_t*)malloc(size);
// ... 用 ...
free(buffer);
// FFmpeg 推荐
uint8_t* buffer = av_malloc(size);
av_free(buffer);
// C++ 风格
auto buffer = new uint8_t[size];
delete[] buffer;
// C++ 现代风格 (推荐)
std::unique_ptr<uint8_t[]> buffer(new uint8_t[size]);
// 自动释放, 不用 delete
1.4 内存泄漏 —— 头号大坑
void process_frame() {
AVFrame* f = av_frame_alloc(); // 分配
// ... 处理 ...
if (error_condition) return; // ⛔ 早退忘记 free
av_frame_free(&f);
}
✅ 用 RAII(C++):
class FrameRAII {
AVFrame* frame_;
public:
FrameRAII() : frame_(av_frame_alloc()) {}
~FrameRAII() { av_frame_free(&frame_); }
AVFrame* get() { return frame_; }
};
void process_frame() {
FrameRAII f;
// ... 用 f.get() ...
// 早退也会自动 free
}
1.5 Use-after-free
av_frame_free(&frame);
uint8_t v = frame->data[0][0]; // ⛔ 已释放, 崩溃
✅ free 后置 NULL:
av_frame_free(&frame);
frame = NULL;
2. 引用计数(Reference Counting)
2.1 为什么需要
AVFrame 可能被多个线程引用
- 解码器持有
- 显示队列持有
- 录制队列持有
每个用完才 free → 引用计数
2.2 FFmpeg 引用计数
// 引用同一个 frame
AVFrame* f1 = av_frame_alloc();
// ... 解码 ...
AVFrame* f2 = av_frame_alloc();
av_frame_ref(f2, f1); // f2 引用 f1 的数据 (refcount++)
// 用 f1
// 用 f2
av_frame_unref(f1); // 引用计数 -1
av_frame_unref(f2); // 计数到 0, 自动释放
av_frame_free(&f1);
av_frame_free(&f2);
2.3 C++ shared_ptr
auto frame = std::shared_ptr<AVFrame>(av_frame_alloc(), [](AVFrame* f) {
av_frame_free(&f);
});
auto f2 = frame; // 引用计数 +1, 自动管理
// 离开作用域 → 计数 -1, 到 0 自动 free
3. 字节序(Endianness)
3.1 大端 vs 小端
0x12345678 在内存里:
小端 (Little Endian, x86 / ARM 默认):
地址 N+0: 0x78
地址 N+1: 0x56
地址 N+2: 0x34
地址 N+3: 0x12
大端 (Big Endian, 网络字节序):
地址 N+0: 0x12
地址 N+1: 0x34
地址 N+2: 0x56
地址 N+3: 0x78
3.2 在音视频里何时遇到
RTMP / RTP 包: 大端 (网络字节序)
mp4 box header: 大端
PCM 音频: 小端 (主流)
H.264 NAL: 没字节序问题 (按字节读)
3.3 转换
#include <arpa/inet.h>
uint32_t host_value = 0x12345678;
uint32_t net_value = htonl(host_value); // host → network (大端)
uint32_t back = ntohl(net_value); // network → host
// 16 bit 版本
uint16_t htons(uint16_t);
uint16_t ntohs(uint16_t);
3.4 不转换的常见 bug
// 解析 mp4 box size (大端)
uint32_t size_le = *(uint32_t*)data; // ⛔ 错, 直接读是小端
uint32_t size_be = ntohl(*(uint32_t*)data); // ✅ 对
很多解析器 bug 都是这。
4. 内存对齐(Alignment)
4.1 SIMD 要求对齐
SSE / NEON 等 SIMD 指令要求数据 16 字节对齐
不对齐 → 性能下降 / 崩溃 (老 ARM)
4.2 FFmpeg 的对齐分配
// 不对齐 (普通 malloc)
uint8_t* buf = malloc(1024);
// 对齐 (FFmpeg 推荐, 32 字节对齐)
uint8_t* buf = av_malloc(1024);
// 32 字节对齐 (AVX 用)
uint8_t* buf = av_mallocz(1024);
4.3 stride 也是对齐结果
// 1920×1080 的 Y plane, 每行 1920 字节
// 但 FFmpeg 内部 stride 可能是 1984 (16 字节对齐)
frame->linesize[0] // 实际行字节数 (包括 padding)
frame->width // 真实宽度
写代码必须按 linesize 跨行,不是按 width。
5. C 风格字符串与 std::string
5.1 C 风格
char filename[256] = "video.mp4";
size_t len = strlen(filename);
// 拼接 (危险, 容易溢出)
strcat(filename, ".bak");
// 安全拼接
snprintf(filename, sizeof(filename), "%s.bak", filename);
5.2 std::string(C++)
std::string path = "video.mp4";
path += ".bak"; // 安全
size_t len = path.length();
const char* c_str = path.c_str(); // 给 C 函数用
5.3 跨语言:JNI 字符串
JNIEXPORT void JNICALL func(JNIEnv* env, jobject obj, jstring jpath) {
const char* path = (*env)->GetStringUTFChars(env, jpath, NULL);
// ... 用 path ...
(*env)->ReleaseStringUTFChars(env, jpath, path); // ⚠️ 必须 release
}
参考第 0.3 篇 JNI。
6. 函数指针(Callback)
6.1 C 风格回调
typedef int (*decode_callback_t)(void* user, AVFrame* frame);
void register_callback(decode_callback_t cb, void* user_data) {
g_callback = cb;
g_user_data = user_data;
}
// 用户实现
int my_decode_callback(void* user, AVFrame* frame) {
MyContext* ctx = (MyContext*)user;
// 处理 frame
return 0;
}
register_callback(my_decode_callback, &my_ctx);
6.2 C++ 现代风格
#include <functional>
void register_callback(std::function<int(AVFrame*)> cb) {
g_callback = cb;
}
// 用 lambda
register_callback([this](AVFrame* f) {
return this->process(f);
});
7. 多线程基础
7.1 pthread
#include <pthread.h>
void* worker(void* arg) {
// 工作线程
return NULL;
}
pthread_t tid;
pthread_create(&tid, NULL, worker, NULL);
pthread_join(tid, NULL);
7.2 互斥锁
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(&mtx);
// 临界区
pthread_mutex_unlock(&mtx);
7.3 条件变量(生产者消费者)
pthread_mutex_t mtx;
pthread_cond_t cond;
Queue* q;
void producer() {
pthread_mutex_lock(&mtx);
queue_put(q, item);
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mtx);
}
void consumer() {
pthread_mutex_lock(&mtx);
while (queue_empty(q)) {
pthread_cond_wait(&cond, &mtx);
}
item = queue_get(q);
pthread_mutex_unlock(&mtx);
}
7.4 C++11 标准库
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
std::queue<int> q;
void producer() {
std::lock_guard<std::mutex> lock(mtx);
q.push(item);
cv.notify_one();
}
void consumer() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [&]{ return !q.empty(); });
item = q.front();
q.pop();
}
8. 几个音视频专用技巧
8.1 ring buffer(环形缓冲区)
typedef struct {
uint8_t* data;
size_t size;
size_t read_pos;
size_t write_pos;
} RingBuffer;
void rb_write(RingBuffer* rb, const uint8_t* src, size_t len) {
if (rb->write_pos + len <= rb->size) {
memcpy(rb->data + rb->write_pos, src, len);
} else {
size_t first = rb->size - rb->write_pos;
memcpy(rb->data + rb->write_pos, src, first);
memcpy(rb->data, src + first, len - first);
}
rb->write_pos = (rb->write_pos + len) % rb->size;
}
音频应用最常用,避免 audio underrun 时数据回环。
8.2 lock-free 队列
音视频高频数据流(每秒万次)用普通锁会卡。lock-free 单生产者单消费者:
typedef struct {
void** items;
atomic_int read_idx;
atomic_int write_idx;
int size;
} LockFreeQueue;
详细实现略,MediaCodec / WebRTC 内部都用 lock-free。
8.3 SIMD 优化
#include <arm_neon.h>
// NEON 加速 YUV → RGB
void yuv_to_rgb_neon(uint8_t* y, uint8_t* u, uint8_t* v, uint8_t* rgb, int n) {
for (int i = 0; i < n; i += 8) {
uint8x8_t y_v = vld1_u8(y + i);
// ... NEON 计算 ...
vst3_u8(rgb + i*3, ...);
}
}
ARM 上比纯 C 快 5-10 倍。FFmpeg 的 swscale 大量用 NEON。
9. 常见崩溃 / 内存错误调试
9.1 用 ASAN(AddressSanitizer)
# 编译时加
gcc -fsanitize=address -g demo.c -o demo
# 运行
./demo
# 任何内存错误会立即打印
ASAN 能抓到:
- 缓冲区溢出
- use-after-free
- 内存泄漏
- double-free
调试 C/C++ 必备。
9.2 valgrind
valgrind --leak-check=full ./demo
更慢但更全面。
9.3 lldb / gdb
# 崩溃后:
lldb demo
> run
> bt # 看堆栈
10. 几个 C/C++ 在音视频里的"惯用法"
10.1 错误码风格
int decode_frame(...) {
if (...) return -1; // 错误
if (...) return AVERROR(EAGAIN);
return 0; // 成功
}
// 调用方:
int ret = decode_frame(...);
if (ret == AVERROR(EAGAIN)) { /* 重试 */ }
else if (ret < 0) { /* 错误 */ }
else { /* 成功 */ }
FFmpeg / MediaCodec 都是这风格。
10.2 上下文结构体(C 的"对象")
typedef struct {
AVFormatContext* fmt;
AVCodecContext* dec;
int video_idx;
pthread_t thread;
// ... 几十个成员 ...
} Player;
Player* player_create();
int player_open(Player* p, const char* url);
void player_close(Player* p);
这是 FFmpeg / VLC 的标准风格——没有 class,但用结构体 + 函数实现"对象"。
10.3 Opaque pointer(PIMPL)
// 头文件
typedef struct PlayerImpl Player;
Player* player_create();
void player_destroy(Player* p);
// 实现文件
struct PlayerImpl {
// 内部细节
};
API 暴露不暴露内部成员,改实现不破坏 ABI。WebRTC / VLC 大量用。
11. 总结
音视频开发的 C/C++ 关键认知:
1. 内存管理 (malloc/free/RAII/refcount)
2. 字节序 (网络字节序 vs 主机字节序)
3. 内存对齐 (SIMD / stride)
4. 函数指针 + 上下文结构体 (C "对象")
5. 多线程 (pthread / std::thread / lock-free)
6. JNI / 跨语言数据传递 (参考 0.3 篇)
学习路径建议:
先会用: C 基础 + 指针 + 引用计数 + pthread
再深入: SIMD + lock-free + RAII + PIMPL
最后: ASAN + 调试技巧
金句:音视频开发的 C/C++ 不需要变成"语言专家"。但掌握指针 / 内存管理 / 字节序这 3 个,就能看懂 80% 的 FFmpeg 源码。
更多推荐
所有评论(0)