webapp/src/components/Article/AudioPlayer/AudioPlayer.tsx

219 lines
5.9 KiB
TypeScript
Raw Normal View History

import { createEffect, createSignal, onMount, Show } from 'solid-js'
import { PlayerHeader } from './PlayerHeader'
import { PlayerPlaylist } from './PlayerPlaylist'
import styles from './AudioPlayer.module.scss'
export type MediaItem = {
id?: number
body: string
pic: string
title: string
url: string
isCurrent: boolean
isPlaying: boolean
}
const prepareMedia = (media: MediaItem[]) =>
media.map((item, index) => ({
...item,
id: index,
isCurrent: false,
isPlaying: false
}))
const progressUpdate = (audioRef, progressFilledRef, duration) => {
progressFilledRef.style.width = `${(audioRef.currentTime / duration) * 100 || 0}%`
}
const scrub = (event, progressRef, duration, audioRef) => {
audioRef.currentTime = (event.offsetX / progressRef.offsetWidth) * duration
}
const getFormattedTime = (point) => new Date(point * 1000).toISOString().slice(14, -5)
export default (props: { media: MediaItem[]; articleSlug: string; body: string }) => {
let audioRef: HTMLAudioElement
let progressRef: HTMLDivElement
let progressFilledRef: HTMLDivElement
const [audioContext, setAudioContext] = createSignal<AudioContext>()
const [gainNode, setGainNode] = createSignal<GainNode>()
const [tracks, setTracks] = createSignal<MediaItem[] | null>(prepareMedia(props.media))
const [duration, setDuration] = createSignal<number>(0)
const [currentTimeContent, setCurrentTimeContent] = createSignal<string>('00:00')
const [currentDurationContent, setCurrentDurationContent] = createSignal<string>('00:00')
const [mousedown, setMousedown] = createSignal<boolean>(false)
const getCurrentTrack = () =>
tracks().find((track) => track.isCurrent) ||
(() => {
setTracks(
tracks().map((track, index) => ({
...track,
isCurrent: index === 0
}))
)
return tracks()[0]
})()
createEffect(() => {
if (audioRef.src !== getCurrentTrack().url) {
audioRef.src = getCurrentTrack().url
audioRef.load()
}
})
createEffect(() => {
if (getCurrentTrack() && duration()) {
setCurrentDurationContent(getFormattedTime(duration()))
}
})
const playMedia = async (m: MediaItem) => {
setTracks(
tracks().map((track) => ({
...track,
isCurrent: track.id === m.id ? true : false,
isPlaying: track.id === m.id ? !track.isPlaying : false
}))
)
progressUpdate(audioRef, progressFilledRef, duration())
if (audioContext().state === 'suspended') audioContext().resume()
if (getCurrentTrack().isPlaying) {
await audioRef.play()
} else {
audioRef.pause()
}
}
const setTimes = () => {
setCurrentTimeContent(getFormattedTime(audioRef.currentTime))
}
const handleAudioEnd = () => {
progressFilledRef.style.width = '0%'
audioRef.currentTime = 0
}
const handleAudioTimeUpdate = () => {
progressUpdate(audioRef, progressFilledRef, duration())
setTimes()
}
onMount(() => {
setAudioContext(new AudioContext())
setGainNode(audioContext().createGain())
setTimes()
const track = audioContext().createMediaElementSource(audioRef)
track.connect(gainNode()).connect(audioContext().destination)
})
const playPrevTrack = () => {
const { id } = getCurrentTrack()
const currIndex = tracks().findIndex((track) => track.id === id)
const getUpdatedStatus = (trackId) =>
currIndex === 0
? trackId === tracks()[tracks().length - 1].id
: trackId === tracks()[currIndex - 1].id
setTracks(
tracks().map((track) => ({
...track,
isCurrent: getUpdatedStatus(track.id),
isPlaying: getUpdatedStatus(track.id)
}))
)
}
const playNextTrack = () => {
const { id } = getCurrentTrack()
const currIndex = tracks().findIndex((track) => track.id === id)
const getUpdatedStatus = (trackId) =>
currIndex === tracks().length - 1
? trackId === tracks()[0].id
: trackId === tracks()[currIndex + 1].id
setTracks(
tracks().map((track) => ({
...track,
isCurrent: getUpdatedStatus(track.id),
isPlaying: getUpdatedStatus(track.id)
}))
)
}
const handleOnAudioMetadataLoad = ({ target }) => {
setDuration(target.duration)
}
return (
<div>
<Show when={getCurrentTrack()}>
<PlayerHeader
onPlayMedia={() => playMedia(getCurrentTrack())}
getCurrentTrack={getCurrentTrack}
playPrevTrack={playPrevTrack}
playNextTrack={playNextTrack}
gainNode={gainNode()}
/>
</Show>
<Show when={getCurrentTrack()}>
<div class={styles.timeline}>
<div
class={styles.progress}
ref={progressRef}
onClick={(e) => scrub(e, progressRef, duration(), audioRef)}
onMouseMove={(e) => mousedown() && scrub(e, progressRef, duration(), audioRef)}
onMouseDown={() => setMousedown(true)}
onMouseUp={() => setMousedown(false)}
>
<div class={styles.progressFilled} ref={progressFilledRef}></div>
</div>
<div class={styles.progressTiming}>
<span>{currentTimeContent()}</span>
<span>{currentDurationContent()}</span>
</div>
<audio
ref={audioRef}
onTimeUpdate={handleAudioTimeUpdate}
onCanPlay={() => {
if (getCurrentTrack().isPlaying) {
audioRef.play()
}
}}
onLoadedMetadata={handleOnAudioMetadataLoad}
onEnded={handleAudioEnd}
crossorigin="anonymous"
/>
</div>
</Show>
<Show when={tracks()}>
<PlayerPlaylist
playMedia={playMedia}
tracks={tracks()}
getCurrentTrack={getCurrentTrack}
articleSlug={props.articleSlug}
body={props.body}
/>
</Show>
</div>
)
}