vue-howlerが未対応
以前の記事ではAudio Playerの作成にvue-howlerを使ってましたが、どうやらNuxt3には対応してないとのことですので、そのままhowler.jsを使ってみました。
インストール
npm i howler
# or
yarn add howler
こちらも入れておきましょう。
npm i @types/howler
# or
yarn add @types/howler
コンポーネント作り
今回は再生と一時停止のトグルとシークバーのみのシンプルなデザインに変更、以下の部分をコンポーネントにしました。
なお、AudioSourceや各オプションを渡して単一のプレイヤーにするという点は前回と同様。表示部は以下の様になります。
<template>
<div class="player">
<button class="toggle" @click="!playerState.playing ? play(true) : pause()">
<font-awesome-icon :icon="['fas', 'play']" class="fa-lg play-icon" :class="playerState.playing ? 'is-hidden' : 'is-active'" />
<font-awesome-icon :icon="['fas', 'pause']" class="fa-lg pause-icon" :class="playerState.playing ? 'is-active' : 'is-hidden'" />
</button>
<div class="contents">
<h4 class="title">{{ playerState.title }}</h4>
<div class="seek-block">
<div class="current-time">{{ playerState.currentTime }}</div>
<div class="bar">
<progress :value="playerState.progress" min="0" max="1"></progress>
<input
v-model="value"
type="range"
min="0"
max="1"
step="0.01"
class="seek-slider"
/>
</div>
<div class="sound-time">{{ playerState.soundTime }}</div>
</div>
</div>
</div>
</template>
受け取るデータをPropsに。
<script setup lang="ts">
import { Howl } from 'howler'
interface Props {
src: string[]
title?: string
volume?: number
html5?: boolean
loop?: boolean
preload?: boolean
autoplay?: boolean
mute?: boolean
}
const props = withDefaults(defineProps<Props>(), {
src: () => [],
title: '',
volume: 1.0,
html5: false,
loop: false,
preload: true,
autoplay: false,
mute: false,
})
...
</script>
templateで利用するプレイヤーの各状態はreactiveでまとめてます。今回はvolumeは利用しないのですが、今後使うかもしれないのでとりあえず書いておきます。
const playerState = reactive({
title: props.title,
playing: false,
isMuted: false,
volume: props.volume,
progress: 0,
currentTime: '0:00',
soundTime: '0:00',
})
オプションを渡してインスタンスを生成。
const sound = ref<Howl>()
sound.value = new Howl({
src: props.src,
volume: props.volume,
html5: props.html5,
loop: props.loop,
preload: props.preload,
autoplay: props.autoplay,
mute: props.mute,
onload: () => {
getDuration()
},
onend: () => {
clearTimer()
playerState.playing = false
},
})
@types/howler
で onload
等の各イベントは定義されてますんで、スッキリと書けます。 onload
では曲の長さを取得、曲が最後まで再生された時に発火する onend
ではTimerをクリアさせてます。[1] durationは秒で取得されますんで、分と秒の時間表示に変換。
const formatTime = (sec: number) => {
return `${Math.floor(sec / 60)}:${('00' + Math.floor(sec % 60)).slice(-2)}`
}
const getDuration = () => {
_duration = sound.value?.duration() || 0
playerState.soundTime = formatTime(_duration)
}
const clearTimer = () => {
if (intervalId) {
clearInterval(intervalId)
}
}
今回も再生やポーズのトグルでイベント発火前に行う動作が必要だったんで、以下のクリックイベントにて別途書いてます。そのトグルボタンのクリックイベントですが、再生するとプログレスバーや再生時間を更新していくためのsetIntervalを設定します。大した精度もいらないので200ms(5Hz)としてます。なお、ポーズした際もonend同様Timerをクリア。
const play = (flg = false) => {
isSelf = flg
if (isSelf) {
emit('beforePlay')
}
sound.value?.play()
intervalId = setInterval(setProgress, 200)
playerState.playing = true
isSelf = false
}
const pause = () => {
if (!isSelf) {
sound.value?.pause()
clearTimer()
playerState.playing = false
}
}
setIntervalで発火するsetProgressで何をやってるかというと、以下の様に現在の再生位置を取得し0~1の値に変換。プログレスバーを <progress :value
としているので、0~1の間で更新されていきます。
const setProgress = () => {
const seek = sound.value?.seek() || 0
if (seek) {
playerState.progress = (seek / _duration) || 0
}
playerState.currentTime = formatTime(seek)
}
シークバーの作成
前回同様、見た目はプログレスバーが進んでいきながらもシークできるような感じにします。後述するget、setにてinputのみでプログレスバーとインプット両方の機能を持たせることもできましたが、再生時のバー更新がなめらかに進んでいかなかったので表示と入力を分ける作りはそのままにしました。
<input
v-model="value"
type="range"
min="0"
max="1"
step="0.01"
class="seek-slider"
/>
Nuxt3はv-modelの書き方が変更されましたね。
const value = computed({
get(): number {
return playerState.progress
},
set(value: number) {
sound.value?.seek(_duration * value)
playerState.progress = value
playerState.currentTime = formatTime(value * _duration)
},
})
上記の通り、setProgressによる更新処理にinputが割り込めるという仕組みにしてます。
二重再生の回避
これも前回やりましたが、再生前に他のプレイヤーが再生中であれば一時停止させます。 const emit = define
とし、再生前にイベント発火。親に伝えます。
親であるページでは、このプレイヤーは
<li v-for="audioData in audioLists[0].list" :key="audioData._id">
<AudioPlayer
:ref="setPlayers"
:title="audioData.data.title"
:src="[`audio/${audioData.data.slug}.mp3`]"
@before-play="pausePlayer"
/>
<DownloadButton
:slug="audioData.data.slug"
:ogg="audioData.data.ogg"
:loop="audioData.data.loop"
:sli="audioData.data.sli"
/>
</li>
となってまして、 before-play
時には pausePlayer
を実行。
refによる参照
Composition APIでは$refsが使えません。配列で取得したい場合はrefに関数をバインドさせるそうなんで、以下の様にセッティング。
<script>
...
const players = []
onBeforeUpdate(() => {
players.value = []
})
const setPlayers = (el) => {
if (el) {
players.push(el)
}
}
const pausePlayer = () => {
players.forEach((el) => {
el.pause()
})
}
</script>
子コンポーネントであるプレイヤーで defineExpose({ pause })
させておき、ポーズを実行できるようにしておきます。
まとめ
もともとvue-howlerがmixinの形式だったためvue-howlerが担ってた箇所を改めて作るという程度で、そんなに違和感なく作り変えることができました。
次はSoundCloudみたいにプログレスバーの部分を波形表示にしたいなと思ってますんで、howlerの機能をさらに使っていくか、wavesurfer.jsに変更するか検討します。
再生中にページ遷移するケースも考慮し、
onBeforeMount
でも併せてクリアさせます。 ↩︎