# 鸿蒙开发入门指南:前端开发者快速掌握视频编码
-
- 一、视频编码到底是干啥的?
- 二、先看一张图:编码器的状态机
- 三、Surface模式:从摄像头到视频文件
-
- 第1步:创建编码器实例
- 第2步:注册回调函数
- 第3步:配置编码参数
- [第4步:获取 Surface 并启动](#第4步:获取 Surface 并启动)
- 第5步:通知编码结束
- 第6步:取编码结果
- 第7步:清理
- 四、Buffer模式:从文件到文件
- 五、运行时动态调整参数
- 六、总结一下两种模式怎么选
- 总结
大家好,我是木斯佳。今天聊点不一样的------鸿蒙的视频编码。说实话,第一次看到这玩意儿我有点懵,这跟前端有啥关系?但后来想想,现在哪个 App 不上传视频?做音视频编辑、视频上传、直播推流,都绕不开编码。鸿蒙这套 API 是 C++ 的,但设计思路其实挺有意思,咱们用前端的思维来理解一下。
一、视频编码到底是干啥的?
先说说我的理解:你手机拍视频,录出来的是原始数据,一秒钟几十兆,根本存不下也传不动。编码就是把这些原始数据压缩成 H.264、H.265 这种格式,体积能小几十倍。
鸿蒙提供的这套 Native API,就是让你在 C++ 层干这个活。支持 H.264、H.265,还支持 HDR Vivid(就是高动态范围标准)。
官网文档里区分了两种输入模式:
- Surface模式:数据来自摄像头预览画面、屏幕录制这些地方,直接对接 GPU 纹理,效率最高
- Buffer模式:数据来自文件,你自己往内存里塞,适合读本地 YUV 文件转码
打个比方:Surface模式像「流水线直接对接」,Buffer模式像「自己搬砖」。
二、先看一张图:编码器的状态机

官网给了个状态机图,比较难理解,我用自己的语言给大家翻译一下,这其实就和我们常用的生命周期是类似原理:
| 状态 | 前端类比 |
|---|---|
| Initialized | new 了一个实例,还没配置 |
| Configured | 参数配好了,宽高、码率都设置完 |
| Prepared | 准备工作做完,等着启动 |
| Executing | 正在编码,处理每一帧 |
| Flushed | 清空了缓冲区,但没停 |
| EOS | 文件末尾,最后一帧处理完了 |
| Error | 出错了,比如输入数据格式不对 |
| Released | 实例销毁,资源释放 |
流程大概是:创建 → 配置 → 准备 → 启动 → 编码 → 结束 → 销毁。
如果中途出错或者想重置,可以走 Flush 或者 Reset 回到之前的状态。
前端视角:这有点像 Video 元素的生命周期,或者 WebCodecs API 的状态管理。
三、Surface模式:从摄像头到视频文件
官网给了完整的示例代码,我把核心步骤串一遍。
第1步:创建编码器实例
有两种方式,按名字创建或者按 MIME 类型创建:
cpp
// 按名字创建(可以指定硬件编码器)
OH_AVCapability *capability = OH_AVCodec_GetCapability(OH_AVCODEC_MIMETYPE_VIDEO_AVC, true);
const char *codecName = OH_AVCapability_GetName(capability);
OH_AVCodec *videoEnc = OH_VideoEncoder_CreateByName(codecName);
// 按 MIME 创建(更简单)
OH_AVCodec *videoEnc = OH_VideoEncoder_CreateByMime(OH_AVCODEC_MIMETYPE_VIDEO_AVC);
前端视角 :类似 new VideoEncoder(),但这里需要指定用软解还是硬解。
第2步:注册回调函数
编码器是异步的,数据准备好了会回调你:
cpp
// 错误回调
static void OnError(OH_AVCodec *codec, int32_t errorCode, void *userData) {
// 编码出错了
}
// 数据流变化回调(比如分辨率变了)
static void OnStreamChanged(OH_AVCodec *codec, OH_AVFormat *format, void *userData) {
// 从 format 里取出新的宽高
OH_AVFormat_GetIntValue(format, OH_MD_KEY_VIDEO_PIC_WIDTH, &width);
OH_AVFormat_GetIntValue(format, OH_MD_KEY_VIDEO_PIC_HEIGHT, &height);
}
// 输入回调(Surface模式下没啥用,数据从 surface 来)
static void OnNeedInputBuffer(OH_AVCodec *codec, uint32_t index, OH_AVBuffer *buffer, void *userData) {
// Surface 模式不处理这个
}
// 输出回调(编码完了一帧,来这里拿数据)
static void OnNewOutputBuffer(OH_AVCodec *codec, uint32_t index, OH_AVBuffer *buffer, void *userData) {
outQueue.Enqueue(...); // 把编码好的数据存起来
}
OH_AVCodecCallback cb = {&OnError, &OnStreamChanged, &OnNeedInputBuffer, &OnNewOutputBuffer};
OH_VideoEncoder_RegisterCallback(videoEnc, cb, nullptr);
前端视角:就是 Promise 或者事件监听,数据到了告诉你。
第3步:配置编码参数
这一步最繁琐,要设置一堆参数:
cpp
auto format = OH_AVFormat_Create();
OH_AVFormat_SetIntValue(format, OH_MD_KEY_WIDTH, 320); // 宽度
OH_AVFormat_SetIntValue(format, OH_MD_KEY_HEIGHT, 240); // 高度
OH_AVFormat_SetIntValue(format, OH_MD_KEY_PIXEL_FORMAT, AV_PIXEL_FORMAT_NV12); // 像素格式
OH_AVFormat_SetDoubleValue(format, OH_MD_KEY_FRAME_RATE, 30); // 帧率
OH_AVFormat_SetLongValue(format, OH_MD_KEY_BITRATE, 5000000); // 码率 5Mbps
OH_AVFormat_SetIntValue(format, OH_MD_KEY_I_FRAME_INTERVAL, 1000); // 关键帧间隔 1 秒
OH_VideoEncoder_Configure(videoEnc, format);
前端视角 :类似 new VideoEncoder({output, error}) 之后调用 configure(),参数差不多。
第4步:获取 Surface 并启动
这是 Surface 模式的关键------从编码器拿一个 Surface,交给相机或者其他生产者:
cpp
// 获取 Surface
OHNativeWindow *nativeWindow;
OH_VideoEncoder_GetSurface(videoEnc, &nativeWindow);
// 准备就绪
OH_VideoEncoder_Prepare(videoEnc);
// 开始编码
OH_VideoEncoder_Start(videoEnc);
拿到 nativeWindow 之后,可以传给相机模块,相机的预览数据就直接送进编码器了,你不需要手动往编码器塞数据。
第5步:通知编码结束
数据送完了,通知编码器收工:
cpp
OH_VideoEncoder_NotifyEndOfStream(videoEnc);
第6步:取编码结果
在 OnNewOutputBuffer 回调里,编码好的数据会送过来,你需要把它写进文件:
cpp
void OnNewOutputBuffer(OH_AVCodec *codec, uint32_t index, OH_AVBuffer *buffer, void *userData) {
// 获取编码后的数据
OH_AVCodecBufferAttr info;
OH_AVBuffer_GetBufferAttr(buffer, &info);
// 写入文件
uint8_t *data = OH_AVBuffer_GetAddr(buffer);
outputFile->write(data, info.size);
// 释放这个 buffer
OH_VideoEncoder_FreeOutputBuffer(videoEnc, index);
}
第7步:清理
用完了记得销毁,不然会内存泄漏:
cpp
OH_NativeWindow_DestroyNativeWindow(nativeWindow);
OH_VideoEncoder_Stop(videoEnc);
OH_VideoEncoder_Destroy(videoEnc);
videoEnc = nullptr;
四、Buffer模式:从文件到文件
Buffer 模式和 Surface 模式的区别在于:数据来源不是 Surface,而是你自己往 buffer 里塞。
大部分步骤一样,主要区别在这几处:
区别1:需要自己准备输入数据
Buffer 模式下,OnNeedInputBuffer 回调是有用的:
cpp
void OnNeedInputBuffer(OH_AVCodec *codec, uint32_t index, OH_AVBuffer *buffer, void *userData) {
// 从文件读取一帧 YUV 数据
uint8_t *addr = OH_AVBuffer_GetAddr(buffer);
inputFile->read(addr, frameSize);
// 配置这一帧的信息(大小、时间戳、是不是关键帧)
OH_AVCodecBufferAttr info;
info.size = frameSize;
info.pts = frameIndex * 1000000 / frameRate; // 时间戳
info.flags = 0; // 普通帧,最后一帧设为 AVCODEC_BUFFER_FLAGS_EOS
OH_AVBuffer_SetBufferAttr(buffer, &info);
// 推给编码器
OH_VideoEncoder_PushInputBuffer(videoEnc, index);
}
区别2:需要处理跨距
YUV 数据在内存里不一定连续存放,有跨距(stride)的概念------每行数据之间可能有 padding。
官网给了个示例代码,把源数据按跨距复制到目标 buffer:
cpp
for (int32_t i = 0; i < rect.height; ++i) {
memcpy(dstTemp, srcTemp, rect.width);
dstTemp += dstRect.wStride; // 跳过 padding
srcTemp += srcRect.wStride;
}
前端视角:类似处理 Canvas 的 ImageData,每行可能有额外的字节对齐。
五、运行时动态调整参数
编码过程中可以动态调整参数,比如突然要切一个关键帧,或者码率要动态变化:
cpp
OH_AVFormat *format = OH_AVFormat_Create();
// 强制下一帧是关键帧
OH_AVFormat_SetIntValue(format, OH_MD_KEY_REQUEST_I_FRAME, true);
// 动态调整码率(VBR 模式)
OH_AVFormat_SetLongValue(format, OH_MD_KEY_BITRATE, 2000000);
// 动态调整帧率
OH_AVFormat_SetDoubleValue(format, OH_MD_KEY_FRAME_RATE, 60.0);
OH_VideoEncoder_SetParameter(videoEnc, format);
OH_AVFormat_Destroy(format);
这个能力挺实用的------网络不好的时候降码率,用户滑动进度条的时候切关键帧。
六、总结一下两种模式怎么选
| 场景 | 推荐模式 | 原因 |
|---|---|---|
| 相机拍摄 | Surface | 直接对接相机输出,零拷贝 |
| 屏幕录制 | Surface | 直接从 GPU 拿数据 |
| 文件转码 | Buffer | 自己控制数据来源 |
| 视频编辑 | Buffer | 需要逐帧处理 |
| 实时滤镜 | Buffer | 需要拿到 YUV 数据做滤镜处理 |
一句话:能走 Surface 就走 Surface,性能好;只有需要逐帧操作数据时才用 Buffer。
总结
虽然这套 API 是 C++ 的,但设计思路和 WebCodecs 有相似之处:
| 概念 | WebCodecs | 鸿蒙 |
|---|---|---|
| 编码器 | VideoEncoder | OH_VideoEncoder |
| 配置参数 | configure() | OH_VideoEncoder_Configure() |
| 编码帧 | encode() | OH_VideoEncoder_PushInputBuffer() |
| 输出帧 | output 回调 | OnNewOutputBuffer 回调 |
| 结束 | flush() | NotifyEndOfStream() / EOS flag |
如果用过 WebCodecs,上手鸿蒙这套 API 会快很多。没接触过也没关系,核心就是:配置参数 → 塞数据 → 拿结果 → 清理,所有编解码器都是这个套路。
最后提醒一下:硬件编码器资源有限,用完必须调用 OH_VideoEncoder_Destroy 释放,否则可能影响其他应用甚至被系统杀掉。另外不能在回调函数里销毁编码器,会死锁。
有问题评论区聊。