智能客服
你问我答,随时在线为你解决问题
从API version 20开始支持ROI视频编码(Region Of Interest Video Coding),该功能是基于硬件H.264/H.265编码能力扩展的高级优化技术。其核心逻辑为对画面中指定的重点区域分配更多编码资源实现高画质编码。在有限带宽条件下保障ROI区域内容清晰呈现,显著提升整体视觉体验。
开发者可自主定义视频画面中的ROI区域(如直播中的人脸、监控中的车牌等),并通过设定质量偏移参数,调节ROI区域与非ROI区域的编码质量差异,实现编码资源的差异化分配。
ROI视频编码适用于因网络带宽限制导致码率不能满足视频画质要求,且能明确定义关键画面内容(ROI区域)的场景。比如视频通话、视频直播、安全监控等。
各场景中ROI区域的选择建议如下:
根据编码模式和ROI配置方式的不同,提供了三种ROI编码开发示例,开发者可根据实际业务和技术架构选择。
| 不同场景对照点 | 直播/视频通话场景 | 录像场景 | 编辑导出/内容发布场景 |
|---|---|---|---|
| ROI信息生产者 | 相机 | 相机 | 应用 |
| ROI信息获取方式 | 从视频帧NativeBuffer元数据提取 | 从视频帧NativeBuffer元数据提取 | 应用自管理 |
| 编码视频帧直接生产者 | 图形 | 图形 | 应用 |
| 编码模式 | Surface模式 | Surface模式 | Buffer模式 |
| ROI参数配置方式 | NativeBuffer元数据配置(推荐) | 编码输入参数回调配置 | 编码输入Buffer回调配置 |
| 开发示例 | Surface模式 - 方式一 | Surface模式 - 方式二 | Buffer模式 |
支持的编码器: H.264 8bit硬件编码、H.265 8bit硬件编码、H.265 10bit硬件编码。
支持的码控模式: VBR(Variable Bit Rate)、CBR(Constant Bit Rate)、SQR(Stable Quality Rate Control)。
依赖ROI检测识别能力: 编码器不具备ROI的检测识别能力,所以ROI编码技术生效依赖于开发者输入的ROI信息。开发者可根据业务场景自行设计并实现ROI识别能力,或通过调用系统相机模块原生提供的人脸区域信息,降低开发成本,具体参考元数据(C/C++)。
ROI配置参数最终以字符串形式下发到编码器。从API版本26.0.0开始支持键值对格式,同时兼容旧版纯数值格式。所有坐标参数和数值参数均为整数。
当ROI数据来源于相机帧的NativeBuffer元数据时,开发者无需手动拼接字符串。可使用OH_VideoMetadata_GetRoiCount、OH_VideoMetadata_ParseRoiString和OH_VideoMetadata_AppendRoiString接口直接从相机帧的ROI元数据中解析区域信息、设置键值对参数(如OH_MD_KEY_VIDEO_METADATA_ROI_DELTA_QP、OH_MD_KEY_VIDEO_METADATA_ROI_SEM_LABEL),并自动生成符合编码器要求的配置字符串。
字符串格式:
矩形区域定义:
ROI是一个矩形区域,Top,Left和Bottom,Right分别定义了ROI的区域在图像中的左上角和右下角的坐标位置(如图1所示)。
键值对参数:
| 参数 | 含义 | 取值范围 | 是否必填 | 默认行为 |
|---|---|---|---|---|
| dqp | 量化参数偏移值(DeltaQP) | [-51, 51] | 否 | 未设置时编码器使用默认QP策略(=-3)。 |
| slb | 语义标签(Semantic Label) | 0(其他)或1(人脸) | 否 | 该参数仅供开发者区分ROI区域类型,不影响编码行为。 |
数量和面积限制:
图1:ROI坐标和最大允许面积占比示意图

配置ROI支持两种方式:NativeBuffer元数据配置方式和编码输入回调配置方式。编码输入回调配置方式包含编码输入参数回调(Surface模式)和编码输入buffer回调(Buffer模式)。
通用生效机制:
NativeBuffer元数据配置方式独有机制: 最大支持256Byte长度字符,超出部分会被截断。
空字符串处理差异:
因空字符串处理存在差异,应避免配置空字符串。如需关闭某一帧的ROI编码,可配置无位置信息的字符串,如"Clear"或";"等。
同时配置时的生效优先级: 当某一帧的两种方式均有配置ROI参数,仅生效以编码输入回调配置方式下发的ROI参数,无论其能否解析出有效ROI信息。
以下开发示例按照编码模式组织。Surface模式和Buffer模式共用相同的ROI信息获取与组装流程,区别在于如何将ROI配置到编码器。
当相机输出视频帧时,如果检测到ROI区域(如人脸),会将ROI信息写入帧的NativeBuffer元数据。开发者可直接从每帧中提取,无需额外的回调接口和时间戳匹配,具体参考OH_NativeBuffer_MetadataKey。
Surface模式下,相机将视频帧输出到OH_NativeImage的Surface上,开发者在帧处理线程中从每帧的NativeBuffer元数据提取ROI信息并组装成配置字符串,随后将ROI字符串写入编码器输入帧的NativeBuffer元数据,实现ROI随帧下发到编码器(如图2所示)。
图2:NativeBuffer元数据接口配置ROI流程图

