把一个"三行 API"的老库鸿蒙化,我踩穿了四个坑:react-native-video-duration × RNOH 0.84 适配实录

关键字:React Native · HarmonyOS / OpenHarmony · RNOH(@react-native-oh/react-native-harmony 0.84)· New Architecture · TurboModule · Platform.OS 分支陷阱 · C++ methodMap

0. 前言

react-native-video-duration 是一个只有 1 个异步方法、无 codegen spec、走老架构 NativeModules 访问的"古董级"三方库。把它适配到鸿蒙 RNOH(React Native OpenHarmony)看似是"注册一个同名 TurboModule 就行"的低成本活,但真机验证阶段连续推翻了 4 个文档级假设:

  1. Metro 根本打不进这个库 —— 宿主 metro.config.jswatchFolders 没包含 symlink 真实目录;
  2. Platform.OS === 'android' 是错的 —— RNOH 0.84.3 真机上报的是 'harmony',JS 三元分支直接走反;
  3. 模块注册成功 ≠ 方法可见 —— 纯 ArkTS 注册 + 裸 ArkTSTurboModule 的 delegate 不会暴露任何方法,必须补 C++ methodMap_ spec 类;
  4. hdc 非 root 写不进应用沙箱 —— 测试视频只能打进 HAP rawfile、由应用启动时自拷贝。

最终方案:双名注册VideoDuration 返回秒 + VideoDurationModule 返回毫秒),HUAWEI Mate 60 Pro 真机返回 52.209 s,与源视频元数据完全一致。


1. 为什么选这个库做"鸿蒙化样板"

在 RNOH(React Native OpenHarmony,New Architecture only)生态里,绝大多数三方库的适配难点是架构判定:老架构库(RN < 0.76)大多没有 codegen spec、没有 TurboModule,只有一套纯 NativeModules.X.method() + Promise 的调用。

react-native-video-duration(v0.1.1)是这个类别的典型样本:

// src/index.tsx —— 库的全部"原生契约"就这两行
const VideoDuration =
  Platform.OS === 'android'
    ? NativeModules.VideoDurationModule   // Android:模块名 + 毫秒
    : NativeModules.VideoDuration;        // iOS:模块名 + 秒

export async function getVideoDuration(videoPath: string): Promise<number> {
  const result = await VideoDuration.getVideoDuration(videoPath);
  return Platform.OS === 'android' ? result / 1000 : result; // 对外统一返回秒
}

原生侧:Android 用 MediaMetadataRetrievergetName() = "VideoDurationModule",返回毫秒);iOS 用 AVURLAsset(模块名 VideoDuration,返回秒)。公开 API 只有 1 个 getVideoDuration(path) → seconds

它小(原生逻辑一行)、契约清晰(模块名 + 单位二元分歧)、且完美命中了"老架构纯 NativeModules"这一 RNOH 适配的第一大坑区。把它跑通,等于把"老库鸿蒙化"的通用路径跑通了一遍。

2. 适配前调研(三件事,一件事想错了)

2.1 接口分析

  • 导出:getVideoDuration(videoPath: string): Promise<number>
  • 无事件订阅、无常量表、无 Fabric 组件 → 方法调用型,适配形态 = ArkTS TurboModule;
  • codegenConfig → JS 侧不会用 TurboModuleRegistry.getEnforcing,而是直接 NativeModules.X

2.2 鸿蒙平台能力对照

iOSAndroid鸿蒙
AVURLAsset.duration + CMTimeGetSecondsMediaMetadataRetriever.getMetadata(DURATION)media.createAVMetadataExtractor()(API 11+,三方可用)+ fdSrc + fetchMetadata()

AVMetadataExtractor.fetchMetadata().duration 返回的是毫秒数值字符串,需要 Number() 转换——这是实现层唯一的类型坑。

2.3 版本配套与宿主

RN 0.84.1 ↔ RNOH 0.84.3 ↔ @rnoh/react-native-openharmony ^0.84.3,宿主用 RNOH 官方示例工程 RNOH084Demo,compileSdk 26 / compatible SDK '5.0.0(12)'

⚠️ 2.4 一个"看着很对、其实错了"的假设

调研时在 node_modules 里翻到 react-native/Libraries/Utilities/Platform.android.js,看到 OS: 'android',于是写下结论:

“RNOH 上 Platform.OS === 'android',JS 三元必走 Android 分支,鸿蒙模块必须注册 VideoDurationModule 并返回毫秒。”

