mm.site/src/components/AudioPlayer.js

369 lines
7.9 KiB
JavaScript

import { LitElement, css, html, unsafeCSS } from 'lit'
import { Task } from '@lit-labs/task'
import WaveSurfer from 'wavesurfer.js'
import { formatSeconds } from '../api/utils'
import MainCSS from '../assets/styles/main.scss?inline'
// temp bg image
import Photo from '/images/Meredith Monk (1974) Photo Lauretta HArris.jpg?url'
// components
import './SvgIcon.js'
import './RangeSlider.js'
class AudioPlayer extends LitElement {
static properties = {
details: { type: Object },
data: { state: true },
playing: { state: true },
volume: { state: true },
track: { state: true },
audio: { state: true },
}
constructor() {
super()
this.details = {}
this.data = {}
this.audio = null
this.playing = false
this.audioEl = {}
this.track = 0
}
_getAudio = new Task(
this,
async () => {
if (!this.details.media) return
if (this.details.tracks) {
console.log(this.details.tracks[0])
if (!this.audio) {
this.initAudio()
}
this.audio.load(this.details.tracks[this.track])
return this.audio
} else {
try {
if (this.audio.media) {
this.audio.destroy()
}
const wsEl = this.initWs()
this.audio.load(`/media/${this.details.media}.mp3`, this.details.data)
return wsEl
} catch (err) {
console.error(err)
}
}
},
() => [ this.details ]
)
initAudio() {
this.audio = document.createElement('audio')
this.audio.addEventListener('loadstart', () => {
this.loading = true
})
this.audio.addEventListener('canplay', () => {
this.loading = false
})
this.audio.addEventListener('play', () => {
this.playing = true
})
this.audio.addEventListener('pause', () => {
this.playing = false
})
this.audio.addEventListener('loadedmetadata', () => {
this.data = {
...this.data,
position: 0,
duration: this.audio.duration
}
})
this.audio.addEventListener('ended', () => {
// auto play next track or end if last track
})
this.audio.addEventListener('timeupdate', () => {
this.data = {
...this.data,
position: this.audio.currentTime
}
})
this.audio.addEventListener('seeking', () => {
this.data = {
...this.data,
position: this.audio.currentTime
}
})
}
initWs() {
const div = document.createElement('div')
div.classList.add('waveform')
this.audio = WaveSurfer.create({
container: div,
waveColor: '#ffffff50',
progressColor: 'white',
cursorColor: 'transparent',
fillParent: true,
barWidth: 4,
barGap: 6,
barRadius: 5,
dragToSeek: true,
})
this.audio.setVolume(0.6)
this.volume = this.audio.getVolume()
this.audio.on('ready', (duration) => {
this.data = {
...this.data,
position: 0,
duration
}
})
this.audio.on('play', () => {
this.playing = true
})
this.audio.on('pause', () => {
this.playing = false
})
this.audio.on('interaction', (newTime) => {
this.data = {
...this.data,
position: newTime
}
})
this.audio.on('timeupdate', (currentTime) => {
this.data = {
...this.data,
position: currentTime
}
})
return div
}
togglePlay() {
this.audio.playPause()
}
skipForward() {
this.audio.skip(15)
}
skipBackward() {
this.audio.skip(-15)
}
nextTrack() {
this.dispatchEvent(new Event('next-track', { bubbles: true, composed: true }))
}
prevTrack() {
this.dispatchEvent(new CustomEvent('prev-track', { bubbles: true, composed: true }))
}
volHandler(evt) {
if (evt.detail) {
this.audio.setVolume(evt.detail.value)
}
}
progressCalc() {
const { position, duration } = this.data
let percentage = 0
if (position && duration) {
percentage = (position / duration) * 100
}
return `${percentage}%`
}
render() {
return html`
<img class="bg-img" src=${Photo} />
<header>
<h2>${this.details.title}</h2>
<p class="details">${this.details.details}</p>
</header>
<div class="info">
<div class="progress">
<div class="bar" style="--progress-bar: ${this.progressCalc()}"></div>
</div>
${this._getAudio.render({
complete: (div) => div
})}
<span class="time_pos">${formatSeconds(this.data.position)}</span> <span class="time_dur">${formatSeconds(this.data.duration)}</span>
</div>
<div class="controls">
<div>
<mm-icon name="skip-prev" @click=${this.prevTrack} ?disabled=${!this.data.duration || this.getAudio?.status == 1}></mm-icon>
<mm-icon name="skip-b15" @click=${this.skipBackward} ?disabled=${!this.data.duration || this.getAudio?.status == 1} class="skip15"></mm-icon>
<mm-icon name="${this.playing ? 'pause' : 'play'}" @click=${this.togglePlay} ?disabled=${!this.data.duration || this.getAudio?.status == 1} class=${this.playing ? 'pause' : 'play'}></mm-icon>
<mm-icon name="skip-f15" @click=${this.skipForward} ?disabled=${!this.data.duration || this.getAudio?.status == 1} class="skip15"></mm-icon>
<mm-icon name="skip-next" @click=${this.nextTrack} ?disabled=${!this.data.duration || this.getAudio?.status == 1}></mm-icon>
</div>
<mm-range
value=${this.volume}
@input=${this.volHandler}
?disabled=${!this.data.duration || this.getAudio?.status == 1}
style="--track-height: 0.4em; --thumb-size: 1.2em; --track-color: #ffffff50"
></mm-range>
</div>
`
}
static styles = [ css`${unsafeCSS(MainCSS)}`, css`
:host {
position: absolute;
inset: 0;
padding: 1em;
display: grid;
grid-template-rows: 0.7fr 0.7fr 1fr;
gap: 1em;
opacity: 0;
animation: fadeIn 0.125s ease-in forwards;
overflow: hidden;
border-radius: 0.75em 0 0 0.75em;
isolation: isolate;
}
.bg-img {
position: absolute;
inset: 0;
object-fit: cover;
object-position: center 25%;
opacity: 0.2;
mix-blend-mode: overlay;
scale: 1.1;
width: 100%;
height: 100%;
}
header {
color: var(--netural-100, white);
font-size: 1.5em;
line-height: 1.2;
padding: 0.5em;
}
header > p {
font-size: 1.5em;
max-width: 25ch;
}
.info, .controls {
position: relative;
}
.progress {
--progress-bar: 0%;
position: absolute;
display: flex;
align-items: center;
inset: 0;
z-index: 3;
pointer-events: none;
}
.progress > .bar {
height: 0.25em;
width: var(--progress-bar);
background: #ffffff80;
border-radius: 100vmax;
}
.time_pos, .time_dur {
font-family: sans-serif;
font-size: 0.85em;
color: var(--neutral-100, white);
position: absolute;
bottom: 1em;
}
.time_dur {
right: 0;
}
.controls {
display: flex;
flex-direction: column;
justify-content: space-evenly;
width: 65%;
margin-inline: auto;
}
.controls > div {
display: flex;
align-items: center;
justify-content: space-evenly;
margin-block-end: 1em;
gap: 0.25em;
}
mm-icon {
color: white;
font-size: 2.5em;
}
mm-icon.skip15 {
font-size: 1.5em;
}
mm-icon:not(.skip15, .play, .pause) {
font-size: 1em;
}
mm-volctrl {
display: block;
width: 80%;
margin-inline: auto;
}
.waveform {
position: relative;
height: 15em;
display: flex;
flex-direction: column;
justify-content: center;
}
@keyframes fadeIn {
to {
opacity: 1;
}
}
` ]
}
customElements.define('mm-aplayer', AudioPlayer)