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:
parent
b5df2c0356
commit
714aad6312
11 changed files with 349 additions and 44 deletions
|
@ -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) => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue