wx小程序-实时语音数据流开发记录
最近有个需求,需要使用 SSE 请求返回的多次分段语音数据流进行实时播放,并需要优化间隔,这可难住了我,经过多次辗转阅读资料后还是简单实现了一版。
首先我想到的是使用单个语音实例去实现
function play(data) {
const instance = wx.createInnerAudioContext()
instance.obeyMuteSwitch = false
instance.autoplay = true
const path = `${wx.env.USER_DATA_PATH}/audio_${Date.now()}.mp3`
const fs = wx.getFileSystemManager()
fs.writeFileSync(path, data, 'binary')
instance.src = path
instance.play()
}
但是这种方式在实际体验中每段语音中间会有中断的间隔,体验不是很好,需要优化。
后来在元宝和朋友提供的思路下,想到创建多个语音上下文实例,交替播放的方案,但是这样还是会有间隔,因为在上一个语音播放的停止事件中,轮换实例去播放,上一个已经播放完成了,才去播放下一个,岂不是还是无法连贯输出语音。
后来去官网文档里去查看,发现一个 onTimeUpdate 的方法,用来监听语音播放进度,而在这个事件里,可以拿到当前的播放到的时间 currentTime 和总时间 duration
这不是正好吗,我监听进度更新到100之前,就播放下一个,只有控制好时间,应该可以做到完美衔接
但是,要有这么简单就好了,实际测试中,这个事件大概 200ms 左右才触发一次,如果分段语音短的情况下,很有可能上一次进度还在 60%,下一次之间完成播放了,所以这个方案也 pass 掉了
之后,我想为何不自己去监听这个进度呢,在语音开始播放的时候,即 onPlay 事件里面,用 setInterval 去轮询查询,总的时间减去当前的时间,当每次剩0.05s的时候去播放下一个,同时不停止上一个的播放,只要查询时间够短就可以监听到,话不多说,直接开干,这是我的监听方法
private _startProgressTracker(index: number) {
this._clearProgressTracker(index) // 先清理旧定时器
const instance = this.instances[index]
this.intervals[index] = setInterval(() => {
if (!instance.duration || instance.duration === Infinity) return
const percent = instance.duration - instance.currentTime
// console.log(`实例${index} 播放进度`, instance.duration, `${percent}s`)
if (percent < 0.045) {
this._clearProgressTracker(index)
this._handlePlayEnd()
}
}, AutoAudioPlayer.CHECK_INTERVAL) as unknown as number // 微信小程序的setInterval返回number
}
实践之后,在 PC 端小程序开发工具里可以做到很不错的效果了,然而,小程序往往会给你深痛一击,直接真机调试,简直就是群魔乱舞,PC上调试的表现跟真机的实际调试还是相差太大了,最后我还是放弃了这个方案,但是,代码逻辑是没有什么问题的,下面是完整代码
export default class AutoAudioPlayer {
private queue: string[] = []
private instances: WechatMiniprogram.InnerAudioContext[]
private currentIndex = 0
private isSwitching = false
private isPlaying = false
private intervals: [number | null, number | null] = [null, null] // 改为setInterval
private retryCount = 0
private static MAX_RETRY = 3
private static CHECK_INTERVAL = 10 // 检测间隔调整为100ms
constructor() {
this.instances = [this._createInstance(0), this._createInstance(1)]
}
private _createInstance(index: number): WechatMiniprogram.InnerAudioContext {
const instance = wx.createInnerAudioContext()
instance.obeyMuteSwitch = false
instance.autoplay = true
instance.onPlay(() => {
console.log(`实例${index} 开始播放`)
this._startProgressTracker(index)
this.isPlaying = true
})
instance.onEnded(() => {
console.log(`实例${index} 播放结束`)
})
instance.onError((err) => {
console.error(`实例${index} 播放错误:`, err)
// this._handleError(index)
})
return instance
}
private _startProgressTracker(index: number) {
this._clearProgressTracker(index) // 先清理旧定时器
const instance = this.instances[index]
this.intervals[index] = setInterval(() => {
if (!instance.duration || instance.duration === Infinity) return
// const percent = Math.floor((instance.currentTime / instance.duration) * 100)
const percent = instance.duration - instance.currentTime
// console.log(`实例${index} 播放进度`, instance.duration, `${percent}s`)
if (percent < 0.045) {
this._clearProgressTracker(index)
this._handlePlayEnd()
}
}, AutoAudioPlayer.CHECK_INTERVAL) as unknown as number // 微信小程序的setInterval返回number
}
private _clearProgressTracker(index: number) {
if (this.intervals[index] !== null) {
clearInterval(this.intervals[index]!)
this.intervals[index] = null
}
}
private async _handlePlayEnd() {
if (this.isSwitching) return
this.isSwitching = true
try {
const nextAudio = this.queue.shift()
if (!nextAudio) {
this.isPlaying = false
return
}
const nextIndex = 1 - this.currentIndex
await this._switchInstance(nextIndex, nextAudio)
this.currentIndex = nextIndex
} finally {
this.isSwitching = false
}
}
private _switchInstance(index: number, path: string): Promise<void> {
return new Promise((resolve) => {
const instance = this.instances[index]
instance.stop()
instance.src = path
instance.onCanplay(() => {
instance.play()
resolve()
})
})
}
private _handleError(index: number) {
if (this.retryCount < AutoAudioPlayer.MAX_RETRY) {
this.retryCount++
this._handlePlayEnd()
} else {
console.error('播放失败,已达到最大重试次数')
this.destroy()
}
}
public play(data: ArrayBuffer) {
try {
const path = this._cacheAudio(data)
this.queue.push(path)
if (!this.isPlaying) {
this._handlePlayEnd()
}
} catch (err) {
console.error('音频缓存失败:', err)
}
}
private _cacheAudio(data: ArrayBuffer): string {
const path = `${wx.env.USER_DATA_PATH}/audio_${Date.now()}.mp3`
const fs = wx.getFileSystemManager()
try {
fs.writeFileSync(path, data, 'binary')
return path
} catch (err) {
throw new Error(`文件写入失败: ${err}`)
}
}
public destroy() {
console.log('释放音频资源')
// 清理所有定时器
this.intervals.forEach((_, index) => this._clearProgressTracker(index))
this.instances.forEach((instance) => {
instance.stop()
instance.destroy()
})
this.queue = []
this.isPlaying = false
}
}
wx小程序-实时语音数据流开发记录
http://blog.cirzear.cn/archives/wxxiao-cheng-xu-shi-shi-yu-yin-shu-ju-liu-kai-fa-ji-lu