File size: 2,915 Bytes
b1a388c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
import { onUnmounted, ref, watch } from 'vue'

/**
 * Wraps a native <audio> element with reactive state + helpers.
 *
 * Usage:
 *   const audio = useAudioControl()
 *   onMounted(() => audio.attach(nativeAudioEl.value))
 *   defineExpose({ seekAndPlay: audio.seekAndPlay })
 */
export function useAudioControl() {
  const audioEl      = ref(null)
  const playing      = ref(false)
  const currentTime  = ref(0)
  const duration     = ref(0)
  const playbackRate = ref(1)

  // ── debounced timeupdate (50 ms) ─────────────────────────────────
  let _debounce = null
  function _onTime() {
    clearTimeout(_debounce)
    _debounce = setTimeout(() => {
      if (audioEl.value) currentTime.value = audioEl.value.currentTime || 0
    }, 50)
  }
  function _onPlay()  { playing.value = true }
  function _onPause() { playing.value = false }
  function _onMeta()  { if (audioEl.value) duration.value = audioEl.value.duration || 0 }

  const _handlers = {
    play: _onPlay, pause: _onPause, timeupdate: _onTime,
    loadedmetadata: _onMeta, durationchange: _onMeta,
  }

  // ── lifecycle ─────────────────────────────────────────────────────
  function attach(el) {
    if (!el) return
    audioEl.value = el
    Object.entries(_handlers).forEach(([evt, fn]) => el.addEventListener(evt, fn))
    el.playbackRate = playbackRate.value
  }

  function detach() {
    clearTimeout(_debounce)
    const el = audioEl.value
    if (!el) return
    Object.entries(_handlers).forEach(([evt, fn]) => el.removeEventListener(evt, fn))
  }

  onUnmounted(detach)

  // ── controls ──────────────────────────────────────────────────────
  function toggle() {
    const a = audioEl.value; if (!a) return
    a.paused ? a.play().catch(() => {}) : a.pause()
  }

  function seekDelta(delta) {
    const a = audioEl.value; if (!a) return
    a.currentTime = Math.max(0, Math.min(a.duration || 0, (a.currentTime || 0) + delta))
  }

  function seekTo(sec) {
    const a = audioEl.value; if (!a) return
    a.currentTime = Math.max(0, Math.min(a.duration || 0, Number(sec) || 0))
  }

  /** Seek to `sec` then start playback immediately. */
  function seekAndPlay(sec) {
    seekTo(sec)
    audioEl.value?.play().catch(() => {})
  }

  function fmt(sec) {
    const s = Math.max(0, Number(sec) || 0)
    return `${Math.floor(s / 60).toString().padStart(2, '0')}:${Math.floor(s % 60).toString().padStart(2, '0')}`
  }

  watch(playbackRate, (v) => { if (audioEl.value) audioEl.value.playbackRate = v })

  return {
    audioEl, playing, currentTime, duration, playbackRate,
    attach, detach, toggle, seekTo, seekDelta, seekAndPlay, fmt,
  }
}