【鸿蒙南向开发】OpenHarmony实战之视频编解码HDI的使用

205 篇文章 0 订阅
204 篇文章 0 订阅

简介

Codec HDI 提供了基于OpenMax(简称OMX)的一整套编解码接口,程序通过简单的几步,即可实现基于OMX的硬件编解码。本文主要介绍基于Codec HDI 开发的编解码功能,包括初始化,Buffer流转以及释放等。

CODEC HDI框架介绍

image.png

Codec HDI Interface (简称HDI)提供基于OpenMax 的标准接口,Media Service 调用这套接口实现硬件编解码能力。HDI 另外提供了实现Callback 机制的接口,Media Service 可以通过HDI 接口创建Callback 信息接收服务端(Codec HDI Callback Remote Service),并且通过SetCallback 方法将对应的客户端代理(Codec HDI Callback Client Proxy)传给OpenMax 实现层,OpenMax 在数据Buffer 处理过程中会通过此代理调用回调方法,告知Media Service 进行对应的数据处理。

目录

./drivers/peripheral/codec
|-- hal									#HDI框架代码目录
|   |-- include							
|   |-- src
|-- hdi_service						    #原HDI框架代码目录
|   |-- codec_proxy
|   |-- codec_service_stub
|   |-- codec_test
|-- interfaces							#codec接口头文件
|   |-- include
|-- test								#unittest 和Demo目录

Codec HDI编解码实例

组件状态流转

image.png

1.通过调用CreateComponent函数,可以打开组件,并让组件进入OMX_StateLoaded 或OMX_StateWaitForResources 状态;
2.通过调用DestoryComponent函数,可以销毁处于OMX_StateLoaded状态的组件实例;
3.其它的状态变化,可以通过SendCommand(OMX_CommandStateSet, <OMX_STATETYPE>)使组件进入相应的状态。

状态

描述

OMX_StateInvalid

组件已损坏或者发生一个不能恢复的错误

OMX_StateLoaded

组件已经加载,但是资源未申请

OMX_StateIdle

组件已经申请到所有资源,但不会轮转任何buffer

OMX_StateExecuting

组件正在处理有效的数据

OMX_StatePause

组件暂停处理数据,可以通过设置组件状态唤醒重新处理数据

OMX_StateWaitForResources

组件正在等待所需要的

编解码流程简介

image.png

主要包含以下几步:

  1. 接口及回调初始化
  2. 组件初始化
  3. 设置编解码参数
  4. 申请输入输出Buffer
  5. 编解码
  6. 组件回调处理
  7. 组件释放

codec HDI编解码主要流程如上图显示,下面我们以解码流程来讲解。

接口及回调初始化

本步骤主要初始化了使用HDI接口必须使用的结构及初始化对应的回调函数。

//初始化HDI ComponentManager实例,用于打开OMX组件
omxMgr_ = GetCodecComponentManager();

//初始化回调
callback_ = CodecCallbackTypeStubGetInstance();
if (!omxMgr_ || !callback_) {
    FUNC_EXIT_ERR();
    return false;
}
//设置回调函数指针
callback_->EventHandler    = &OMXCore::OnEvent;
callback_->EmptyBufferDone = &OMXCore::OnEmptyBufferDone;
callback_->FillBufferDone  = &OMXCore::OnFillBufferDone;

组件初始化

组件初始化包含打开对应的编解码组件并获取组件的版本,后续会使用到该版本参数。

//新建组件实例,组件实例为 client_ 后续接口中需要使用 client_
auto err = omxMgr_->CreateComponent(&client_, "OMX.rk.video_decoder.avc", 0, 0, callback_);
if (err != HDF_SUCCESS) {
    HDF_LOGE("failed to CreateComponent");
    FUNC_EXIT_ERR();
    return false;
}
//获取组件版本号
char                  name[MAX_LEN] = {0};
union OMX_VERSIONTYPE omxVer;
union OMX_VERSIONTYPE omxVerspec;
uint8_t               uuid[MAX_LEN] = {0};
uint32_t              uuidLen   = MAX_LEN; // MAX_LEN = 128
err = client_->GetComponentVersion(client_, name, &omxVer, &omxVerspec, uuid, uuidLen);
if (err != HDF_SUCCESS) {
    HDF_LOGE("failed to CreateComponent");
    FUNC_EXIT_ERR();
    return false;
}

设置编解码参数

设置参数时会使用上一步获取到的版本信息,注意填写。

1.设置输入的宽和高

