HarmonyOS鸿蒙Next中如何用Surface模式开发解码功能?

HarmonyOS鸿蒙Next中如何用Surface模式开发解码功能? 如何用Surface模式开发解码功能?

3 回复

在鸿蒙(HarmonyOS)中,使用Surface模式开发视频解码功能,主要涉及利用OHNativeWindow传递解码后的数据,实现高效送显。该模式适用于需要将解码画面直接渲染到屏幕的场景,例如视频播放、实时预览等。以下是基于Native API(C/C++)实现Surface模式视频解码的关键步骤和核心代码示例。

1. 添加头文件

#include <multimedia/player_framework/native_avcodec_videodecoder.h>
#include <multimedia/player_framework/native_avcapability.h>
#include <multimedia/player_framework/native_avcodec_base.h>
#include <multimedia/player_framework/native_avformat.h>
#include <multimedia/player_framework/native_avbuffer.h>
#include <condition_variable>
#include <memory>
#include <mutex>
#include <queue>
#include <shared_mutex>
#include <fstream>

2. 定义基础结构

解码器回调buffer的信息

struct CodecBufferInfo {
    CodecBufferInfo(uint32_t index, OH_AVBuffer *buffer): index(index), buffer(buffer), isValid(true) {}
    // 回调buffer
    OH_AVBuffer *buffer = nullptr;
    // 回调buffer对应的index
    uint32_t index = 0;
    // 判断当前buffer信息是否有效
    bool isValid = true;
};

解码输入输出队列

class CodecBufferQueue {
public:
    // 将回调buffer的信息传入队列
    void Enqueue(const std::shared_ptr<CodecBufferInfo> bufferInfo)
    {
        std::unique_lock<std::mutex> lock(mutex_);
        bufferQueue_.push(bufferInfo);
        cond_.notify_all();
    }

    // 获取回调buffer的信息
    std::shared_ptr<CodecBufferInfo> Dequeue(int32_t timeoutMs = 1000)
    {
        std::unique_lock<std::mutex> lock(mutex_);
        (void)cond_.wait_for(lock, std::chrono::milliseconds(timeoutMs), [this]() { return !bufferQueue_.empty(); });
        if (bufferQueue_.empty()) {
            return nullptr;
        }
        std::shared_ptr<CodecBufferInfo> bufferInfo = bufferQueue_.front();
        bufferQueue_.pop();
        return bufferInfo;
    }

    // 清空队列,之前的回调buffer设置为不可用
    void Flush()
    {
        std::unique_lock<std::mutex> lock(mutex_);
        while (!bufferQueue_.empty()) {
            std::shared_ptr<CodecBufferInfo> bufferInfo = bufferQueue_.front();
            // Flush、Stop、Reset、Destroy操作之后,之前回调的buffer信息设置为无效
            bufferInfo->isValid = false;
            bufferQueue_.pop();
        }
    }

private:
    std::mutex mutex_;
    std::condition_variable cond_;
    std::queue<std::shared_ptr<CodecBufferInfo>> bufferQueue_;
};

全局变量

// 视频帧宽度
int32_t width = 320;
// 视频帧高度
int32_t height = 240;
// 视频像素格式
OH_AVPixelFormat pixelFormat = AV_PIXEL_FORMAT_NV12;
// 视频宽跨距
int32_t widthStride = 0;
// 视频高跨距
int32_t heightStride = 0;
// 解码器实例指针
OH_AVCodec *videoDec = nullptr;
// 解码器同步锁
std::shared_mutex codecMutex;
// 解码器输入队列
CodecBufferQueue inQueue;
// 解码器输出队列
CodecBufferQueue outQueue;

3. 创建解码器实例对象

调用者可以通过名称或媒体类型创建解码器。示例中的变量说明如下:

  • videoDec:视频解码器实例的指针;
  • capability:解码器能力查询实例的指针;
  • OH_AVCODEC_MIMETYPE_VIDEO_AVC:AVC格式视频编解码器。