详细开发步骤如下:
在CMakeList.txt中链接动态库。
set(BASE_LIBRARY
libace_napi.z.so libEGL.so libGLESv3.so libace_ndk.z.so libuv.so libhilog_ndk.z.so
libnative_media_codecbase.so libnative_media_core.so libnative_media_vdec.so libnative_window.so
libnative_media_venc.so libnative_media_acodec.so libnative_media_avdemuxer.so libnative_media_avsource.so
libnative_media_avmuxer.so libohaudio.so libnative_buffer.so libnative_vsync.so libnative_image.so libdeviceinfo_ndk.z.so
)
add_library(recorder SHARED recorder/RecorderNative.cpp
recorder/Recorder.cpp
capbilities/codec/Muxer.cpp
capbilities/codec/VideoEncoder.cpp
capbilities/codec/AudioCapturer.cpp
capbilities/codec/AudioEncoder.cpp
capbilities/codec/CodecCallback.cpp
capbilities/render/egl_render_context.cpp
capbilities/render/render_thread.cpp
capbilities/render/shader_program.cpp
common/RoiQueue.cpp
)
target_link_libraries(recorder PUBLIC ${BASE_LIBRARY} player) 上述'recorder','player'字样仅为示例,此处应由开发者根据具体的CMake工程目标名称进行替换。
从视频帧NativeBuffer元数据提取ROI信息。
相机输出视频帧时,若检测到ROI区域(如人脸),会将ROI信息写入帧的NativeBuffer元数据中。开发者可通过OH_NativeBuffer_GetMetadataValue提取ROI原始字符串。
OH_NativeBuffer *nativeBuffer = nullptr;
int32_t ret = OH_NativeBuffer_FromNativeWindowBuffer(InBuffer, &nativeBuffer);
if (ret == 0 && nativeBuffer != nullptr) {
int32_t roiSize = 0;
uint8_t *roiData = nullptr;
ret = OH_NativeBuffer_GetMetadataValue(nativeBuffer,
OH_NativeBuffer_MetadataKey::OH_REGION_OF_INTEREST_METADATA, &roiSize, &roiData);
if (ret == 0 && roiData != nullptr && roiSize > 0) {
return std::string(reinterpret_cast<char*>(roiData), roiSize);
}
} InBuffer是通过OH_NativeImage_AcquireNativeWindowBuffer从OH_NativeImage获取的相机帧,相机输出的ROI信息已写入该Buffer的元数据中。关于NativeImage的创建和帧接收机制,请参考开发指南视频编码中Surface模式的相关说明。
使用OH_VideoMetadata API组装ROI配置字符串。
提取的ROI原始字符串包含区域坐标信息,开发者需要通过OH_VideoMetadata_GetRoiCount、OH_VideoMetadata_ParseRoiString和OH_VideoMetadata_AppendRoiString接口解析原始ROI并追加DeltaQP参数,生成完整的ROI配置字符串(如"100,50-300,200=dqp:-6")。
if (currentRoiStr.empty()) {
return "";
}
uint32_t roiCount = 0;
OH_AVErrCode roiRet = OH_VideoMetadata_GetRoiCount(currentRoiStr.c_str(), &roiCount);
if (roiRet != AV_ERR_OK || roiCount == 0) {
return "";
}
std::vector<OH_AVFormat*> parsedFormats(roiCount, nullptr);
uint32_t actualCount = 0;
roiRet = OH_VideoMetadata_ParseRoiString(currentRoiStr.c_str(), parsedFormats.data(),
roiCount, &actualCount);
if (roiRet != AV_ERR_OK || actualCount == 0) {
return "";
}
char *assembledStr = nullptr;
for (uint32_t i = 0; i < actualCount; i++) {
if (parsedFormats[i] != nullptr) {
OH_AVFormat_SetIntValue(parsedFormats[i], OH_MD_KEY_VIDEO_METADATA_ROI_DELTA_QP,
ROI_DELTA_QP);
OH_VideoMetadata_AppendRoiString(&assembledStr, parsedFormats[i]);
OH_AVFormat_Destroy(parsedFormats[i]);
parsedFormats[i] = nullptr;
}
}
std::string result;
if (assembledStr != nullptr) {
result = std::string(assembledStr);
free(assembledStr);
} OH_VideoMetadata_AppendRoiString会在内部进行内存扩容,返回的C风格字符串需要开发者调用free()释放。
将ROI配置字符串写入编码器输入帧的NativeBuffer元数据。
在Surface模式下,编码器通过Surface接收视频帧。开发者需要先通过OH_NativeWindow_NativeWindowRequestBuffer从编码器Surface请求Buffer,再将组装好的ROI字符串写入该Buffer的NativeBuffer元数据中。
OH_NativeBuffer *encoderNativeBuffer = nullptr;
int32_t ret = OH_NativeBuffer_FromNativeWindowBuffer(OutBufferEncoder, &encoderNativeBuffer);
if (ret == 0 && encoderNativeBuffer != nullptr) {
int32_t roiStrSize = static_cast<int32_t>(assembledRoiStr.size());
ret = OH_NativeBuffer_SetMetadataValue(encoderNativeBuffer,
OH_NativeBuffer_MetadataKey::OH_REGION_OF_INTEREST_METADATA,
roiStrSize, reinterpret_cast<uint8_t*>(const_cast<char*>(assembledRoiStr.data())));
if (ret != 0) {
OH_LOG_Print(LOG_APP, LOG_WARN, LOG_PRINT_DOMAIN, "RenderThread",
"OH_NativeBuffer_SetMetadataValue failed, ret: %{public}d", ret);
} else {
OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "RenderThread",
"ROI metadata written to encoder buffer: %{public}s", assembledRoiStr.c_str());
}
} 当相机帧未检测到ROI区域时,assembledRoiStr为空字符串。NativeBuffer元数据配置方式不支持空字符串,需写入无位置信息的字符串(如"Clear"或";")。具体参考生效机制说明。
将Buffer提交给编码器并释放资源。
ROI配置完成后,通过OH_NativeWindow_NativeWindowFlushBuffer将预览Buffer和编码器Buffer提交,并释放相机帧资源。
void RenderThread::FlushAndCleanup(OHNativeWindowBuffer *InBuffer, int32_t fenceFd1,
OHNativeWindowBuffer *OutBuffer, OHNativeWindowBuffer *OutBufferEncoder)
{
OH_NativeWindow_NativeObjectUnreference(InBuffer);
OH_NativeImage_ReleaseNativeWindowBuffer(nativeImage_, InBuffer, fenceFd1);
Region region{nullptr, 0};
int acquireFenceFd = -1;
OH_NativeWindow_NativeWindowFlushBuffer(nativeWindow_, OutBuffer, acquireFenceFd, region);
if (OutBufferEncoder != nullptr) {
OH_NativeWindow_NativeWindowFlushBuffer(encoderNativeWindow_, OutBufferEncoder, acquireFenceFd, region);
}
}此方式同样适用于Surface模式。从帧的NativeBuffer元数据中提取ROI信息,配置方式通过OH_VideoEncoder_RegisterParameterCallback注册的编码输入参数回调完成(如图3所示)。
编码器在接收到视频帧时触发参数回调,由于参数回调不包含帧时间戳信息,开发者需要使用以PTS(时间戳)为键的同步队列(RoiQueue)将帧处理线程中的ROI数据按时间顺序传递到编码回调。回调中从队列弹出PTS最小的ROI条目,通过OH_AVFormat_SetStringValue配置到参数格式中。
图3:编码输入参数回调接口配置ROI流程图

