React Native 三方库「react-native-media-notification」的鸿蒙化适配指南

开发工具: 华为云码道

本文配套仓库: oh-react-native/react-native-media-notification

上游仓库: mbpictures/react-native-media-notification

react-native-media-notification 为 React Native 应用提供媒体信息、播放控制和系统媒体卡片能力。鸿蒙实现使用 AVSession 保存当前曲目、播放进度和控制事件,再通过 RNOH TurboModule 把系统事件传回 JavaScript。本文以 @react-native-ohos/react-native-media-notification 0.4.6-0.0.1 为例,介绍依赖准备、媒体会话桥接、示例接入和构建验证。

本适配保留上游 Android/iOS API,并在鸿蒙侧加入 AVSession 实现。源码位于 AtomGit 配套仓库,文中的代码以提交 71aad26a04447f9d12592b288ef77ef4d527cf7f 为参考。鸿蒙端支持媒体元数据、播放状态、队列、系统控制事件和会话停止/重建;Android Auto 专用按钮和车载连接状态在鸿蒙端只保留接口兼容性。


给初学者的阅读提示:

本文操作的插件目录是 react-native-media-notification/。HAR 是插件的鸿蒙原生模块包,HAP 是可以安装到设备上的应用包;AVSession 是鸿蒙系统管理媒体播放状态和控制按钮的会话,RNOH 负责连接 React Native 与鸿蒙,TurboModule 负责 JavaScript 和原生模块之间的调用。

updateMetadata 使用秒作为时间单位,鸿蒙 AVSession 内部使用毫秒,适配层会自动转换。调用 updateMetadata 会创建或更新媒体会话,系统卡片上的播放、暂停、上一首、下一首和拖动进度等操作,再通过事件回传给 React Native 页面。

一、插件简介与适配目标

播放音乐、播客或其他音频时,应用通常需要把曲名、歌手、封面和进度交给系统,这样用户在通知栏或系统媒体卡片中就能看到当前内容并进行控制。应用还需要把系统按钮产生的事件交给播放器,真正改变播放状态。

react-native-media-notification 将这些工作封装成 React Native 方法。updateMetadata 用于创建或更新当前媒体会话;setControlEnabled 决定系统应注册哪些控制事件;addEventListener 接收系统返回的播放、暂停、切歌和进度事件;setMediaLibrary 可以把可播放项目提交为队列;stopMediaNotification 和 shutdown 用于停用会话。

鸿蒙实现使用 @ohos.multimedia.avsession。第一次调用 updateMetadata 时,模块通过当前 UIAbilityContext 创建名为 ReactNativeMediaControls 的音频会话,后续调用复用同一个会话。停止会话后再次更新元数据,会重新创建 AVSession。

部分上游方法只为跨平台 API 保持一致:enableBackgroundMode 在鸿蒙端不切换系统后台模式,已激活的 AVSession 是否继续显示由系统管理;enableAudioInterruption 当前不管理鸿蒙音频焦点;setCustomButtons 和 isCarConnected 属于 Android Auto 相关能力,鸿蒙端不显示自定义车载按钮,车载连接状态始终为 false。


二、环境准备

环境搭建参考社区文档:React Native OpenHarmony 环境初始化,完成 Node.js、React Native、RNOH、Harmony CLI 和 DevEco Studio 配置;框架能力与版本关系见 ohos_react_native。

在开发电脑的终端执行以下命令,macOS 和 Windows PowerShell 均可使用:

bash 复制代码
node --version
corepack yarn --version
hdc list targets

前两条命令应输出版本号;hdc list targets 应列出已连接的鸿蒙设备或模拟器。列表为空时,先检查 USB 调试授权或模拟器连接。

项目 版本或配置 用途
Node.js .nvmrc 为 20.19.0 执行 Yarn、Metro 和构建脚本
Yarn 3.6.1 安装工作区依赖
React Native 0.84.1 示例应用的 RN 版本
React 19.2.3 与示例应用配套
RNOH / Harmony CLI 0.84.3 RN 与鸿蒙之间的运行层和命令行工具
OHPM RNOH 0.84.3 鸿蒙原生运行时依赖
compileSdkVersion 26.0.0 编译使用的 SDK
targetSdkVersion 26.0.0 应用目标行为版本
compatibleSdkVersion 5.0.5(17) 工程声明的最低兼容版本
DevEco Studio 26.0.0 Release(HarmonyOS SDK 26.0.0 / API 26) 打开并构建 example/harmony 工程
适配包版本 0.4.6-0.0.1 鸿蒙 npm 包版本
原生实现 ArkTS / C++ AVSession 逻辑与 RNOH 桥接
插件产物 harmony/media_notification.har 供宿主应用引入的原生模块包

示例工程使用 RNOH 0.84.3 和 React Native 0.84.1。第一次接入时建议先使用这组版本,不要只升级其中一个依赖。