// 通过codecname创建解码器,应用有特殊需求,比如选择支持某种分辨率规格的解码器,可先查询capability,再根据codec name创建解码器。
OH_AVCapability *capability = OH_AVCodec_GetCapability(OH_AVCODEC_MIMETYPE_VIDEO_AVC, false);
// 创建硬件解码器实例
OH_AVCapability *capability= OH_AVCodec_GetCapabilityByCategory(OH_AVCODEC_MIMETYPE_VIDEO_AVC, false, HARDWARE);
const char *name = OH_AVCapability_GetName(capability);
OH_AVCodec *videoDec = OH_VideoDecoder_CreateByName(name);
// 通过MIME TYPE创建解码器,只能创建系统推荐的特定编解码器
// 涉及创建多路编解码器时,优先创建硬件解码器实例,硬件资源不够时再创建软件解码器实例
// 软/硬解: 创建H264解码器
OH_AVCodec *videoDec = OH_VideoDecoder_CreateByMime(OH_AVCODEC_MIMETYPE_VIDEO_AVC);
// 软/硬解: 创建H265解码器
OH_AVCodec *videoDec = OH_VideoDecoder_CreateByMime(OH_AVCODEC_MIMETYPE_VIDEO_HEVC);

4. 调用OH_VideoDecoder_RegisterCallback()设置回调函数

注册回调函数指针集合OH_AVCodecCallback,包括:

  • OH_AVCodecOnError 解码器运行错误
  • OH_AVCodecOnStreamChanged 码流信息变化,如码流宽、高变化;
  • OH_AVCodecOnNeedInputBuffer 运行过程中需要新的输入数据,即解码器已准备好,可以输入数据;
  • OH_AVCodecOnNewOutputBuffer 运行过程中产生了新的输出数据,即解码完成(注:Surface模式buffer参数为空)。

调用者可以通过处理该回调报告的信息,确保解码器正常运转。

// 解码异常回调OH_AVCodecOnError实现
static void OnError(OH_AVCodec *codec, int32_t errorCode, void *userData)
{
    // 回调的错误码由调用者判断处理
    (void)codec;
    (void)errorCode;
    (void)userData;
}

// 解码数据流变化回调OH_AVCodecOnStreamChanged实现
static void OnStreamChanged(OH_AVCodec *codec, OH_AVFormat *format, void *userData)
{
    // 可通过format获取到变化后的视频宽、高等
    (void)codec;
    (void)userData;
    OH_AVFormat_GetIntValue(format, OH_MD_KEY_VIDEO_PIC_WIDTH, &width);
    OH_AVFormat_GetIntValue(format, OH_MD_KEY_VIDEO_PIC_HEIGHT, &height);
}

// 解码输入回调OH_AVCodecOnNeedInputBuffer实现
static void OnNeedInputBuffer(OH_AVCodec *codec, uint32_t index, OH_AVBuffer *buffer, void *userData)
{
    // 输入帧的数据buffer和对应的index送入inQueue队列
    (void)codec;
    (void)userData;
    inQueue.Enqueue(std::make_shared<CodecBufferInfo>(index, buffer));
}

// 解码输出回调OH_AVCodecOnNewOutputBuffer实现
static void OnNewOutputBuffer(OH_AVCodec *codec, uint32_t index, OH_AVBuffer *buffer, void *userData)
{
    // 完成帧的数据buffer和对应的index送入outQueue队列
    (void)codec;
    (void)userData;
    outQueue.Enqueue(std::make_shared<CodecBufferInfo>(index, buffer));
}
// 配置异步回调,调用 OH_VideoDecoder_RegisterCallback 接口
OH_AVCodecCallback cb = {&OnError, &OnStreamChanged, &OnNeedInputBuffer, &OnNewOutputBuffer};
// 配置异步回调
int32_t ret = OH_VideoDecoder_RegisterCallback(videoDec, cb, nullptr); // nullptr:用户特定数据userData为空
if (ret != AV_ERR_OK) {
    // 异常处理
}

说明:

  1. 在回调函数中,对数据队列进行操作时,需要注意多线程同步的问题。
  2. 播放视频时,若视频码流的SPS中包含颜色信息,解码器会把这些信息(RangeFlag、ColorPrimary、MatrixCoefficient、TransferCharacteristic)通过 OH_AVCodecOnStreamChanged接口中的OH_AVFormat返回。
  3. 视频解码的Surface模式下,内部数据默认是走HEBC(High Efficiency Bandwidth Compression,高效带宽压缩),无法获取到widthStride和heightStride的值。

5. 调用OH_VideoDecoder_Configure()配置解码器