详细开发步骤如下:
在CMakeList.txt中链接动态库。
从视频帧NativeBuffer元数据提取ROI信息。
使用OH_VideoMetadata API组装ROI配置字符串。
实现PTS同步队列与回调用户数据结构。
由于参数回调不包含帧时间戳信息,无法直接将ROI数据与编码帧对齐。建议实现一个以PTS为键的同步队列,将帧处理线程中的ROI数据按PTS顺序传递到编码回调。编码回调通过用户数据结构中的队列指针访问队列,弹出PTS最小的条目获取ROI配置字符串。
CodecUserRoi是定义在VideoEncoder类内部的嵌套结构体,包含队列指针以便回调直接访问。
// 参数回调配置的用户数据结构。
struct CodecUserRoi {
VideoEncoder* vencoder = nullptr;
RoiQueue* roiQueue = nullptr;
};注册编码输入参数回调。
在创建编码器后、Configure之前,注册编码输入参数回调。必须在Configure之前注册,否则回调不会生效。
// 参数回调配置:在Configure之前注册参数回调。
if (roiPathType_ == ROI_PATH_METADATA_CALLBACK) {
userData_ = std::make_unique<CodecUserRoi>();
if (userData_) {
userData_->vencoder = this;
userData_->roiQueue = &roiQueue_;
}
int32_t ret = OH_VideoEncoder_RegisterParameterCallback(encoder_, OnNeedInputParameter, userData_.get());
CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, SAMPLE_ERR_ERROR,
"OH_VideoEncoder_RegisterParameterCallback failed, ret: %{public}d", ret);
SAMPLE_LOGI("参数回调配置: ROI parameter callback registered.");
} OH_VideoEncoder_RegisterParameterCallback必须在OH_VideoEncoder_Configure之前调用。回调注册后,编码器每接收一帧输入数据都会触发OnNeedInputParameter回调。
在编码输入参数回调中配置ROI信息。
当编码器接收到输入数据时,会触发OnNeedInputParameter回调。在回调中,从RoiQueue中弹出PTS最小的ROI条目,并通过OH_AVFormat_SetStringValue配置到参数格式中。空字符串同样需配置至编码参数,以明确标识该帧未配置ROI区域。
static void OnNeedInputParameter(OH_AVCodec *codec, uint32_t index, OH_AVFormat *parameter, void *userData)
{
auto *roiUserData = static_cast<VideoEncoder::CodecUserRoi *>(userData);
if (!roiUserData || !roiUserData->roiQueue) {
OH_VideoEncoder_PushInputParameter(codec, index);
return;
}
std::string roiStr = roiUserData->roiQueue->Pop();
OH_AVFormat_SetStringValue(parameter, OH_MD_KEY_VIDEO_ENCODER_ROI_PARAMS, roiStr.c_str());
if (!roiStr.empty()) {
SAMPLE_LOGI("ROI configured (parameter callback path): %{public}s", roiStr.c_str());
}
OH_VideoEncoder_PushInputParameter(codec, index);
}将组装好的ROI字符串连同帧PTS传递到VideoEncoder的RoiQueue。
在帧处理线程中提取并组装ROI字符串后,需要将ROI字符串以PTS为索引,推入VideoEncoder的RoiQueue中。RoiQueue以PTS为键排序,确保编码回调按帧顺序获取ROI数据,避免错位。
int64_t pts = OH_NativeImage_GetTimestamp(nativeImage_);
if (roiPathType_ == ROI_PATH_METADATA_CALLBACK && onRoiStrAssembled_) {
onRoiStrAssembled_(pts, assembledRoiStr);
} VideoEncoder将ROI数据推入RoiQueue:
// 参数回调配置:将ROI条目按PTS推入RoiQueue。
void VideoEncoder::PushRoiEntry(int64_t pts, const std::string &roiStr)
{
roiQueue_.Push(pts, roiStr);
}
// 参数回调配置:ROI关闭时清空RoiQueue。
void VideoEncoder::ClearRoiQueue()
{
roiQueue_.Clear();
}RoiQueue以PTS为键排序存储ROI条目。编码回调取出PTS最小的条目,保证帧与ROI的顺序一致性。如果队列为空,最多等待3ms再返回空字符串。队列会自动清理超过2秒的陈旧条目,防止无限增长。每帧均应把ROI字符串入队(包括空ROI字符串),空ROI字符串用于明确标识该帧未配置ROI区域;关闭ROI时调用ClearRoiQueue清空队列。
Buffer模式下,视频帧通过OH_VideoEncoder_PushInputBuffer送入编码器,开发者需要在OnNeedInputBuffer回调中填充帧像素数据的同时配置ROI信息。由于Buffer模式没有编码器Surface,帧像素数据需要从相机帧Buffer中拷贝出来,连同ROI字符串一起推入帧队列供编码回调消费(如图4所示)。
图4:编码输入Buffer回调接口配置ROI流程图