2.1 开发套件版本与工程中的 SDK 版本配置

example/harmony/build-profile.example.json5 中的 product 配置如下:

json5 复制代码
{
  "name": "default",
  "compileSdkVersion": "26.0.0",
  "compatibleSdkVersion": "5.0.5(17)",
  "targetSdkVersion": "26.0.0",
  "runtimeOS": "HarmonyOS"
}

compileSdkVersion 决定电脑上需要安装哪个 SDK,compatibleSdkVersion 是应用声明的最低安装门槛,targetSdkVersion 决定应用面向的系统行为版本。IDE 版本、编译 SDK 和设备系统版本分别影响不同环节,最低兼容配置也不等于所有设备都已经验证。


三、从源码仓库开始准备适配工程

3.1 将上游源码同步到 AtomGit

本例上游为 mbpictures/react-native-media-notification,基线版本为 0.4.6,采用 MIT 许可证。配套 AtomGit 仓库已经包含鸿蒙适配代码,运行示例时直接克隆即可。

如果需要继续维护,可以在 AtomGit 创建自己的 Fork,保留上游许可证、源码和提交历史。本文介绍仓库根目录中的 react-native-media-notification 包,不涉及上游的 Android Auto 专用配置。

3.2 将代码拉取到宿主机

在准备存放项目的目录执行以下命令,macOS 和 Windows PowerShell 共用:

bash 复制代码
git clone https://atomgit.com/oh-react-native/react-native-media-notification.git
cd react-native-media-notification
git switch --detach 71aad26a04447f9d12592b288ef77ef4d527cf7f
git status --short --branch

git clone 会创建仓库目录;git switch --detach 把源码固定到本文参考版本。已有本地修改时,先保存修改再切换提交。

3.3 在仓库根目录创建适配分支

配套仓库使用 main 分支。需要继续修改时,从固定提交创建维护分支:

bash 复制代码
git switch -c feat/ohos_media_notification_0.4.6-0.0.1

插件的 package.json、src/、harmony/ 和 example/ 都位于仓库根目录。依赖安装、类型检查和构建命令也在这个目录执行。

3.4 说明现有 OHOS 适配结构

仓库已经包含 harmony/media_notification/ 原生模块和 example/harmony/ 应用工程。首次使用先安装工作区依赖:

bash 复制代码
corepack yarn install

安装成功后应生成依赖目录。仓库使用 Yarn 3.6.1 管理工作区,建议通过 Corepack 使用项目声明的 Yarn 版本。

直接打开 DevEco Studio 前,还需将模板复制为本地构建配置。macOS 在仓库根目录执行:

bash 复制代码
if [ ! -f example/harmony/build-profile.json5 ]; then
  cp example/harmony/build-profile.example.json5 example/harmony/build-profile.json5
fi

Windows PowerShell 执行:

powershell 复制代码
if (-not (Test-Path 'example/harmony/build-profile.json5')) {
  Copy-Item 'example/harmony/build-profile.example.json5' 'example/harmony/build-profile.json5'
}

模板不包含个人证书和密码。已有的 build-profile.json5 可能包含本机签名配置,提交前不要把它加入公开提交。

3.5 适配后的项目目录

text 复制代码
react-native-media-notification/
├── package.json
├── src/
│   ├── index.tsx
│   └── NativeMediaControls.ts
├── harmony/media_notification/
│   ├── media_notification.har
│   ├── index.ets
│   ├── ts.ts
│   ├── oh-package.json5
│   └── src/main/
│       ├── ets/RNMediaControlsTurboModule.ts
│       ├── ets/RNMediaControlsPackage.ets
│       ├── generated/turboModules/
│       ├── cpp/RNMediaControlsPackage.h
│       └── module.json5
├── example/
│   ├── src/App.harmony.tsx
│   ├── index.harmony.js
│   ├── metro.harmony.config.js
│   └── harmony/entry/
├── scripts/build-harmony.sh
├── README.harmony.md
├── README.upstream.md
└── VALIDATION.md
文件 主要职责
src/index.tsx 对外导出媒体元数据、事件、队列和会话控制方法
src/NativeMediaControls.ts 声明 MediaControls TurboModule 名称、参数和返回类型
RNMediaControlsTurboModule.ts 创建 AVSession、更新元数据、注册事件和管理生命周期
RNMediaControlsPackage.ets / RNMediaControlsPackage.h 注册 ArkTS 和 C++ Package
generated/turboModules/ codegen 生成的 ArkTS 类型和 C++ 方法表
example/src/App.harmony.tsx 演示播放状态、进度、队列和停止/重建
scripts/build-harmony.sh 构建 HAR 或示例 HAP