OMX_PARAM_PORTDEFINITIONTYPE param;
InitParam(param);
param.nPortIndex = (uint32_t)PortIndex::kPortIndexInput;
auto err         = client_->GetParameter(client_, OMX_IndexParamPortDefinition, (int8_t *)&param, sizeof(param));
if (err != HDF_SUCCESS) {
    HDF_LOGE("failed to GetParameter with PortIndex::kPortIndexInput, index is "
             "OMX_IndexParamPortDefinition");

    return false;
}
HDF_LOGI("PortIndex::kPortIndexInput: eCompressionFormat = %{public}d, "
         "eColorFormat=%{public}d",
         param.format.video.eCompressionFormat, param.format.video.eColorFormat);
param.format.video.nFrameWidth  = width_;
param.format.video.nFrameHeight = height_;
param.format.video.nStride      = width_;
param.format.video.nSliceHeight = height_;
err = client_->SetParameter(client_, OMX_IndexParamPortDefinition, (int8_t *)&param, sizeof(param));
if (err != HDF_SUCCESS) {
    HDF_LOGE("failed to SetParameter with PortIndex::kPortIndexInput, index is "
             "OMX_IndexParamPortDefinition");

    return false;
}

2.设置输出视频的宽和高以及格式

InitParam(param);
param.nPortIndex = (uint32_t)PortIndex::kPortIndexOutput;
err              = client_->GetParameter(client_, OMX_IndexParamPortDefinition, (int8_t *)&param, sizeof(param));
if (err != HDF_SUCCESS) {
    HDF_LOGE("failed to GetParameter with PortIndex::kPortIndexOutput, index is "
             "OMX_IndexParamPortDefinition");

    return false;
}
HDF_LOGI("PortIndex::kPortIndexOutput eCompressionFormat = %{public}d, "
         "eColorFormat=%{public}d",
         param.format.video.eCompressionFormat, param.format.video.eColorFormat);
param.format.video.nFrameWidth  = width_;
param.format.video.nFrameHeight = height_;
param.format.video.nStride      = width_;
param.format.video.nSliceHeight = height_;
param.format.video.eColorFormat = AV_COLOR_FORMAT;  // 输出数据格式设置,YUV420SP
err = client_->SetParameter(client_, OMX_IndexParamPortDefinition, (int8_t *)&param, sizeof(param));
if (err != HDF_SUCCESS) {
    HDF_LOGE("failed to SetParameter with PortIndex::kPortIndexOutput, index is "
             "OMX_IndexParamPortDefinition");

    return false;
}

3.设置输入的格式和帧率

OMX_VIDEO_PARAM_PORTFORMATTYPE param;
InitParam(param);
param.nPortIndex = (uint32_t)PortIndex::kPortIndexInput;
auto err         = client_->GetParameter(client_, OMX_IndexParamVideoPortFormat, (int8_t *)&param, sizeof(param));
if (err != HDF_SUCCESS) {
    HDF_LOGE("failed to GetParameter with PortIndex::kPortIndexInput, index is "
             "OMX_IndexParamVideoPortFormat");
    return false;
}
HDF_LOGI("set Format PortIndex::kPortIndexInput eCompressionFormat = %{public}d, "
         "eColorFormat=%{public}d",
         param.eCompressionFormat, param.eColorFormat);
param.xFramerate         = FRAME;                // 30fps,Q16 format
param.eCompressionFormat = OMX_VIDEO_CodingAVC;  // H264
err = client_->SetParameter(client_, OMX_IndexParamVideoPortFormat, (int8_t *)&param, sizeof(param));
if (err != HDF_SUCCESS) {
    HDF_LOGE("failed to SetParameter with PortIndex::kPortIndexInput, index is "
             "OMX_IndexParamVideoPortFormat");

    return false;
}

申请输入输出Buffer

参数设置完成后,我们需要设置输入输出端口的Buffer,需要调用UseBuffer 方法,注意该方法只有在组件处于OMX_StateLoaded 或OMX_StateWaitForResources 或者端口处于Disabled 状态才能使用。实例代码中的portIndex可以是输入端口PortIndex::kPortIndexInput, 也可以是输出端口PortIndex::kPortIndexOutput,两个都需要设置。

1.获取端口需要的buffer大小和数量以及enable状态

OMX_PARAM_PORTDEFINITIONTYPE param;
InitParam(param);
param.nPortIndex = (OMX_U32)portIndex;
auto err         = client_->GetParameter(client_, OMX_IndexParamPortDefinition, (int8_t *)&param, sizeof(param));
if (err != HDF_SUCCESS) {
    HDF_LOGE("failed to GetParameter with OMX_IndexParamPortDefinition : "
             "portIndex[%{public}d]",
             portIndex);

    return false;
}

