背景
项目中需要实现文字播报功能,在 PC 和 iPhone 等设备上运行都很正常,但在一些国产 Android 手机上却出现问题:播放按钮点击无反应,语音播报功能不可用。经过排查发现,根本原因在于:
国产 Android 浏览器中
window.speechSynthesis为undefined即浏览器本身不支持 Web Speech API 的语音合成功能。
缺少关键 API,自然就无法使用原生的文字转语音能力。这一下踩了个"大坑"。 但俗话说:办法总比困难多。冷静喝口热茶后,我开始了兼容方案探索。
摸索解决方案
既然无法依赖浏览器原生能力,那么就将文本交给支持 TTS(Text-To-Speech)的云服务, 由云端生成 .wav / .mp3 音频,再进行播放。
然而我司暂无自建 TTS 服务,短期内只能先接第三方服务。
选择TTS服务
网上查询后发现,国内外都有 TTS 服务可用,
但国外接口调用可能需要科学上网,考虑到稳定性,优先选择国内厂商。
| 服务商 | 特点 |
|---|---|
| 阿里云语音合成 | 中文质量高,可调节语速音量多音色 |
| 科大讯飞 | 行业成熟,中文表现强 |
| 百度智能云语音 | 神经网络合成,调用简单,文档充足 |
| 腾讯云语音合成 | 支持多场景拟人语音 |
我已有福报厂和某度账号,对比后挑了一个对个人用户提供的免费额度更充足的云TTS服务供应商。
某度TTS云服务注册使用方法
因某度 AI 控制台菜单繁多,为避免「找不到入口」,我整理了完整流程及关键页面:
1. 登录 某度大模型语音合成,点击立即选购
建议收藏这个入口,不然下次进入可能会迷路。 
2.进入某度智能云控制台完成实名认证
未认证将无法使用服务 
选择刷脸认证更快,填写的信息更少 
3. 进入语音技术页面
首次进入会显示创建应用提示

4.创建应用
应用名称随意填写,给应用起个好记的名称。描述简单填一下。服务接口全选即可。 
5. 获取API Key和Secret Key
应用创建成功后,会看到API Key和Secret Key, 这两个key是用来生成最终要使用的access_token

6. 点击发起调试,获取access_token
现在只想快速验证一下,并未按正常流程生成和维护access_token, 点击发起调试按钮后,会生成一个时效只有一天的访问凭证。后续上线需按正式流程获取 Token

