React Native 三方库 react-native-tts 鸿蒙适配实战:从零到语音播报
是一个 React Native Text-To-Speech 语音合成库,支持 iOS、Android 和 Windows 平台,提供文本朗读、语速/音调控制、语音列表获取等能力。鸿蒙适配版本由CPF-RN团队维护,已适配 OpenHarmony 平台,使用鸿蒙原生语音合成引擎实现底层能力。将集成到 HarmonyOS NEXT 平台的核心流程可概括为“三端对齐 + Codegen 桥接”JS
React Native 三方库 react-native-tts 鸿蒙适配实战:从零到语音播报
本文记录了将开源 React Native 三方库
react-native-tts安装集成到 HarmonyOS NEXT 平台的完整过程,涵盖 RN 工程初始化、OHOS 原生工程搭建、三方库 JS 端与 ArkTS 端集成、Codegen 桥接代码生成、C++ 原生层配置,以及踩坑排障的完整复盘。
一、背景
1.1 三方库简介
react-native-tts 是一个 React Native Text-To-Speech 语音合成库,支持 iOS、Android 和 Windows 平台,提供文本朗读、语速/音调控制、语音列表获取等能力。
鸿蒙适配版本由 CPF-RN 团队维护,已适配 OpenHarmony 平台,使用鸿蒙原生 @kit.CoreSpeechKit 语音合成引擎实现底层能力。
1.2 适配目标
| 目标项 | 说明 |
|---|---|
| 三方库 | react-native-tts → @react-native-ohos/react-native-tts |
| RN 框架版本 | @rnoh/react-native-openharmony 0.82.30 |
| React Native 版本 | 0.82.1 |
| 目标平台 | HarmonyOS NEXT (API 23+) |
| 目标能力 | 语音合成(speak)、语速/音调设置、播放控制(stop/pause/resume)、事件监听 |
1.3 支持能力一览
| API | 功能 | 鸿蒙支持 |
|---|---|---|
getInitStatus() |
获取 TTS 引擎初始化状态 | ✅ |
speak(utterance) |
朗读文本 | ✅ |
stop(onWordBoundary) |
停止朗读 | ✅ |
pause(onWordBoundary) |
暂停朗读 | ✅ |
resume() |
恢复朗读 | ✅ |
setDefaultRate(rate) |
设置默认语速 | ✅ |
setDefaultPitch(pitch) |
设置默认音调 | ✅ |
voices() |
获取音色列表 | ✅ |
addEventListener() |
添加事件监听 | ✅ |
removeEventListener() |
移除事件监听 | ✅ |
setDucking() |
降低其他音频音量 | ❌ |
setDefaultEngine() |
设置默认引擎 | ❌ |
setDefaultVoice() |
设置默认音色 | ❌ |
setDefaultLanguage() |
设置默认语言 | ❌ |
setIgnoreSilentSwitch() |
忽略静音开关 | ❌ |
engines() |
获取引擎列表 | ❌ |
二、适配路线图
整个适配过程分为以下阶段:
| 阶段 | 内容 | 关键产出 |
|---|---|---|
| 阶段一 | RN 工程创建与 OHOS 工程初始化 | AwesomeProject + Myrndemo 双工程 |
| 阶段二 | JS 端依赖安装与 Metro 配置 | package.json + metro.config.js |
| 阶段三 | OHOS 端源码集成 | tts/ 目录 5 个 ArkTS 文件 |
| 阶段四 | Codegen 桥接代码生成 | generated/ 目录 8 个文件 |
| 阶段五 | C++ 原生层配置 | CMakeLists.txt + PackageProvider.cpp |
| 阶段六 | App 业务代码与 Bundle 构建 | App.tsx + bundle.harmony.js |
| 阶段七 | 构建部署与验证 | 设备端语音播报成功 |
三、逐步适配过程
阶段一:RN 工程创建与 OHOS 工程初始化
1.1 创建 React Native 项目
npx @react-native-community/cli init AwesomeProject --version 0.82.1
cd AwesomeProject
⚠️ 版本注意:React Native 版本必须与
@rnoh/react-native-openharmony版本对齐。0.82.30 对应 RN 0.82.x,版本不匹配会导致 TurboModule 找不到或白屏。
1.2 安装 Harmony 依赖
在 AwesomeProject/package.json 中添加:
{
"dependencies": {
"react": "18.3.1",
"react-native": "0.82.1",
"@react-native-oh/react-native-harmony": "^0.82.30",
"@react-native-oh/react-native-harmony-cli": "^0.82.30",
"metro": "^0.83.7"
}
}
执行安装:
npm install
1.3 配置 Metro
创建 AwesomeProject/metro.config.js:
const {mergeConfig, getDefaultConfig} = require('@react-native/metro-config');
const {createHarmonyMetroConfig} = require('@react-native-oh/react-native-harmony/metro.config');
const config = {
transformer: {
getTransformOptions: async () => ({
transform: {
experimentalImportSupport: false,
inlineRequires: true,
},
}),
},
};
module.exports = mergeConfig(getDefaultConfig(__dirname), createHarmonyMetroConfig({
reactNativeHarmonyPackageName: '@react-native-oh/react-native-harmony',
}), config);
💡 Metro 配置的作用是将
react-native-tts的 import 自动重定向到@react-native-ohos/react-native-tts。
1.4 创建 OHOS 工程
使用 DevEco Studio 创建 HarmonyOS NEXT 项目 Myrndemo,选择 Empty Ability 模板。项目结构如下:
Myrndemo/
├── AppScope/
├── entry/
│ ├── src/main/
│ │ ├── ets/
│ │ │ ├── entryability/EntryAbility.ets
│ │ │ ├── pages/Index.ets
│ │ │ └── RNPackagesFactory.ets
│ │ ├── cpp/
│ │ │ ├── CMakeLists.txt
│ │ │ └── PackageProvider.cpp
│ │ ├── resources/rawfile/
│ │ └── module.json5
│ ├── oh-package.json5
│ └── build-profile.json5
├── oh_modules/
└── build-profile.json5
1.5 配置环境变量
# 在 ~/.zshrc 中添加
export RNOH_C_API_ARCH=1
export HDC_SERVER_PORT=7035
export PATH="/Applications/DevEco-Studio.app/Contents/sdk/{版本路径}/openharmony/toolchains:$PATH"
source ~/.zshrc
阶段二:JS 端依赖安装与 Metro 配置
2.1 安装 react-native-tts
cd AwesomeProject
npm install @react-native-ohos/react-native-tts
安装完成后,package.json 中会新增:
{
"dependencies": {
"@react-native-ohos/react-native-tts": "^4.1.1-0.0.3"
}
}
2.2 验证 Metro 重定向
执行 Bundle 构建时,Metro 会自动输出重定向信息:
[INFO] Redirected imports to 1 harmony-specific third-party package(s):
[INFO] • react-native-tts → @react-native-ohos/react-native-tts
这说明 import Tts from 'react-native-tts' 会被自动重定向到鸿蒙适配版本 @react-native-ohos/react-native-tts。
阶段三:OHOS 端源码集成
在 Myrndemo/entry/src/main/ets/ 下创建 tts/ 目录,放置 5 个 ArkTS 文件。
3.1 目录结构
entry/src/main/ets/
├── tts/
│ ├── RNTTSPackage.ts # RN Package 注册入口
│ ├── RNTTSTurboModule.ts # TurboModule 实现(桥接层)
│ ├── TextToSpeechManager.ts # TTS 引擎管理(核心逻辑)
│ ├── AudioPlayer.ts # 音频渲染器(播放层)
│ └── Logger.ts # 日志工具
├── generated/ # Codegen 生成(阶段四产出)
│ ├── ts.ts
│ ├── index.ets
│ ├── turboModules/
│ │ ├── TTSNativeModule.ts
│ │ └── ts.ts
│ └── components/
│ └── ts.ts
├── RNPackagesFactory.ets
└── pages/Index.ets
3.2 RNTTSPackage.ts — Package 注册入口
/*
* Copyright (c) 2024 Huawei Device Co., Ltd. All rights reserved
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*/
import type { TurboModule, TurboModuleContext } from '@rnoh/react-native-openharmony/ts';
import { RNPackage, TurboModulesFactory } from '@rnoh/react-native-openharmony/ts';
import { TM } from '../generated/ts';
import { RNTTSTurboModule } from './RNTTSTurboModule';
class RNTTSTurboModuleFactory extends TurboModulesFactory {
createTurboModule(name: string): TurboModule | null {
if (name === TM.TTSNativeModule.NAME) {
return new RNTTSTurboModule(this.ctx);
}
return null;
}
hasTurboModule(name: string): boolean {
return name === TM.TTSNativeModule.NAME;
}
}
export class RNTTSPackage extends RNPackage {
createTurboModulesFactory(ctx: TurboModuleContext): TurboModulesFactory {
return new RNTTSTurboModuleFactory(ctx);
}
}
💡
TM.TTSNativeModule.NAME来自 Codegen 生成的类型,值为'TTSNativeModule',确保 JS 端与 ArkTS 端的模块名一致。
3.3 RNTTSTurboModule.ts — TurboModule 桥接层
/*
* Copyright (c) 2024 Huawei Device Co., Ltd. All rights reserved
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*/
import { TurboModule } from '@rnoh/react-native-openharmony/ts';
import { TM } from '../generated/ts';
import { TextToSpeechManager } from './TextToSpeechManager';
type EventCallback = (event: string) => void;
export class RNTTSTurboModule extends TurboModule implements TM.TTSNativeModule.Spec {
constructor(ctx) {
super(ctx)
}
private TextToSpeechManager = new TextToSpeechManager(this.ctx);
public getInitStatus(): Promise<unknown> {
return this.TextToSpeechManager.getInitStatus();
}
public requestInstallEngine(): Promise<unknown> {
return Promise.resolve('success');
}
public requestInstallData(): Promise<unknown> {
return Promise.resolve('success');
}
public setDucking(enabled: boolean): Promise<unknown> {
return Promise.resolve('success');
}
public setDefaultEngine(engineName: string): Promise<boolean> {
return Promise.resolve(true);
}
public setDefaultVoice(voiceId: string): Promise<unknown> {
return Promise.resolve('success');
}
public setDefaultRate(rate: number, skipTransform: boolean): Promise<unknown> {
return this.TextToSpeechManager.setDefaultRate(rate, skipTransform);
}
public setDefaultPitch(pitch: number): Promise<unknown> {
return this.TextToSpeechManager.setDefaultPitch(pitch);
}
public setDefaultLanguage(language: string): Promise<unknown> {
return Promise.resolve('success');
}
public setIgnoreSilentSwitch(ignoreSilentSwitch: boolean): Promise<boolean> {
return Promise.resolve(true);
}
public voices(): Promise<TM.TTSNativeModule.Voice[]> {
return this.TextToSpeechManager.voices();
}
public engines(): Promise<TM.TTSNativeModule.Engine[]> {
return Promise.resolve([]);
}
public speak(utterance: string, params: {}): Object {
return this.TextToSpeechManager.speak(utterance, params as Record<string, string>);
}
public stop(onWordBoundary: boolean): Promise<boolean> {
return this.TextToSpeechManager.stop(onWordBoundary);
}
public pause(onWordBoundary: boolean): Promise<boolean> {
return this.TextToSpeechManager.pause(onWordBoundary);
}
public resume(): Promise<boolean> {
return this.TextToSpeechManager.resume();
}
public addEventListener(type: string, handler: (event: Object) => Object): void {
return this.TextToSpeechManager.addEventListener(type, handler as EventCallback);
}
public removeEventListener(type: string, handler: (event: Object) => Object): void {
return this.TextToSpeechManager.removeEventListener(type, handler as EventCallback);
}
}
💡 TurboModule 是 RN 新架构中 JS 与原生通信的桥梁。
implements TM.TTSNativeModule.Spec确保方法签名与 Codegen 生成的接口一致。未支持的 API 返回空实现。
3.4 TextToSpeechManager.ts — TTS 核心逻辑
/*
* Copyright (c) 2024 Huawei Device Co., Ltd. All rights reserved
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*/
import { textToSpeech } from '@kit.CoreSpeechKit';
import { util } from '@kit.ArkTS';
import { RNOHContext, RNOHLogger } from '@rnoh/react-native-openharmony/ts';
import { TM } from '../generated/ts';
import { AudioPlayer } from './AudioPlayer';
type EventCallback = (id: string) => void;
export class TextToSpeechManager {
private context: RNOHContext | undefined = undefined;
private tts: textToSpeech.TextToSpeechEngine;
private ready: boolean;
private processFlag: boolean;
private audioPlayer: AudioPlayer;
private speakParams: Record<string, Object> = {
// 语速,0.5-2
"speed": 1,
// 音调,0-2
"pitch": 1,
// 合成类型,为0返回音频流,为1播放
"playType": 0,
}
constructor(ctx: RNOHContext) {
this.context = ctx;
this.initEngine();
this.audioPlayer = new AudioPlayer(ctx);
}
/*创建tts实例*/
private initEngine(): Promise<string> {
const extraParam: Record<string, Object> = {
"style": 'interaction-broadcast',
"locate": 'CN',
"name": 'EngineName'
};
const initParamsInfo: textToSpeech.CreateEngineParams = {
// 当前仅支持"zh-CN"中文
language: 'zh-CN',
// 音色,不可修改
person: 0,
// 0为在线,1为离线,当前仅支持离线模式
online: 1,
extraParams: extraParam
};
return new Promise((resolve, reject) => {
textToSpeech.createEngine(initParamsInfo).then(res => {
this.tts = res;
this.tts.setListener(this.speakCallback);
this.ready = true;
resolve('Success');
}).catch(error => {
reject(JSON.stringify(error));
});
})
}
/*设置speak的回调信息*/
private get speakCallback() {
const that = this;
return {
// 开始播报回调
onStart(requestId: string, response: textToSpeech.StartResponse) {
that.processFlag = false;
},
// 合成完成及播报完成回调
onComplete(requestId: string, response: textToSpeech.CompleteResponse) {
that.emitEvent('tts-start', requestId);
that.audioPlayer.sortBufferQueue();
that.audioPlayer.processQueue(requestId, () => {
that.emitEvent('tts-finish', requestId);
});
},
// 停止播报回调
onStop(requestId: string, response: textToSpeech.StopResponse) {},
// 返回音频流
onData(requestId: string, audio: ArrayBuffer, response: textToSpeech.SynthesisResponse) {
if (response.sequence > 0) {
if (!that.processFlag) {
that.emitEvent('tts-progress', requestId);
that.processFlag = true;
}
that.audioPlayer.receiveData({ buffer: audio, index: response.sequence }, requestId);
}
},
// 错误回调
onError(requestId: string, errorCode: number, errorMessage: string) {
that.emitEvent('tts-error', requestId);
that.audioPlayer.stop();
that.audioPlayer.flush();
}
}
}
/*获取当前初始化状态*/
public getInitStatus(): Promise<"success"> {
return new Promise((resolve, reject) => {
if (!this.ready) {
try {
this.initEngine().then(() => {
resolve('success');
}).catch(err => {
reject(JSON.stringify(err));
})
} catch (exception) {
reject(JSON.stringify(exception));
}
} else {
resolve('success');
}
})
}
/*获取当前音色列表*/
public voices(): Promise<TM.TTSNativeModule.Voice[]> {
let voicesQuery: textToSpeech.VoiceQuery = {
requestId: util.generateRandomUUID(false),
online: 1
};
return new Promise((resolve, reject) => {
try {
this.tts.listVoices(voicesQuery).then(res => {
const rList = res.map(v => {
return {
id: '',
name: '',
language: v.language,
quality: 0,
latency: 0,
networkConnectionRequired: false,
notInstalled: false
}
});
resolve(rList);
})
} catch (e) {
reject(JSON.stringify(e));
}
})
}
/*设置默认语速*/
public setDefaultRate(rate: number, skipTransform?: boolean): Promise<"success"> {
return new Promise((resolve, reject) => {
try {
this.speakParams.speed = rate;
resolve('success');
} catch (e) {
reject(JSON.stringify(e));
}
});
}
/*设置默认音调*/
public setDefaultPitch(pitch: number): Promise<"success"> {
return new Promise((resolve, reject) => {
try {
this.speakParams.pitch = pitch;
resolve('success');
} catch (e) {
reject(JSON.stringify(e));
}
});
}
/*停止语音播放*/
public stop(onWordBoundary?: boolean): Promise<boolean> {
return this.audioPlayer.stop();
}
/*开始合成语音并播放*/
public speak(utterance: string, params: Record<string, string> = {}): string | number {
if (!this.ready) {
return;
}
if (this.tts.isBusy()) {
return;
}
const utteranceId = util.generateRandomUUID(false);
const speakParams = { requestId: utteranceId, extraParams: { ...this.speakParams, ...params } };
try {
this.audioPlayer.start().then(() => {
this.audioPlayer.clearCacheData().then(() => {
this.tts.speak(utterance, speakParams);
})
})
} catch (exception) {
throw new Error(JSON.stringify(exception));
}
return utteranceId;
}
/*暂停语音播放*/
public pause(onWordBoundary: boolean): Promise<boolean> {
return this.audioPlayer.pause();
}
/*重新播放之前暂停的语音*/
public resume(): Promise<boolean> {
return this.audioPlayer.resume();
}
private emitEvent(name: string, id: string) {
this.context.rnInstance.emitDeviceEvent(name, id);
}
/*RN内置事件发射器调用所必需的*/
public addEventListener(type: string, listener: EventCallback) {
// Keep: Required for RN built in Event Emitter Calls.
}
/*RN内置事件发射器调用所必需的*/
public removeEventListener(type: string, listener: EventCallback) {
// Keep: Required for RN built in Event Emitter Calls.
}
}
💡 关键设计:
playType: 0表示返回音频流(而非直接播放),由AudioPlayer自行控制播放流程onComplete回调触发时先排序缓冲区再播放,避免音频碎片乱序emitEvent通过rnInstance.emitDeviceEvent将事件传递到 JS 端- 鸿蒙 TTS 引擎当前仅支持
zh-CN中文和离线模式(online: 1)
3.5 AudioPlayer.ts — 音频渲染器
/*
* Copyright (c) 2024 Huawei Device Co., Ltd. All rights reserved
* Use of this source code is governed by a MIT license that can be
* found in the LICENSE file.
*/
import audio from '@ohos.multimedia.audio';
import { RNOHContext } from '@rnoh/react-native-openharmony/ts';
type DataItem = {buffer: ArrayBuffer, index: number};
type Callback = () => void;
export class AudioPlayer {
private TAG: string = 'AudioPlayer';
private context: RNOHContext | undefined = undefined;
private audioRenderer: audio.AudioRenderer;
private bufferQueue: DataItem[] = [];
private isWriting: boolean = false;
public writeId: string = '';
constructor(ctx: RNOHContext) {
this.context = ctx;
this.getAudioRenderer();
}
private get audioStatus(): number {
return this.audioRenderer.state.valueOf();
}
public get isPrepare(): boolean {
return this.audioStatus === audio.AudioState.STATE_PREPARED;
}
public get isRunning(): boolean {
return this.audioStatus === audio.AudioState.STATE_RUNNING;
}
public get isPause(): boolean {
return this.audioStatus === audio.AudioState.STATE_PAUSED;
}
public get isStop(): boolean {
return this.audioStatus === audio.AudioState.STATE_STOPPED;
}
/*获取音频渲染器*/
private getAudioRenderer(): Promise<string> {
const audioStreamInfo: audio.AudioStreamInfo = {
samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_16000,
channels: audio.AudioChannel.CHANNEL_1,
sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE,
encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW
};
const audioRendererInfo: audio.AudioRendererInfo = {
usage: audio.StreamUsage.STREAM_USAGE_MUSIC,
rendererFlags: 0
};
const audioRendererOptions: audio.AudioRendererOptions = {
streamInfo: audioStreamInfo,
rendererInfo: audioRendererInfo
};
return new Promise((resolve, reject) => {
audio.createAudioRenderer(audioRendererOptions,(err, data) => {
if (err) {
reject(JSON.stringify(err));
throw new Error(JSON.stringify(err));
} else {
this.audioRenderer = data;
resolve('Success');
}
});
})
}
// 清空缓存数据
public clearCacheData(): Promise<void> {
return new Promise((resolve, reject) => {
try {
this.bufferQueue = [];
this.writeId = '';
this.isWriting = false;
resolve();
} catch (e) {
reject();
throw new Error(JSON.stringify(e));
}
})
}
// 接收音频数据流
public receiveData(data: DataItem, requestId: string) {
this.writeId = requestId;
this.bufferQueue.push(data);
}
// 接收到的音频流顺序有误,不预先排序会产生杂音
public sortBufferQueue(){
this.bufferQueue.sort((a: DataItem, b: DataItem) => a.index -b.index);
}
// 处理缓冲队列
public async processQueue(requestId: string, callback?: Callback) {
this.writeId = requestId;
for (const item of this.bufferQueue) {
await this.writeBuffer(item.buffer);
}
this.isWriting = false;
if (callback) {
callback();
}
}
// 启动音频渲染器
public start(): Promise<boolean> {
return new Promise((resolve, reject) => {
try {
if (this.isStop || !this.isRunning && !this.isPause && !this.isPrepare) {
this.audioRenderer.start((err) => {
if (err) {
reject(JSON.stringify(err));
} else {
resolve(true);
}
});
} else {
resolve(true);
}
} catch (e) {
reject(JSON.stringify(e));
throw new Error(JSON.stringify(e));
}
})
}
// 写入音频数据到渲染器
private writeBuffer(buffer: ArrayBuffer): Promise<void> {
return new Promise((resolve, reject) => {
try {
this.isWriting = true;
this.audioRenderer.write(buffer, (err) => {
if (err) {
this.isWriting = false;
reject(JSON.stringify(err));
} else {
resolve();
}
});
} catch (e) {
this.isWriting = false;
reject(JSON.stringify(e));
}
})
}
/*停止播放*/
public stop(): Promise<boolean> {
return new Promise((resolve, reject) => {
try {
if (this.isRunning || this.isPause) {
this.audioRenderer.stop();
this.isWriting = false;
resolve(true);
}
} catch (e) {
reject(JSON.stringify(e));
throw new Error(JSON.stringify(e));
}
})
}
/*暂停*/
public pause(): Promise<boolean> {
return new Promise((resolve, reject) => {
try {
if(this.isRunning && !this.isPause){
this.emitEvent('tts-pause');
this.audioRenderer.pause();
this.isWriting = false;
resolve(true);
}
} catch (e) {
reject(JSON.stringify(e));
throw new Error(JSON.stringify(e));
}
})
}
/*继续播放*/
public resume(): Promise<boolean> {
return new Promise((resolve, reject) => {
try {
if(this.isPause && !this.isRunning){
this.audioRenderer.start().then(() => {
this.emitEvent('tts-resume');
this.processQueue(this.writeId);
resolve(true);
}).catch((e) => reject(JSON.stringify(e)));
}
} catch (e) {
reject(JSON.stringify(e));
throw new Error(JSON.stringify(e));
}
})
}
/*清空缓存区*/
public flush(): Promise<boolean> {
return new Promise((resolve, reject) => {
try {
this.audioRenderer.flush();
resolve(true);
} catch (e) {
reject(JSON.stringify(e));
throw new Error(JSON.stringify(e));
}
})
}
private emitEvent(name: string){
this.context.rnInstance.emitDeviceEvent(name, this.writeId);
}
}
💡 关键设计:
- 使用
@ohos.multimedia.audio的AudioRenderer渲染 PCM 音频流- 采样率 16kHz、单声道、S16LE 格式——与鸿蒙 TTS 引擎输出匹配
sortBufferQueue()对音频碎片按 sequence 排序,避免杂音processQueue()顺序写入缓冲区后回调通知播放完成
3.6 Logger.ts — 日志工具
import hilog from '@ohos.hilog';
class Logger {
private domain: number;
private prefix: string;
private format: string = '%{public}s, %{public}s';
private isDebug: boolean;
constructor(prefix: string = 'TTS', domain: number = 0xFF00, isDebug: boolean = false) {
this.prefix = prefix;
this.domain = domain;
this.isDebug = isDebug;
}
debug(...args: string[]): void {
if (this.isDebug) {
hilog.debug(this.domain, this.prefix, this.format, args);
}
}
info(...args: string[]): void {
hilog.info(this.domain, this.prefix, this.format, args);
}
warn(...args: string[]): void {
hilog.warn(this.domain, this.prefix, this.format, args);
}
error(...args: string[]): void {
hilog.error(this.domain, this.prefix, this.format, args);
}
}
export default new Logger('TTS', 0xFF00, false)
3.7 注册 RNTTSPackage
修改 RNPackagesFactory.ets:
import { RNPackageContext, RNPackage } from '@rnoh/react-native-openharmony/ts';
import { RNTTSPackage } from './tts/RNTTSPackage';
export function createRNPackages(ctx: RNPackageContext): RNPackage[] {
return [
new RNTTSPackage(ctx),
];
}
3.8 配置 oh-package.json5
{
"name": "entry",
"version": "1.0.0",
"description": "Please describe the basic information.",
"main": "",
"author": "",
"license": "",
"dependencies": {
"@rnoh/react-native-openharmony": "0.82.30",
"@ppd/ffrt": "1.1.5"
},
"devDependencies": {},
"dynamicDependencies": {}
}
⚠️ 注意:此处不需要添加
@react-native-ohos/react-native-tts的.har依赖,因为我们采用的是源码直接集成方式,将 5 个 ArkTS 文件直接放入项目中。
阶段四:Codegen 桥接代码生成
Codegen 是 RN 新架构的关键工具,根据 TurboModule Spec 自动生成跨语言桥接代码。
4.1 执行 Codegen
cd AwesomeProject
npx react-native codegen-harmony \
--ets-output-path ../Myrndemo/entry/src/main/ets/generated \
--cpp-output-path ../Myrndemo/entry/src/main/cpp/generated \
--project-root-path . \
--no-safety-check
4.2 生成结果
• ../Myrndemo/entry/src/main/cpp/generated/RNOHGeneratedPackage.h
• ../Myrndemo/entry/src/main/cpp/generated/TTSNativeModule.cpp
• ../Myrndemo/entry/src/main/cpp/generated/TTSNativeModule.h
• ../Myrndemo/entry/src/main/ets/generated/components/ts.ts
• ../Myrndemo/entry/src/main/ets/generated/index.ets
• ../Myrndemo/entry/src/main/ets/generated/ts.ts
• ../Myrndemo/entry/src/main/ets/generated/turboModules/TTSNativeModule.ts
• ../Myrndemo/entry/src/main/ets/generated/turboModules/ts.ts
info Generated 8 file(s)
4.3 关键生成文件解读
generated/ts.ts — 统一导出入口:
export * as RNC from "./components/ts"
export * as TM from "./turboModules/ts"
generated/turboModules/TTSNativeModule.ts — TM 类型定义:
import { Tag } from "@rnoh/react-native-openharmony/ts"
export namespace TTSNativeModule {
export const NAME = 'TTSNativeModule' as const
export type Engine = {name: string, label: string, default: boolean, icon: number}
export type Voice = {id: string, name: string, language: string, quality: number,
latency: number, networkConnectionRequired: boolean, notInstalled: boolean}
export interface Spec {
getInitStatus(): Promise<unknown>;
requestInstallEngine(): Promise<unknown>;
requestInstallData(): Promise<unknown>;
setDucking(enabled: boolean): Promise<unknown>;
setDefaultEngine(engineName: string): Promise<boolean>;
setDefaultVoice(voiceId: string): Promise<unknown>;
setDefaultRate(rate: number, skipTransform: boolean): Promise<unknown>;
setDefaultPitch(pitch: number): Promise<unknown>;
setDefaultLanguage(language: string): Promise<unknown>;
setIgnoreSilentSwitch(ignoreSilentSwitch: boolean): Promise<boolean>;
voices(): Promise<Voice[]>;
engines(): Promise<Engine[]>;
speak(utterance: string, params: {}): Object;
stop(onWordBoundary: boolean): Promise<boolean>;
pause(onWordBoundary: boolean): Promise<boolean>;
resume(): Promise<boolean>;
addEventListener(type: string, handler: (event: Object) => Object): void;
removeEventListener(type: string, handler: (event: Object) => Object): void;
}
}
💡
TM.TTSNativeModule.Spec是 TurboModule 必须实现的接口,TM.TTSNativeModule.NAME是模块注册名('TTSNativeModule'),确保 C++ → ArkTS → JS 三端一致。
generated/RNOHGeneratedPackage.h — C++ Package 注册:
class RNOHGeneratedPackageTurboModuleFactoryDelegate : public TurboModuleFactoryDelegate {
public:
SharedTurboModule createTurboModule(Context ctx, const std::string &name) const override {
if (name == "TTSNativeModule") {
return std::make_shared<TTSNativeModule>(ctx, name);
}
return nullptr;
};
};
class RNOHGeneratedPackage : public Package {
public:
RNOHGeneratedPackage(Package::Context ctx) : Package(ctx){};
std::unique_ptr<TurboModuleFactoryDelegate> createTurboModuleFactoryDelegate() override {
return std::make_unique<RNOHGeneratedPackageTurboModuleFactoryDelegate>();
}
};
generated/TTSNativeModule.cpp — C++ 方法映射表:
TTSNativeModule::TTSNativeModule(const ArkTSTurboModule::Context ctx, const std::string name)
: ArkTSTurboModule(ctx, name) {
methodMap_ = {
ARK_ASYNC_METHOD_METADATA(getInitStatus, 0),
ARK_ASYNC_METHOD_METADATA(requestInstallEngine, 0),
ARK_ASYNC_METHOD_METADATA(requestInstallData, 0),
ARK_ASYNC_METHOD_METADATA(setDucking, 1),
ARK_ASYNC_METHOD_METADATA(setDefaultEngine, 1),
ARK_ASYNC_METHOD_METADATA(setDefaultVoice, 1),
ARK_ASYNC_METHOD_METADATA(setDefaultRate, 2),
ARK_ASYNC_METHOD_METADATA(setDefaultPitch, 1),
ARK_ASYNC_METHOD_METADATA(setDefaultLanguage, 1),
ARK_ASYNC_METHOD_METADATA(setIgnoreSilentSwitch, 1),
ARK_ASYNC_METHOD_METADATA(voices, 0),
ARK_ASYNC_METHOD_METADATA(engines, 0),
ARK_METHOD_METADATA(speak, 2),
ARK_ASYNC_METHOD_METADATA(stop, 1),
ARK_ASYNC_METHOD_METADATA(pause, 1),
ARK_ASYNC_METHOD_METADATA(resume, 0),
ARK_METHOD_METADATA(addEventListener, 2),
ARK_METHOD_METADATA(removeEventListener, 2),
};
}
💡
ARK_ASYNC_METHOD_METADATA标记异步方法(返回 Promise),ARK_METHOD_METADATA标记同步方法。数字为参数个数。
阶段五:C++ 原生层配置
5.1 CMakeLists.txt
修改 entry/src/main/cpp/CMakeLists.txt:
project(rnapp)
cmake_minimum_required(VERSION 3.4.1)
set(CMAKE_SKIP_BUILD_RPATH TRUE)
set(OH_MODULE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../../oh_modules")
set(RNOH_APP_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
set(RNOH_CPP_DIR "${OH_MODULE_DIR}/@rnoh/react-native-openharmony/src/main/cpp")
set(RNOH_GENERATED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/generated")
set(CMAKE_ASM_FLAGS "-Wno-error=unused-command-line-argument -Qunused-arguments")
set(CMAKE_CXX_FLAGS "-fstack-protector-strong -Wl,-z,relro,-z,now,-z,noexecstack -s -fPIE -pie")
add_compile_definitions(WITH_HITRACE_SYSTRACE)
set(WITH_HITRACE_SYSTRACE 1)
add_subdirectory("${RNOH_CPP_DIR}" ./rn)
add_library(rnoh_app SHARED
"./PackageProvider.cpp"
"${RNOH_CPP_DIR}/RNOHAppNapiBridge.cpp"
"${RNOH_GENERATED_DIR}/RNOHGeneratedPackage.h"
"${RNOH_GENERATED_DIR}/TTSNativeModule.cpp"
)
target_include_directories(rnoh_app PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}"
"${RNOH_GENERATED_DIR}"
)
target_link_libraries(rnoh_app PUBLIC rnoh)
💡 相比空白模板,新增了
RNOH_GENERATED_DIR、TTSNativeModule.cpp和target_include_directories。
5.2 PackageProvider.cpp
修改 entry/src/main/cpp/PackageProvider.cpp:
#include "RNOH/PackageProvider.h"
#include "generated/RNOHGeneratedPackage.h"
using namespace rnoh;
std::vector<std::shared_ptr<Package>> PackageProvider::getPackages(Package::Context ctx) {
return {
std::make_shared<RNOHGeneratedPackage>(ctx),
};
}
💡
RNOHGeneratedPackage是 Codegen 自动生成的 C++ Package,内部注册了TTSNativeModule的方法映射表,是 JS 调用 ArkTS 的 C++ 通道。
阶段六:App 业务代码与 Bundle 构建
6.1 App.tsx
import React, { useState, useEffect } from 'react';
import {
View,
Text,
TextInput,
TouchableOpacity,
StyleSheet,
ScrollView,
Alert,
} from 'react-native';
import Tts from '@react-native-ohos/react-native-tts';
function App() {
const [text, setText] = useState('你好,鸿蒙!欢迎使用 React Native 语音合成功能。');
const [status, setStatus] = useState('就绪');
const [isSpeaking, setIsSpeaking] = useState(false);
const [rate, setRate] = useState(1.0);
const [pitch, setPitch] = useState(1.0);
useEffect(() => {
Tts.getInitStatus().then(() => {
setStatus('TTS 引擎已就绪');
}).catch((err) => {
setStatus('TTS 引擎初始化失败: ' + JSON.stringify(err));
});
const onStart = Tts.addEventListener('tts-start', (event) => {
setIsSpeaking(true);
setStatus('正在朗读...');
});
const onFinish = Tts.addEventListener('tts-finish', (event) => {
setIsSpeaking(false);
setStatus('朗读完成');
});
const onError = Tts.addEventListener('tts-error', (event) => {
setIsSpeaking(false);
setStatus('朗读出错');
});
const onCancel = Tts.addEventListener('tts-cancel', (event) => {
setIsSpeaking(false);
setStatus('已停止');
});
return () => {
onStart.remove();
onFinish.remove();
onError.remove();
onCancel.remove();
};
}, []);
const handleSpeak = () => {
if (!text.trim()) {
Alert.alert('提示', '请输入要朗读的文本');
return;
}
Tts.speak(text);
};
const handleStop = () => {
Tts.stop(true);
};
const handleRateChange = (delta) => {
const newRate = Math.max(0.5, Math.min(2.0, rate + delta));
setRate(newRate);
Tts.setDefaultRate(newRate);
};
const handlePitchChange = (delta) => {
const newPitch = Math.max(0.5, Math.min(2.0, pitch + delta));
setPitch(newPitch);
Tts.setDefaultPitch(newPitch);
};
return (
<View style={styles.container}>
<Text style={styles.title}>语音合成 Demo</Text>
<View style={styles.statusBar}>
<Text style={styles.statusText}>状态:{status}</Text>
</View>
<TextInput
style={styles.input}
value={text}
onChangeText={setText}
multiline
placeholder="输入要朗读的文本..."
/>
<View style={styles.controlRow}>
<Text style={styles.label}>语速:{rate.toFixed(1)}</Text>
<TouchableOpacity style={styles.smallBtn} onPress={() => handleRateChange(-0.1)}>
<Text style={styles.btnText}>-</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.smallBtn} onPress={() => handleRateChange(0.1)}>
<Text style={styles.btnText}>+</Text>
</TouchableOpacity>
</View>
<View style={styles.controlRow}>
<Text style={styles.label}>音调:{pitch.toFixed(1)}</Text>
<TouchableOpacity style={styles.smallBtn} onPress={() => handlePitchChange(-0.1)}>
<Text style={styles.btnText}>-</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.smallBtn} onPress={() => handlePitchChange(0.1)}>
<Text style={styles.btnText}>+</Text>
</TouchableOpacity>
</View>
<View style={styles.btnRow}>
<TouchableOpacity
style={[styles.btn, styles.speakBtn, isSpeaking && styles.btnDisabled]}
onPress={handleSpeak}
disabled={isSpeaking}
>
<Text style={styles.btnText}>朗读</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.btn, styles.stopBtn, !isSpeaking && styles.btnDisabled]}
onPress={handleStop}
disabled={!isSpeaking}
>
<Text style={styles.btnText}>停止</Text>
</TouchableOpacity>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
backgroundColor: '#f5f5f5',
justifyContent: 'center',
},
title: {
fontSize: 24,
fontWeight: 'bold',
textAlign: 'center',
marginBottom: 20,
color: '#333',
},
statusBar: {
backgroundColor: '#e3f2fd',
padding: 10,
borderRadius: 8,
marginBottom: 15,
},
statusText: {
fontSize: 14,
color: '#1565c0',
textAlign: 'center',
},
input: {
backgroundColor: '#fff',
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 8,
padding: 12,
fontSize: 16,
minHeight: 80,
marginBottom: 15,
textAlignVertical: 'top',
},
controlRow: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 10,
justifyContent: 'center',
},
label: {
fontSize: 16,
color: '#555',
width: 100,
},
smallBtn: {
backgroundColor: '#607d8b',
width: 36,
height: 36,
borderRadius: 18,
alignItems: 'center',
justifyContent: 'center',
marginHorizontal: 5,
},
btnRow: {
flexDirection: 'row',
justifyContent: 'center',
marginTop: 15,
},
btn: {
paddingHorizontal: 30,
paddingVertical: 12,
borderRadius: 8,
marginHorizontal: 10,
minWidth: 100,
alignItems: 'center',
},
speakBtn: {
backgroundColor: '#4caf50',
},
stopBtn: {
backgroundColor: '#f44336',
},
btnDisabled: {
backgroundColor: '#ccc',
},
btnText: {
color: '#fff',
fontSize: 16,
fontWeight: 'bold',
},
});
export default App;
6.2 构建 JS Bundle
cd AwesomeProject
npx react-native bundle-harmony --dev false
输出:
[INFO] Redirected imports to 1 harmony-specific third-party package(s):
[INFO] • react-native-tts → @react-native-ohos/react-native-tts
info Created harmony/entry/src/main/resources/rawfile/bundle.harmony.js
6.3 复制 Bundle 到 OHOS 工程
cp AwesomeProject/harmony/entry/src/main/resources/rawfile/bundle.harmony.js \
Myrndemo/entry/src/main/resources/rawfile/bundle.harmony.js
阶段七:构建部署与验证
7.1 构建 HAP
cd Myrndemo
export RNOH_C_API_ARCH=1
/Applications/DevEco-Studio.app/Contents/tools/hvigor/bin/hvigorw \
--mode module -p module=entry@default -p product=default assembleHap --no-daemon
构建成功输出:
> hvigor BUILD SUCCESSFUL in 45 s 642 ms
7.2 部署到设备
hdc install Myrndemo/entry/build/default/outputs/default/entry-default-signed.hap
7.3 启动应用
hdc shell aa start -a EntryAbility -b com.nutpi.rndemo

7.4 验证日志
hdc hilog -x | grep -iE "RNOH|TTS|tts"
成功日志:
A00001/com.huawei.hmsapp.hiai.tts/HiAI_TtsMaintenanceReportUtil: TTS callInfo:
[{"appName":"com.nutpi.rndemo","appVersion":"1.0.0"},
{"moduleName":"tts","interfaceName":"isBusy","serviceName":"TtsAbility",
"runtime":"0","resultCode":"0","detailMessage":"query isBusy success"}]
A00001/com.huawei.hmsapp.hiai.tts/HiAI_TtsSpeakListener: onTtsStart,
requestId: bc28b9ca-96e8-4b4d-bba1-31ebcb77db56
A00001/com.huawei.hmsapp.hiai.tts/HiAI_OfflineTtsService: synthesis complete,
sequence: 14 audioSize: 175546 requestId: bc28b9ca-96e8-4b4d-bba1-31ebcb77db56
A00001/com.huawei.hmsapp.hiai.tts/HiAI_TtsSpeakListener: onTtsComplete,
requestId: bc28b9ca-96e8-4b4d-bba1-31ebcb77db56, msg: synthesis complete
四、完整文件变更清单
| 文件路径 | 变更类型 | 说明 |
|---|---|---|
AwesomeProject/package.json |
修改 | 添加 RNOH + TTS 依赖 |
AwesomeProject/metro.config.js |
新增 | Harmony Metro 配置 |
AwesomeProject/App.tsx |
修改 | TTS Demo 业务代码 |
Myrndemo/entry/src/main/ets/tts/RNTTSPackage.ts |
新增 | RN Package 注册 |
Myrndemo/entry/src/main/ets/tts/RNTTSTurboModule.ts |
新增 | TurboModule 桥接 |
Myrndemo/entry/src/main/ets/tts/TextToSpeechManager.ts |
新增 | TTS 引擎核心 |
Myrndemo/entry/src/main/ets/tts/AudioPlayer.ts |
新增 | 音频渲染 |
Myrndemo/entry/src/main/ets/tts/Logger.ts |
新增 | 日志工具 |
Myrndemo/entry/src/main/ets/RNPackagesFactory.ets |
修改 | 注册 RNTTSPackage |
Myrndemo/entry/src/main/ets/generated/* |
Codegen 生成 | ETS 桥接类型 |
Myrndemo/entry/src/main/cpp/CMakeLists.txt |
修改 | 添加 generated 源文件 |
Myrndemo/entry/src/main/cpp/PackageProvider.cpp |
修改 | 注册 RNOHGeneratedPackage |
Myrndemo/entry/src/main/cpp/generated/* |
Codegen 生成 | C++ 桥接代码 |
Myrndemo/entry/oh-package.json5 |
修改 | 添加 RNOH 依赖 |
Myrndemo/entry/src/main/resources/rawfile/bundle.harmony.js |
新增 | JS Bundle |
五、关键决策说明
决策 1:源码集成 vs .har 依赖
选择:源码直接集成
理由:
.har路径依赖(file:./libs/rn_tts.har)在不同构建环境下经常出现路径解析错误- 源码集成便于调试和定制(如修改 TTS 引擎参数)
- 避免了
.har版本与 RNOH 框架版本不兼容的问题
决策 2:React Native 版本降级至 0.82.1
选择:将 RN 从 0.84.1 降级到 0.82.1
理由:
@rnoh/react-native-openharmony0.82.30 仅兼容 RN 0.82.x- 版本不匹配导致 TurboModule(如
NativePerformanceCxx)找不到,应用白屏
决策 3:Codegen 必须手动执行
选择:使用 npx react-native codegen-harmony 手动生成
理由:
- RNOH 0.82.x 不支持 autolink,需要手动执行 Codegen
- Codegen 生成的
TM.TTSNativeModule.Spec接口是 TurboModule 实现的类型约束,确保方法签名三端一致 - C++ 侧的
RNOHGeneratedPackage也由 Codegen 生成,包含方法映射表
决策 4:使用 ResourceJSBundleProvider 优先
选择:Bundle 加载优先使用 ResourceJSBundleProvider
理由:
MetroJSBundleProvider需要连接 Metro 开发服务器,在无开发环境时会阻塞启动ResourceJSBundleProvider从应用资源直接加载,离线可用- 使用
AnyJSBundleProvider按优先级尝试多种加载方式
六、踩坑与排障记录
问题 1:应用白屏,无 RNOH 日志
现象:应用启动后白屏,hdc hilog 无任何 RNOH 输出
原因:MetroJSBundleProvider 在列表中排第一,Metro 服务器不可用时阻塞了整个初始化
解决:调整 Index.ets 中 AnyJSBundleProvider 的顺序,将 ResourceJSBundleProvider 放在最前面
问题 2:TurboModule 找不到(NativePerformanceCxx、RNCSafeAreaContext)
现象:RNOH 初始化后报错,找不到 TurboModule
原因:RN 0.84.1 的 TurboModule 列表与 RNOH 0.82.30 不匹配
解决:将 React Native 降级到 0.82.1,确保版本对齐
问题 3:缺少 @ppd/ffrt 依赖
现象:C++ 构建阶段链接失败
原因:@rnoh/react-native-openharmony 0.82.30 依赖 @ppd/ffrt,需在 oh-package.json5 中显式声明
解决:在 oh-package.json5 的 dependencies 中添加 "@ppd/ffrt": "1.1.5"
问题 4:Codegen 未执行导致 TM 类型缺失
现象:ArkTS 编译报错,TM.TTSNativeModule 未定义
原因:未执行 react-native codegen-harmony 命令,generated/ 目录不存在
解决:执行 Codegen 命令生成桥接代码
问题 5:C++ 构建找不到 generated 头文件
现象:C++ 编译报 RNOHGeneratedPackage.h: No such file or directory
原因:CMakeLists.txt 未添加 target_include_directories 包含 generated 目录
解决:在 CMakeLists.txt 中添加:
target_include_directories(rnoh_app PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}"
"${RNOH_GENERATED_DIR}"
)
七、遗留问题
| 问题 | 状态 | 说明 |
|---|---|---|
| 仅支持中文语音合成 | 已知限制 | 鸿蒙 TTS 引擎当前仅支持 zh-CN,详见 issue#3 |
| 边合成边播放不可用 | 已知限制 | 当前采用合成完成再播放模式,详见 issue#7 |
setDucking 不支持 |
已知限制 | 鸿蒙平台无对应 API,详见 issue#4 |
setDefaultVoice 不支持 |
已知限制 | 鸿蒙 TTS 音色不可修改,详见 issue#6 |
skipTransform、onWordBoundary 参数无效 |
已知限制 | 参数不参与实际逻辑,详见 issue#8 |
八、总结
将 react-native-tts 集成到 HarmonyOS NEXT 平台的核心流程可概括为 “三端对齐 + Codegen 桥接”:
- JS 端:安装
@react-native-ohos/react-native-tts,Metro 自动重定向 import - ArkTS 端:源码集成 5 个 ArkTS 文件,实现 TurboModule 接口,调用鸿蒙原生 TTS API
- C++ 端:Codegen 自动生成桥接代码,CMakeLists.txt 和 PackageProvider.cpp 注册 Package
最关键的三个注意事项:
- 版本对齐:RN 版本、RNOH 版本、三方库版本必须严格匹配
- Codegen 必执行:不执行 Codegen 就没有
TM.TTSNativeModule类型和 C++ 方法映射表 - Bundle 加载顺序:
ResourceJSBundleProvider必须优先于MetroJSBundleProvider
参考文档
更多推荐



所有评论(0)