bufferSize  = param.nBufferSize;
bufferCount = param.nBufferCountActual;
bPortEnable = param.bEnabled;

2.初始化多个Buffer并调用UseBuffer方法,注意返回的bufferId,后续操作都可以用bufferId来标识OmxCodecBuffer

for (int i = 0; i < bufferCount; i++) {
	//初始化OmxCodecBuffer, 目前只支持共享内存的方式
    OmxCodecBuffer *omxBuffer = new OmxCodecBuffer();
    memset_s(omxBuffer, sizeof(OmxCodecBuffer), 0, sizeof(OmxCodecBuffer));
    omxBuffer->size                    = sizeof(OmxCodecBuffer);
    omxBuffer->version.s.nVersionMajor = 1;
    omxBuffer->bufferType              = BUFFER_TYPE_AVSHARE_MEM_FD;
    int                fd              = AshmemCreate(0, bufferSize);
    shared_ptr<Ashmem> spSharedMem     = make_shared<Ashmem>(fd, bufferSize);
    omxBuffer->bufferLen               = FD_SIZE;
    omxBuffer->buffer                  = (uint8_t *)(unsigned long)fd;
    omxBuffer->allocLen                = bufferSize;
    omxBuffer->fenceFd                 = -1;

    if (portIndex == PortIndex::kPortIndexInput) {
        omxBuffer->type = READ_ONLY_TYPE;  // 对输入端口,服务是只读的
        spSharedMem->MapReadAndWriteAshmem();
    } else {
        omxBuffer->type = READ_WRITE_TYPE; //对输出端口,服务是可写入的
        spSharedMem->MapReadOnlyAshmem();
    }
    auto err = client_->UseBuffer(client_, (uint32_t)portIndex, omxBuffer);
    if (err != HDF_SUCCESS) {
        HDF_LOGE("failed to UseBuffer with  portIndex[%{public}d]", portIndex);

        spSharedMem->UnmapAshmem();
        spSharedMem->CloseAshmem();
        spSharedMem = nullptr;
        return false;
    }

    omxBuffer->bufferLen = 0;
    HDF_LOGI("UseBuffer returned bufferID [%{public}d]", omxBuffer->bufferId);
	//保存omxBuffer 以及共享内存映射地址
    BufferInfo *bufferInfo  = new BufferInfo;
    bufferInfo->omxBuffer   = omxBuffer;
    bufferInfo->avSharedPtr = spSharedMem;
    bufferInfo->portIndex   = portIndex;
    omxBuffers_.insert(std::make_pair<int, BufferInfo *>(omxBuffer->bufferId, std::move(bufferInfo)));
    if (portIndex == PortIndex::kPortIndexInput) {
        unUsedInBuffers_.push_back(omxBuffer->bufferId);
    } else {
        unUsedOutBuffers_.push_back(omxBuffer->bufferId);
    }
    int fdret = (int)omxBuffer->buffer;
    HDF_LOGI("{bufferID = %{public}d, srcfd = %{public}d, retfd = %{public}d}", omxBuffer->bufferId, fd, fdret);
}

3.调用完UseBuffer之后,我们需要判断端口是否处于enable状态,如果是非使能状态,需要发送命令使端口处于使能状态

// set port enable
if (!bPortEnable) {
    auto err = client_->SendCommand(client_, OMX_CommandPortEnable, portIndex, NULL, 0);
    if (err != HDF_SUCCESS) {
        HDF_LOGE("SendCommand OMX_CommandPortEnable::kPortIndexInput error");
        FUNC_EXIT_ERR();
        return false;
    }
}

4.Buffer 设置完成后,需要使组件进入OMX_StateIdle状态

HDF_LOGI("...command to IDLE....");
auto err = client_->SendCommand(client_, OMX_CommandStateSet, OMX_StateIdle, NULL, 0);
if (err != HDF_SUCCESS) {
    HDF_LOGE("failed to SendCommand with OMX_CommandStateSet:OMX_StateIdle");
    FUNC_EXIT_ERR();
    return false;
}

HDF_LOGI("Wait for OMX_StateIdle status");
this->WaitForStatusChanged();

编解码

组件进入IDLE状态之后,我们要先让组件进入OMX_StateExecuting 状态,组件即可正常编解码了。
1.发送命令,使组件进入OMX_StateExecuting 状态

HDF_LOGI("...command to OMX_StateExecuting....");
err = client_->SendCommand(client_, OMX_CommandStateSet, OMX_StateExecuting, NULL, 0);
if (err != HDF_SUCCESS) {
    HDF_LOGE("failed to SendCommand with OMX_CommandStateSet:OMX_StateIdle");
    FUNC_EXIT_ERR();
    return false;
}

