JS使用AudioContext实现音频流实时播放
使用场景
在项目中开发中,遇到这样的需求:有一段文字,需要通过后台接口转成语音传到前端进行播放。 因为文字是实时生成的,为保证实时性,需要在生成文字的过程中,转为一段一段的音频流通过websocket传递到前端,前端拿到音频流后立即开始播放,接收到后续的音频流后追加到播放音频里继续播放,达到实时生成文字,实时转换音频流,前端实时播放的效果。
解决方案
刚接到这个需求时,想到的解决方案是这样的
- 前端接收到音频数据后放入缓存数组
- 检测缓存数组中是否存在音频数据
- 存在音频数据则将音频数据转为Audio的src播放出来
- Audio播放完毕后转到第2步继续检测
实现后发现在音频传递过程中,每段音频和文字的断句并不一样,两段音频断在一个字的音中间,但是Audio的音频解析到播放需要消耗时间,导致播放时会有卡顿的感觉。 后来了解到Web Audio API
中的AudioContext
接口可以处理音频流数据并播放,就有了下面的方案。
- 创建
AudioContext
/MediaSource
接口实例 MediaSource
实例打开后创建sourceBuffer
,并监听update
事件- 接收到音频流数据后查看
sourceBuffer
是否空闲 - 如果
sourceBuffer
处于空闲状态,则将音频流追加到sourceBuffer
内并开始播放 - 如果
sourceBuffer
处于工作状态,则将音频流放入缓存数组待用 sourceBuffer
监听到update
事件后表示sourceBuffer
空闲,则检测缓存数据是否有音频数据,如有则执行第4步
音频实时播放类
// 音频实时播放 class AudioPlayer { mediaSource: MediaSource // 媒体资源 audio: HTMLAudioElement // 音频元素 audioContext: AudioContext // 音频上下文 sourceBuffer?: SourceBuffer // 音频数据缓冲区 cacheBuffers: ArrayBuffer[] = [] // 音频数据列表 pauseTimer: number | null = null // 暂停定时器 constructor() { const AudioContext = window.AudioContext this.audioContext = new AudioContext() this.mediaSource = new MediaSource() this.audio = new Audio() this.audio.src = URL.createObjectURL(this.mediaSource) this.audioContextConnect() this.listenMedisSource() } // 连接音频上下文 private audioContextConnect() { const source = this.audioContext.createMediaElementSource(this.audio) source.connect(this.audioContext.destination) } // 监听媒体资源 private listenMedisSource() { this.mediaSource.addEventListener('sourceopen', () => { if (this.sourceBuffer) return this.sourceBuffer = this.mediaSource.addSourceBuffer('audio/mpeg') this.sourceBuffer.addEventListener('update', () => { if (this.cacheBuffers.length && !this.sourceBuffer?.updating) { const cacheBuffer = this.cacheBuffers.shift()! this.sourceBuffer?.appendBuffer(cacheBuffer) } this.pauseAudio() }) }) } // 暂停音频 private pauseAudio() { const neePlayTime = this.sourceBuffer!.timestampOffset - this.audio.currentTime || 0 this.pauseTimer && clearTimeout(this.pauseTimer) // 播放完成5秒后还没有新的音频流过来,则暂停音频播放 this.pauseTimer = setTimeout(() => this.audio.pause(), neePlayTime * 1000 + 5000) } private playAudio() { // 为防止下一段音频流传输过来时,上一段音频已经播放完毕,造成音频卡顿现象, // 这里做了1秒的延时,可根据实际情况修正 setTimeout(() => { if (this.audio.paused) { try { this.audio.play() } catch (e) { this.playAudio() } } }, 1000) } // 接收音频数据 public receiveAudioData(audioData: ArrayBuffer) { if (!audioData.byteLength) return if (this.sourceBuffer?.updating) { this.cacheBuffers.push(audioData) } else { this.sourceBuffer?.appendBuffer(audioData) } this.playAudio() } } export default AudioPlayer
WebSocket 封装
如果websocket需要支持心跳、重连等机制可以查看WebSocket 心跳检测,断开重连,消息订阅 js/ts
const BASE_URL = import.meta.env.VITE_WS_BASE_URL type ObserverType<T> = { type: string callback: (data: T) => void } class SocketConnect<T> { private url: string public ws: WebSocket | undefined //websocket实例 private observers: ObserverType<T>[] = [] //消息订阅者列表 private waitingMessages: string[] = [] //待执行命令列表 private openCb?: () => void constructor(url = '', openCb?: () => void) { this.url = BASE_URL + url if (openCb) this.openCb = openCb this.connect() } //websocket连接 connect() { this.ws = new WebSocket(this.url) this.ws.onopen = () => { this.openCb && this.openCb() // 发送所有等待发送的信息 const length = this.waitingMessages.length for (let i = 0; i < length; ++i) { const message = this.waitingMessages.shift() this.send(message) } } this.ws.onclose = (event) => { console.log('webSocket closed:', event) } this.ws.onerror = (error) => { console.log('webSocket error:', error) } this.ws.onmessage = (event: MessageEvent) => { this.observers.forEach((observer) => { observer.callback(event.data) }) } } //发送信息 send(message?: string) { if (message) { //发送信息时若websocket还未连接,则将信息放入待发送信息中等待连接成功后发送 if (this.onReady() !== WebSocket.OPEN) { this.waitingMessages.push(message) return this } this.ws && this.ws.send(message) } return this } //订阅webSocket信息 observe(callback: (data: T) => void, type = 'all') { const observer = { type, callback } this.observers.push(observer) return observer } //取消订阅信息 cancelObserve(cancelObserver: ObserverType<T>) { this.observers.forEach((observer, index) => { if (cancelObserver === observer) { this.observers.splice(index, 1) } }) } // 关闭websocket close() { this.ws && this.ws.close() } // websocket连接状态 onReady() { return this.ws && this.ws.readyState } } export default SocketConnect
工具函数
// 从十六进制字符串转换为字节数组 export function hexStringToByteArray(hexString: string): Uint8Array { const byteArray: number[] = [] for (let i = 0; i < hexString.length; i += 2) { byteArray.push(parseInt(hexString.substring(i, i + 2), 16)) } return new Uint8Array(byteArray) } // 从字节数组转换为 ArrayBuffer export function byteArrayToArrayBuffer(byteArray: Uint8Array): ArrayBuffer { const arrayBuffer = new ArrayBuffer(byteArray.length) const uint8Array = new Uint8Array(arrayBuffer) uint8Array.set(byteArray) return arrayBuffer } // 从十六进制字符串转换为 ArrayBuffer export function hexStringToArrayBuffer(hexString: string): ArrayBuffer { return byteArrayToArrayBuffer(hexStringToByteArray(hexString)) }
函数调用
const ws = new SocketConnect<string>('/audio') const audioPlayer = new AudioPlayer() ws.observe((data) => { console.log('receivebytes:'+new Date().getTime()) // 接收到的16进制字符串数据转换为ArrayBuffer传递给audioPlay const arrayBuffer = hexStringToArrayBuffer(data) audioPlayer.receiveAudioData(arrayBuffer) })
以上就是JS使用AudioContext实现音频流实时播放的详细内容,更多关于JS AudioContext音频流播放的资料请关注脚本之家其它相关文章!
相关文章
深入理解JavaScript中为什么string可以拥有方法
下面小编就为大家带来一篇深入理解JavaScript中为什么string可以拥有方法。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧2016-05-05Highslide.js是一款基于js实现的网页中图片展示插件
这篇文章主要介绍了Highslide.js是一款基于js实现的网页中图片预览查看工具,需要的朋友可以参考下2007-05-05
最新评论