创建解码器后,必须配置视频帧宽度、高度、像素格式等基本参数。这些参数通过OH_AVFormat设置。

OH_AVFormat *format = OH_AVFormat_Create();
// 写入format
OH_AVFormat_SetIntValue(format, OH_MD_KEY_WIDTH, width); // 必须配置
OH_AVFormat_SetIntValue(format, OH_MD_KEY_HEIGHT, height); // 必须配置
OH_AVFormat_SetIntValue(format, OH_MD_KEY_PIXEL_FORMAT, pixelFormat);
// 可选,配置低时延解码
OH_AVFormat_SetIntValue(format, OH_MD_KEY_VIDEO_ENABLE_LOW_LATENCY, 1);
// 配置解码器
int32_t ret = OH_VideoDecoder_Configure(videoDec, format);
if (ret != AV_ERR_OK) {
    // 异常处理
}
OH_AVFormat_Destroy(format);

6. 设置surface

本例中的nativeWindow,有两种方式获取:

  1. 如果解码后直接显示,则从XComponent组件获取;
  2. 如果解码后接OpenGL后处理,则从NativeImage获取。

Surface模式,调用者可以在解码过程中执行该步骤,即动态切换surface。

// 配置送显窗口参数
int32_t ret = OH_VideoDecoder_SetSurface(videoDec, window);    // 从XComponent获取window
if (ret != AV_ERR_OK) {
    // 异常处理
}

7. (可选)OH_VideoDecoder_SetParameter()动态配置解码器surface参数

OH_AVFormat *format = OH_AVFormat_Create();
// 配置显示旋转角度
OH_AVFormat_SetIntValue(format, OH_MD_KEY_ROTATION, 90);
// 配置视频与显示屏匹配模式(缩放与显示窗口适配,裁剪与显示窗口适配)
OH_AVFormat_SetIntValue(format, OH_MD_KEY_SCALING_MODE, SCALING_MODE_SCALE_CROP);
int32_t ret = OH_VideoDecoder_SetParameter(videoDec, format);
OH_AVFormat_Destroy(format);

8. 调用OH_VideoDecoder_Prepare()解码器就绪

该接口将在解码器运行前进行一些数据的准备工作。

ret = OH_VideoDecoder_Prepare(videoDec);
if (ret != AV_ERR_OK) {
    // 异常处理
}

9. 调用OH_VideoDecoder_Start()启动解码器

// 启动解码器,开始解码
int32_t ret = OH_VideoDecoder_Start(videoDec);
if (ret != AV_ERR_OK) {
    // 异常处理
}

10. 调用OH_VideoDecoder_PushInputBuffer()写入解码码流

送入输入队列进行解码,以下示例中:

  • buffer:回调函数OnNeedInputBuffer传入的参数,可以通过OH_AVBuffer_GetAddr接口获取输入码流虚拟地址;
  • index:回调函数OnNeedInputBuffer传入的参数,与buffer唯一对应的标识;
  • size, offset, pts, frameData:输入尺寸、偏移量、时间戳、帧数据等字段信息;
  • flags:缓冲区标记的类别,请参考OH_AVCodecBufferFlags。
std::shared_ptr<CodecBufferInfo> bufferInfo = inQueue.Dequeue();
std::shared_lock<std::shared_mutex> lock(codecMutex);
if (bufferInfo == nullptr || !bufferInfo->isValid) {
    // 异常处理
}
// 写入码流数据
uint8_t *addr = OH_AVBuffer_GetAddr(bufferInfo->buffer);
int32_t capcacity = OH_AVBuffer_GetCapacity(bufferInfo->buffer);
if (size > capcacity) {
    // 异常处理
}
memcpy(addr, frameData, size);
// 配置帧数据的输入尺寸、偏移量、时间戳等字段信息
OH_AVCodecBufferAttr info;
info.size = size;
info.offset = offset;
info.pts = pts;
info.flags = flags;
// info信息写入buffer
int32_t ret = OH_AVBuffer_SetBufferAttr(bufferInfo->buffer, &info);
if (ret != AV_ERR_OK) {
    // 异常处理
}
// 送入解码输入队列进行解码,index为对应buffer队列的下标
ret = OH_VideoDecoder_PushInputBuffer(videoDec, bufferInfo->index);
if (ret != AV_ERR_OK) {
    // 异常处理
}

