266 lines
6.7 KiB
TypeScript
266 lines
6.7 KiB
TypeScript
import type { Track } from '~/types'
|
|
|
|
import { computed, watchEffect, ref, watch } from 'vue'
|
|
import { Howler } from 'howler'
|
|
import { useRafFn, useTimeoutFn } from '@vueuse/core'
|
|
import useQueue from '~/composables/audio/useQueue'
|
|
import useSound from '~/composables/audio/useSound'
|
|
import toLinearVolumeScale from '~/composables/audio/toLinearVolumeScale'
|
|
import store from '~/store'
|
|
import axios from 'axios'
|
|
|
|
const PRELOAD_DELAY = 15
|
|
|
|
const { currentSound, loadSound, onSoundProgress } = useSound()
|
|
const { isShuffling, currentTrack, currentIndex } = useQueue()
|
|
|
|
const looping = computed(() => store.state.player.looping)
|
|
const playing = computed(() => store.state.player.playing)
|
|
const loading = computed(() => store.state.player.isLoadingAudio)
|
|
const errored = computed(() => store.state.player.errored)
|
|
const focused = computed(() => store.state.ui.queueFocused === 'player')
|
|
|
|
// Cache sound if we have currentTrack available
|
|
if (currentTrack.value) {
|
|
loadSound(currentTrack.value)
|
|
}
|
|
|
|
// Playing
|
|
const playTrack = async (track: Track, oldTrack?: Track) => {
|
|
const oldSound = currentSound.value
|
|
|
|
// TODO (wvffle): Move oldTrack to watcher
|
|
if (oldSound && track !== oldTrack) {
|
|
oldSound.stop()
|
|
}
|
|
|
|
if (!track) {
|
|
return
|
|
}
|
|
|
|
if (!isShuffling.value) {
|
|
if (!track.uploads.length) {
|
|
// we don't have any information for this track, we need to fetch it
|
|
track = await axios.get(`tracks/${track.id}/`)
|
|
.then(response => response.data, () => null)
|
|
}
|
|
|
|
if (track === null) {
|
|
store.commit('player/isLoadingAudio', false)
|
|
store.dispatch('player/trackErrored')
|
|
return
|
|
}
|
|
|
|
currentSound.value = loadSound(track)
|
|
|
|
if (playing.value) {
|
|
currentSound.value.play()
|
|
store.commit('player/playing', true)
|
|
} else {
|
|
store.commit('player/isLoadingAudio', false)
|
|
}
|
|
|
|
store.commit('player/errored', false)
|
|
store.dispatch('player/updateProgress', 0)
|
|
}
|
|
}
|
|
|
|
const { start: loadTrack, stop: cancelLoading } = useTimeoutFn((track, oldTrack) => {
|
|
playTrack(track as Track, oldTrack as Track)
|
|
}, 100, { immediate: false }) as {
|
|
start: (a: Track, b: Track) => void
|
|
stop: () => void
|
|
}
|
|
|
|
watch(currentTrack, (track, oldTrack) => {
|
|
cancelLoading()
|
|
currentSound.value?.pause()
|
|
store.commit('player/isLoadingAudio', true)
|
|
loadTrack(track, oldTrack)
|
|
})
|
|
|
|
// Volume
|
|
const volume = computed({
|
|
get: () => store.state.player.volume,
|
|
set: (value) => store.commit('player/volume', value)
|
|
})
|
|
|
|
watchEffect(() => Howler.volume(toLinearVolumeScale(volume.value)))
|
|
|
|
const mute = () => store.dispatch('player/mute')
|
|
const unmute = () => store.dispatch('player/unmute')
|
|
const toggleMute = () => store.dispatch('player/toggleMute')
|
|
|
|
// Time and duration
|
|
const duration = computed(() => store.state.player.duration)
|
|
const currentTime = computed({
|
|
get: () => store.state.player.currentTime,
|
|
set: (time) => {
|
|
if (time < 0 || time > duration.value) {
|
|
return
|
|
}
|
|
|
|
if (!currentSound.value?.getSource() || time === currentSound.value.seek()) {
|
|
return
|
|
}
|
|
|
|
currentSound.value.seek(time)
|
|
|
|
// Update progress immediately to ensure updated UI
|
|
progress.value = time
|
|
}
|
|
})
|
|
|
|
const durationFormatted = computed(() => store.getters['player/durationFormatted'])
|
|
const currentTimeFormatted = computed(() => store.getters['player/currentTimeFormatted'])
|
|
|
|
// Progress
|
|
const progress = computed({
|
|
get: () => store.getters['player/progress'],
|
|
set: (time) => {
|
|
if (currentSound.value?.state() === 'loaded') {
|
|
store.state.player.currentTime = time
|
|
|
|
const duration = currentSound.value.duration()
|
|
currentSound.value.triggerSoundProgress(time, duration)
|
|
}
|
|
}
|
|
})
|
|
|
|
const bufferProgress = computed(() => store.state.player.bufferProgress)
|
|
onSoundProgress(({ node, time, duration }) => {
|
|
const toPreload = store.state.queue.tracks[currentIndex.value + 1]
|
|
if (!nextTrackPreloaded.value && toPreload && (time > PRELOAD_DELAY || duration - time < 30)) {
|
|
loadSound(toPreload)
|
|
nextTrackPreloaded.value = true
|
|
}
|
|
|
|
if (time > duration / 2) {
|
|
if (!isListeningSubmitted.value) {
|
|
store.dispatch('player/trackListened', currentTrack.value)
|
|
isListeningSubmitted.value = true
|
|
}
|
|
}
|
|
|
|
// from https://github.com/goldfire/howler.js/issues/752#issuecomment-372083163
|
|
|
|
const { buffered, currentTime } = node
|
|
|
|
let range = 0
|
|
try {
|
|
while (buffered.start(range) >= currentTime || currentTime >= buffered.end(range)) {
|
|
range += 1
|
|
}
|
|
} catch (IndexSizeError) {
|
|
return
|
|
}
|
|
|
|
let loadPercentage
|
|
|
|
const start = buffered.start(range)
|
|
const end = buffered.end(range)
|
|
|
|
if (range === 0) {
|
|
// easy case, no user-seek
|
|
const loadStartPercentage = start / node.duration
|
|
const loadEndPercentage = end / node.duration
|
|
loadPercentage = loadEndPercentage - loadStartPercentage
|
|
} else {
|
|
const loaded = end - start
|
|
const remainingToLoad = node.duration - start
|
|
// user seeked a specific position in the audio, our progress must be
|
|
// computed based on the remaining portion of the track
|
|
loadPercentage = loaded / remainingToLoad
|
|
}
|
|
|
|
if (loadPercentage * 100 === bufferProgress.value) {
|
|
return
|
|
}
|
|
|
|
store.commit('player/bufferProgress', loadPercentage * 100)
|
|
})
|
|
|
|
const observeProgress = ref(false)
|
|
useRafFn(() => {
|
|
if (observeProgress.value && currentSound.value?.state() === 'loaded') {
|
|
progress.value = currentSound.value.seek()
|
|
}
|
|
})
|
|
|
|
watch(playing, async (isPlaying) => {
|
|
if (currentSound.value) {
|
|
if (isPlaying) {
|
|
currentSound.value.play()
|
|
} else {
|
|
currentSound.value.pause()
|
|
}
|
|
} else {
|
|
await playTrack(currentTrack.value)
|
|
}
|
|
|
|
observeProgress.value = isPlaying
|
|
})
|
|
|
|
const isListeningSubmitted = ref(false)
|
|
const nextTrackPreloaded = ref(false)
|
|
watch(currentTrack, () => (nextTrackPreloaded.value = false))
|
|
|
|
// Controls
|
|
const pause = () => store.dispatch('player/pausePlayback')
|
|
const resume = () => store.dispatch('player/resumePlayback')
|
|
|
|
const { next } = useQueue()
|
|
const seek = (step: number) => {
|
|
// seek right
|
|
if (step > 0) {
|
|
if (currentTime.value + step < duration.value) {
|
|
store.dispatch('player/updateProgress', (currentTime.value + step))
|
|
} else {
|
|
next()
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// seek left
|
|
const position = Math.max(currentTime.value + step, 0)
|
|
store.dispatch('player/updateProgress', position)
|
|
}
|
|
|
|
const togglePlayback = () => {
|
|
if (playing.value) return pause()
|
|
return resume()
|
|
}
|
|
|
|
export default () => {
|
|
return {
|
|
looping,
|
|
playing,
|
|
loading,
|
|
errored,
|
|
focused,
|
|
isListeningSubmitted,
|
|
|
|
playTrack,
|
|
|
|
volume,
|
|
mute,
|
|
unmute,
|
|
toggleMute,
|
|
|
|
duration,
|
|
currentTime,
|
|
|
|
durationFormatted,
|
|
currentTimeFormatted,
|
|
|
|
progress,
|
|
bufferProgress,
|
|
|
|
pause,
|
|
resume,
|
|
seek,
|
|
togglePlayback
|
|
}
|
|
}
|