详细开发步骤如下:
在CMakeList.txt中链接动态库。
从视频帧NativeBuffer元数据提取ROI信息。
使用OH_VideoMetadata API组装ROI配置字符串。
定义帧数据结构和帧队列。
Buffer模式下,编码器通过回调请求输入Buffer。开发者需要将相机帧的像素数据和ROI字符串封装为帧数据项,推入线程安全的帧队列供编码回调消费。
// Buffer模式编码的帧数据项。
constexpr uint32_t FRAME_QUEUE_POP_TIMEOUT_MS = 4;
constexpr size_t FRAME_QUEUE_MAX_SIZE = 3;
struct FrameItem {
std::vector<uint8_t> pixels;
int32_t width = 0;
int32_t height = 0;
std::string roiStr;
};将帧像素数据和ROI字符串推入帧队列。
在帧处理线程中,从相机帧Buffer读取像素数据,连同组装好的ROI字符串一起推入帧队列。
// Buffer模式:从相机帧读取像素数据并推入帧队列。
BufferHandle *bufferHandle = OH_NativeWindow_GetBufferHandleFromNative(InBuffer);
if (bufferHandle == nullptr) {
return;
}
OH_NativeBuffer *cameraNativeBuffer = nullptr;
int32_t ret = OH_NativeBuffer_FromNativeWindowBuffer(InBuffer, &cameraNativeBuffer);
if (ret != 0 || cameraNativeBuffer == nullptr) {
return;
}
void *virAddr = nullptr;
ret = OH_NativeBuffer_Map(cameraNativeBuffer, &virAddr);
if (ret != 0 || virAddr == nullptr) {
return;
}
int32_t frameWidth = bufferHandle->width;
int32_t frameHeight = bufferHandle->height;
int32_t stride = bufferHandle->stride;
int32_t frameSize = stride * frameHeight;
FrameItem frameItem;
frameItem.width = frameWidth;
frameItem.height = frameHeight;
frameItem.roiStr = assembledRoiStr;
frameItem.pixels.resize(frameSize);
std::copy(static_cast<uint8_t *>(virAddr),
static_cast<uint8_t *>(virAddr) + frameSize,
frameItem.pixels.data());
frameQueue_->Push(frameItem);
OH_NativeBuffer_Unmap(cameraNativeBuffer);
OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "RenderThread",
"Buffer模式: pushed frame to queue, size: %{public}d, ROI: %{public}s",
frameSize, assembledRoiStr.c_str()); Buffer模式需要从相机帧Buffer拷贝像素数据到应用内存,存在额外的数据拷贝开销,相比Surface模式的零拷贝机制会有更高的延迟。开发者应根据实际场景选择合适的编码模式。
在编码输入Buffer回调中配置ROI信息。
当编码器请求输入Buffer时,从帧队列弹出帧数据项,将像素数据拷贝到编码器Buffer中,并通过OH_AVBuffer_GetParameter获取格式后设置ROI字符串。
其中FillBufferModeInput的实现如下:
static void FillBufferModeInput(OH_AVCodec *codec, uint32_t index, OH_AVBuffer *buffer,
CodecUserData *codecUserData)
{
FrameItem frameItem;
if (!codecUserData->frameQueue->Pop(frameItem, std::chrono::milliseconds(FRAME_QUEUE_POP_TIMEOUT_MS))) {
OH_VideoEncoder_PushInputBuffer(codec, index);
return;
}
uint8_t *bufferAddr = OH_AVBuffer_GetAddr(buffer);
int32_t bufferCapacity = OH_AVBuffer_GetCapacity(buffer);
if (bufferAddr != nullptr && bufferCapacity >= static_cast<int32_t>(frameItem.pixels.size())) {
std::copy(frameItem.pixels.data(), frameItem.pixels.data() + frameItem.pixels.size(), bufferAddr);
OH_AVCodecBufferAttr attr;
attr.size = static_cast<int32_t>(frameItem.pixels.size());
attr.offset = 0;
attr.flags = AVCODEC_BUFFER_FLAGS_NONE;
OH_AVBuffer_SetBufferAttr(buffer, &attr);
}
if (!frameItem.roiStr.empty()) {
OH_AVFormat *format = OH_AVBuffer_GetParameter(buffer);
if (format != nullptr) {
OH_AVFormat_SetStringValue(format, OH_MD_KEY_VIDEO_ENCODER_ROI_PARAMS, frameItem.roiStr.c_str());
}
}
OH_VideoEncoder_PushInputBuffer(codec, index);
}
void CodecCallback::OnNeedInputBuffer(OH_AVCodec *codec, uint32_t index, OH_AVBuffer *buffer, void *userData)
{
if (userData == nullptr) {
return;
}
CodecUserData *codecUserData = static_cast<CodecUserData *>(userData);
// Buffer模式:用帧队列的像素数据和ROI填充编码器Buffer。
if (codecUserData->roiPathType == ROI_PATH_BUFFER_MODE && codecUserData->frameQueue != nullptr) {
FillBufferModeInput(codec, index, buffer, codecUserData);
return;
}