11. 调用OH_VideoDecoder_RenderOutputBuffer()/OH_VideoDecoder_RenderOutputBufferAtTime()显示并释放解码帧,或调用OH_VideoDecoder_FreeOutputBuffer()释放解码帧

以下示例中:

  • index:回调函数OnNewOutputBuffer传入的参数,与buffer唯一对应的标识;
  • buffer:回调函数OnNewOutputBuffer传入的参数,Surface模式调用者无法通过OH_AVBuffer_GetAddr。
std::shared_ptr<CodecBufferInfo> bufferInfo = outQueue.Dequeue();
std::shared_lock<std::shared_mutex> lock(codecMutex);
if (bufferInfo == nullptr || !bufferInfo->isValid) {
    // 异常处理
}
// 获取解码后信息
OH_AVCodecBufferAttr info;
int32_t ret = OH_AVBuffer_GetBufferAttr(bufferInfo->buffer, &info);
if (ret != AV_ERR_OK) {
    // 异常处理
}
// 值由调用者决定
bool isRender;
bool isNeedRenderAtTime;
if (isRender) {
    // 显示并释放已完成处理的信息,index为对应buffer队列的下标
    if (isNeedRenderAtTime){
        // 获取系统绝对时间,renderTimestamp由调用者结合业务指定显示时间
        int64_t renderTimestamp =
            std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::high_resolution_clock::now().time_since_epoch()).count();
        ret = OH_VideoDecoder_RenderOutputBufferAtTime(videoDec, bufferInfo->index, renderTimestamp);
    } else {
        ret = OH_VideoDecoder_RenderOutputBuffer(videoDec, bufferInfo->index);
    }

} else {
    // 释放已完成处理的信息
    ret = OH_VideoDecoder_FreeOutputBuffer(videoDec, bufferInfo->index);
}
if (ret != AV_ERR_OK) {
    // 异常处理
}

注意: 如果要获取buffer的属性,如pixel_format、stride等可通过调用OH_NativeWindow_NativeWindowHandleOpt接口获取。

12.(可选)调用OH_VideoDecoder_Flush()刷新解码器

调用OH_VideoDecoder_Flush接口后,解码器仍处于运行态,但会清除解码器中缓存的输入和输出数据及参数集如H264格式的PPS/SPS。

此时需要调用OH_VideoDecoder_Start接口重新开始解码。

以下示例中:

  • xpsData, xpsSize:PPS/SPS信息。
std::unique_lock<std::shared_mutex> lock(codecMutex);
// 刷新解码器videoDec
int32_t ret = OH_VideoDecoder_Flush(videoDec);
if (ret != AV_ERR_OK) {
    // 异常处理
}
inQueue.Flush();
outQueue.Flush();
// 重新开始解码
ret = OH_VideoDecoder_Start(videoDec);
if (ret != AV_ERR_OK) {
    // 异常处理
}

std::shared_ptr<CodecBufferInfo> bufferInfo = outQueue.Dequeue();
if (bufferInfo == nullptr || !bufferInfo->isValid) {
    // 异常处理
}
// 重传PPS/SPS
// 配置帧数据PPS/SPS信息
uint8_t *addr = OH_AVBuffer_GetAddr(bufferInfo->buffer);
int32_t capcacity = OH_AVBuffer_GetCapacity(bufferInfo->buffer);
if (xpsSize > capcacity) {
    // 异常处理
}
memcpy(addr, xpsData, xpsSize);
OH_AVCodecBufferAttr info;
info.flags = AVCODEC_BUFFER_FLAG_CODEC_DATA;
// info信息写入buffer
ret = OH_AVBuffer_SetBufferAttr(bufferInfo->buffer, &info);
if (ret != AV_ERR_OK) {
    // 异常处理
}
// 将帧数据推送到解码器中,index为对应buffer队列的下标
ret = OH_VideoDecoder_PushInputBuffer(videoDec, bufferInfo->index);
if (ret != AV_ERR_OK) {
    // 异常处理
}

13. (可选)调用OH_VideoDecoder_Reset()重置解码器

调用OH_VideoDecoder_Reset接口后,解码器回到初始化的状态,需要调用OH_VideoDecoder_Configure接口、OH_VideoDecoder_SetSurface接口和OH_VideoDecoder_Prepare接口重新配置。