3.6 详细步骤

  1. 准备环境。 在仓库外选择工作目录,确认 Node.js、Corepack、DevEco Studio 和 hdc 可用。Windows 使用 PowerShell,macOS 使用终端。

    macOS:

    bash 复制代码
    node --version
    corepack enable
    corepack yarn --version
    hdc list targets

    Windows PowerShell:

    powershell 复制代码
    node --version
    corepack enable
    corepack yarn --version
    hdc list targets

    预期能看到 Node.js 和 Yarn 版本;hdc list targets 有设备或模拟器编号。没有设备时仍可进行类型检查和 HAR 构建。

  2. 拉取并固定源码。 在准备存放仓库的目录执行第三章的 git clone、cd 和 git switch --detach 命令。看到 ## HEAD (no branch) 或对应提交信息,说明定位成功;已有未提交修改时先保存。

  3. 创建维护分支。 在仓库根目录执行:

    macOS 与 Windows PowerShell 共用:

    bash 复制代码
    git switch -c feat/ohos_media_notification_0.4.6-0.0.1

    这一步只创建本地分支,不会自动上传远程;分支名称可按团队规则调整。

  4. 安装依赖。 回到仓库根目录执行:

    macOS:

    bash 复制代码
    corepack yarn install

    Windows PowerShell:

    powershell 复制代码
    corepack yarn install

    预期工作区依赖安装完成。若 Corepack 无法准备 Yarn,先检查 Node.js 版本和网络;不要把安装产生的缓存或签名文件加入提交。

  5. 准备示例工程。 在仓库根目录复制 example/harmony/build-profile.example.json5 为 build-profile.json5,然后用 DevEco Studio 打开 example/harmony。首次打开等待 Sync 完成,确认项目视图中有 entry 和 media_notification 两个模块。

  6. 检查桥接和原生文件。 按第四、五章核对 MediaControls 名称、RNMediaControlsPackage 注册、HAR 依赖和 CMake target。新增或修改 src/NativeMediaControls.ts 后,重新执行项目使用的 codegen 流程,再构建 HAR;codegen 只生成桥接文件,不会实现 AVSession 逻辑。

  7. 运行检查和构建。 先执行 corepack yarn typecheck、corepack yarn test,再执行 corepack yarn build:har。需要运行示例时配置签名,执行 corepack yarn build:example:harmony,产物会出现在 example/harmony/entry/build/default/outputs/default/。

  8. 安装和验收。 通过 DevEco Studio 运行 entry,或使用 hdc install -r 安装 HAP。打开示例页面后,先确认元数据状态,再观察系统媒体卡片的播放、暂停、切歌和进度操作。最后测试停止会话、重新发布元数据和返回页面后的状态。

  9. 检查并提交。 回到仓库根目录执行 git diff --check 和 git status --short,确认没有 build-profile.json5、证书、密码、缓存、HAP 或日志。提交前运行第八章中的检查命令,再按第六章示例提交和推送。


四、JS/TS 接口与原生桥接分析

这个库的调用分为三层:React Native 页面调用公开 API,TurboModule 把数据交给 ArkTS,ArkTS 再调用 HarmonyOS AVSession。系统按钮产生的事件沿相反方向返回页面。

公开入口 TurboModule 方法或 JS 逻辑 OHOS 实现 返回或事件
updateMetadata(metadata) MediaControls.updateMetadata 创建或复用 AVSession,写入 AVMetadata 和 AVPlaybackState Promise<void>
setControlEnabled(name, enabled) MediaControls.setControlEnabled 注册或移除对应 AVSession 回调 无返回值
addEventListener(event, handler) DeviceEventEmitter 转为 fbemitter 事件 监听 MediaControlsEvent EventSubscription
setMediaLibrary(library) MediaControls.setMediaLibrary 展平可播放节点并调用 setAVQueueItems 无返回值
stopMediaNotification() MediaControls.stopMediaNotification deactivate() 后 destroy() Promise<void>
shutdown() MediaControls.shutdown 异步停止并释放 AVSession 无返回值
enableAudioInterruption() 保留兼容性 当前直接完成 Promise Promise<void>
enableBackgroundMode() 保留兼容性 当前不切换系统后台模式 无返回值
isCarConnected() 保留 Android Auto API 鸿蒙端返回 false 同步 boolean

4.1 跨端架构与调用时序

