gaoluyang
2 天以前 b64a0deae5b5d33f9e20671a68936b27f0b9b00b
src/views/im/home/composables/useVoicePlayer.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,82 @@
import { ref } from 'vue'
/**
 * è¯­éŸ³æ’­æ”¾å…¨å±€äº’æ–¥
 *
 * æ¨¡å—级单例:同一时刻最多只有一条语音在播;新点的语音停掉旧的
 * ä¸‰å¤„入口共享同一播放态:
 * - MessageBubble:气泡点击 toggle å½“前条
 * - MessagePanel:切会话 stop()
 * - MessageHistory / MessageMergeDetailDialog:关闭时 stop()
 *
 * key ç”± MessageBubble åœ¨ setup é‡Œ Symbol() ç”Ÿæˆå®žä¾‹çº§å”¯ä¸€èº«ä»½ï¼›ä¸åŒæ¥æºï¼ˆä¸»é¢æ¿ / åŽ†å²æŠ½å±‰ /
 * åˆå¹¶è¯¦æƒ…)的同一条语音 url åœ¨ä¸åŒæ°”泡里也是不同 key,避免一处卸载误停另一处仍可见的播放。
 */
export type VoiceKey = symbol
interface VoiceTask {
  key: VoiceKey
  url: string
  audio: HTMLAudioElement
}
const currentTask = ref<null | VoiceTask>(null)
/**
 * æ˜¾å¼åœæ­¢
 *
 * - ä¸ä¼  key:强停(切会话 / å…³å¼¹çª—用)
 * - ä¼  key:仅当当前 task æ˜¯è¯¥ key æ—¶æ‰åœï¼ˆæ°”泡卸载兜底用,避免误停别人)
 */
function stop(key?: VoiceKey) {
  const task = currentTask.value
  if (!task) {
    return
  }
  if (key !== undefined && task.key !== key) {
    return
  }
  task.audio.pause()
  // removeAttribute('src') + load() æ˜¯ W3C æŽ¨èçš„释放姿势:不会触发空 src åŠ è½½å¯¼è‡´çš„ error äº‹ä»¶ï¼Œ
  // ä¹Ÿèƒ½è®©æµè§ˆå™¨ç«‹å³é‡Šæ”¾åº•层 decoder buffer,比 audio.src = '' æ›´å¹²å‡€
  task.audio.removeAttribute('src')
  task.audio.load()
  currentTask.value = null
}
/**
 * åˆ‡æ¢æ’­æ”¾
 *
 * - åŒ key å†ç‚¹ï¼šåœæŽ‰
 * - åˆ‡åˆ°æ–° key:停旧起新
 */
function play(key: VoiceKey, url: string) {
  if (!url) {
    return
  }
  if (currentTask.value?.key === key) {
    stop(key)
    return
  }
  stop()
  const audio = new Audio(url)
  const task: VoiceTask = { key, url, audio }
  /** æ’­æ”¾ç»“束 / å¼‚常清栈;只清当前任务,避免被后续新任务的回调误清 */
  const finalize = () => {
    if (currentTask.value === task) {
      currentTask.value = null
    }
  }
  audio.addEventListener('ended', finalize, { once: true })
  audio.addEventListener('error', finalize, { once: true })
  currentTask.value = task
  audio.play().catch(finalize)
}
export function useVoicePlayer() {
  /** æŒ‡å®š key æ˜¯å¦æ­£åœ¨æ’­æ”¾ */
  function isPlaying(key: VoiceKey): boolean {
    return currentTask.value?.key === key
  }
  return { isPlaying, play, stop }
}