HDF_LOGI("Wait for OMX_StateExecuting status");
this->WaitForStatusChanged();

2.先将输出Buffer传给组件,让组件填充这些buffer

for (auto bufferId : unUsedOutBuffers_) {
    HDF_LOGI("fill bufferid [%{public}d]", bufferId);
    auto iter = omxBuffers_.find(bufferId);
    if (iter != omxBuffers_.end()) {
        BufferInfo *bufferInfo = iter->second;
        auto        err        = client_->FillThisBuffer(client_, bufferInfo->omxBuffer);
        if (err != HDF_SUCCESS) {
            HDF_LOGE("FillThisBuffer error");
            return;
        }
    }
}

3.读取H264 或 H265文件中的数据,让组件消费,这里请注意下,组件只支持已分帧的数据,我们读取文件时,需要按照start code(0x000001 或者 0x00000001)进行分帧

bool bEndOfFile = false;
while (!bEndOfFile) {
    HDF_LOGI(" inputput run");
    int bufferID = GetFreeBufferId();
    if (this->exit_) {
        break;
    }
    if (bufferID < 0) {
        usleep(10000);
        continue;
    }
    auto iter = omxBuffers_.find(bufferID);
    if (iter == omxBuffers_.end()) {
        continue;
    }
    BufferInfo *bufferInfo = iter->second;
    void       *sharedAddr = (void *)bufferInfo->avSharedPtr->ReadFromAshmem(0, 0);
    bool bEOS = (size_t)this->ReadOnePacket(fpIn_.get(), (char *)sharedAddr, bufferInfo->omxBuffer->filledLen);
    HDF_LOGI("read data size is %{public}d", bufferInfo->omxBuffer->filledLen);
    bufferInfo->omxBuffer->offset = 0;
    if (bEOS) {
        bufferInfo->omxBuffer->flag = OMX_BUFFERFLAG_EOS;
        bEndOfFile                  = true;
    }
    auto err = client_->EmptyThisBuffer(client_, bufferInfo->omxBuffer);
    if (err != HDF_SUCCESS) {
        HDF_LOGE("EmptyThisBuffer error");

        return;
    }
}

4.解码完成后,需要将组件设置为OMX_StateIdle 状态

// command to IDLE
client_->SendCommand(client_, OMX_CommandStateSet, OMX_StateIdle, NULL, 0);

组件回调处理

codec HDI 提供了3大回调,分别对应了omx组件的3个回调函数。
1.EventHandler, 包含状态变化,错误通知等等,具体参数含义请见下表格

eEvent

data1

data2

eventData

OMX_EventCmdComplete

OMX_CommandStateSet

OMX组件状态

NULL

OMX_CommandFlush

端口

NULL

OMX_CommandPortDisable

端口

NULL

OMX_CommandPortEnable

端口

NULL

OMX_CommandMarkBuffer

端口

NULL

OMX_EventError

Error Code

0

NULL

OMX_EventMark

0

0

make buffer

OMX_EventPortSettingsChanged

端口

0

NULL

OMX_EventBufferFlag

端口

nFlags

NULL

OMX_EventResourcesAcquired

0

0

NULL

OMX_EventDynamicResourcesAvailable

0

0

NULL

Demo 中逻辑处理比较简单,只是对OMX_EventCmdComplete 做了简单处理

int32_t CodecHdiDecode::OnEvent(struct CodecCallbackType *self, int8_t *pAppData, uint32_t pAppDataLen,
                            enum OMX_EVENTTYPE eEvent, uint32_t nData1, uint32_t nData2, int8_t *pEventData,
                            uint32_t pEventDataLen)
{
   	 HDF_LOGI("onEvent: pAppData[0x%{public}p], eEvent [%{public}d], "
             "nData1[%{public}d]",
             pAppData, eEvent, nData1);
    switch (eEvent) {
        case OMX_EventCmdComplete: {
            OMX_COMMANDTYPE cmd = (OMX_COMMANDTYPE)nData1;
            if (OMX_CommandStateSet == cmd) {
                HDF_LOGI("OMX_CommandStateSet reached, status is %{public}d", nData2);
                g_core->onStatusChanged();
            }
            break;
        }

        default:
            break;
    }
    return HDF_SUCCESS;
}

2.EmptyBufferDone, 输入buffer处理完毕通知
接收到此通知后,根据bufferId找到具体被消耗的OmxCodecBuffer,然后可以继续将待解码数据写入该buffer,通过调用EmptyThisBuffer将buffer传递给组件处理其数据。