系统媒体卡片 HarmonyOS AVSession RNMediaControlsTurboModule src/index.tsx React Native 页面 系统媒体卡片 HarmonyOS AVSession RNMediaControlsTurboModule src/index.tsx React Native 页面 #mermaid-svg-ToSR2gbmx5EZz09q{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ToSR2gbmx5EZz09q .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ToSR2gbmx5EZz09q .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ToSR2gbmx5EZz09q .error-icon{fill:#552222;}#mermaid-svg-ToSR2gbmx5EZz09q .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ToSR2gbmx5EZz09q .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ToSR2gbmx5EZz09q .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ToSR2gbmx5EZz09q .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ToSR2gbmx5EZz09q .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ToSR2gbmx5EZz09q .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ToSR2gbmx5EZz09q .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ToSR2gbmx5EZz09q .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ToSR2gbmx5EZz09q .marker.cross{stroke:#333333;}#mermaid-svg-ToSR2gbmx5EZz09q svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ToSR2gbmx5EZz09q p{margin:0;}#mermaid-svg-ToSR2gbmx5EZz09q .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ToSR2gbmx5EZz09q text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-ToSR2gbmx5EZz09q .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-ToSR2gbmx5EZz09q .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-ToSR2gbmx5EZz09q .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-ToSR2gbmx5EZz09q .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-ToSR2gbmx5EZz09q #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-ToSR2gbmx5EZz09q .sequenceNumber{fill:white;}#mermaid-svg-ToSR2gbmx5EZz09q #sequencenumber{fill:#333;}#mermaid-svg-ToSR2gbmx5EZz09q #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-ToSR2gbmx5EZz09q .messageText{fill:#333;stroke:none;}#mermaid-svg-ToSR2gbmx5EZz09q .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ToSR2gbmx5EZz09q .labelText,#mermaid-svg-ToSR2gbmx5EZz09q .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-ToSR2gbmx5EZz09q .loopText,#mermaid-svg-ToSR2gbmx5EZz09q .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-ToSR2gbmx5EZz09q .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-ToSR2gbmx5EZz09q .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-ToSR2gbmx5EZz09q .noteText,#mermaid-svg-ToSR2gbmx5EZz09q .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-ToSR2gbmx5EZz09q .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ToSR2gbmx5EZz09q .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ToSR2gbmx5EZz09q .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ToSR2gbmx5EZz09q .actorPopupMenu{position:absolute;}#mermaid-svg-ToSR2gbmx5EZz09q .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-ToSR2gbmx5EZz09q .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ToSR2gbmx5EZz09q .actor-man circle,#mermaid-svg-ToSR2gbmx5EZz09q line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-ToSR2gbmx5EZz09q :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} updateMetadata(metadata) MediaControls.updateMetadata createAVSession / setAVMetadata / setAVPlaybackState 显示曲目、进度和控制按钮 播放、暂停、切歌或拖动进度 回调事件 MediaControlsEvent addEventListener 收到事件

4.2 数据模型与返回语义

MediaTrackMetadata 的时间字段统一使用秒:duration 是曲目总时长,position 是当前播放位置。鸿蒙实现写入 AVSession 时乘以 1000;系统返回 seek、seekForward 和 seekBackward 事件时,再把毫秒除以 1000 后放入 seekPosition。

常用字段如下:

