PAYATHON 2026

OneSignal 收到通知时如何使用 React Native TTS 播报?

支付阿杰

结论

foregroundWillDisplay 已正常触发,说明问题不在 OneSignal,而是在 TTS 初始化环节。当前代码需要补上这些处理:

  • 等待 Tts.getInitStatus() 和 Tts.setDefaultLanguage() 完成,并捕获失败。
  • 确认 Android 模拟器已安装并启用 TTS engine,以及对应语言的语音包。
  • 在 Android 11 及以上版本的 AndroidManifest.xml 中声明 TTS service 查询。
  • 应用启动时预先初始化 TTS,不要等到收到通知后才首次初始化。
  • 检查媒体音量、音频流和 TTS 播放事件。

Android 使用系统 TTS 不需要麦克风权限,也没有专门的 TTS 运行时权限。POST_NOTIFICATIONS 只控制通知能否显示,不能解决语音不播放的问题。

完善 AndroidManifest.xml

在 android/app/src/main/AndroidManifest.xml 中,把下面的 <queries> 放在 <manifest> 的直属位置,不要放进 <application>:

<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <queries>
        <intent>
            <action android:name="android.intent.action.TTS_SERVICE" />
        </intent>
    </queries>

    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

    <application
        android:name=".MainApplication"
        android:label="@string/app_name">
        <!-- ... -->
    </application>
</manifest>

<queries> 属于 Android 11 及以上版本的包可见性机制。修改原生清单后,需要重新编译并安装应用,只刷新 JavaScript bundle 不会生效。

如果项目使用自动链接,还要确认 react-native-tts 已进入 Android 原生构建。现代 React Native 项目通常会自动完成链接,不要再手动重复注册。

在应用启动时初始化 TTS

不必在每次收到通知时重新初始化。可以缓存初始化 Promise,让通知处理函数等待同一个初始化过程:

import {Platform} from 'react-native';
import Tts from 'react-native-tts';

let ttsReadyPromise: Promise<void> | null = null;

function ensureTtsReady(): Promise<void> {
  if (!ttsReadyPromise) {
    ttsReadyPromise = (async () => {
      await Tts.getInitStatus();

      // 使用完整 locale 通常比 "en" 更可靠
      await Tts.setDefaultLanguage('en-US');

      if (Platform.OS === 'android') {
        await Tts.setDefaultRate(0.5);
        await Tts.setDefaultPitch(1.0);
      }
    })().catch(error => {
      // 允许后续重新尝试初始化
      ttsReadyPromise = null;
      throw error;
    });
  }

  return ttsReadyPromise;
}

在 App 挂载时主动初始化,并记录错误:

useEffect(() => {
  ensureTtsReady().catch(error => {
    console.error('TTS initialization failed:', error);

    if (error?.code === 'no_engine') {
      // Android 会尝试打开 TTS engine 安装页面
      Tts.requestInstallEngine();
    }
  });
}, []);

不要无条件调用 requestInstallEngine()。只有错误明确表示缺少 engine 时才调用,否则应用可能反复弹出安装页面。

在 OneSignal 回调中等待初始化

通知处理函数可以这样修改:

const handleForegroundNotification = (event: any) => {
  console.log('Foreground Notification:', event);

  // 该调用方式应以当前项目使用的 OneSignal SDK 版本为准。
  // 现有代码已经能够显示通知,说明这一部分目前有效。
  event.notification.display();

  void (async () => {
    try {
      await ensureTtsReady();

      const amount = numberToWords(36500);
      const text = `Payment accepted. ${amount} rupiah`;

      Tts.stop();

      Tts.speak(text, {
        androidParams: {
          KEY_PARAM_STREAM: 'STREAM_MUSIC',
          KEY_PARAM_VOLUME: 1,
          KEY_PARAM_PAN: 0,
        },
      });
    } catch (error) {
      console.error('Unable to speak notification:', error);
    }
  })();
};

这里要注意两点:

  1. setDefaultLanguage() 也必须等待。原代码调用它之后立刻执行 getInitStatus(),没有等语言设置完成。
  2. 所有 Promise 都要捕获异常。原代码中的 getInitStatus().then(...) 没有 .catch(...)。如果 engine 不存在或初始化失败,表面现象往往只是没有声音,也看不到明显错误。

