JS使用AudioContext實(shí)現(xiàn)音頻流實(shí)時播放
使用場景
在項(xiàng)目中開發(fā)中,遇到這樣的需求:有一段文字,需要通過后臺接口轉(zhuǎn)成語音傳到前端進(jìn)行播放。 因?yàn)槲淖质菍?shí)時生成的,為保證實(shí)時性,需要在生成文字的過程中,轉(zhuǎn)為一段一段的音頻流通過websocket傳遞到前端,前端拿到音頻流后立即開始播放,接收到后續(xù)的音頻流后追加到播放音頻里繼續(xù)播放,達(dá)到實(shí)時生成文字,實(shí)時轉(zhuǎn)換音頻流,前端實(shí)時播放的效果。
解決方案
剛接到這個需求時,想到的解決方案是這樣的
- 前端接收到音頻數(shù)據(jù)后放入緩存數(shù)組
- 檢測緩存數(shù)組中是否存在音頻數(shù)據(jù)
- 存在音頻數(shù)據(jù)則將音頻數(shù)據(jù)轉(zhuǎn)為Audio的src播放出來
- Audio播放完畢后轉(zhuǎn)到第2步繼續(xù)檢測
實(shí)現(xiàn)后發(fā)現(xiàn)在音頻傳遞過程中,每段音頻和文字的斷句并不一樣,兩段音頻斷在一個字的音中間,但是Audio的音頻解析到播放需要消耗時間,導(dǎo)致播放時會有卡頓的感覺。 后來了解到Web Audio API中的AudioContext接口可以處理音頻流數(shù)據(jù)并播放,就有了下面的方案。
- 創(chuàng)建
AudioContext/MediaSource接口實(shí)例 MediaSource實(shí)例打開后創(chuàng)建sourceBuffer,并監(jiān)聽update事件- 接收到音頻流數(shù)據(jù)后查看
sourceBuffer是否空閑 - 如果
sourceBuffer處于空閑狀態(tài),則將音頻流追加到sourceBuffer內(nèi)并開始播放 - 如果
sourceBuffer處于工作狀態(tài),則將音頻流放入緩存數(shù)組待用 sourceBuffer監(jiān)聽到update事件后表示sourceBuffer空閑,則檢測緩存數(shù)據(jù)是否有音頻數(shù)據(jù),如有則執(zhí)行第4步
音頻實(shí)時播放類
// 音頻實(shí)時播放
class AudioPlayer {
mediaSource: MediaSource // 媒體資源
audio: HTMLAudioElement // 音頻元素
audioContext: AudioContext // 音頻上下文
sourceBuffer?: SourceBuffer // 音頻數(shù)據(jù)緩沖區(qū)
cacheBuffers: ArrayBuffer[] = [] // 音頻數(shù)據(jù)列表
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)
}
// 監(jiān)聽媒體資源
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() {
// 為防止下一段音頻流傳輸過來時,上一段音頻已經(jīng)播放完畢,造成音頻卡頓現(xiàn)象,
// 這里做了1秒的延時,可根據(jù)實(shí)際情況修正
setTimeout(() => {
if (this.audio.paused) {
try {
this.audio.play()
} catch (e) {
this.playAudio()
}
}
}, 1000)
}
// 接收音頻數(shù)據(jù)
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需要支持心跳、重連等機(jī)制可以查看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實(shí)例
private observers: ObserverType<T>[] = [] //消息訂閱者列表
private waitingMessages: string[] = [] //待執(zhí)行命令列表
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()
// 發(fā)送所有等待發(fā)送的信息
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)
})
}
}
//發(fā)送信息
send(message?: string) {
if (message) {
//發(fā)送信息時若websocket還未連接,則將信息放入待發(fā)送信息中等待連接成功后發(fā)送
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)
}
})
}
// 關(guān)閉websocket
close() {
this.ws && this.ws.close()
}
// websocket連接狀態(tài)
onReady() {
return this.ws && this.ws.readyState
}
}
export default SocketConnect
工具函數(shù)
// 從十六進(jìn)制字符串轉(zhuǎn)換為字節(jié)數(shù)組
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)
}
// 從字節(jié)數(shù)組轉(zhuǎn)換為 ArrayBuffer
export function byteArrayToArrayBuffer(byteArray: Uint8Array): ArrayBuffer {
const arrayBuffer = new ArrayBuffer(byteArray.length)
const uint8Array = new Uint8Array(arrayBuffer)
uint8Array.set(byteArray)
return arrayBuffer
}
// 從十六進(jìn)制字符串轉(zhuǎn)換為 ArrayBuffer
export function hexStringToArrayBuffer(hexString: string): ArrayBuffer {
return byteArrayToArrayBuffer(hexStringToByteArray(hexString))
}
函數(shù)調(diào)用
const ws = new SocketConnect<string>('/audio')
const audioPlayer = new AudioPlayer()
ws.observe((data) => {
console.log('receivebytes:'+new Date().getTime())
// 接收到的16進(jìn)制字符串?dāng)?shù)據(jù)轉(zhuǎn)換為ArrayBuffer傳遞給audioPlay
const arrayBuffer = hexStringToArrayBuffer(data)
audioPlayer.receiveAudioData(arrayBuffer)
})
以上就是JS使用AudioContext實(shí)現(xiàn)音頻流實(shí)時播放的詳細(xì)內(nèi)容,更多關(guān)于JS AudioContext音頻流播放的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
淺談Sticky組件的改進(jìn)實(shí)現(xiàn)
這篇文章主要介紹了Sticky組件的改進(jìn)實(shí)現(xiàn)的相關(guān)資料,需要的朋友可以參考下2016-03-03
前端JavaScript解決防盜鏈(Referer Check)圖片加載問題的常用方法
防盜鏈(Referer Check)是服務(wù)器通過檢查請求頭中的 Referer 字段,來判斷請求是否來自合法的來源,本文介紹了解決防盜鏈圖片加載問題的幾種常用方法,有需要的可以了解下2026-01-01
ElementUI的Dialog彈窗實(shí)現(xiàn)拖拽移動功能示例代碼
這篇文章主要介紹了ElementUI的Dialog彈窗實(shí)現(xiàn)拖拽移動功能,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-07-07
JS解決IE使用JSON.stringify報JSON未定義錯誤的問題
在IE6,IE7,IE8這些老版本的瀏覽器中,原生并不支持JSON對象,這導(dǎo)致了在使用??JSON.stringify??時會遇到JSON未定義的錯誤,下面我們就來看看如何解決這一錯誤吧2025-06-06
利用Javascript實(shí)現(xiàn)BMI計算器
BMI指數(shù)計算器相信大家都用過,那用JavaScript怎么實(shí)現(xiàn)呢?其實(shí)很簡單,這篇文章給出了實(shí)例代碼,有需要的可以參考學(xué)習(xí)。2016-08-08

