OpenHarmony开发者论坛
标题:
OpenHarmony 3.2 Release版本Codec HDI适配过程
[打印本页]
作者:
Laval社区小助手
时间:
2023-11-1 09:41
标题:
OpenHarmony 3.2 Release版本Codec HDI适配过程
[md]详情:[OpenHarmony 3.2 Release版本Codec HDI适配过程](
https://laval.csdn.net/64c8b1587 ... tml?login=from_csdn
)
# 简介
OpenHarmony Codec HDI(Hardware Device Interface)驱动框架基于OpenMax实现了视屏硬件编解码驱动,提供Codec基础能力接口供上层媒体服务调用,包括获取组件编解码能力、创建组件、参数设置、数据的轮转和控制、以及销毁组件等功能,实现对视频数据的编解码处理。
## 视频编解码驱动架构
![](
https://devpress.csdnimg.cn/bbbce375dfe34244b684b2b24e1c2405.png
)
![](file:///D:/cwx1148087/Doc/md/codec/img/codec_hdi.png?lastModify=1690874039)
**Codec HDI 2.0接口依赖OpenMax IL的标准接口。OMX Wrapper将OMX接口的实现封装成libOMX\_Core.z.so供HDI层调用。如果codec驱动为实现OpenMax标准接口,则需根据驱动适配实现OMX Interface接口。**
Codec HDI 2.0接口列表:
| 头文件 | 接口名称 | 功能描述 |
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
| codec\_component \_manager.h | int32\_t (\*GetComponentNum)(); | 获取Codec编解码组件数量 |
| | int32\_t (\*GetComponentCapabilityList)(CodecCompCapability \*capList, int32\_t count); | 获取编解码能力集表 |
| | int32\_t (\*CreateComponent)(struct CodecComponentType \*\*component, uint32\_t \*componentId, char \*compName, int64\_t appData, struct CodecCallbackType \*callbacks); | 创建Codec组件实例 |
| | int32\_t (\*DestroyComponent)(uint32\_t componentId); | 销毁组件实例 |
| codec\_component \_if.h | int32\_t (\*GetComponentVersion)(struct CodecComponentType \*self, struct CompVerInfo \*verInfo); | 获取Codec组件版本号 |
| | int32\_t (\*SendCommand)(struct CodecComponentType \*self, enum OMX\_COMMANDTYPE cmd, uint32\_t param, int8\_t \*cmdData, uint32\_t cmdDataLen); | 发送命令给组件 |
| | int32\_t (\*GetParameter)(struct CodecComponentType \*self, uint32\_t paramIndex, int8\_t \*paramStruct, uint32\_t paramStructLen); | 获取组件参数设置 |
| | int32\_t (\*SetParameter)(struct CodecComponentType \*self, uint32\_t index, int8\_t \*paramStruct, uint32\_t paramStructLen); | 设置组件需要的参数 |
| | int32\_t (\*GetConfig)(struct CodecComponentType \*self, uint32\_t index, int8\_t \*cfgStruct, uint32\_t cfgStructLen); | 获取组件的配置结构 |
| | int32\_t (\*SetConfig)(struct CodecComponentType \*self, uint32\_t index, int8\_t \*cfgStruct, uint32\_t cfgStructLen); | 设置组件的配置 |
| | int32\_t (\*GetExtensionIndex)(struct CodecComponentType \*self, const char \*paramName, uint32\_t \*indexType); | 根据字符串获取组件的扩展索引 |
| | int32\_t (\*GetState)(struct CodecComponentType \*self, enum OMX\_STATETYPE \*state); | 获取组件的状态 |
| | int32\_t (\*ComponentTunnelRequest)(struct CodecComponentType \*self, uint32\_t port, int32\_t tunneledComp, uint32\_t tunneledPort, struct OMX\_TUNNELSETUPTYPE \*tunnelSetup); | 设置组件Tunneled方式通信 |
| | int32\_t (\*UseBuffer)(struct CodecComponentType \*self, uint32\_t portIndex, struct OmxCodecBuffer \*buffer); | 指定组件端口的buffer |
| | int32\_t (\*AllocateBuffer)(struct CodecComponentType \*self, uint32\_t portIndex, struct OmxCodecBuffer \*buffer); | 向组件申请端口buffer |
| | int32\_t (\*FreeBuffer)(struct CodecComponentType \*self, uint32\_t portIndex, const struct OmxCodecBuffer \*buffer); | 释放buffer |
| | int32\_t (\*EmptyThisBuffer)(struct CodecComponentType \*self, const struct OmxCodecBuffer \*buffer); | 编解码输入待处理buffer |
| | int32\_t (\*FillThisBuffer)(struct CodecComponentType \*self, const struct OmxCodecBuffer \*buffer); | 编解码输出填充buffer |
| | int32\_t (\*SetCallbacks)(struct CodecComponentType \*self, struct CodecCallbackType \*callback, int64\_t appData); | 设置Codec组件的回调函数 |
| | int32\_t (\*ComponentDeInit)(struct CodecComponentType \*self); | 组件去初始化 |
| | int32\_t (\*UseEglImage)(struct CodecComponentType \*self, struct OmxCodecBuffer \*buffer, uint32\_t portIndex, int8\_t \*eglImage, uint32\_t eglImageLen); | 使用已在ELG中申请的空间 |
| | int32\_t (\*ComponentRoleEnum)(struct CodecComponentType \*self, uint8\_t \*role, uint32\_t roleLen, uint32\_t index); | 获取组件角色 |
| codec\_callback\_if.h | int32\_t (\*EventHandler)(struct CodecCallbackType \*self, enum OMX\_EVENTTYPE event, struct EventInfo \*info); | 事件上报 |
| | int32\_t (\*EmptyBufferDone)(struct CodecCallbackType \*self, int64\_t appData, const struct OmxCodecBuffer \*buffer); | 上报输入buffer编码或者解码处理完毕 |
| | int32\_t (\*FillBufferDone)(struct CodecCallbackType \*self, int64\_t appData, const struct OmxCodecBuffer \*buffer); | 上报输出buffer填充完毕 |
## Codec HDI相关目录接口
```
├── //drivers/peripheral/codec
│ ├── hal
│ │ ├── BUILD.gn
│ │ ├── idl_service
│ │ ├── include
│ │ ├── passthrough # v2.0到v1.0的转换,v1.0接口已弃用,无需实现
│ │ ├── src
│ │ ├── v1.0 # codec hdi v1.0接口的实现,已弃用,MediaService已不对接相关接口。
│ │ └── v2.0 # codec hdi v2.0接口的实现,依赖OpenMax接口,需封装实现libOMX_Core.z.so
│ ├── hdi_service # codec_host相关实现
│ │ ├── BUILD.gn
│ │ ├── codec_proxy
│ │ ├── codec_service_stub
│ │ └── common
│ ├── interfaces
│ │ └── include
│ └── test
```
## OMX\_Core相关接口
codec hdi V2.0的实现依赖libOMX\_Core.z.so,需根据OpenMax标准接口封装实现。
参考drivers/peripheral/codec/hal/v2.0/hdi\_impl/include/codec\_omx\_core.h的定义调用过程。
```
typedef OMX_ERRORTYPE (*InitFunc)();
typedef OMX_ERRORTYPE (*DeinitFunc)();
typedef OMX_ERRORTYPE (*ComponentNameEnumFunc)(OMX_STRING, OMX_U32, OMX_U32);
typedef OMX_ERRORTYPE (*GetHandleFunc)(OMX_HANDLETYPE *, OMX_STRING, OMX_PTR, OMX_CALLBACKTYPE *);
typedef OMX_ERRORTYPE (*FreeHandleFunc)(OMX_HANDLETYPE);
typedef OMX_ERRORTYPE (*GetRolesOfComponentFunc)(OMX_STRING, OMX_U32 *, OMX_U8 **);
```
## HCS配置
### 配置codec\_host服务
./hdf\_config/uhdf/device\_info.hcs
```
codec :: host {
hostName = "codec_host";
priority = 50;
gid = ["codec_host", "uhdf_driver", "vendor_mpp_driver"];
codec_omx_device :: device {
device0 :: deviceNode {
policy = 2;
priority = 100;
moduleName = "libcodec_hdi_omx_server.z.so";
serviceName = "codec_hdi_omx_service";
deviceMatchAttr = "media_codec_capabilities";
}
}
}
```
### 配置codec\_capabilities
根据codec::host中配置的deviceMatchAttr,配置hdf.hcs
./hdf\_config/uhdf/hdf.hcs
```
#include "media_codec_capabilitie.hcs"
```
参考media\_codec\_capabilitie.hcs
```
root {
module = "master";
codec_config {
match_attr = "media_codec_capabilities";
use_openmax = true;
// capsMask: 0x01, Adaptive playback; 0x02, Secure playback; 0x04, Tunnel playback.
// allocateMask: 0x01, Input buffer allocated within the Codec module;
// allocateMask: 0x02, Input buffer allocated by an external user;
// allocateMask: 0x04, Output buffer allocated within the Codec module;
// allocateMask: 0x08, Output buffer allocated by an external user.
VideoHwEncoders {
/* node name explanation -- HDF_video_hw_enc_avc_rk:
**
** HDF____________video__________________hw____________________enc____________avc_______rk
** | | | | | |
** HDF or OMX video or audio hardware or software encoder or decoder mime vendor
*/
HDF_video_hw_enc_avc_rk {
role = 1;
type = 1;
name = "OMX.rk.video_encoder.avc";
supportProfiles = [1, 32768, 2, 32768, 8, 32768];
maxInst = 4;
isSoftwareCodec = false;
processModeMask = [];
capsMask = [0x01];
minBitRate = 1;
maxBitRate = 40000000;
minWidth = 176;
minHeight = 144;
maxWidth = 1920;
maxHeight = 1088;
widthAlignment = 16;
heightAlignment = 8;
minBlockCount = 99;
maxBlockCount = 8160;
minBlocksPerSecond = 99;
maxBlocksPerSecond = 489600;
blockSizeWidth = 16;
blockSizeHeight = 16;
supportPixelFmts = [28, 24, 20, 12];
measuredFrameRate = [320, 240, 165, 165, 720, 480, 149, 149, 1280, 720, 73, 73, 1920, 1080, 18, 18];
bitRateMode = [1, 2];
minFrameRate = 1;
maxFrameRate = 60;
}
......
......
......
}
}
```
## RK3568的参考适配过程
### OMX Wrapper的封装
根据gn文件://drivers/peripheral/codec/BUILD.gn
```
OMX_IL_PATH = rebase_path(
"//device/soc/${device_company}/${product_name}/hardware/omx_il")
cmd = "if [ -f ${OMX_IL_PATH}/BUILD.gn ]; then echo true; else echo false; fi"
HAVE_OMX_IL_PATH =
exec_script("//build/lite/run_shell_cmd.py", [ cmd ], "value")
if (HAVE_OMX_IL_PATH) {
deps += [ "${OMX_IL_PATH}:lib_omx" ]
}
```
需创建lib\_omx工程,并封装实现libOMX\_Core.z.so,供codec hdi接口调用。
如果codec驱动已实现OpenMax标准接口,则可直接封装libOMX\_Core库,否则需要根据私有驱动实现OpenMax接口。
参考RK3568的适配过程,因codec驱动使用rockchip的mpp平台实现,需根据私有驱动实现OpenMax的接口。
参考gn文件://device/soc/rockchip/rk3568/hardware/omx\_il/BUILD.gn
```
group("lib_omx") {
if (product_name == "rk3568") {
deps = [
"//device/soc/rockchip/rk3568/hardware/omx_il/component/video/dec:libomxvpu_dec",
"//device/soc/rockchip/rk3568/hardware/omx_il/component/video/enc:libomxvpu_enc",
"//device/soc/rockchip/rk3568/hardware/omx_il/core:libOMX_Core",
"//device/soc/rockchip/rk3568/hardware/omx_il/libOMXPlugin:libOMX_Pluginhw",
]
}
}
```
### hcs配置
* 配置codec\_host服务
//vendor/hihope/rk3568/hdf\_config/uhdf/device\_info.hcs
```
codec :: host {
hostName = "codec_host";
priority = 50;
gid = ["codec_host", "uhdf_driver", "vendor_mpp_driver"];
codec_omx_device :: device {
device0 :: deviceNode {
policy = 2;
priority = 100;
moduleName = "libcodec_hdi_omx_server.z.so";
serviceName = "codec_hdi_omx_service";
deviceMatchAttr = "codec_component_capabilities";
}
}
}
```
* 配置codec hcs
需根据硬件信息配置编解码组件的配置参数
//vendor/hihope/rk3568/hdf\_config/uhdf/hdf.hcs
```
#include "media_codec/codec_component_capabilities.hcs"
```
//vendor/hihope/rk3568/hdf\_config/uhdf/media\_codec/codec\_component\_capabilities.hcs
```
root {
module = "master";
codec_config {
match_attr = "codec_component_capabilities";
use_openmax = true;
// capsMask: 0x01, Adaptive playback; 0x02, Secure playback; 0x04, Tunnel playback.
// allocateMask: 0x01, Input buffer allocated within the Codec module;
// allocateMask: 0x02, Input buffer allocated by an external user;
// allocateMask: 0x04, Output buffer allocated within the Codec module;
// allocateMask: 0x08, Output buffer allocated by an external user.
VideoHwEncoders {
/* node name explanation -- HDF_video_hw_enc_avc_rk:
**
** HDF____________video__________________hw____________________enc____________avc_______rk
** | | | | | |
** HDF or OMX video or audio hardware or software encoder or decoder mime vendor
*/
HDF_video_hw_enc_avc_rk {
role = 1;
type = 1;
name = "OMX.rk.video_encoder.avc";
supportProfiles = [1, 32768, 2, 32768, 8, 32768];
maxInst = 4;
isSoftwareCodec = false;
processModeMask = [];
capsMask = [0x01];
minBitRate = 1;
maxBitRate = 40000000;
minWidth = 176;
minHeight = 144;
maxWidth = 1920;
maxHeight = 1088;
widthAlignment = 16;
heightAlignment = 8;
minBlockCount = 0xFFFFFFFF;
maxBlockCount = 0xFFFFFFFF;
minBlocksPerSecond = 0xFFFFFFFF;
maxBlocksPerSecond = 0xFFFFFFFF;
blockSizeWidth = 0xFFFFFFFF;
blockSizeHeight = 0xFFFFFFFF;
supportPixelFmts = [28, 24, 20, 12];
measuredFrameRate = [320, 240, 165, 165, 720, 480, 149, 149, 1280, 720, 73, 73, 1920, 1080, 18, 18];
bitRateMode = [1, 2];
minFrameRate = 0;
maxFrameRate = 0;
}
}
VideoHwDecoders {
HDF_video_hw_dec_avc_rk {
role = 1;
type = 0;
name = "OMX.rk.video_decoder.avc";
supportProfiles = [1, 32768, 2, 32768, 8, 32768];
maxInst = 6;
isSoftwareCodec = false;
processModeMask = [];
capsMask = [0x01];
minBitRate = 1;
maxBitRate = 10000000;
minWidth = 176;
minHeight = 144;
maxWidth = 4096;
maxHeight = 2160;
widthAlignment = 8;
heightAlignment = 8;
minBlockCount = 0xFFFFFFFF;
maxBlockCount = 0xFFFFFFFF;
minBlocksPerSecond = 1;
maxBlocksPerSecond = 244800;
blockSizeWidth = 16;
blockSizeHeight = 16;
supportPixelFmts = [24];
measuredFrameRate = [320, 240, 617, 617, 720, 480, 559, 559, 1280, 720, 276, 276, 1920, 1080, 164, 164, 3840, 2160, 30, 30];
bitRateMode = [];
minFrameRate = 0;
maxFrameRate = 0;
}
HDF_video_hw_dec_mpeg2_rk {
role = 0xFFFFFFFF;
type = 0;
name = "OMX.rk.video_decoder.m2v";
supportProfiles = [0, 3, 1, 3];
maxInst = 6;
isSoftwareCodec = false;
processModeMask = [];
capsMask = [0x01];
minBitRate = 1;
maxBitRate = 10000000;
minWidth = 176;
minHeight = 144;
maxWidth = 1920;
maxHeight = 1088;
widthAlignment = 8;
heightAlignment = 8;
minBlockCount = 0xFFFFFFFF;
maxBlockCount = 0xFFFFFFFF;
minBlocksPerSecond = 1;
maxBlocksPerSecond = 244800;
blockSizeWidth = 16;
blockSizeHeight = 8;
supportPixelFmts = [24];
measuredFrameRate = [];
bitRateMode = [];
minFrameRate = 0;
maxFrameRate = 0;
}
HDF_video_hw_dec_v8p_rk {
role = 0xFFFFFFFF;
type = 0;
name = "OMX.rk.video_decoder.vp8";
supportProfiles = [];
maxInst = 6;
isSoftwareCodec = false;
processModeMask = [];
capsMask = [0x01];
minBitRate = 1;
maxBitRate = 10000000;
minWidth = 176;
minHeight = 144;
maxWidth = 1920;
maxHeight = 1088;
widthAlignment = 8;
heightAlignment = 8;
minBlockCount = 0xFFFFFFFF;
maxBlockCount = 0xFFFFFFFF;
minBlocksPerSecond = 1;
maxBlocksPerSecond = 244800;
blockSizeWidth = 16;
blockSizeHeight = 16;
supportPixelFmts = [24];
measuredFrameRate = [320, 180, 500, 500, 640, 360, 387, 387, 1280, 720, 112, 112, 1920, 1080, 77, 77];
bitRateMode = [];
minFrameRate = 0;
maxFrameRate = 0;
}
HDF_video_hw_dec_h263_rk {
role = 0xFFFFFFFF;
type = 0;
name = "OMX.rk.video_decoder.h263";
supportProfiles = [1, 1, 1, 2, 1, 4, 1, 16, 8, 1, 8, 2, 8, 4, 8, 16];
maxInst = 6;
isSoftwareCodec = false;
processModeMask = [];
capsMask = [0x01];
minBitRate = 1;
maxBitRate = 10000000;
minWidth = 176;
minHeight = 144;
maxWidth = 1920;
maxHeight = 1088;
widthAlignment = 8;
heightAlignment = 8;
minBlockCount = 0xFFFFFFFF;
maxBlockCount = 0xFFFFFFFF;
minBlocksPerSecond = 1;
maxBlocksPerSecond = 244800;
blockSizeWidth = 16;
blockSizeHeight = 16;
supportPixelFmts = [24];
measuredFrameRate = [176, 144, 600, 600, 352, 288, 600, 600];
bitRateMode = [];
minFrameRate = 0;
maxFrameRate = 0;
}
HDF_video_hw_dec_m4v_rk {
role = 3;
type = 0;
name = "OMX.rk.video_decoder.m4v";
supportProfiles = [1, 1, 1, 2, 1, 4, 1, 8, 1, 16];
maxInst = 6;
isSoftwareCodec = false;
processModeMask = [];
capsMask = [0x01];
minBitRate = 1;
maxBitRate = 10000000;
minWidth = 176;
minHeight = 144;
maxWidth = 1920;
maxHeight = 1088;
widthAlignment = 8;
heightAlignment = 8;
minBlockCount = 0xFFFFFFFF;
maxBlockCount = 0xFFFFFFFF;
minBlocksPerSecond = 1;
maxBlocksPerSecond = 244800;
blockSizeWidth = 16;
blockSizeHeight = 16;
supportPixelFmts = [24];
measuredFrameRate = [176, 144, 600, 600];
bitRateMode = [];
minFrameRate = 0;
maxFrameRate = 0;
}
HDF_video_hw_dec_flv_rk {
role = 0xFFFFFFFF;
type = 0;
name = "OMX.rk.video_decoder.flv1";
supportProfiles = [];
maxInst = 6;
isSoftwareCodec = false;
processModeMask = [];
capsMask = [0x01];
minBitRate = 1;
maxBitRate = 10000000;
minWidth = 176;
minHeight = 144;
maxWidth = 1920;
maxHeight = 1088;
widthAlignment = 8;
heightAlignment = 8;
minBlockCount = 0xFFFFFFFF;
maxBlockCount = 0xFFFFFFFF;
minBlocksPerSecond = 1;
maxBlocksPerSecond = 244800;
blockSizeWidth = 16;
blockSizeHeight = 16;
supportPixelFmts = [24];
measuredFrameRate = [];
bitRateMode = [];
minFrameRate = 0;
maxFrameRate = 0;
}
HDF_video_hw_dec_mjpeg_rk {
role = 0;
type = 0;
name = "OMX.rk.video_decoder.mjpeg";
supportProfiles = [];
maxInst = 6;
isSoftwareCodec = false;
processModeMask = [];
capsMask = [0x01];
minBitRate = 1;
maxBitRate = 10000000;
minWidth = 176;
minHeight = 144;
maxWidth = 1920;
maxHeight = 1088;
widthAlignment = 8;
heightAlignment = 8;
minBlockCount = 0xFFFFFFFF;
maxBlockCount = 0xFFFFFFFF;
minBlocksPerSecond = 1;
maxBlocksPerSecond = 244800;
blockSizeWidth = 16;
blockSizeHeight = 16;
supportPixelFmts = [24];
measuredFrameRate = [];
bitRateMode = [];
minFrameRate = 0;
maxFrameRate = 0;
}
HDF_video_hw_dec_hevc_rk {
role = 2;
type = 0;
name = "OMX.rk.video_decoder.hevc";
supportProfiles = [1, 1, 1, 4, 1, 16, 1, 64, 1, 256, 1, 1024, 1, 4096, 1, 16384, 1, 65536, 2, 65536];
maxInst = 6;
isSoftwareCodec = false;
processModeMask = [];
capsMask = [0x01];
minBitRate = 1;
maxBitRate = 160000000;
minWidth = 176;
minHeight = 144;
maxWidth = 1920;
maxHeight = 1088;
widthAlignment = 2;
heightAlignment = 2;
minBlockCount = 0xFFFFFFFF;
maxBlockCount = 0xFFFFFFFF;
minBlocksPerSecond = 1;
maxBlocksPerSecond = 244800;
blockSizeWidth = 16;
blockSizeHeight = 16;
supportPixelFmts = [24];
measuredFrameRate = [352, 288, 700, 700, 720, 480, 700, 700, 640, 360, 980, 980, 1280, 720, 600, 600, 1920, 1080, 130, 130, 3840, 2160, 130, 130];
bitRateMode = [];
minFrameRate = 0;
maxFrameRate = 0;
}
}
VideoSwEncoders {
}
VideoSwDecoders {
}
AudioHwEncoders {
}
AudioHwDecoders {
}
AudioSwEncoders {
}
AudioSwDecoders {
}
}
}
```
## 适配验证
* 当系统启动后,拉起codec\_host进程,日志中有加载hcs配置的component相关组件的log,则初步判定适配过程正常。
* 当前系统通过gstreamer插件实现视频编解码功能。当未实现硬件编解码时,默认使用FFmpeg软件编解码。
codec hdi插件加载过程如下:
///foundation/multimedia/player\_framework/services/engine/gstreamer/plugins/codec/hdi\_plugins/hdi\_init.cpp
```
void HdiInit::AddHdiCap(CodecCompCapability &hdiCap)
{
MEDIA_LOGI("Add codec name %{public}s", hdiCap.compName);
CapabilityData codecCap;
codecCap.codecName = hdiCap.compName;
codecCap.codecType = GetCodecType(hdiCap.type);
codecCap.mimeType = GetCodecMime(hdiCap.role);
codecCap.isVendor = !hdiCap.isSoftwareCodec;
codecCap.alignment = {hdiCap.port.video.whAlignment.widthAlignment, hdiCap.port.video.whAlignment.heightAlignment};
codecCap.bitrateMode = GetBitrateMode(hdiCap.port.video);
codecCap.width = {hdiCap.port.video.minSize.width, hdiCap.port.video.maxSize.width};
codecCap.height = {hdiCap.port.video.minSize.height, hdiCap.port.video.maxSize.height};
codecCap.bitrate = {hdiCap.bitRate.min, hdiCap.bitRate.max};
codecCap.frameRate = {hdiCap.port.video.frameRate.min, hdiCap.port.video.frameRate.max};
codecCap.format = GetCodecFormats(hdiCap.port.video);
codecCap.blockPerFrame = {hdiCap.port.video.blockCount.min, hdiCap.port.video.blockCount.max};
codecCap.blockPerSecond = {hdiCap.port.video.blocksPerSecond.min, hdiCap.port.video.blocksPerSecond.max};
codecCap.blockSize = {hdiCap.port.video.blockSize.width, hdiCap.port.video.blockSize.height};
codecCap.measuredFrameRate = GetMeasuredFrameRate(hdiCap.port.video);
codecCap.profileLevelsMap = GetCodecProfileLevels(hdiCap);
capabilitys_.push_back(codecCap);
}
```
[/md]
欢迎光临 OpenHarmony开发者论坛 (https://forums.openharmony.cn/)
Powered by Discuz! X3.5