std::unique_lock<std::shared_mutex> lock(codecMutex);
// 重置解码器videoDec
int32_t ret = OH_VideoDecoder_Reset(videoDec);
if (ret != AV_ERR_OK) {
    // 异常处理
}
inQueue.Flush();
outQueue.Flush();
// 重新配置解码器参数
ret = OH_VideoDecoder_Configure(videoDec, format);
if (ret != AV_ERR_OK) {
    // 异常处理
}
// Surface模式重新配置surface,而Buffer模式不需要配置surface
ret = OH_VideoDecoder_SetSurface(videoDec, window);
if (ret != AV_ERR_OK) {
    // 异常处理
}
// 解码器重新就绪
ret = OH_VideoDecoder_Prepare(videoDec);
if (ret != AV_ERR_OK) {
    // 异常处理
}

14. (可选)调用OH_VideoDecoder_Stop()停止解码器

调用OH_VideoDecoder_Stop()后,解码器保留了解码实例,释放输入输出buffer。调用者可以直接调用OH_VideoDecoder_Start接口继续解码,输入的第一个buffer需要携带参数集,从IDR帧开始送入。

std::unique_lock<std::shared_mutex> lock(codecMutex);
// 终止解码器videoDec
int32_t ret = OH_VideoDecoder_Stop(videoDec);
if (ret != AV_ERR_OK) {
    // 异常处理
}
inQueue.Flush();
outQueue.Flush();

15. 调用OH_VideoDecoder_Destroy()销毁解码器实例,释放资源

说明:

  1. 不能在回调函数中调用;
  2. 执行该步骤之后,需要调用者将videoDec指向nullptr,防止野指针导致程序错误。
std::unique_lock<std::shared_mutex> lock(codecMutex);
// 调用OH_VideoDecoder_Destroy,注销解码器
int32_t ret = AV_ERR_OK;
if (videoDec != nullptr) {
    ret = OH_VideoDecoder_Destroy(videoDec);
    videoDec = nullptr;
}
if (ret != AV_ERR_OK) {
    // 异常处理
}
inQueue.Flush();
outQueue.Flush();

更多关于HarmonyOS鸿蒙Next中如何用Surface模式开发解码功能?的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


在HarmonyOS Next中,使用Surface模式开发解码功能:创建OH_AVCodec实例(如OH_VideoDecoder_CreateByMime),配置OH_MD_KEY_OUTPUT_SURFACE为Surface对象,其余解码参数参照视频编码格式设置。调用Configure后Start,解码输出将直接渲染至Surface,无需额外获取输出Buffer。

在HarmonyOS Next中,利用Surface模式开发解码功能的核心是将解码输出直接渲染到 Surface,避免数据拷贝。简要步骤如下:

  1. 获取Surface
    通过 XComponent 的 getXComponentSurfaceId() 获取 SurfaceId,再调用 XComponentController.getSurface() 得到 Surface 对象。

  2. 创建解码器
    使用 AVCodec.createDecoderByMime()createDecoderByName() 创建视频解码器实例,并注册解码回调。

  3. 配置Surface输出模式
    调用 decoder.configure() 时,将 surface 参数设置为上述获取的 Surface,并指定输出格式(如 AVCodecOutputFormat.SURFACE)。

  4. 开始解码
    调用 decoder.start() 启动解码器,随后将编码数据通过 decoder.pushInputBuffer() 写入,解码后的帧会自动渲染到 Surface 上显示。

  5. 结束
    解码完成后释放解码器资源。

代码示例(核心片段):

import { media } from '@kit.MediaKit';
import { XComponent } from '@kit.ArkUI';

// 获取XComponent的Surface
const xComponent: XComponent = ...;
const surfaceId = xComponent.getXComponentSurfaceId();
const surface = xComponent.getXComponentContext()!.getSurface();

// 创建解码器
let decoder = await media.createVideoDecoderByMime('video/avc');

// 配置 Surface 输出模式
let format: media.AVCodecFormat = {
  surface: surface,
  width: 1920,
  height: 1080,
  pixelFormat: media.VideoPixelFormat.NV12
};
await decoder.configure(format, surface); 

// 开始解码
await decoder.start();
// 之后循环 pushInputBuffer,Surface 会自动显示解码结果
回到顶部