字段 类型 作用
id string 曲目标识,不传时使用默认标识
title / artist / album string 系统媒体卡片显示的文本
duration / position number 总时长和当前进度,单位为秒
artwork string 或图片资源 封面地址;鸿蒙原生层接收字符串地址
isPlaying boolean 播放状态,未设置时按暂停处理
isLoading boolean 缓冲状态,设置为 true 时使用 buffering 状态
repeatMode `'off' 'one'
shuffle boolean 是否随机播放

addEventListener 的事件数据会放在回调参数中。seek、seekForward 和 seekBackward 使用 { seekPosition: number };shuffle 使用 { shuffleMode: boolean };repeatMode 使用 { repeatMode: 'off' | 'one' | 'all' }。播放、暂停、停止和切歌事件没有额外数据。

setMediaLibrary 接收一个根节点。原生层递归查找 playable: true 的子节点,把它们转换为 AVQueueItem;目录层级本身不会在系统媒体卡片中显示。提交队列是异步执行的,页面可在提交后显示自己的状态。

4.3 公开 API 与接口封装

最小调用顺序是先注册需要的事件,再发布元数据:

ts 复制代码
import * as MediaControls from 'react-native-media-notification';

MediaControls.setControlEnabled('play', true);
MediaControls.setControlEnabled('pause', true);
MediaControls.setControlEnabled('seek', true);

const playSubscription = MediaControls.addEventListener('play', () => {
  // 在这里调用播放器的 play 方法
});

const seekSubscription = MediaControls.addEventListener(
  'seek',
  (data) => {
    const position = data?.seekPosition ?? 0;
    // 将播放器跳转到 position 秒
  }
);

await MediaControls.updateMetadata({
  id: 'track-1',
  title: '示例曲目',
  artist: '示例歌手',
  duration: 180,
  position: 30,
  isPlaying: false,
  repeatMode: 'off',
  shuffle: false,
});

playSubscription.remove();
seekSubscription.remove();

setControlEnabled 只接受 play、pause、stop、skipToNext、skipToPrevious、seekForward、seekBackward、seek、shuffle 和 repeatMode。传入其他名称时,JS 层会抛出错误。组件卸载时要移除订阅;如果应用不再播放,也可以调用 stopMediaNotification() 停止并销毁会话。

4.4 模块协议与调用处理

src/NativeMediaControls.ts 把原生模块名称声明为 MediaControls,并通过 TurboModuleRegistry.getEnforcing 获取模块。codegen 根据 codegenConfig 生成 ArkTS 类型和 C++ 方法表,方法参数数量和异步属性必须与 TypeScript 协议一致。

鸿蒙侧的 RNMediaControlsTurboModule 继承 RNOH 的 UITurboModule,在 setControlEnabled 中维护启用状态。模块只在会话存在时注册回调;播放、暂停和切歌等回调通过 ctx.rnInstance.emitDeviceEvent('MediaControlsEvent', event) 发给 React Native,src/index.tsx 再将它们转发为公开事件。


五、补全 OHOS 原生实现与工程配置

5.1 原生实现

RNMediaControlsTurboModule.ts 维护一个 AVSession 引用、元数据和已注册控制集合。调用 getSession() 时使用 ctx.uiAbilityContext 创建音频会话:

ts 复制代码
private async getSession(): Promise<avSession.AVSession> {
  if (this.session) return this.session;
  const session = await avSession.createAVSession(
    this.ctx.uiAbilityContext,
    'ReactNativeMediaControls',
    'audio'
  );
  this.session = session;
  this.registerEnabledCommands(session);
  return session;
}

更新元数据时,原生层只把类型正确且有值的字段写入 AVMetadata,把秒转换为毫秒,再设置播放状态并激活会话:

ts 复制代码
const avMetadata: avSession.AVMetadata = {
  assetId: typeof id === 'string' && id.length > 0 ? id : 'rn-media-track',
};
if (typeof m.title === 'string') avMetadata.title = m.title;
if (typeof m.artist === 'string') avMetadata.artist = m.artist;
if (typeof m.album === 'string') avMetadata.album = m.album;
if (typeof m.duration === 'number' && Number.isFinite(m.duration) && m.duration >= 0) {
  avMetadata.duration = m.duration * 1000;
}
if (typeof m.artwork === 'string' && m.artwork.length > 0) {
  avMetadata.mediaImage = m.artwork;
}
await session.setAVMetadata(avMetadata);

播放、缓冲和暂停分别映射为 AVSession 的播放状态。repeatMode 和 shuffle 会映射为顺序、单曲、列表或随机模式;鸿蒙端没有调用 API 26 才提供的循环模式设置方法,因此兼容配置不会因为系统版本较低而直接失败。

控制事件使用稳定的回调引用注册和移除:play 对应 session.on('play', callback),上一首和下一首对应 playPrevious、playNext,拖动进度对应 seek。调用 stopMediaNotification() 时先停用会话,再销毁会话并清空已注册控制。

5.2 声明插件和宿主权限

插件模块的 module.json5 声明类型为 har,支持 phone、tablet 和 2in1。示例宿主在 entry/src/main/module.json5 中声明 entry 模块并请求 ohos.permission.INTERNET,用于示例可能使用的网络资源;媒体会话桥接本身没有额外的运行时权限申请。

如果封面使用网络地址,宿主仍要保证地址可访问;如果使用本地资源,可以先在 JavaScript 层把图片资源解析为可传给原生层的字符串地址。

5.3 注册并导出模块

ArkTS Package factory 将 MediaControls 映射到 RNMediaControlsTurboModule:

ts 复制代码
import { RNOHPackage, UITurboModule, UITurboModuleContext } from '@rnoh/react-native-openharmony';
import { RNMediaControlsTurboModule } from './RNMediaControlsTurboModule';

export class RNMediaControlsPackage extends RNOHPackage {
  getUITurboModuleFactoryByNameMap() {
    return new Map([
      [RNMediaControlsTurboModule.NAME, (ctx: UITurboModuleContext) =>
        new RNMediaControlsTurboModule(ctx)],
    ]);
  }
}

插件入口从 index.ets 导出 RNMediaControlsPackage。C++ 侧的 Package 继承 codegen 生成的基类:

cpp 复制代码
class RNMediaControlsPackage
    : public BaseReactNativeOhosReactNativeMediaNotificationPackage {
 public:
  explicit RNMediaControlsPackage(Package::Context ctx)
      : BaseReactNativeOhosReactNativeMediaNotificationPackage(ctx) {}
};

宿主的 RNPackagesFactory.ets 返回这个 Package,PackageProvider.cpp 返回同名 C++ Package,并在 CMake 中加入插件源码目录和 rnoh_media_notification target:

cmake 复制代码
add_subdirectory(
  "${OH_MODULE_DIR}/@react-native-ohos/react-native-media-notification/src/main/cpp"
  ./media_notification
)
target_link_libraries(rnoh_app PUBLIC rnoh rnoh_media_notification)

5.4 检查 example 的 OHOS 应用结构

示例工程位于 example/harmony,包含 entry 和 media_notification 两个模块。entry/oh-package.json5 通过本地 HAR 依赖插件,RNPackagesFactory.ets 和 PackageProvider.cpp 完成 ArkTS、C++ 双侧注册,metro.harmony.config.js 将 react-native-media-notification 解析到当前工作区。

打开 DevEco Studio 时选择 example/harmony 目录,等待 Sync 完成后再运行 entry。只打开 harmony/media_notification 只能查看 HAR 模块,不能启动完整的 React Native 示例页面。


六、补全交付文件并提交适配分支

6.1 交付文件

文件或目录 交付内容
src/ React Native 公开 API 和 TurboModule 协议
harmony/media_notification/ ArkTS 模块、C++ codegen 桥接和 HAR 工程
harmony/media_notification.har release HAR 产物
example/harmony/ 可构建和签名的 HarmonyOS 示例应用
scripts/build-harmony.sh HAR/HAP 构建入口
README.harmony.md 鸿蒙接入、平台差异和限制说明
VALIDATION.md 构建与功能验收记录

npm 包还需要包含 src、lib、harmony、类型入口和鸿蒙说明文件;HAP、构建缓存、日志以及个人签名文件不应进入 npm 包或 Git 提交。

6.2 提交前检查

在仓库根目录执行:

bash 复制代码
corepack yarn typecheck
corepack yarn test
corepack yarn build:har
corepack yarn build:example:harmony
git diff --check
git status --short

构建命令会产生 HAR、HAP、Metro bundle 和日志。检查 git status --short,确认只暂存源代码、文档和允许交付的 HAR,不要暂存 example/harmony/entry/build、build-profile.json5、证书、密码或设备信息。

打包 npm 文件时:

bash 复制代码
mkdir -p artifacts
npm pack --ignore-scripts --pack-destination artifacts

完成打包后应检查 tarball 中包含 harmony/media_notification.har 和 C++ 桥接源码,且不包含本机签名配置。

6.3 提交适配分支

确认差异和检查结果后,再提交本地分支:

bash 复制代码
git add src harmony package.json README.harmony.md VALIDATION.md scripts example

git commit -m "feat: add HarmonyOS media notification support"
git push origin feat/ohos_media_notification_0.4.6-0.0.1

如果没有配套仓库的推送权限,可以把仓库 Fork 到自己的 AtomGit 账号,将 origin 改为自己的地址后再推送,并通过网页创建合并请求。命令中的分支名称应与第三章保持一致。


七、使用 example 演示接入

7.1 使用本地构建包

如果应用要消费已经构建的 npm 包,先在插件目录执行:

bash 复制代码
corepack yarn build:har
mkdir -p artifacts
npm pack --ignore-scripts --pack-destination artifacts

在业务应用目录安装生成的 tarball:

bash 复制代码
npm install /absolute/path/to/react-native-ohos-react-native-media-notification-0.4.6-0.0.1.tgz

随后在宿主 Harmony 工程中执行 npx react-native link-harmony --harmony-project-path ./harmony,或者按第五章手动添加 HAR 依赖、Package factory 和 CMake target。一次接入只选择自动链接或手动链接其中一种方式。

7.2 使用已发布的 npm 包

如果包已发布到 npm,也可以在业务应用中安装:

bash 复制代码
npm install @react-native-ohos/react-native-media-notification@0.4.6-0.0.1

JavaScript 仍然使用上游包名导入:

ts 复制代码
import * as MediaControls from 'react-native-media-notification';

harmony.alias 会让 Harmony Metro 将这个导入解析到适配包。原生侧仍需安装 HAR 并完成 Package 注册;只安装 npm 依赖不会自动把原生代码放进已有的 Harmony 工程。

7.3 完整最小 RN 页面

下面的页面展示了元数据发布、播放事件、进度事件和会话清理。播放器本身仍由业务代码负责,这个页面只演示媒体会话桥接:

tsx 复制代码
import { useEffect, useState } from 'react';
import { Button, Text, View } from 'react-native';
import * as MediaControls from 'react-native-media-notification';

type EventData = {
  seekPosition?: number;
};

export default function MediaPage() {
  const [playing, setPlaying] = useState(false);
  const [position, setPosition] = useState(30);
  const [message, setMessage] = useState('准备中');

  useEffect(() => {
    MediaControls.setControlEnabled('play', true);
    MediaControls.setControlEnabled('pause', true);
    MediaControls.setControlEnabled('seek', true);

    const playSubscription = MediaControls.addEventListener('play', () => {
      setPlaying(true);
      setMessage('系统请求播放');
    });
    const pauseSubscription = MediaControls.addEventListener('pause', () => {
      setPlaying(false);
      setMessage('系统请求暂停');
    });
    const seekSubscription = MediaControls.addEventListener(
      'seek',
      (data?: EventData) => {
        if (data?.seekPosition !== undefined) {
          setPosition(data.seekPosition);
        }
        setMessage('系统请求调整进度');
      }
    );

    MediaControls.updateMetadata({
      id: 'track-1',
      title: '示例曲目',
      artist: '示例歌手',
      duration: 180,
      position: 30,
      isPlaying: false,
      repeatMode: 'off',
      shuffle: false,
    }).catch((error) => setMessage(String(error)));

    return () => {
      playSubscription.remove();
      pauseSubscription.remove();
      seekSubscription.remove();
      void MediaControls.stopMediaNotification();
    };
  }, []);

  const updatePlaying = (nextPlaying: boolean) => {
    setPlaying(nextPlaying);
    void MediaControls.updateMetadata({
      isPlaying: nextPlaying,
      position,
    });
  };

  return (
    <View style={{ padding: 24, gap: 12 }}>
      <Text>{message}</Text>
      <Text>{playing ? '播放中' : '已暂停'}:{position} 秒</Text>
      <Button title="播放" onPress={() => updatePlaying(true)} />
      <Button title="暂停" onPress={() => updatePlaying(false)} />
    </View>
  );
}

updateMetadata 的部分字段可以单独更新,原生层会与上一次元数据合并。真实应用收到 play、pause 或 seek 后,要同时调用播放器,并再次发布新的播放状态和进度。

7.4 页面退出时处理资源

addEventListener 返回的订阅对象必须在页面卸载时调用 remove()。应用不再使用媒体会话时调用 stopMediaNotification();如果只是页面暂时离开而播放器仍在后台播放,不要在卸载时销毁会话,应由播放器页面或播放服务统一管理。

setBackgroundMessageHandler 可以注册 React Native Headless Task,但鸿蒙端终止态系统事件没有端到端保证。需要稳定后台播放时,仍要结合应用自身的播放器和生命周期方案验证。


八、验证、构建与鸿蒙设备运行效果

建议按"类型检查 → 单元测试 → HAR → HAP → 系统媒体卡片"的顺序排查。这样可以先区分 JavaScript 类型问题,再确认 RNOH 原生桥接、打包和设备交互。

8.1 分别验证插件与 example

在仓库根目录执行:

bash 复制代码
corepack yarn typecheck
corepack yarn test
corepack yarn build:har
corepack yarn build:example:harmony

typecheck 检查公开 API 和 TurboModule 类型;test 执行 Jest 测试;build:har 生成 release HAR;build:example:harmony 先生成 Harmony bundle,再构建示例 HAP。Windows PowerShell 可以直接执行前两项;后两个脚本内部使用 Bash,并默认查找 macOS 的 DevEco 安装目录,Windows 应在 DevEco Studio 中执行对应 Hvigor 任务,或按本机工具路径调整脚本。

8.2 确认设备连接

bash 复制代码
hdc list targets

首次连接设备时,在设备端确认调试授权。列表为空时检查 USB、调试开关和授权状态。没有设备也可以完成 typecheck、test 和 HAR 构建。

8.3 配置签名

  1. 用 DevEco Studio 打开 example/harmony;
  2. 等待 Sync 完成,确认项目中有 entry 和 media_notification;
  3. 打开 File > Project Structure > Signing Configs;
  4. 为 default product 配置自己的签名;
  5. 确认包名、证书、Profile 和设备匹配;
  6. 不要把含有个人证书路径和密码的 build-profile.json5 提交到公开仓库。

HAR 本身不需要应用签名,安装到设备的是宿主应用生成的 HAP。

8.4 运行示例

macOS 在仓库根目录执行:

bash 复制代码
corepack yarn build:example:harmony

脚本会生成 Harmony bundle 并构建 entry HAP。产物目录为:

text 复制代码
example/harmony/entry/build/default/outputs/default/

配置签名后,可以直接在 DevEco Studio 运行 entry,也可以使用 hdc 安装应用包:

bash 复制代码
hdc install -r example/harmony/entry/build/default/outputs/default/entry-default-signed.hap

启动应用后,示例页会先发布一条媒体元数据,然后可以更新播放状态、调整进度、提交队列、停止会话和重新创建会话。

8.5 在设备上测试核心操作

  1. 打开示例页面,确认页面状态从初始化变为成功,系统媒体卡片能显示曲名、歌手、时长和进度。
  2. 点击"播放"或"暂停",确认页面状态和系统卡片保持一致。
  3. 在系统媒体卡片中点击播放、暂停、上一首、下一首或拖动进度,确认对应事件回到 React Native 页面。
  4. 提交示例队列,确认原生层接受队列项目;系统卡片是否展示队列内容由系统界面决定。
  5. 点击停止会话,再发布元数据,确认 AVSession 可以重新创建。
  6. 将应用切到后台后返回,检查媒体会话是否仍然存在;后台行为受系统版本和播放器实现影响。
  7. 不要把 setCustomButtons、isCarConnected 或 Android Auto 事件当作鸿蒙端已支持功能。

8.6 鸿蒙设备运行效果

示例页面展示当前曲目、歌手、进度条和播放按钮,并提供刷新元数据、提交队列、停止会话和重新创建会话的操作。页面底部显示原生状态、最近一次系统事件以及车载连接接口返回值。系统媒体卡片中的操作会通过 AVSession 回到 React Native 页面。

鸿蒙端可以使用媒体元数据、播放状态、队列和基础播放控制。系统是否显示快进、快退、随机和循环按钮,取决于系统媒体卡片和当前会话配置;自定义车载按钮不会出现在鸿蒙媒体卡片中。


九、FAQ:适配过程与使用问题

9.1 Missing SDK components

这个错误发生在 Hvigor 同步阶段。确认 DevEco Studio 已安装 product 使用的 SDK 组件,再检查命令行和 IDE 使用的 SDK 路径是否一致,重新执行 Sync。

9.2 DevEco Studio 中看不到 entry

打开 example/harmony,不要只打开仓库根目录或 harmony/media_notification。确认 build-profile.json5 声明了 entry 和 media_notification,并等待 Sync 完成。

9.3 无法配置签名

签名配置依附于 entry 模块和 default product。确认 SDK Sync 已完成,证书和应用包名匹配;HAR 模块本身没有应用签名入口。

9.4 找不到原生模块 MediaControls

检查 JavaScript 中的 TurboModule 名称、RNMediaControlsPackage.ets、PackageProvider.cpp、HAR 依赖和 CMake target 是否一致。新增原生模块后要完整重建应用,JS 刷新不能补上未编译的原生代码。

9.5 系统媒体卡片没有显示按钮

先确认应用已经成功调用 updateMetadata 并激活 AVSession,再检查是否对相应事件调用了 setControlEnabled。媒体卡片只显示系统和当前会话支持的控制,Android Auto 的自定义按钮不会显示在鸿蒙端。

9.6 更新元数据时报类型或参数错误

检查可选字段的类型。duration 和 position 必须是大于等于零的秒数,封面应是非空字符串;不要把 JavaScript 的 undefined 直接当作原生元数据字段传入。

9.7 进度跳转不准确

JavaScript API 使用秒,AVSession 使用毫秒。确认业务播放器收到 seekPosition 后按秒处理,不要再次乘以 1000;发布新的 position 时也传秒。

9.8 队列提交后系统卡片没有列表

setMediaLibrary 只会递归收集 playable: true 的节点,并把它们转换成平面队列。父节点需要通过 items 包含子节点;目录层级不会在系统媒体卡片中展示。

9.9 RN CLI 或 Metro 使用了错误入口

在仓库根目录安装依赖,并使用 example/metro.harmony.config.js。Harmony Metro 通过 harmony.alias 解析库入口;不要同时加载上游包和适配包的两套实现。

9.10 改了 ArkTS,页面为什么没有变化

原生代码变化需要重新构建 HAR,并重新构建宿主 HAP。JS Fast Refresh 只更新 JavaScript,不能替代原生模块重建;还要确认宿主实际引用的是最新 HAR。

9.11 编译成功但安装失败

检查 HAP 签名、设备调试授权、包名、Profile 和 compatibleSdkVersion 是否匹配。设备中已有相同包名但由其他证书签名的应用时,先卸载旧应用或使用匹配的签名。

9.12 后台或终止态没有收到事件

setBackgroundMessageHandler 保留了上游 API,但鸿蒙端终止态事件没有端到端保证。后台播放场景应让播放器和 AVSession 的生命周期由宿主应用统一管理,并在目标系统版本上单独验证。


相关链接

相关推荐
GitCode官方2 小时前
从大语言模型到具身智能:百度百舸 LoongForge 入驻 AtomGit
人工智能·react native·百度·harmonyos·atomgit
梦想不只是梦与想8 小时前
鸿蒙 AI自动化测试实现
单元测试·harmonyos·hypium
用户09340777351414 小时前
HarmonyOS WPS Open SDK 工程实践:把 registerApp 做成就绪门禁
typescript·harmonyos
大雷神17 小时前
HarmonyOS7应用《拼棋派对》实战:双人键盘操作与窗口焦点恢复
harmonyos·arkui·harmonyos 7.0·玩转 arkts
2501_9462447819 小时前
开源鸿蒙平台 CMP 三方库「compose-continuous-rounded-corner-shape」适配全流程
华为·开源·harmonyos
KevinChaos19 小时前
灶台菜谱设计手记 03|HarmonyOS 7.0 空间化:沉浸光感反馈
harmonyos
大雷神19 小时前
HarmonyOS7应用《拼棋派对》实战:折叠屏与电脑的窗口布局适配
harmonyos·arkui·harmonyos 7.0·玩转 arkts
2501_9462447819 小时前
开源鸿蒙平台 CMP 三方库「Waterfall」适配全流程
华为·开源·harmonyos
2401_zq136y0319 小时前
开源鸿蒙平台 CMP 三方库「Jewel」适配全流程
华为·开源·harmonyos