更新记录

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 服务输出 pcms16lelinear16raw 等无容器音频流。
  • AI 对话、语音播报、面试官语音、客服语音等需要低延迟播放的 App 页面。
  • 不希望先把完整音频写成 mp3/wav 文件,再交给 uni.createInnerAudioContext() 播放。

不适用场景

  • 输入已经是 mp3aacm4awavogg 等封装格式音频。
  • 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
采样率 8000Hz192000Hz
声道数 12
单声道排列 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 采样率,支持 8000192000
channels number 1 声道数,支持 12
bufferMs number 320 播放缓冲,支持 1201000

当传入参数超出范围时,插件会自动归一化:

  • sampleRate 不在 8000192000 时使用 24000
  • channels 不是 2 时使用 1
  • bufferMs 小于 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)

仅支持 12。传入其他值时按单声道处理。

推荐生命周期

首次播放

收到第一段音频 -> 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 流式播放建议使用 200ms500ms 左右的音频分片。
  • 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 使用 AVAudioEngineAVAudioPlayerNode 播放。

隐私、权限声明

1. 本插件需要申请的系统权限列表:

无。本插件仅播放调用方传入的 Base64 PCM 音频数据,不申请麦克风、存储、定位、通讯录等系统权限。

2. 本插件采集的数据、发送的服务器地址、以及数据用途说明:

插件不包含广告,不采集、存储或上传用户数据。音频 Base64 数据仅在本地内存中解码,并交给 Android AudioTrack 或 iOS AVAudioEngine 播放。

3. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率:

暂无用户评论。