OpenHarmony实战之视频编解码HDI的使用

系统 OpenHarmony
本文主要介绍基于Codec HDI 开发的编解码功能,包括初始化,Buffer流转以及释放等。

​想了解更多内容,请访问:​

​51CTO和华为官方合作共建的鸿蒙技术社区​

​https://ost.51cto.com​

简介

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

CODEC HDI框架介绍

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

Codec HDI编解码实例

组件状态流转

1.通过调用CreateComponent函数,可以打开组件,并让组件进入OMX_StateLoaded 或OMX_StateWaitForResources 状态。

2.通过调用DestoryComponent函数,可以销毁处于OMX_StateLoaded状态的组件实例。

3.其它的状态变化,可以通过SendCommand(OMX_CommandStateSet, )使组件进入相应的状态。

<table><thread align=“center”>
<tr>
<th align=“center”><p>状态</p></th>
<th align=“center”><p>描述</p></th>
</tr>
</thread>
<tbody>
<tr>
<td align=“center” ><p>OMX_StateInvalid</p></td>
<td align=“center”><p>组件已损坏或者发生一个不能恢复的错误</p></td>
</tr>
<tr>
<td align=“center”><p>OMX_StateLoaded</p></td>
<td align=“center”><p>组件已经加载,但是资源未申请</p></td>
</tr>
<tr>
<td align=“center”><p>OMX_StateIdle</p></td>
<td align=“center”><p>组件已经申请到所有资源,但不会轮转任何buffer</p></td>
</tr>
<tr>
<td align=“center”><p>OMX_StateExecuting</p></td>
<td align=“center”><p>组件正在处理有效的数据 </p></td>
</tr>
<tr>
<td align=“center”><p>OMX_StatePause</p></td>
<td align=“center”><p>组件暂停处理数据,可以通过设置组件状态唤醒重新处理数据</p></td>
</tr>
<tr>
<td align=“center”><p>OMX_StateWaitForResources</p></td>
<td align=“center”><p>组件正在等待所需要的</p></td>
</tr>
</tbody>
</table>

编解码流程简介

主要包含以下几步:

  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, 包含状态变化,错误通知等等,具体参数含义请见下表格。

<table><thread align=“center”>
<tr>
<th align=“center”><p>eEvent</p></th>
<th align=“center”><p>data1</p></th>
<th align=“center”><p>data2</p></th>
<th align=“center”><p>eventData</p></th>
</tr>
</thread>
<tbody>
<tr>
<td align=“center” rowspan=“5”><p>OMX_EventCmdComplete</p></td>
<td align=“center”><p>OMX_CommandStateSet</p></td>
<td align=“center”><p>OMX组件状态</p></td>
<td align=“center”><p>NULL</p></td>
</tr>
<tr>
<td align=“center”><p>OMX_CommandFlush</p></td>
<td align=“center”><p>端口</p></td>
<td align=“center”><p>NULL</p></td>
</tr>
<tr>
<td align=“center”><p>OMX_CommandPortDisable</p></td>
<td align=“center”><p>端口</p></td>
<td align=“center”><p>NULL</p></td>
</tr>
<tr>
<td align=“center”><p>OMX_CommandPortEnable</p></td>
<td align=“center”><p>端口</p></td>
<td align=“center”><p>NULL</p></td>
</tr>
<tr>
<td align=“center”><p>OMX_CommandMarkBuffer</p></td>
<td align=“center”><p>端口</p></td>
<td align=“center”><p>NULL</p></td>
</tr>
<tr>
<td align=“center”><p>OMX_EventError</p></td>
<td align=“center”><p>Error Code</p></td>
<td align=“center”><p>0</p></td>
<td align=“center”><p>NULL</p></td>
</tr>
<tr>
<td align=“center”><p>OMX_EventMark</p></td>
<td align=“center”><p>0</p></td>
<td align=“center”><p>0</p></td>
<td align=“center”><p>make buffer</p></td>
</tr>
<tr>
<td align=“center”><p>OMX_EventPortSettingsChanged</p></td>
<td align=“center”><p>端口</p></td>
<td align=“center”><p>0</p></td>
<td align=“center”><p>NULL</p></td>
</tr>
<tr>
<td align=“center”><p>OMX_EventBufferFlag</p></td>
<td align=“center”><p>端口</p></td>
<td align=“center”><p>nFlags</p></td>
<td align=“center”><p>NULL</p></td>
</tr>
<tr>
<td align=“center”><p>OMX_EventResourcesAcquired</p></td>
<td align=“center”><p>0</p></td>
<td align=“center”><p>0</p></td>
<td align=“center”><p>NULL</p></td>
</tr>
<tr>
<td align=“center”><p>OMX_EventDynamicResourcesAvailable</p></td>
<td align=“center”><p>0</p></td>
<td align=“center”><p>0</p></td>
<td align=“center”><p>NULL</p></td>
</tr>
</tbody>
</table>

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的解码过程,需要在正确的组件状态下调用相应的接口。

​想了解更多内容,请访问:​

​51CTO和华为官方合作共建的鸿蒙技术社区​

​https://ost.51cto.com​

责任编辑:jianghua 来源: 鸿蒙社区
相关推荐

2021-09-10 15:10:50

鸿蒙HarmonyOS应用

2023-08-23 14:53:05

火山引擎视频云视频编解码

2021-09-16 15:05:39

鸿蒙HarmonyOS应用

2024-02-28 08:22:07

2021-08-03 08:38:21

Netty解码器使用

2020-12-21 06:58:12

Web安全编解码工具

2021-06-11 21:46:31

RocketMQ数据JSON

2021-10-08 09:38:57

NettyChannelHand架构

2022-08-20 07:56:48

FFmpeg数字音频流浏览器

2022-10-10 10:38:22

FedoraopenSUSE视频编解码

2024-03-14 08:24:25

MediaCodec解码播放Android

2022-06-29 13:59:40

家居应用鸿蒙

2021-11-08 18:37:45

MySQL解码测试

2022-09-05 08:12:28

Google二进制Protobuf

2021-07-20 10:20:55

腾讯云音视频超高清

2022-05-26 15:28:03

网络管理Socket 模块

2021-04-22 11:21:03

Windows 10Windows微软

2021-04-07 13:52:57

GoogleLyra编译器

2022-03-15 16:53:12

FFmpeg视频解码Android

2023-07-26 16:31:09

Windows 10Windows 11微软
点赞
收藏

51CTO技术栈公众号