feat(frontend): Audio player で波形を表示するように (MisskeyIO#827)

Co-authored-by: あわわわとーにゅ <17376330+u1-liquid@users.noreply.github.com>
Co-authored-by: tar_bin <tar.bin.master@gmail.com>
This commit is contained in:
まっちゃてぃー。 2024-12-18 02:59:55 +09:00 committed by GitHub
parent b5df2c0356
commit 714aad6312
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 349 additions and 44 deletions

View file

@ -35,40 +35,43 @@ SPDX-License-Identifier: AGPL-3.0-only
</audio>
</div>
<div v-else :class="$style.audioControls">
<audio
ref="audioEl"
preload="metadata"
>
<source :src="audio.url">
</audio>
<div :class="[$style.controlsChild, $style.controlsLeft]">
<button class="_button" :class="$style.controlButton" @click.prevent.stop="togglePlayPause">
<i v-if="isPlaying" class="ti ti-player-pause-filled"></i>
<i v-else class="ti ti-player-play-filled"></i>
</button>
</div>
<div :class="[$style.controlsChild, $style.controlsRight]">
<button class="_button" :class="$style.controlButton" @click.prevent.stop="showMenu">
<i class="ti ti-settings"></i>
</button>
</div>
<div :class="[$style.controlsChild, $style.controlsTime]">{{ hms(elapsedTimeMs) }}</div>
<div :class="[$style.controlsChild, $style.controlsVolume]">
<button class="_button" :class="$style.controlButton" @click.prevent.stop="toggleMute">
<i v-if="volume === 0" class="ti ti-volume-3"></i>
<i v-else class="ti ti-volume"></i>
</button>
<div v-else>
<MkAudioVisualizer v-if="user" ref="audioVisualizer" :audioEl="audioEl" :analyser="analyserNode" :user="user" :profileImage="user.avatarUrl"/>
<div :class="$style.audioControls">
<audio
ref="audioEl"
preload="metadata"
>
<source :src="audio.url">
</audio>
<div :class="[$style.controlsChild, $style.controlsLeft]">
<button class="_button" :class="$style.controlButton" @click.prevent.stop="togglePlayPause">
<i v-if="isPlaying" class="ti ti-player-pause-filled"></i>
<i v-else class="ti ti-player-play-filled"></i>
</button>
</div>
<div :class="[$style.controlsChild, $style.controlsRight]">
<button class="_button" :class="$style.controlButton" @click.prevent.stop="showMenu">
<i class="ti ti-settings"></i>
</button>
</div>
<div :class="[$style.controlsChild, $style.controlsTime]">{{ hms(elapsedTimeMs) }}</div>
<div :class="[$style.controlsChild, $style.controlsVolume]">
<button class="_button" :class="$style.controlButton" @click.prevent.stop="toggleMute">
<i v-if="volume === 0" class="ti ti-volume-3"></i>
<i v-else class="ti ti-volume"></i>
</button>
<MkMediaRange
v-model="volume"
:class="$style.volumeSeekbar"
/>
</div>
<MkMediaRange
v-model="volume"
:class="$style.volumeSeekbar"
v-model="rangePercent"
:class="$style.seekbarRoot"
:buffer="bufferedDataRatio"
/>
</div>
<MkMediaRange
v-model="rangePercent"
:class="$style.seekbarRoot"
:buffer="bufferedDataRatio"
/>
</div>
</div>
</template>
@ -82,12 +85,14 @@ import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
import bytes from '@/filters/bytes.js';
import { hms } from '@/filters/hms.js';
import MkAudioVisualizer from '@/components/MkAudioVisualizer.vue';
import MkMediaRange from '@/components/MkMediaRange.vue';
import { pleaseLogin } from '@/scripts/please-login.js';
import { $i, iAmModerator } from '@/account.js';
const props = defineProps<{
audio: Misskey.entities.DriveFile;
user?: Misskey.entities.UserLite;
}>();
const keymap = {
@ -126,6 +131,7 @@ function hasFocus() {
const playerEl = shallowRef<HTMLDivElement>();
const audioEl = shallowRef<HTMLAudioElement>();
const audioVisualizer = ref<InstanceType<typeof MkAudioVisualizer>>();
// eslint-disable-next-line vue/no-setup-props-destructure
const hide = ref((defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.audio.isSensitive && defaultStore.state.nsfw !== 'ignore'));
@ -240,6 +246,11 @@ const isPlaying = ref(false);
const isActuallyPlaying = ref(false);
const elapsedTimeMs = ref(0);
const durationMs = ref(0);
const audioContext = ref<AudioContext | null>(null);
const sourceNode = ref<MediaElementAudioSourceNode | null>(null);
const gainNode = ref<GainNode | null>(null);
const analyserGainNode = ref<GainNode | null>(null);
const analyserNode = ref<AnalyserNode | null>(null);
const rangePercent = computed({
get: () => {
return (elapsedTimeMs.value / durationMs.value) || 0;
@ -262,11 +273,33 @@ const bufferedDataRatio = computed(() => {
function togglePlayPause() {
if (!isReady.value || !audioEl.value) return;
if (!sourceNode.value) {
audioContext.value = new (window.AudioContext || window.webkitAudioContext)();
sourceNode.value = audioContext.value.createMediaElementSource(audioEl.value);
analyserGainNode.value = audioContext.value.createGain();
gainNode.value = audioContext.value.createGain();
analyserNode.value = audioContext.value.createAnalyser();
sourceNode.value.connect(analyserGainNode.value);
analyserGainNode.value.connect(analyserNode.value);
analyserNode.value.connect(gainNode.value);
gainNode.value.connect(audioContext.value.destination);
analyserNode.value.fftSize = 2048;
analyserGainNode.value.gain.setValueAtTime(0.8, audioContext.value.currentTime);
gainNode.value.gain.setValueAtTime(volume.value, audioContext.value.currentTime);
}
if (isPlaying.value) {
audioEl.value.pause();
audioVisualizer.value?.pauseAnimation();
isPlaying.value = false;
} else {
audioEl.value.play();
audioVisualizer.value?.resumeAnimation();
isPlaying.value = true;
oncePlayed.value = true;
}
@ -324,6 +357,7 @@ function init() {
oncePlayed.value = false;
isActuallyPlaying.value = false;
isPlaying.value = false;
audioVisualizer.value?.pauseAnimation();
});
durationMs.value = audioEl.value.duration * 1000;
@ -332,8 +366,7 @@ function init() {
durationMs.value = audioEl.value.duration * 1000;
}
});
audioEl.value.volume = volume.value;
gainNode.value?.gain.setValueAtTime(volume.value, audioContext.value?.currentTime);
}
}, {
immediate: true,
@ -341,7 +374,7 @@ function init() {
}
watch(volume, (to) => {
if (audioEl.value) audioEl.value.volume = to;
if (audioEl.value) gainNode.value?.gain.setValueAtTime(to, audioContext.value?.currentTime);
});
watch(speed, (to) => {