更新记录
1.0(2026-06-02)
- 新增 Android/iOS App 端 Base64 PCM 16-bit little-endian 流式播放能力。
- 支持采样率、声道数、播放缓冲初始化与动态重建。
- 支持增量写入、结束标记、停止播放和资源释放。
- Android 使用
AudioTrack流式播放,iOS 使用AVAudioEngine播放。
平台兼容性
uni-app
| Vue2 | Vue3 | Chrome | Safari | app-vue | app-nvue | Android | iOS | 鸿蒙 |
|---|---|---|---|---|---|---|---|---|
| - | - | × | × | - | - | - | - | × |
| 微信小程序 | 支付宝小程序 | 抖音小程序 | 百度小程序 | 快手小程序 | 京东小程序 | 鸿蒙元服务 | QQ小程序 | 飞书小程序 | 小红书小程序 | 快应用-华为 | 快应用-联盟 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| × | × | × | × | × | × | × | × | × | × | × | × |
Finsight PCM Stream Player 使用说明
Finsight PCM Stream Player 是一个 uni-app App 端 UTS API 插件,用于播放服务端实时返回的 Base64 PCM 音频流。插件底层分别封装 Android AudioTrack 和 iOS AVAudioEngine,适合 TTS 流式播报、AI 对话语音回复、面试问答语音播放、实时语音片段播放等场景。
插件信息
| 项目 | 内容 |
|---|---|
| 插件 ID | finsight-pcm-stream-player |
| 插件类型 | UTS 插件 / API 插件 |
| 当前版本 | 0.1.0 |
| 默认采样率 | 24000Hz |
| 默认声道数 | 1,单声道 |
| 默认缓冲 | 320ms |
| 输入格式 | Base64 PCM |
| PCM 编码 | signed 16-bit little-endian |
| 是否申请权限 | 否 |
| 是否采集数据 | 否 |
适用场景
- 服务端按片段持续返回 PCM Base64 数据,需要边接收边播放。
- TTS 服务输出
pcm、s16le、linear16、raw等无容器音频流。 - AI 对话、语音播报、面试官语音、客服语音等需要低延迟播放的 App 页面。
- 不希望先把完整音频写成 mp3/wav 文件,再交给
uni.createInnerAudioContext()播放。
不适用场景
- 输入已经是
mp3、aac、m4a、wav、ogg等封装格式音频。 - H5、小程序、快应用等非 App 端运行环境。
- 需要录音、变声、混音、音频可视化、音频文件转码等能力。
- 需要播放 float PCM、8-bit PCM、24-bit PCM 或 big-endian PCM 的场景。
平台兼容性
| 平台 | 支持情况 | 说明 |
|---|---|---|
| App-Android | 支持 | 最低 Android API 21 |
| App-iOS | 支持 | 最低 iOS 11 |
| app-vue | 支持 | 可在 Vue 页面中使用 |
| app-nvue | 支持 | 可在 nvue 页面中使用 |
| app-uvue | 支持声明 | 建议发布前完成目标设备实机验证 |
| H5 | 不支持 | UTS 原生音频实现仅在 App 端生效 |
| 微信小程序 | 不支持 | 小程序不支持该原生 UTS 播放实现 |
| 支付宝等其他小程序 | 不支持 | 同上 |
音频数据要求
插件接收的是 Base64 字符串,Base64 解码后的二进制内容必须是 PCM 原始采样数据。
| 参数 | 要求 |
|---|---|
| 编码 | signed 16-bit little-endian |
| 采样率 | 8000Hz 到 192000Hz |
| 声道数 | 1 或 2 |
| 单声道排列 | sample1, sample2, sample3... |
| 双声道排列 | 按帧交错:L1, R1, L2, R2... |
| Base64 形式 | 支持纯 Base64,也支持 data:*;base64, 前缀 |
| URL-safe Base64 | 支持,插件会将 -、_ 转为标准 Base64 字符 |
示例服务端数据结构可以是:
{
"type": "tts_audio",
"format": "pcm",
"sampleRate": 24000,
"channels": 1,
"bufferMs": 320,
"audio": "Base64PCMChunk..."
}
也可以使用下划线字段:
{
"type": "tts_audio",
"audio_format": "s16le",
"sample_rate": 24000,
"channels": 1,
"buffer_ms": 320,
"audio_data": "Base64PCMChunk..."
}
安装方式
将插件放入项目目录:
uni_modules/finsight-pcm-stream-player
目录结构应类似:
uni_modules/finsight-pcm-stream-player/
├─ package.json
└─ utssdk/
├─ interface.uts
├─ index.uts
├─ app-android/
│ ├─ config.json
│ ├─ index.uts
│ └─ FinsightPcmStreamPlayerNative.kt
└─ app-ios/
├─ config.json
├─ index.uts
└─ FinsightPcmStreamPlayerNative.swift
引入方式
由于插件只支持 App 端,建议始终使用条件编译包住导入和调用:
// #ifdef APP-PLUS
import {
dispose,
finishWrite,
initPlayer,
isPlaying,
play,
setChannels,
setSampleRate,
stop,
writeBase64,
} from '@/uni_modules/finsight-pcm-stream-player'
// #endif
不要在 H5、小程序等环境中直接引入该插件,否则编译或运行时可能出现平台不支持的问题。
最小播放示例
// #ifdef APP-PLUS
initPlayer({
sampleRate: 24000,
channels: 1,
bufferMs: 320,
})
play()
const didWrite = writeBase64(audioBase64Chunk)
if (!didWrite) {
stop()
}
finishWrite()
// #endif
调用顺序建议:
initPlayer() -> play() -> writeBase64() 多次 -> finishWrite() -> stop()/dispose()
流式 TTS 示例
下面示例适合服务端通过 WebSocket、SSE 或普通轮询持续返回音频片段的场景。
// #ifdef APP-PLUS
import {
dispose,
finishWrite,
initPlayer,
isPlaying,
play,
stop,
writeBase64,
} from '@/uni_modules/finsight-pcm-stream-player'
// #endif
const DEFAULT_SAMPLE_RATE = 24000
const DEFAULT_CHANNELS = 1
const DEFAULT_BUFFER_MS = 320
let currentOptions = null
export function playTtsAudioChunk(packet) {
// #ifdef APP-PLUS
if (!packet?.audio) return
const options = {
sampleRate: Number(packet.sampleRate || packet.sample_rate) || DEFAULT_SAMPLE_RATE,
channels: Number(packet.channels) || DEFAULT_CHANNELS,
bufferMs: Number(packet.bufferMs || packet.buffer_ms) || DEFAULT_BUFFER_MS,
}
const shouldReset =
!currentOptions ||
currentOptions.sampleRate !== options.sampleRate ||
currentOptions.channels !== options.channels ||
currentOptions.bufferMs !== options.bufferMs
if (shouldReset) {
stop()
initPlayer(options)
currentOptions = options
}
play()
const didWrite = writeBase64(packet.audio)
if (!didWrite) {
stop()
currentOptions = null
}
// #endif
}
export function finishTtsAudioStream() {
// #ifdef APP-PLUS
if (!currentOptions) return
finishWrite()
// #endif
}
export function stopTtsAudioStream() {
// #ifdef APP-PLUS
stop()
currentOptions = null
// #endif
}
export function disposeTtsAudioStream() {
// #ifdef APP-PLUS
dispose()
currentOptions = null
// #endif
}
Vue 页面示例
<script setup>
import { onUnmounted, ref } from 'vue'
// #ifdef APP-PLUS
import {
dispose,
finishWrite,
initPlayer,
isPlaying,
play,
stop,
writeBase64,
} from '@/uni_modules/finsight-pcm-stream-player'
// #endif
const playing = ref(false)
let options = null
let timer = null
function startPlayingMonitor() {
if (timer) return
timer = setInterval(() => {
// #ifdef APP-PLUS
playing.value = isPlaying()
if (!playing.value) {
stopPlayingMonitor()
}
// #endif
}, 200)
}
function stopPlayingMonitor() {
clearInterval(timer)
timer = null
}
function handleAudioChunk(packet) {
// #ifdef APP-PLUS
const nextOptions = {
sampleRate: Number(packet.sampleRate) || 24000,
channels: Number(packet.channels) || 1,
bufferMs: Number(packet.bufferMs) || 320,
}
if (
!options ||
options.sampleRate !== nextOptions.sampleRate ||
options.channels !== nextOptions.channels ||
options.bufferMs !== nextOptions.bufferMs
) {
stop()
initPlayer(nextOptions)
options = nextOptions
}
play()
playing.value = writeBase64(packet.audio)
startPlayingMonitor()
// #endif
}
function handleAudioFinished() {
// #ifdef APP-PLUS
finishWrite()
startPlayingMonitor()
// #endif
}
function closePlayer() {
stopPlayingMonitor()
// #ifdef APP-PLUS
dispose()
// #endif
options = null
playing.value = false
}
onUnmounted(closePlayer)
</script>
API 说明
initPlayer(options)
初始化播放器。
type PcmStreamPlayerOptions = {
sampleRate: number
channels: number
bufferMs: number
}
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
sampleRate |
number |
是 | 24000 |
采样率,支持 8000 到 192000 |
channels |
number |
是 | 1 |
声道数,支持 1 或 2 |
bufferMs |
number |
是 | 320 |
播放缓冲,支持 120 到 1000 |
当传入参数超出范围时,插件会自动归一化:
sampleRate不在8000到192000时使用24000channels不是2时使用1bufferMs小于120时按120处理,大于1000时按1000处理
play()
开始或恢复播放。如果底层播放器尚未创建,插件会按当前配置创建播放器。
play()
writeBase64(base64)
写入一段 Base64 PCM 数据。
const ok = writeBase64(base64)
| 返回值 | 说明 |
|---|---|
true |
数据写入成功或空数据被安全忽略 |
false |
Base64 解码失败,数据未写入 |
建议每次收到服务端音频分片后立即调用。分片不需要对齐完整句子,但解码后的 PCM 字节最好是完整采样帧的整数倍:
- 单声道 16-bit:每帧
2字节 - 双声道 16-bit:每帧
4字节
finishWrite()
标记当前音频流写入结束。
finishWrite()
调用后插件会继续播放已写入但还未播完的数据。它不会立即中断播放,适合服务端发送 “音频结束” 事件时调用。
stop()
停止播放并清空待播放队列。
stop()
适合用户打断、切换会话、重新开始播放等场景。调用后播放器仍可继续复用,后续可以重新 initPlayer() 或继续写入新音频。
dispose()
停止播放并释放底层音频资源。
dispose()
适合页面卸载、组件销毁、退出业务流程时调用。释放后再次播放需重新初始化。
isPlaying()
返回当前是否正在播放或仍有待播放数据。
const active = isPlaying()
可用于页面播放状态展示。流式场景中,建议结合 “最近一次收到音频分片的时间” 一起判断,因为服务端分片间隔较长时,播放器可能短暂进入无缓存状态。
setSampleRate(sampleRate)
修改采样率。
setSampleRate(24000)
修改后底层播放器会按新的采样率重新初始化。
setChannels(channels)
修改声道数。
setChannels(1)
仅支持 1 或 2。传入其他值时按单声道处理。
推荐生命周期
首次播放
收到第一段音频 -> initPlayer(options) -> play() -> writeBase64(chunk)
持续播放
收到后续音频 -> writeBase64(chunk)
正常结束
收到服务端结束事件 -> finishWrite() -> 等待播放完成
用户打断
用户点击停止/切换会话 -> stop()
页面卸载
onUnload/onUnmounted -> dispose()
与 InnerAudioContext 的区别
uni.createInnerAudioContext() 更适合播放完整的 mp3、m4a、aac、wav 文件或 URL。对于服务端持续返回的 PCM 原始流,通常需要先把数据拼成完整文件,等待时间更长。
本插件直接播放 PCM 分片,优势是:
- 首包到达后即可开始播放,延迟更低。
- 不需要生成临时音频文件。
- 不依赖音频容器封装。
- 更适合 AI TTS、实时语音播报等流式场景。
如果服务端返回的是 mp3、m4a、aac 等完整音频,建议继续使用 uni.createInnerAudioContext()。
WebSocket 对接示例
// #ifdef APP-PLUS
import {
dispose,
finishWrite,
initPlayer,
play,
stop,
writeBase64,
} from '@/uni_modules/finsight-pcm-stream-player'
// #endif
let currentOptions = null
function connectTtsSocket(url) {
const socketTask = uni.connectSocket({ url })
socketTask.onMessage((event) => {
const message = JSON.parse(event.data)
if (message.type === 'tts_audio') {
writeAudioMessage(message)
}
if (message.type === 'tts_done') {
// #ifdef APP-PLUS
finishWrite()
// #endif
}
})
socketTask.onClose(() => {
// #ifdef APP-PLUS
dispose()
currentOptions = null
// #endif
})
return socketTask
}
function writeAudioMessage(message) {
// #ifdef APP-PLUS
const options = {
sampleRate: Number(message.sampleRate || message.sample_rate) || 24000,
channels: Number(message.channels) || 1,
bufferMs: Number(message.bufferMs || message.buffer_ms) || 320,
}
if (
!currentOptions ||
currentOptions.sampleRate !== options.sampleRate ||
currentOptions.channels !== options.channels ||
currentOptions.bufferMs !== options.bufferMs
) {
stop()
initPlayer(options)
currentOptions = options
}
play()
writeBase64(message.audio || message.audio_data || message.data || '')
// #endif
}
常见问题
1. 为什么 H5 或小程序不能使用?
插件底层依赖 Android 和 iOS 原生音频 API,只在 App 端生效。H5 可使用 Web Audio API,小程序可使用平台提供的音频能力,但它们不是本插件的实现范围。
2. 为什么播放声音变快或变慢?
通常是 sampleRate 与服务端实际 PCM 采样率不一致。例如服务端输出 24000Hz,前端按 16000Hz 播放,就会导致速度和音调异常。请确认服务端返回的采样率,并传给 initPlayer()。
3. 为什么声音有噪音?
常见原因:
- PCM 编码不是 signed 16-bit little-endian。
- Base64 解码后的数据不是原始 PCM,而是 mp3/wav 等封装格式。
- 双声道数据没有按
L/R/L/R交错排列。 - 分片中混入了非音频文本或协议头。
4. 为什么 writeBase64() 返回 false?
表示传入字符串无法按 Base64 解码。请检查:
- 是否传入空字符串以外的非法内容。
- 是否包含 JSON、换行标记、日志文本等非 Base64 内容。
- 如果是 Data URI,格式是否类似
data:audio/pcm;base64,xxxx。
5. finishWrite() 和 stop() 有什么区别?
finishWrite() 表示服务端已经没有更多音频分片,但已写入的数据会继续播放完。
stop() 表示立即停止当前播放,并清空队列,适合用户主动打断。
6. 什么时候用 dispose()?
页面卸载、退出会话、组件销毁时使用。dispose() 会释放底层音频资源,比 stop() 更彻底。
7. 能不能连续播放多句话?
可以。建议每句话开始前确认采样率、声道、缓冲配置是否一致。一致时可持续写入;配置变化时先 stop(),再 initPlayer()。
8. 为什么 iOS 静音开关下仍可能播放?
插件使用 iOS AVAudioSession 的 playback 类别进行播放,具体表现还会受系统设置、设备状态和宿主 App 音频策略影响。
性能建议
- 服务端分片不要过小,过小会增加调用频率和调度成本。
- 服务端分片也不要过大,过大会增加首包播放等待时间。
- TTS 流式播放建议使用
200ms到500ms左右的音频分片。 bufferMs越小延迟越低,但网络抖动时更容易断续。bufferMs越大播放越稳,但首次听到声音的延迟可能更高。- 页面卸载时务必调用
dispose(),避免音频资源残留。
发布插件包注意事项
按 DCloud 插件市场上传要求,插件包建议只包含必要文件:
package.json
README.md
utssdk/
不要包含:
unpackage/
node_modules/
.git/
.svn/
manifest.json
pages.json
App.vue
main.js
压缩包应使用标准 zip 格式,不要把 rar 或其他格式改名为 zip。
如果在插件市场表单中填写插件 ID,应与 package.json 中的 id 保持一致:
{
"id": "finsight-pcm-stream-player"
}
权限与隐私说明
本插件不申请系统权限。
本插件不包含广告。
本插件不采集、存储或上传用户数据。
调用方传入的 Base64 PCM 音频数据仅在本机内存中解码,并交给系统音频播放组件播放:
- Android:
AudioTrack - iOS:
AVAudioEngine/AVAudioPlayerNode
插件不会主动访问麦克风、相册、通讯录、定位、蓝牙、存储文件等系统能力。
更新日志
0.1.0
- 新增 Android/iOS App 端 Base64 PCM 流式播放能力。
- 支持 signed 16-bit little-endian PCM 播放。
- 支持纯 Base64、Data URI Base64、URL-safe Base64。
- 支持单声道和双声道。
- 支持采样率、声道数、缓冲时长配置。
- 支持
writeBase64()增量写入音频分片。 - 支持
finishWrite()标记音频流结束。 - 支持
stop()停止播放并清空队列。 - 支持
dispose()释放底层音频资源。 - Android 使用
AudioTrack流式播放。 - iOS 使用
AVAudioEngine和AVAudioPlayerNode播放。

收藏人数:
购买普通授权版(
试用
赞赏(0)
下载 0
赞赏 0
下载 12131791
赞赏 1918
赞赏
京公网安备:11010802035340号