7.调用验证
编写小页面测试是否可正常播放音频,写一个极简的调用某度TTS接口服务的程序,输入要播放的文字, 点击播放按钮,听一下有无声音发出。操作了一下,可以听到声音。
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>TTS语音合成测试</title>
</head>
<body>
<h3>TTS语音合成测试</h3>
<textarea id="text" rows="4" cols="50" placeholder="请输入要合成的中文文本"></textarea>
<br />
<button id="playBtn">播放语音</button>
<script>
const ACCESS_TOKEN = "替换为你的_access_token";
const API_URL = "https://tsn.baidu.com/text2audio";
async function playBaiduTTS(text) {
if (!text.trim()) {
alert("请输入要合成的文本");
return;
}
const params = new URLSearchParams({
tex: text,
tok: ACCESS_TOKEN,
cuid: "test_user_" + Date.now(),
ctp: "1",
lan: "zh",
spd: "5", // 语速
pit: "5", // 音调
vol: "5", // 音量
per: "0", // 发音人
aue: "3" // MP3格式
});
const response = await fetch(`${API_URL}?${params.toString()}`);
if (!response.ok) {
alert("请求失败,请检查网络或token");
return;
}
const contentType = response.headers.get("content-type");
if (contentType.includes("application/json")) {
const err = await response.json();
alert("合成失败: " + JSON.stringify(err));
return;
}
const blob = await response.blob();
const audioUrl = URL.createObjectURL(blob);
const audio = new Audio(audioUrl);
audio.play();
}
document.getElementById("playBtn").onclick = () => {
const text = document.getElementById("text").value;
playBaiduTTS(text);
};
</script>
</body>
</html>
8. 查看额度
使用第三方的TTS服务, 比较关心使用额度,这个菜单不是很好找, 为了避免超额停服,务必记得收藏查询入口 
兼容性最终解决方案
封装一个 TTS 播放 Hook,优先使用 Web 原生 SpeechSynthesis ,某度TTS作为降级方案
这样综合使用可最大化兼顾性能与兼容性。
| 指标 | 原生播报 | 某度 TTS |
|---|---|---|
| 请求延迟 | ✅ 无 | ❌ 有网络开销 |
| 播放速度体验 | ✅ 即点即播 | ⏳ 需要等待 |
| 支持范围 | ❌ 国产安卓支持性差 | ✅ 兼容度高 |
Hook 核心逻辑结构:
1. 初始化
-
通过
isNativeSupported判断浏览器是否支持speechSynthesis。 -
初始化内部状态:
isPlaying:当前是否有语音正在播放。utteranceRef:存储原生 TTS 的SpeechSynthesisUtterance实例。baiduAudioRef和baiduAudioUrlRef:存储某度 TTS 音频对象及其 URL。currentTextRef:记录当前播放的文本内容。
2. 播放语音 (play)
-
停止当前播放:
- 如果正在播放原生 TTS 或某度音频,先调用
stop()停止它们。
- 如果正在播放原生 TTS 或某度音频,先调用
-
选择播放方式:
- 原生 TTS 支持 → 创建
SpeechSynthesisUtterance实例播放。 - 原生 TTS 不支持 → 调用
playBaiduTTS发起TTS 请求并播放返回音频。
- 原生 TTS 支持 → 创建
-
状态更新:
isPlaying置为true。- 设置播放结束或出错时清理资源并更新状态。
3. 某度 TTS 播放 (playBaiduTTS)
-
构造请求 URL(包含文本、语速、音调等参数)。
-
fetch请求某度 TTS API:- 如果返回 JSON → 出错处理。
- 返回音频 Blob → 创建
Audio对象并播放。
-
注册事件:
onplay→ 更新isPlaying = trueonpause→ 更新isPlaying = falseonended→ 清理资源、重置状态onerror→ 提示播放失败,清理资源
4. 播放控制
-
暂停 (
pause) :- 原生 TTS →
window.speechSynthesis.pause() - 某度 TTS →
audio.pause()
- 原生 TTS →
-
继续 (
resume) :- 原生 TTS →
window.speechSynthesis.resume() - 某度 TTS →
audio.play()
- 原生 TTS →
-
停止 (
stop) :- 原生 TTS →
cancel()并清空utteranceRef - 某度 TTS → 暂停音频、释放 URL、清空
baiduAudioRef
- 原生 TTS →
5. 资源清理
- 某度 TTS 播放结束或组件卸载时,释放音频 URL 避免内存泄漏。
- 原生 TTS 结束时清空
utteranceRef。
实现语音播报的完整代码如下:
js
import { useRef, useState, useCallback } from 'react';
import { message } from 'antd';
interface UseTTSOptions {
baiduAccessToken?: string;
}
interface UseTTSReturn {
isNativeSupported: boolean;
isPlaying: boolean;
play: (text: string) => Promise<void>;
pause: () => void;
resume: () => void;
stop: () => void;
}
export const useTTS = ({ baiduAccessToken }: UseTTSOptions = {}): UseTTSReturn => {
const isNativeSupported = typeof window !== 'undefined' && 'speechSynthesis' in window;
const [isPlaying, setIsPlaying] = useState(false);
const utteranceRef = useRef<SpeechSynthesisUtterance | null>(null);
const baiduAudioRef = useRef<HTMLAudioElement | null>(null);
const baiduAudioUrlRef = useRef<string | null>(null);
const currentTextRef = useRef<string | null>(null);
const cleanupBaiduAudio = useCallback(() => {
if (baiduAudioRef.current) {
baiduAudioRef.current.pause();
baiduAudioRef.current = null;
}
if (baiduAudioUrlRef.current) {
URL.revokeObjectURL(baiduAudioUrlRef.current);
baiduAudioUrlRef.current = null;
}
setIsPlaying(false);
}, []);
const stop = useCallback(() => {
if (isNativeSupported) {
window.speechSynthesis.cancel();
utteranceRef.current = null;
} else {
cleanupBaiduAudio();
}
}, [isNativeSupported, cleanupBaiduAudio]);
const playBaiduTTS = useCallback(
async (text: string) => {
if (!baiduAccessToken) {
message.error('未提供TTS Access Token');
return;
}
try {
const params = new URLSearchParams({
tex: text,
tok: baiduAccessToken,
cuid: `user_${Date.now()}`,
ctp: '1',
lan: 'zh',
spd: '5',
pit: '5',
vol: '5',
per: '0',
aue: '3',
});
const response = await fetch(`https://tsn.baidu.com/text2audio?${params.toString()}`);
if (!response.ok) throw new Error('TTS请求失败');
const contentType = response.headers.get('content-type') || '';
if (contentType.includes('application/json')) {
const err = await response.json();
throw new Error(err.err_msg || '语音合成失败');
}
const blob = await response.blob();
const audioUrl = URL.createObjectURL(blob);
if (baiduAudioUrlRef.current) URL.revokeObjectURL(baiduAudioUrlRef.current);
baiduAudioUrlRef.current = audioUrl;
const audio = new Audio(audioUrl);
baiduAudioRef.current = audio;
audio.onplay = () => setIsPlaying(true);
audio.onpause = () => setIsPlaying(false);
audio.onended = () => {
setIsPlaying(false);
currentTextRef.current = null;
cleanupBaiduAudio();
};
audio.onerror = () => {
message.error('音频播放失败');
setIsPlaying(false);
currentTextRef.current = null;
cleanupBaiduAudio();
};
await audio.play();
} catch (error: any) {
console.error('TTS播放失败:', error);
message.error('语音播放失败,请稍后重试');
setIsPlaying(false);
currentTextRef.current = null;
}
},
[baiduAccessToken, cleanupBaiduAudio]
);
const play = useCallback(
async (text: string) => {
if (!text.trim()) return;
stop();
currentTextRef.current = text;
if (isNativeSupported) {
const utterance = new SpeechSynthesisUtterance(text);
utterance.lang = 'zh-CN';
utterance.volume = 1;
utterance.rate = 1;
utterance.pitch = 1;
utterance.onend = () => {
setIsPlaying(false);
currentTextRef.current = null;
utteranceRef.current = null;
};
utterance.onerror = () => {
message.error('语音播放失败');
setIsPlaying(false);
currentTextRef.current = null;
utteranceRef.current = null;
};
utteranceRef.current = utterance;
window.speechSynthesis.speak(utterance);
setIsPlaying(true);
} else {
await playBaiduTTS(text);
}
},
[isNativeSupported, playBaiduTTS, stop]
);
const pause = useCallback(() => {
if (isNativeSupported && window.speechSynthesis.speaking) {
window.speechSynthesis.pause();
setIsPlaying(false);
} else if (baiduAudioRef.current && !baiduAudioRef.current.paused) {
baiduAudioRef.current.pause();
}
}, [isNativeSupported]);
const resume = useCallback(() => {
if (isNativeSupported && utteranceRef.current) {
window.speechSynthesis.resume();
setIsPlaying(true);
} else if (baiduAudioRef.current && baiduAudioRef.current.paused) {
baiduAudioRef.current.play();
}
}, [isNativeSupported]);
return { isNativeSupported, isPlaying, play, pause, resume, stop };
};
调用方式:
js
import React from 'react';
import { Button } from 'antd';
import { useTTS } from './useTTS';
export const TTSDemo: React.FC = () => {
const { play, pause, resume, stop, isPlaying, isNativeSupported } = useTTS({
baiduAccessToken: 'TTSAccessToken',
});
return (
<div>
<p>当前TTS方式: {isNativeSupported ? '浏览器原生' : '某度TTS'}</p>
<Button onClick={() => play('你好,欢迎使用语音合成')}>{isPlaying ? '重新播放' : '播放'}</Button>
<Button onClick={pause}>暂停</Button>
<Button onClick={resume}>继续</Button>
<Button onClick={stop}>停止</Button>
</div>
);
};
总结
通过本次 TTS 功能的兼容性改造,实现了原生能力优先、云服务兜底 的方案设计,不仅成功解决了国产 Android 浏览器中 speechSynthesis 不支持的问题,也大幅提升了整体用户体验。方案本身结构清晰、扩展性强,可在未来继续复用。
当前方案具备以下优势:
- 完整覆盖国产 Android 浏览器兼容性
- 在支持原生能力的设备上优先使用,保证体验最优
- 在不支持原生能力时自动降级到云端 TTS
- 模块化封装,易维护、易迁移
更重要的是,当前实现已预留扩展空间:
一旦公司上线自研 TTS 服务,只需修改请求逻辑,即可无缝切换,无需改动上层业务逻辑。
这一方案有效平衡了性能与兼容性,为跨平台语音播报能力打下坚实基础。