//回调处理函数
int32_t CodecHdiDecode::OnEmptyBufferDone(struct CodecCallbackType *self, int8_t *pAppData, uint32_t pAppDataLen,
                                      const struct OmxCodecBuffer *buffer)
{
    HDF_LOGI("onEmptyBufferDone: pBuffer.bufferID [%{public}d]", pBuffer->bufferId);
    g_core->OnEmptyBufferDone(buffer);

    return HDF_SUCCESS;
}

int32_t CodecHdiDecode::OnEmptyBufferDone(const struct OmxCodecBuffer *buffer)
{
    unique_lock<mutex> ulk(lockInputBuffers_);
    unUsedInBuffers_.push_back(buffer->bufferId);
    return HDF_SUCCESS;
}

3.FillBufferDone, 输出buffer填充完毕通知
接收到此通知后,根据bufferId找到具体被填充的OmxCodecBuffer,然后取出解码数据(如果需要的话),再调用FillThisBuffer将buffer传递给组件,组件解码成功一帧数据后,会将解码数据填充到此buffer,并再次出发该回调。

//回调处理函数
int32_t CodecHdiDecode::OnFillBufferDone(struct CodecCallbackType *self, int8_t *pAppData, uint32_t pAppDataLen,
                                     struct OmxCodecBuffer *buffer)
{
    HDF_LOGI("onFillBufferDone: pBuffer.bufferID [%{public}d]", buffer->bufferId);
    g_core->OnFillBufferDone(buffer);
    return HDF_SUCCESS;
}

int32_t CodecHdiDecode::OnFillBufferDone(struct OmxCodecBuffer *buffer)
{
    if (exit_) {
        return HDF_SUCCESS;
    }

    auto iter = omxBuffers_.find(buffer->bufferId);
    if (iter == omxBuffers_.end() || !iter->second) {
        return HDF_SUCCESS;
    }
    // read buffer
    BufferInfo *bufferInfo = iter->second;
    const void *addr        = pBufferInfo->avSharedPtr->ReadFromAshmem(buffer->filledLen, buffer->offset);
    // save to file
    (void)fwrite(addr, 1, buffer->filledLen, fpOut_.get());
    (void)fflush(fpOut_.get());
    // reset buffer
    buffer->offset    = 0;
    buffer->filledLen = 0;
    if (buffer->flag == OMX_BUFFERFLAG_EOS) {
        // 解码结束
        exit_ = true;
        HDF_LOGI("OnFillBufferDone the END coming");
        return HDF_SUCCESS;
    }
    // call fillthisbuffer again
    auto err = client_->FillThisBuffer(client_, bufferInfo->omxBuffer);
    if (err != HDF_SUCCESS) {
        HDF_LOGE("FillThisBuffer error");
        return HDF_SUCCESS;
    }
    return HDF_SUCCESS;
}

组件释放

编解码完成后,我们需要对使用的buffer进行销毁,同时,需要销毁接口相关的结构。
1.先发送命令让组件进入OMX_StateLoaded状态

// command to loaded
client_->SendCommand(client_, OMX_CommandStateSet, OMX_StateLoaded, nullptr, 0);

2.释放所有使用的Buffer

 // release all the buffers
auto iter = omxBuffers_.begin();
while (iter != omxBuffers_.end()) {
    BufferInfo *bufferInfo = iter->second;
    client_->FreeBuffer(client_, (uint32_t)bufferInfo->portIndex, bufferInfo->omxBuffer);
    delete bufferInfo;
    iter++;
}
omxBuffers_.clear();
unUsedInBuffers_.clear();
unUsedOutBuffers_.clear();

3.释放所有buffer之后,等待组件真正进入OMX_StateLoaded 状态
enum OMX_STATETYPE status;
client_->GetState(client_, &status);
// wait loaded
if (status != OMX_StateLoaded) {
HDF_LOGI(“Wait for OMX_StateLoaded status”);
this->WaitForStatusChanged();
} else {
HDF_LOGI(" status is %{public}d", status);
}

4.组件进入Loaded状态,可以释放组件及接口相关实例

// 关闭组件
omxMgr_->DestoryComponent(client_); // 这里已经释放了client_
client_ = nullptr;
// 释放omxMgr_
CodecComponentManagerRelease();

总结

本文只是简单的介绍了下视频编解码Codec HDI的解码过程,需要在正确的组件状态下调用相应的接口。

写在最后

**●如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:
●点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
●关注小编,同时可以期待后续文章ing🚀,不定期分享原创知识。

在这里插入图片描述

  • 14
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值