| import Toast from '@/app/components/base/toast' |
| import { textToAudioStream } from '@/service/share' |
|
|
| declare global { |
| |
| interface Window { |
| ManagedMediaSource: any |
| } |
| } |
|
|
| export default class AudioPlayer { |
| mediaSource: MediaSource | null |
| audio: HTMLAudioElement |
| audioContext: AudioContext |
| sourceBuffer?: any |
| cacheBuffers: ArrayBuffer[] = [] |
| pauseTimer: number | null = null |
| msgId: string | undefined |
| msgContent: string | null | undefined = null |
| voice: string | undefined = undefined |
| isLoadData = false |
| url: string |
| isPublic: boolean |
| callback: ((event: string) => {}) | null |
|
|
| constructor(streamUrl: string, isPublic: boolean, msgId: string | undefined, msgContent: string | null | undefined, voice: string | undefined, callback: ((event: string) => {}) | null) { |
| this.audioContext = new AudioContext() |
| this.msgId = msgId |
| this.msgContent = msgContent |
| this.url = streamUrl |
| this.isPublic = isPublic |
| this.voice = voice |
| this.callback = callback |
|
|
| |
| const MediaSource = window.ManagedMediaSource || window.MediaSource |
| if (!MediaSource) { |
| Toast.notify({ |
| message: 'Your browser does not support audio streaming, if you are using an iPhone, please update to iOS 17.1 or later.', |
| type: 'error', |
| }) |
| } |
| this.mediaSource = MediaSource ? new MediaSource() : null |
| this.audio = new Audio() |
| this.setCallback(callback) |
| if (!window.MediaSource) { |
| this.audio.disableRemotePlayback = true |
| this.audio.controls = true |
| } |
| this.audio.src = this.mediaSource ? URL.createObjectURL(this.mediaSource) : '' |
| this.audio.autoplay = true |
|
|
| const source = this.audioContext.createMediaElementSource(this.audio) |
| source.connect(this.audioContext.destination) |
| this.listenMediaSource('audio/mpeg') |
| } |
|
|
| public resetMsgId(msgId: string) { |
| this.msgId = msgId |
| } |
|
|
| private listenMediaSource(contentType: string) { |
| this.mediaSource?.addEventListener('sourceopen', () => { |
| if (this.sourceBuffer) |
| return |
|
|
| this.sourceBuffer = this.mediaSource?.addSourceBuffer(contentType) |
| }) |
| } |
|
|
| public setCallback(callback: ((event: string) => {}) | null) { |
| this.callback = callback |
| if (callback) { |
| this.audio.addEventListener('ended', () => { |
| callback('ended') |
| }, false) |
| this.audio.addEventListener('paused', () => { |
| callback('paused') |
| }, true) |
| this.audio.addEventListener('loaded', () => { |
| callback('loaded') |
| }, true) |
| this.audio.addEventListener('play', () => { |
| callback('play') |
| }, true) |
| this.audio.addEventListener('timeupdate', () => { |
| callback('timeupdate') |
| }, true) |
| this.audio.addEventListener('loadeddate', () => { |
| callback('loadeddate') |
| }, true) |
| this.audio.addEventListener('canplay', () => { |
| callback('canplay') |
| }, true) |
| this.audio.addEventListener('error', () => { |
| callback('error') |
| }, true) |
| } |
| } |
|
|
| private async loadAudio() { |
| try { |
| const audioResponse: any = await textToAudioStream(this.url, this.isPublic, { content_type: 'audio/mpeg' }, { |
| message_id: this.msgId, |
| streaming: true, |
| voice: this.voice, |
| text: this.msgContent, |
| }) |
|
|
| if (audioResponse.status !== 200) { |
| this.isLoadData = false |
| if (this.callback) |
| this.callback('error') |
| } |
|
|
| const reader = audioResponse.body.getReader() |
| while (true) { |
| const { value, done } = await reader.read() |
|
|
| if (done) { |
| this.receiveAudioData(value) |
| break |
| } |
|
|
| this.receiveAudioData(value) |
| } |
| } |
| catch (error) { |
| this.isLoadData = false |
| this.callback && this.callback('error') |
| } |
| } |
|
|
| |
| public playAudio() { |
| if (this.isLoadData) { |
| if (this.audioContext.state === 'suspended') { |
| this.audioContext.resume().then((_) => { |
| this.audio.play() |
| this.callback && this.callback('play') |
| }) |
| } |
| else if (this.audio.ended) { |
| this.audio.play() |
| this.callback && this.callback('play') |
| } |
| if (this.callback) |
| this.callback('play') |
| } |
| else { |
| this.isLoadData = true |
| this.loadAudio() |
| } |
| } |
|
|
| private theEndOfStream() { |
| const endTimer = setInterval(() => { |
| if (!this.sourceBuffer?.updating) { |
| this.mediaSource?.endOfStream() |
| clearInterval(endTimer) |
| } |
| }, 10) |
| } |
|
|
| private finishStream() { |
| const timer = setInterval(() => { |
| if (!this.cacheBuffers.length) { |
| this.theEndOfStream() |
| clearInterval(timer) |
| } |
|
|
| if (this.cacheBuffers.length && !this.sourceBuffer?.updating) { |
| const arrayBuffer = this.cacheBuffers.shift()! |
| this.sourceBuffer?.appendBuffer(arrayBuffer) |
| } |
| }, 10) |
| } |
|
|
| public async playAudioWithAudio(audio: string, play = true) { |
| if (!audio || !audio.length) { |
| this.finishStream() |
| return |
| } |
|
|
| const audioContent = Buffer.from(audio, 'base64') |
| this.receiveAudioData(new Uint8Array(audioContent)) |
| if (play) { |
| this.isLoadData = true |
| if (this.audio.paused) { |
| this.audioContext.resume().then((_) => { |
| this.audio.play() |
| this.callback && this.callback('play') |
| }) |
| } |
| else if (this.audio.ended) { |
| this.audio.play() |
| this.callback && this.callback('play') |
| } |
| else if (this.audio.played) { } |
|
|
| else { |
| this.audio.play() |
| this.callback && this.callback('play') |
| } |
| } |
| } |
|
|
| public pauseAudio() { |
| this.callback && this.callback('paused') |
| this.audio.pause() |
| this.audioContext.suspend() |
| } |
|
|
| private cancer() { |
|
|
| } |
|
|
| private receiveAudioData(unit8Array: Uint8Array) { |
| if (!unit8Array) { |
| this.finishStream() |
| return |
| } |
| const audioData = this.byteArrayToArrayBuffer(unit8Array) |
| if (!audioData.byteLength) { |
| if (this.mediaSource?.readyState === 'open') |
| this.finishStream() |
| return |
| } |
|
|
| if (this.sourceBuffer?.updating) { |
| this.cacheBuffers.push(audioData) |
| } |
| else { |
| if (this.cacheBuffers.length && !this.sourceBuffer?.updating) { |
| this.cacheBuffers.push(audioData) |
| const cacheBuffer = this.cacheBuffers.shift()! |
| this.sourceBuffer?.appendBuffer(cacheBuffer) |
| } |
| else { |
| this.sourceBuffer?.appendBuffer(audioData) |
| } |
| } |
| } |
|
|
| private byteArrayToArrayBuffer(byteArray: Uint8Array): ArrayBuffer { |
| const arrayBuffer = new ArrayBuffer(byteArray.length) |
| const uint8Array = new Uint8Array(arrayBuffer) |
| uint8Array.set(byteArray) |
| return arrayBuffer |
| } |
| } |
|
|