这个结论直到真机验证前都是自洽的——构建、注册、文档全部围绕它展开。它错在:Platform.android.js 只是文件,不是运行时行为。RNOH 经 harmony 平台扩展解析出的实际常量是 OS: 'harmony'。这个教训我会在坑 2 里展开。


3. 架构方案:为什么是"双名注册"

初版按"Android 分支假设"实现:ArkTS 只注册 VideoDurationModule(返回毫秒),C++ delegate 只匹配 "VideoDurationModule"。真机一跑立刻穿帮(见坑 2),随后收敛为最终方案:

ArkTS 侧VideoDurationTurboModule.ets):一个抽象基类持有真正的实现 getVideoDurationMs()AVMetadataExtractor + fdSrc,毫秒),派生出两个导出类:

export class VideoDurationTurboModule extends Base {
  static readonly NAME = 'VideoDuration';               // 非 Android 分支(iOS 名)
  async getVideoDuration(p: string) { return (await this.getVideoDurationMs(p)) / 1000; }  // 秒
}
export class VideoDurationModuleTurboModule extends Base {
  static readonly NAME = 'VideoDurationModule';         // Android 分支名
  async getVideoDuration(p: string) { return this.getVideoDurationMs(p); }  // 毫秒
}

注册与胶水层VideoDurationPackage.getAnyThreadTurboModuleFactoryByNameMap() 把两个 NAME 都注册;C++ delegate 同时匹配两个名字;C++ 侧新增继承 ArkTSTurboModule 的 spec 类,在构造函数里用 ARK_ASYNC_METHOD_METADATA(getVideoDuration, 1) 填充 methodMap_(为什么必须,见坑 3)。

为什么不一刀切只注册一个?因为 RNOH 生态里两类环境并存:0.84.x 主线上报 'harmony',部分 fork / 旧版本仍上报 'android'双名注册让库在任何一类环境下都能按名解析、且单位与 JS 分支匹配——JS 拿到的最终 getVideoDuration() 始终是秒,行为与 iOS/Android 完全一致,JS 侧零改动。


4. 真机验证踩坑实录(四坑,按时间顺序)

真机:HUAWEI Mate 60 Pro(HarmonyOS 7.0.0.105,API 12+),hdc 经 DevEco 内置 SDK toolchains 连接。

坑 1:JS 报 Cannot find module 'react-native-video-duration' —— Metro 的 watchFolders

  • 现象:宿主在 rawfile 打了预打包 bundle 并正常启动,JS 侧却报模块找不到;
  • 根因:宿主通过 file: 依赖把本库 symlink 进 node_modules,但 Metro 的 watchFolders 只列了工程内目录——symlink 的真实目录在工程外../react-native-video-duration),Metro 压根没把这个库打进去;宿主 demo 里其它已适配库(screenshot-aware 等)都是逐一在 metro.config.js 里补过 watchFolders 的;
  • 修复metro.config.jswatchFolders 数组补上 path.resolve(__dirname, '../react-native-video-duration'),重新 yarn dev(bundle-harmony)+ assembleHap;
  • 经验:老库鸿蒙化清单第一项不是写代码,而是确认 Metro 能解析到库源码——任何 file:/symlink 依赖都要进 watchFolders,否则真机上跑的 bundle 是"没有这个库"的旧版(案例三的坑 9 同源)。

坑 2:注册了 VideoDurationModule,JS 却请求 VideoDuration —— Platform.OS 分支走反

  • 现象(真机 hilog):
Creating Turbo Module: VideoDuration
Turbo Module 'VideoDuration' not found.
Couldn't provide turbo module "VideoDuration"
[video-duration-test] ERROR: TypeError: Cannot read property 'getVideoDuration' of null
  • 根因:RNOH 0.84.3 真机 Platform.OS === 'harmony'不是假设中的 ‘android’),JS 三元走了 iOS 分支 → 请求 NativeModules.VideoDuration。我在调研期根据 Platform.android.js 写下的"必走 Android 分支"结论是错的——RNOH 经 harmony 平台扩展解析出的运行时 OS 是 'harmony'
  • 验证手段:宿主 JS 打 console.log(Platform.OS)(hilog 的 RNOH_JS 行可见),或在打包产物里 grep -o "OS:[^,}]*" bundle.harmony.js → 得到 OS: 'harmony'
  • 修复:改为双名注册(见第 3 节),C++ delegate 匹配两个名字。修复后日志:
Creating Turbo Module: VideoDuration
TM created: VideoDuration          // ArkTS 侧实例创建成功
TM created: VideoDurationModule
  • 经验永远以真机实测的 Platform.OS 为准,不要以 node_modules 里某个 Platform 文件为准。老库的 Platform.OS === 'android' ? A : B 三元分支 + 单位分歧(毫秒/秒)是最高发的静默错误区:分支选错要么 null 崩溃,要么数值差 1000 倍。

坑 3:模块能创建,方法却不可见 —— C++ methodMap 方法表缺失

  • 现象:坑 2 修复后模块已按名创建(Creating Turbo Module + TM created 都出现),JS 拿到的对象也不再是 null,但调用方法报:
[video-duration-test] ... hasFn=false
[video-duration-test] ERROR: TypeError: undefined is not a function
  • 根因:JS 端能看到模块的哪些方法,由 C++ 侧 TurboModule::methodMap_ 决定。RNOH 核心模块(RNOHCorePackage/TurboModules/*.cpp)都是继承 ArkTSTurboModule 并在构造函数里 methodMap_ = { ARK_ASYNC_METHOD_METADATA(...) }。而我此前照"纯 ArkTS 库"模板写的 delegate 直接返回ArkTSTurboModule——模块创建成功,但方法表为空 → JS 侧 hasFn=false
  • 修复:C++ 头文件新增 spec 类:
class VideoDurationTurboModule : public ArkTSTurboModule {
 public:
  VideoDurationTurboModule(const ArkTSTurboModule::Context ctx, const std::string name)
      : rnoh::ArkTSTurboModule(ctx, name) {
    methodMap_ = { ARK_ASYNC_METHOD_METADATA(getVideoDuration, 1) }; // async 方法
  }
};
// delegate: if (name == "VideoDuration" || name == "VideoDurationModule")
//   return std::make_shared<VideoDurationTurboModule>(ctx, name);
  • ⚠️ 改 C++ 后必须强制重编 .so:hvigor 增量对头文件改动可能漏编,宿主链接的 librnoh__react_native_video_duration.so 时间戳若不新于改动,hasFn 依旧 false。修法:删 entry/build/default/intermediates/{cmake,libs,stripped_native_libs}/default/arm64-v8a/librnoh__*.so(或 touch .cpp)再 assembleHap;
  • 经验"注册成功"与"方法可见"是两件事。纯 ArkTS 实现 + C++ 空壳 delegate 只能保证模块对象存在;任何需要 JS 调用的方法,都必须在 C++ spec 类的 methodMap_ 里显式声明(等价于 codegen 生成的那份 spec 干的事)。

坑 4:hdc 非 root 写不进应用沙箱 + 宿主自带库崩溃干扰

  • 现象 Ahdc file send test.mp4 /data/app/el2/100/base/com.xxx/haps/entry/files/permission denied——hdc shell 是 uid 2000 的普通用户,应用目录属主是应用 uid,且设备无 su
  • 解法 A:测试视频打进 HAP rawfile,在 EntryAbility.onCreate 里用 resourceManager.getRawFileContent() 自拷贝到 context.filesDir,用 hilog 探针确认字节数完整(4372373 字节一致)——顺便这也是"RNOH 宿主如何自带测试素材"的可复用姿势;
  • 现象 B:应用启动即闪退,jscrash 指向宿主 demo 自带的 react-native-emoji-popupEmojiPopupView.etsCannot read property 'type_' of undefined,RNOH 0.84.3 渲染崩溃)——与本库适配无关,但会挡住整条验证链路;
  • 解法 B:临时从宿主 App.tsx 移除该卡片隔离问题,验证完再还原;
  • 经验宿主 demo 不是"干净基座"——自带的其他已适配库可能是脏的。出问题先看 jscrash 归属哪个包,别把宿主的问题记到自己适配头上。

5. 真机验证结果

修复完四个坑之后,App 启动即自动触发一次 getVideoDuration()(App.tsx 挂载时调用 + hilog 探针),完整链路日志:

Creating Turbo Module: VideoDuration        # C++ 工厂按名创建成功
TM created: VideoDuration                   # ArkTS 侧实例创建(AnyThread TM)
TM created: VideoDurationModule
[video-duration-test] Platform.OS=harmony
  typeOf(NM.VideoDuration)=object
  typeOf(NM.VideoDurationModule)=object
[video-duration-test] TM get(VideoDuration)=object hasFn=true   # 方法已暴露
[video-duration-test] OK: 52.209 s (/data/storage/el2/base/haps/entry/files/trailer.mp4)

ef33a957-b712-4e0b-bc1e-5abfe80579e5

对照验证:

  • 本机对源视频 mdls -name kMDItemDurationSeconds52.209,与真机返回值逐位一致 → 说明 AVMetadataExtractor 读时长、毫秒→秒换算、Promise 回传整条链路正确;
  • 双模块名均创建成功、hasFn=true,无 “not found” / undefined is not a function 类报错;
  • 资源释放验证:finallyextractor.release() + fs.closeSync(file),多次触发无 fd 泄漏日志;
  • 错误路径验证:传不存在的路径会以 VIDEO_DURATION_ERROR 前缀 reject(与 Android 错误契约一致)。

验证环境:HUAWEI Mate 60 Pro / HarmonyOS 7.0.0.105 / RNOH 0.84.3 / RN 0.84.1,autolinking 接入(@rnoh/react-native-video-duration--video_duration HAR)。

6. 成果与沉淀

一次适配,四类产出,全部落到组织仓库:

  1. 可运行的鸿蒙适配版atomgit.com/oh-react-native/react-native-video-duration,commit b842a80(双名注册 + C++ 方法表 spec + HAR 产物 + autolinking 配置),tag v0.1.1-harmony + Release;
  2. 双语文档README.OpenHarmony.md / README.OpenHarmony_CN.md——三平台实现对比表、真机验证结果、坑位注意事项(Platform.OS 陷阱、methodMap 声明),放在仓库根,npm/ohpm 生态里可直接被开发者读到;
  3. 甄别后的干净提交:只提交适配相关(harmony/ 模块 + .har + package.json autolinking + 文档 + .gitignore),排除 oh-package-lock.json5(含宿主绝对路径)、BuildProfile.ets(构建生成物)、测试视频等环境残留;
  4. 技能沉淀:把四个坑回填到 atomgit.com/oh-react-native/rnoh-skills(commit 77d84f6)——SKILL.md 步骤 4 模板升级为"方法表 spec 类"版本并新增 Platform.OS 陷阱与验证清单项;EXAMPLES.md 案例四新增坑 11(Platform.OS === 'harmony' 分支走反)与坑 12(methodMap 缺失),含现象/根因/修复/验证四段式,供后续所有老库适配直接复用。

7. 总结:老库鸿蒙化的可复用清单

把这个只有 1 个方法的库跑通,暴露的是方法调用型老库在 RNOH 上适配的完整检查链。给同样要适配这类库的团队一份 checklist:

  1. Metro 可达性先行file:/symlink 依赖必须进 metro.config.js watchFolders,否则真机 bundle 里根本没有这个库;
  2. 实测 Platform.OS 再定模块名:RNOH 0.84.x 真机是 'harmony'——用 hilog/console.log 实测,别信 node_modules 里的 Platform 文件;Platform.OS === 'android' ? A : B 分支决定模块名和单位
  3. 无法预判单位分歧时用双名注册:一个名对应一个分支语义(秒/毫秒各自匹配 JS 是否 /1000),两类 RNOH 环境通吃;
  4. 方法必须进 C++ methodMap_:delegate 返回继承 ArkTSTurboModule 的 spec 类(ARK_ASYNC_METHOD_METADATA(name, argc)),改 C++ 后记得删 .so 强制重编;
  5. 宿主不是净土:jscrash 先定位归属包,隔离宿主自带库的问题再继续验证;
  6. 测试素材进 rawfile:hdc 非 root 写不进沙箱时,由 EntryAbility 启动自拷贝是可靠路径;
  7. 验证要有"外部基准":用源视频在本机的 mdls 元数据做数值基准,功能是否真的对,一个数字就能证明;
  8. 沉淀优先于炫耀:文档 + skill 回填,让下一个适配者不用再踩这四个坑。

老库鸿蒙化的难点从来不在"写一个 ArkTS 模块",而在把文档假设逐一变成真机事实。四个坑里有三个是"想当然"造成的——而这正是把经验写成博客、回填技能仓库的价值所在。

8. 参考

组织 / 仓库地址说明
CPF-RN 社区主页https://atomgit.com/CPF-RNRNOH 中文化与技能集组织
RNOH 主仓库(ohos_react_native)https://atomgit.com/CPF-RN/ohos_react_nativeRNOH 0.84 版本线官方仓库(含 RNOH084Demo 示例工程与版本配套)
react-native-video-duration(鸿蒙适配版)https://atomgit.com/oh-react-native/react-native-video-duration本文适配目标库的仓库:双名注册 TurboModule + C++ 方法表 spec + HAR 产物 + autolinking 配置 + 双语适配文档(tag v0.1.1-harmony),供开发者对照学习
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