开发工具: 华为云码道
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 详细步骤
-
准备环境。 在仓库外选择工作目录,确认 Node.js、Corepack、DevEco Studio 和
hdc可用。Windows 使用 PowerShell,macOS 使用终端。macOS:
bashnode --version corepack enable corepack yarn --version hdc list targetsWindows PowerShell:
powershellnode --version corepack enable corepack yarn --version hdc list targets预期能看到 Node.js 和 Yarn 版本;
hdc list targets有设备或模拟器编号。没有设备时仍可进行类型检查和 HAR 构建。 -
拉取并固定源码。 在准备存放仓库的目录执行第三章的
git clone、cd和git switch --detach命令。看到## HEAD (no branch)或对应提交信息,说明定位成功;已有未提交修改时先保存。 -
创建维护分支。 在仓库根目录执行:
macOS 与 Windows PowerShell 共用:
bashgit switch -c feat/ohos_media_notification_0.4.6-0.0.1这一步只创建本地分支,不会自动上传远程;分支名称可按团队规则调整。
-
安装依赖。 回到仓库根目录执行:
macOS:
bashcorepack yarn installWindows PowerShell:
powershellcorepack yarn install预期工作区依赖安装完成。若 Corepack 无法准备 Yarn,先检查 Node.js 版本和网络;不要把安装产生的缓存或签名文件加入提交。
-
准备示例工程。 在仓库根目录复制
example/harmony/build-profile.example.json5为build-profile.json5,然后用 DevEco Studio 打开example/harmony。首次打开等待 Sync 完成,确认项目视图中有entry和media_notification两个模块。 -
检查桥接和原生文件。 按第四、五章核对
MediaControls名称、RNMediaControlsPackage注册、HAR 依赖和 CMake target。新增或修改src/NativeMediaControls.ts后,重新执行项目使用的 codegen 流程,再构建 HAR;codegen 只生成桥接文件,不会实现 AVSession 逻辑。 -
运行检查和构建。 先执行
corepack yarn typecheck、corepack yarn test,再执行corepack yarn build:har。需要运行示例时配置签名,执行corepack yarn build:example:harmony,产物会出现在example/harmony/entry/build/default/outputs/default/。 -
安装和验收。 通过 DevEco Studio 运行 entry,或使用
hdc install -r安装 HAP。打开示例页面后,先确认元数据状态,再观察系统媒体卡片的播放、暂停、切歌和进度操作。最后测试停止会话、重新发布元数据和返回页面后的状态。 -
检查并提交。 回到仓库根目录执行
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 配置签名
- 用 DevEco Studio 打开
example/harmony; - 等待 Sync 完成,确认项目中有
entry和media_notification; - 打开 File > Project Structure > Signing Configs;
- 为
defaultproduct 配置自己的签名; - 确认包名、证书、Profile 和设备匹配;
- 不要把含有个人证书路径和密码的
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 在设备上测试核心操作
- 打开示例页面,确认页面状态从初始化变为成功,系统媒体卡片能显示曲名、歌手、时长和进度。
- 点击"播放"或"暂停",确认页面状态和系统卡片保持一致。
- 在系统媒体卡片中点击播放、暂停、上一首、下一首或拖动进度,确认对应事件回到 React Native 页面。
- 提交示例队列,确认原生层接受队列项目;系统卡片是否展示队列内容由系统界面决定。
- 点击停止会话,再发布元数据,确认 AVSession 可以重新创建。
- 将应用切到后台后返回,检查媒体会话是否仍然存在;后台行为受系统版本和播放器实现影响。
- 不要把
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 的生命周期由宿主应用统一管理,并在目标系统版本上单独验证。