# 鸿蒙开发入门指南:前端开发者快速掌握视频编码
- 一、视频编码到底是干啥的?
- 二、先看一张图:编码器的状态机
- 三、Surface模式:从摄像头到视频文件
- 第1步:创建编码器实例
- 第2步:注册回调函数
- 第3步:配置编码参数
- 第4步:获取 Surface 并启动
- 第5步:通知编码结束
- 第6步:取编码结果
- 第7步:清理
- 四、Buffer模式:从文件到文件
- 区别1:需要自己准备输入数据
- 区别2:需要处理跨距
- 五、运行时动态调整参数
- 六、总结一下两种模式怎么选
- 总结
大家好,我是木斯佳。今天聊点不一样的——鸿蒙的视频编码。说实话,第一次看到这玩意儿我有点懵,这跟前端有啥关系?但后来想想,现在哪个 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 类型创建:
// 按名字创建(可以指定硬件编码器)OH_AVCapability*capability=OH_AVCodec_GetCapability(OH_AVCODEC_MIMETYPE_VIDEO_AVC,true);constchar*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步:注册回调函数
编码器是异步的,数据准备好了会回调你:
// 错误回调staticvoidOnError(OH_AVCodec*codec,int32_terrorCode,void*userData){// 编码出错了}// 数据流变化回调(比如分辨率变了)staticvoidOnStreamChanged(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 来)staticvoidOnNeedInputBuffer(OH_AVCodec*codec,uint32_tindex,OH_AVBuffer*buffer,void*userData){// Surface 模式不处理这个}// 输出回调(编码完了一帧,来这里拿数据)staticvoidOnNewOutputBuffer(OH_AVCodec*codec,uint32_tindex,OH_AVBuffer*buffer,void*userData){outQueue.Enqueue(...);// 把编码好的数据存起来}OH_AVCodecCallback cb={&OnError,&OnStreamChanged,&OnNeedInputBuffer,&OnNewOutputBuffer};OH_VideoEncoder_RegisterCallback(videoEnc,cb,nullptr);前端视角:就是 Promise 或者事件监听,数据到了告诉你。
第3步:配置编码参数
这一步最繁琐,要设置一堆参数:
autoformat=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);// 码率 5MbpsOH_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,交给相机或者其他生产者:
// 获取 SurfaceOHNativeWindow*nativeWindow;OH_VideoEncoder_GetSurface(videoEnc,&nativeWindow);// 准备就绪OH_VideoEncoder_Prepare(videoEnc);// 开始编码OH_VideoEncoder_Start(videoEnc);拿到nativeWindow之后,可以传给相机模块,相机的预览数据就直接送进编码器了,你不需要手动往编码器塞数据。
第5步:通知编码结束
数据送完了,通知编码器收工:
OH_VideoEncoder_NotifyEndOfStream(videoEnc);第6步:取编码结果
在OnNewOutputBuffer回调里,编码好的数据会送过来,你需要把它写进文件:
voidOnNewOutputBuffer(OH_AVCodec*codec,uint32_tindex,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);// 释放这个 bufferOH_VideoEncoder_FreeOutputBuffer(videoEnc,index);}第7步:清理
用完了记得销毁,不然会内存泄漏:
OH_NativeWindow_DestroyNativeWindow(nativeWindow);OH_VideoEncoder_Stop(videoEnc);OH_VideoEncoder_Destroy(videoEnc);videoEnc=nullptr;四、Buffer模式:从文件到文件
Buffer 模式和 Surface 模式的区别在于:数据来源不是 Surface,而是你自己往 buffer 里塞。
大部分步骤一样,主要区别在这几处:
区别1:需要自己准备输入数据
Buffer 模式下,OnNeedInputBuffer回调是有用的:
voidOnNeedInputBuffer(OH_AVCodec*codec,uint32_tindex,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_EOSOH_AVBuffer_SetBufferAttr(buffer,&info);// 推给编码器OH_VideoEncoder_PushInputBuffer(videoEnc,index);}区别2:需要处理跨距
YUV 数据在内存里不一定连续存放,有跨距(stride)的概念——每行数据之间可能有 padding。
官网给了个示例代码,把源数据按跨距复制到目标 buffer:
for(int32_ti=0;i<rect.height;++i){memcpy(dstTemp,srcTemp,rect.width);dstTemp+=dstRect.wStride;// 跳过 paddingsrcTemp+=srcRect.wStride;}前端视角:类似处理 Canvas 的 ImageData,每行可能有额外的字节对齐。
五、运行时动态调整参数
编码过程中可以动态调整参数,比如突然要切一个关键帧,或者码率要动态变化:
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释放,否则可能影响其他应用甚至被系统杀掉。另外不能在回调函数里销毁编码器,会死锁。
有问题评论区聊。