监听播放状态

可以临时添加事件监听,用来判断是没有执行 speak,还是已经执行但系统没有输出声音:

useEffect(() => {
  const startSubscription = Tts.addEventListener('tts-start', event => {
    console.log('TTS started:', event);
  });

  const finishSubscription = Tts.addEventListener('tts-finish', event => {
    console.log('TTS finished:', event);
  });

  const cancelSubscription = Tts.addEventListener('tts-cancel', event => {
    console.log('TTS cancelled:', event);
  });

  return () => {
    startSubscription.remove();
    finishSubscription.remove();
    cancelSubscription.remove();
  };
}, []);

如果 tts-start 和 tts-finish 都出现了,却仍然听不到声音,就应重点检查媒体音量、系统 TTS 输出和模拟器音频,不必继续排查 OneSignal。

检查 Android 15 模拟器

Android 模拟器很可能是问题所在。部分 AOSP 镜像没有预装可用的 TTS engine;有些镜像虽然带有 engine,却没有下载 en-US 语音数据。

可以先输出系统中可用的 engine 和 voice:

async function inspectTts() {
  try {
    const engines = await Tts.engines();
    const voices = await Tts.voices();

    console.log('TTS engines:', engines);
    console.log(
      'English voices:',
      voices.filter(voice =>
        voice.language?.toLowerCase().startsWith('en'),
      ),
    );
  } catch (error) {
    console.error('Unable to inspect TTS:', error);
  }
}

还要在模拟器中逐项检查:

  • 系统设置的“文字转语音输出”中是否有可用的 engine。
  • 能否在系统设置中播放示例语音。
  • 是否已经安装 en-US 或实际使用语言的语音数据。
  • 媒体音量是否为零。
  • 模拟器宿主机是否静音,或者选择了错误的音频输出设备。
  • 是否有其他应用持续占用或改变音频焦点。

如果当前使用纯 AOSP system image,可以换成带 Google APIs 或 Play Store 的镜像测试,并确认 Speech Services 已安装。模拟器镜像不同,具体设置路径和预装组件也会不同。

最好再用一台真实 Android 设备测试,以排除模拟器音频后端的问题。

检查语言设置

'en' 是比较宽泛的语言代码,部分 engine 可能要求完整 locale,例如:

await Tts.setDefaultLanguage('en-US');

如果实际需要用印尼语播报金额,应先确认设备上存在对应 voice,再尝试:

await Tts.setDefaultLanguage('id-ID');

不能假定所有 Android 设备都安装了同样的语音包。生产环境应为语言设置准备降级方案,例如在 id-ID 不可用时退回 en-US,同时记录错误。

OneSignal 的适用范围

这套方案只能保证应用处于前台且 React Native JavaScript 正在运行时进行播报。应用进入后台、被系统终止或设备重启后,即使收到了推送,React Native 回调也不一定能够运行。因此,不能依赖同一段 JavaScript 在所有后台状态下自动朗读。

foregroundWillDisplay 是否需要先调用 preventDefault(),以及该用哪种方式显示通知,取决于项目所用的 OneSignal React Native SDK 版本。当前日志和通知显示结果已经证明回调生效,所以没有必要通过调整 OneSignal 通知权限来解决 TTS 故障。

iOS 注意事项

iOS 使用系统语音合成同样不需要麦克风权限,但静音开关和 Audio Session 可能影响播放。如果需要忽略静音开关,可以只在 iOS 上配置:

if (Platform.OS === 'ios') {
  Tts.setIgnoreSilentSwitch('ignore');
}

如果应用还使用播放器、通话或录音组件,还需检查这些组件是否修改了 iOS Audio Session。当前尚未在 iOS 上验证,不能直接认定 Android 上的表现会在 iOS 上完全一致。

支付通知可能包含金额、客户或账户信息。自动朗读功能最好提供独立开关,同时避免在锁屏、共享设备或无法确认用户佩戴情况时播报敏感内容。

备注:内容仅供参考。