audioPlayerHowler.ts 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. // 使用 Howler.js 的音频播放器
  2. import { Howl } from 'howler'
  3. export interface AudioPlayerConfig {
  4. onPlay?: () => void
  5. onEnded?: () => void
  6. onError?: (error: Error) => void
  7. }
  8. export class HowlerAudioPlayer {
  9. private audioChunks: ArrayBuffer[] = []
  10. private playing = false
  11. private currentHowl: Howl | null = null
  12. private config: AudioPlayerConfig
  13. private audioReady = false
  14. constructor(config: AudioPlayerConfig = {}) {
  15. this.config = config
  16. }
  17. async init(): Promise<void> {
  18. console.log('使用 Howler.js 音频播放器')
  19. this.audioReady = true
  20. return Promise.resolve()
  21. }
  22. /**
  23. * 在用户交互时调用(激活音频)
  24. */
  25. prepareForPlayback() {
  26. if (this.audioReady) {
  27. console.log('🎯 Howler.js 已准备就绪')
  28. return
  29. }
  30. // Howler.js 不需要特殊激活,但可以预加载
  31. this.audioReady = true
  32. console.log('✅ Howler.js 音频播放器已激活')
  33. }
  34. appendAudioChunk(chunk: ArrayBuffer) {
  35. console.log(`📦 收集音频块: ${chunk.byteLength} bytes`)
  36. this.audioChunks.push(chunk)
  37. }
  38. async play() {
  39. if (this.audioChunks.length === 0 || this.playing) {
  40. console.log('⚠️ 没有音频数据或正在播放')
  41. return
  42. }
  43. console.log(`🎵 使用 Howler.js 播放音频,共 ${this.audioChunks.length} 块`)
  44. this.playing = true
  45. // 合并音频块
  46. const totalLength = this.audioChunks.reduce((sum, chunk) => sum + chunk.byteLength, 0)
  47. const combined = new Uint8Array(totalLength)
  48. let offset = 0
  49. for (const chunk of this.audioChunks) {
  50. combined.set(new Uint8Array(chunk), offset)
  51. offset += chunk.byteLength
  52. }
  53. // 检测格式
  54. let format = 'ogg' // 默认格式
  55. const firstByte = combined[0]
  56. if (firstByte === 0x4F && combined[1] === 0x67) {
  57. format = 'ogg'
  58. } else if (firstByte === 0x1A && combined[1] === 0x45) {
  59. format = 'webm'
  60. }
  61. console.log('检测到的音频格式:', format)
  62. // 创建 Blob URL
  63. const blob = new Blob([combined.buffer], {
  64. type: format === 'ogg' ? 'audio/ogg' : 'audio/webm'
  65. })
  66. const url = URL.createObjectURL(blob)
  67. // 清理旧的 Howl
  68. if (this.currentHowl) {
  69. this.currentHowl.unload()
  70. }
  71. // 创建新的 Howl 实例
  72. this.currentHowl = new Howl({
  73. src: [url],
  74. format: [format],
  75. html5: true, // 使用 HTML5 Audio(兼容性更好)
  76. volume: 1.0, // 最大音量
  77. onplay: () => {
  78. console.log('🔊 Howler.js 开始播放')
  79. this.config.onPlay?.()
  80. },
  81. onend: () => {
  82. console.log('✅ Howler.js 播放完成')
  83. URL.revokeObjectURL(url)
  84. this.playing = false
  85. this.audioChunks = [] // 清空缓存
  86. this.config.onEnded?.()
  87. },
  88. onloaderror: (id, error) => {
  89. console.error('❌ Howler.js 加载失败:', error)
  90. URL.revokeObjectURL(url)
  91. this.playing = false
  92. this.config.onError?.(new Error(`音频加载失败: ${error}`))
  93. },
  94. onplayerror: (id, error) => {
  95. console.error('❌ Howler.js 播放失败:', error)
  96. URL.revokeObjectURL(url)
  97. this.playing = false
  98. this.config.onError?.(new Error(`音频播放失败: ${error}`))
  99. },
  100. })
  101. // 开始播放
  102. this.currentHowl.play()
  103. }
  104. pause() {
  105. if (this.currentHowl) {
  106. this.currentHowl.pause()
  107. }
  108. }
  109. stop() {
  110. console.log('🛑 停止 Howler.js 播放')
  111. if (this.currentHowl) {
  112. this.currentHowl.stop()
  113. this.currentHowl.unload()
  114. this.currentHowl = null
  115. }
  116. this.audioChunks = []
  117. this.playing = false
  118. }
  119. clear() {
  120. console.log('🧹 清理 Howler.js 播放器状态')
  121. this.stop()
  122. }
  123. destroy() {
  124. this.stop()
  125. this.audioReady = false
  126. }
  127. isPlaying(): boolean {
  128. return this.playing
  129. }
  130. getCurrentTime(): number {
  131. return this.currentHowl?.seek() || 0
  132. }
  133. getBufferedProgress(): number {
  134. return this.audioChunks.length
  135. }
  136. /**
  137. * 设置音量(0.0-1.0)
  138. * 注意:Howler.js 的 volume 最大是 1.0,如需更大音量需要用 Web Audio API
  139. */
  140. setVolume(volume: number) {
  141. const clampedVolume = Math.max(0, Math.min(1.0, volume))
  142. if (this.currentHowl) {
  143. this.currentHowl.volume(clampedVolume)
  144. console.log('🔊 Howler.js 音量设置为:', clampedVolume)
  145. }
  146. }
  147. }