本文记录CoralReefPlayer这个项目在移植到HarmonyOS NEXT/OpenHarmony时遇到的各种问题. 为了避免读者不了解该项目, 从而不理解本文在说些什么, 这里先对该项目做一些简要介绍.
CoralReefPlayer 即珊瑚礁播放器,是一款使用 C++20 开发的跨平台流媒体播放器库,目前支持播放 RTSP 和 MJPEG over HTTP 流,可为基于网络进行视频流传输的机器人上位机提供可定制、高性能、低延迟的推拉流、编解码及录像能力。
该库为使用C++语言开发的原生库, 对外暴露C语言接口, 其他语言可利用其与C语言的互操作性来调用该库的接口, 并将这些调用封装为该语言的面向对象形式, 从而在其他语言中较为优雅地调用该原生库, 实现视频流解码等功能.
在适配鸿蒙操作系统之前, 该库已经实现了Node.js binding, 而鸿蒙的js具有两种与C语言互操作的方法, 其中一种为JSVM-API, 其类似于jni, 另一种即为Node-API, 考虑到我们已有基于Node-API开发的Node.js binding, 因此选择了Node-API方案, 理论上代码无需修改即可在鸿蒙上运行, 但是理论归理论, 实际是实际, 由于Node.js和鸿蒙在底层实现上的根本差异, 在整个移植的过程中还是踩了不少坑, 特此记录一下.
一. node-addon-api
鸿蒙装包只能用ohpm, 之前是能用npm的, 但是4.0开始不能了, 据说是安全问题, 防止供应链投毒吧, 那我欠的node-addon-api谁给我补呢(
好在就在我开始移植的几乎同一时间, 有个第三方维护的node-addon-api-ohos上ohpm了, 所以直接装就搞定了, 省着自己再移植了
二. Buffer不继承自Uint8Array
鸿蒙的Buffer不继承自Uint8Array, 所以对Buffer对象调用napi_get_typedarray_info
会返回napi_invalid_arg
异常
详见 gitee.com/openharmony/arkui_napi/blob/master/native_engine/native_api.cpp#L2738
我已经给维护者提了一个pr, 但是截至本文发布时尚未被合入, 如果有需要可以自己cherry pick下: fix: make Buffer extend Object directly by DawningW · Pull Request #2 · richerfu/node-addon-api-ohos
PS: 看来HarmonyOS NEXT真是基于OpenHarmony的了, 不是Android套壳了, 要不然log不可能和开源鸿蒙代码对的上, 而且我也不可能这么快发现问题
三. Buffer有大小限制
之前Node.js binding是采用Buffer传递视频/音频帧数据的, 虽然我忘了当初为啥要用Buffer不用ArrayBuffer了, 但是我决定继承, 结果在模拟器上一跑, 挂了, 一看日志, 说buffer大小超限制了…
然后去翻OpenHarmony的源码, 看到了这个判断, 在gitee.com/openharmony/arkui_napi/blob/master/native_engine/native_api.cpp#L2604
static constexpr size_t MAX_BYTE_LENGTH = 2097152; static constexpr size_t ONEMIB_BYTE_SIZE = 1048576; // ... NAPI_EXTERN napi_status napi_create_external_buffer(napi_env env, size_t length, void* data, napi_finalize finalize_cb, void* finalize_hint, napi_value* result) { NAPI_PREAMBLE(env); CHECK_ARG(env, result); CHECK_ARG(env, data); RETURN_STATUS_IF_FALSE(env, length > 0, napi_invalid_arg); auto callback = reinterpret_cast<panda::NativePointerCallback>(finalize_cb); if (!data) { HILOG_ERROR("data is empty"); return napi_set_last_error(env, napi_invalid_arg); } if (length > MAX_BYTE_LENGTH) { HILOG_ERROR("Creat failed, current size: %{public}2f MiB, limit size: %{public}2f MiB", static_cast<float>(length) / static_cast<float>(ONEMIB_BYTE_SIZE), static_cast<float>(MAX_BYTE_LENGTH) / static_cast<float>(ONEMIB_BYTE_SIZE)); data = nullptr; return napi_set_last_error(env, napi_invalid_arg); } auto engine = reinterpret_cast<NativeEngine*>(env); auto vm = engine->GetEcmaVm(); Local<panda::BufferRef> object = panda::BufferRef::New(vm, data, length, callback, finalize_hint); void* ptr = object->GetBuffer(vm); CHECK_ARG(env, ptr); *result = JsValueFromLocalValue(object); return GET_RETURN_STATUS(env); }
从上面的代码中可以看出, Buffer最大居然只能是2MB, 最大只有2MB的Buffer有啥用啊…然后去翻ArrayBuffer的源码, 发现是没有这个大小限制的, 于是我决定用__OHOS__
这个宏判一下, 为了避免依赖Node.js binding的存量代码出现问题, 在Node.js上还是用Buffer, 在鸿蒙上改用ArrayBuffer
PS: 看来HarmonyOS NEXT真的真的是基于OpenHarmony的了, 就连Buffer大小限制都是一样的, 但是为啥Buffer限大小ArrayBuffer不限, 这不是有毛病…
四. 为Android编译的原生库居然不需要重编
表面上看非常神奇, 但是其实是合理的, 因为架构一样, 都是x86/x86_64/armv7/aarch64, 然后C ABI又是稳定的, 所以不需要重编就能跑是很正常的, 并不能说明鸿蒙是安卓套壳
然后我还在OpenHarmony用的musl库里看到这样一段话, 嗯, 非常合理…
基于openharmony的需求,为musl新增的特性: ● 加载器地址随机化,RELRO共享机制 ● 提供加载器namespace机制 ● OHOS容器中能够运行依赖bionic的库 ● musl全球化接口适配locale数据能力 ● mallocng堆内存分配器安全增强,默认开启meta指针混淆。地址随机化通过MALLOC_SECURE_ALL宏开关。可在编译命令中增加--gn-args="musl_secure_level=3"开启 等。在新增特性基础上,也进行了对于musl接口功能的完善与错误的修复。
五. libc库差异导致的符号缺失问题
Android用的是自研bionic库, 鸿蒙用的是裁剪过的musl库, 虽然已经尽可能做了兼容, 但是难免还是有一些bionic库中的私有符号是在musl库中不存在的, 这时候就需要我们分析日志, 比对bionic库和鸿蒙用的musl库的源码, 去实现这些私有符号
1. __errno
bionic的errno和musl的errno都是宏, 只不过bionic定义成了__errno, musl定义成了__errno_location, 所以自己实现下__errno就行了
int* __errno(void) { return __errno_location(); }
2. __sF
这个问题比较棘手, 需要去bionic源码中搜:
// libc/include/stdio.h struct __sFILE; typedef struct __sFILE FILE; #if __ANDROID_API__ >= 23 extern FILE* _Nonnull stdin __INTRODUCED_IN(23); extern FILE* _Nonnull stdout __INTRODUCED_IN(23); extern FILE* _Nonnull stderr __INTRODUCED_IN(23); /* C99 and earlier plus current C++ standards say these must be macros. */ #define stdin stdin #define stdout stdout #define stderr stderr #else /* Before M the actual symbols for stdin and friends had different names. */ extern FILE __sF[] __REMOVED_IN(23, "Use stdin/stdout/stderr"); #define stdin (&__sF[0]) #define stdout (&__sF[1]) #define stderr (&__sF[2]) #endif
可以看到在Android API 23之前这三个变量并非和其他libc库一样是三个指针, 而是对一个数组中的三个元素取地址, 这样的话可移植性非常差, 所以在API 23之后改为了三个指针
之所以会出现这种问题, 是因为我使用的ffmpeg库的目标API版本很低, 我记得是21, 低于23, 所以引用了__sF这个变量. 最初我想自己构造__sF这个数组, 然后把stdout之类的值都拷进去, 结果发现musl的FILE结构体比bionic的要大得多, 所以作罢. 然后我又想出了一个临时解决方案, 目前只有ffmpeg是为Android平台预编译的库, 然后又只有log用到了stdout, 那么只要调用av_log_set_callback(nullptr)
不让ffmpeg输出日志就好了, 然后代码就这么水灵灵地上库了, 我真机智(bushi
但是上述解决方案显然不够优雅, 而且不够一劳永逸, 这是ffmpeg提供了设置日志打印回调的接口, 如果别的库没提供呢, 所以正确的做法应该是以高于API23的级别重新编译ffmpeg, API24是Android7.0, 现在99%设备都应该是7.0以上版本, 所以我决定把目标API设为24再重新编译ffmpeg库, 从而从根本上解决这个问题
3. __register_atfork
bionic的pthread_atfork
调的是__register_atfork
, 然而musl的pthread_atfork
直接就是实现, 所以会报找不到__register_atfork
, 解决办法也很简单, 自己实现一个就行:
int __register_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void), void* dso) { (void)(dso); return pthread_atfork(prepare, parent, child); }
六. 适配OpenHarmony
别急, 环境没有呢还