2023-07-14 13:06:21 +00:00
|
|
|
import { createEffect, createSignal, on, onMount, Show } from 'solid-js'
|
2023-07-01 13:05:59 +00:00
|
|
|
import { PlayerHeader } from './PlayerHeader'
|
|
|
|
import { PlayerPlaylist } from './PlayerPlaylist'
|
|
|
|
import styles from './AudioPlayer.module.scss'
|
2023-07-14 13:06:21 +00:00
|
|
|
import { MediaItem } from '../../../pages/types'
|
2023-07-15 06:18:20 +00:00
|
|
|
import { audioProxy } from '../../../utils/imageProxy'
|
2023-07-14 13:06:21 +00:00
|
|
|
|
|
|
|
export type Audio = {
|
|
|
|
pic?: string
|
|
|
|
index?: number
|
|
|
|
isCurrent?: boolean
|
|
|
|
isPlaying?: boolean
|
|
|
|
} & MediaItem
|
|
|
|
|
|
|
|
type Props = {
|
|
|
|
media: Audio[]
|
|
|
|
articleSlug?: string
|
|
|
|
body?: string
|
|
|
|
editorMode?: boolean
|
|
|
|
onAudioChange?: (index: number, field: string, value: string) => void
|
2023-07-01 13:05:59 +00:00
|
|
|
}
|
|
|
|
|
2023-07-14 13:06:21 +00:00
|
|
|
const prepareMedia = (media: Audio[]) =>
|
2023-07-01 13:05:59 +00:00
|
|
|
media.map((item, index) => ({
|
|
|
|
...item,
|
2023-07-15 06:18:20 +00:00
|
|
|
url: audioProxy(item.url),
|
2023-07-14 13:06:21 +00:00
|
|
|
index: index,
|
2023-07-01 13:05:59 +00:00
|
|
|
isCurrent: false,
|
|
|
|
isPlaying: false
|
|
|
|
}))
|
|
|
|
|
|
|
|
const progressUpdate = (audioRef, progressFilledRef, duration) => {
|
2023-07-14 13:06:21 +00:00
|
|
|
progressFilledRef.current.style.width = `${(audioRef.current.currentTime / duration) * 100 || 0}%`
|
2023-07-01 13:05:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const scrub = (event, progressRef, duration, audioRef) => {
|
2023-07-14 13:06:21 +00:00
|
|
|
audioRef.current.currentTime = (event.offsetX / progressRef.current.offsetWidth) * duration
|
2023-07-01 13:05:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const getFormattedTime = (point) => new Date(point * 1000).toISOString().slice(14, -5)
|
|
|
|
|
2023-07-14 13:06:21 +00:00
|
|
|
export const AudioPlayer = (props: Props) => {
|
|
|
|
const audioRef: { current: HTMLAudioElement } = { current: null }
|
|
|
|
const progressRef: { current: HTMLDivElement } = { current: null }
|
|
|
|
const progressFilledRef: { current: HTMLDivElement } = { current: null }
|
2023-07-01 13:05:59 +00:00
|
|
|
|
|
|
|
const [audioContext, setAudioContext] = createSignal<AudioContext>()
|
|
|
|
const [gainNode, setGainNode] = createSignal<GainNode>()
|
2023-07-14 13:06:21 +00:00
|
|
|
const [tracks, setTracks] = createSignal<Audio[] | null>(prepareMedia(props.media))
|
2023-07-01 13:05:59 +00:00
|
|
|
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)
|
|
|
|
|
2023-07-14 13:06:21 +00:00
|
|
|
createEffect(
|
|
|
|
on(
|
|
|
|
() => props.media,
|
|
|
|
() => {
|
|
|
|
setTracks(prepareMedia(props.media))
|
|
|
|
}
|
|
|
|
)
|
|
|
|
)
|
2023-07-01 13:05:59 +00:00
|
|
|
const getCurrentTrack = () =>
|
|
|
|
tracks().find((track) => track.isCurrent) ||
|
|
|
|
(() => {
|
|
|
|
setTracks(
|
|
|
|
tracks().map((track, index) => ({
|
|
|
|
...track,
|
|
|
|
isCurrent: index === 0
|
|
|
|
}))
|
|
|
|
)
|
|
|
|
return tracks()[0]
|
|
|
|
})()
|
|
|
|
|
|
|
|
createEffect(() => {
|
2023-07-14 13:06:21 +00:00
|
|
|
if (audioRef.current.src !== getCurrentTrack().url) {
|
|
|
|
audioRef.current.src = getCurrentTrack().url
|
2023-07-01 13:05:59 +00:00
|
|
|
|
2023-07-14 13:06:21 +00:00
|
|
|
audioRef.current.load()
|
2023-07-01 13:05:59 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
createEffect(() => {
|
|
|
|
if (getCurrentTrack() && duration()) {
|
|
|
|
setCurrentDurationContent(getFormattedTime(duration()))
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
2023-07-14 13:06:21 +00:00
|
|
|
const playMedia = async (m: Audio) => {
|
2023-07-01 13:05:59 +00:00
|
|
|
setTracks(
|
|
|
|
tracks().map((track) => ({
|
|
|
|
...track,
|
2023-07-14 13:06:21 +00:00
|
|
|
isCurrent: track.index === m.index,
|
|
|
|
isPlaying: track.index === m.index ? !track.isPlaying : false
|
2023-07-01 13:05:59 +00:00
|
|
|
}))
|
|
|
|
)
|
|
|
|
|
|
|
|
progressUpdate(audioRef, progressFilledRef, duration())
|
|
|
|
|
2023-07-14 13:06:21 +00:00
|
|
|
if (audioContext().state === 'suspended') await audioContext().resume()
|
2023-07-01 13:05:59 +00:00
|
|
|
|
|
|
|
if (getCurrentTrack().isPlaying) {
|
2023-07-14 13:06:21 +00:00
|
|
|
await audioRef.current.play()
|
2023-07-01 13:05:59 +00:00
|
|
|
} else {
|
2023-07-14 13:06:21 +00:00
|
|
|
audioRef.current.pause()
|
2023-07-01 13:05:59 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const setTimes = () => {
|
2023-07-14 13:06:21 +00:00
|
|
|
setCurrentTimeContent(getFormattedTime(audioRef.current.currentTime))
|
2023-07-01 13:05:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const handleAudioEnd = () => {
|
2023-07-14 13:06:21 +00:00
|
|
|
progressFilledRef.current.style.width = '0%'
|
|
|
|
audioRef.current.currentTime = 0
|
2023-07-01 13:05:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const handleAudioTimeUpdate = () => {
|
|
|
|
progressUpdate(audioRef, progressFilledRef, duration())
|
|
|
|
|
|
|
|
setTimes()
|
|
|
|
}
|
|
|
|
|
|
|
|
onMount(() => {
|
|
|
|
setAudioContext(new AudioContext())
|
|
|
|
setGainNode(audioContext().createGain())
|
|
|
|
|
|
|
|
setTimes()
|
|
|
|
|
2023-07-14 13:06:21 +00:00
|
|
|
const track = audioContext().createMediaElementSource(audioRef.current)
|
2023-07-01 13:05:59 +00:00
|
|
|
track.connect(gainNode()).connect(audioContext().destination)
|
|
|
|
})
|
|
|
|
|
|
|
|
const playPrevTrack = () => {
|
2023-07-14 13:06:21 +00:00
|
|
|
const { index } = getCurrentTrack()
|
|
|
|
const currIndex = tracks().findIndex((track) => track.index === index)
|
2023-07-01 13:05:59 +00:00
|
|
|
|
|
|
|
const getUpdatedStatus = (trackId) =>
|
|
|
|
currIndex === 0
|
2023-07-14 13:06:21 +00:00
|
|
|
? trackId === tracks()[tracks().length - 1].index
|
|
|
|
: trackId === tracks()[currIndex - 1].index
|
2023-07-01 13:05:59 +00:00
|
|
|
|
|
|
|
setTracks(
|
|
|
|
tracks().map((track) => ({
|
|
|
|
...track,
|
2023-07-14 13:06:21 +00:00
|
|
|
isCurrent: getUpdatedStatus(track.index),
|
|
|
|
isPlaying: getUpdatedStatus(track.index)
|
2023-07-01 13:05:59 +00:00
|
|
|
}))
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
const playNextTrack = () => {
|
2023-07-14 13:06:21 +00:00
|
|
|
const { index } = getCurrentTrack()
|
|
|
|
const currIndex = tracks().findIndex((track) => track.index === index)
|
2023-07-01 13:05:59 +00:00
|
|
|
|
|
|
|
const getUpdatedStatus = (trackId) =>
|
|
|
|
currIndex === tracks().length - 1
|
2023-07-14 13:06:21 +00:00
|
|
|
? trackId === tracks()[0].index
|
|
|
|
: trackId === tracks()[currIndex + 1].index
|
2023-07-01 13:05:59 +00:00
|
|
|
|
|
|
|
setTracks(
|
|
|
|
tracks().map((track) => ({
|
|
|
|
...track,
|
2023-07-14 13:06:21 +00:00
|
|
|
isCurrent: getUpdatedStatus(track.index),
|
|
|
|
isPlaying: getUpdatedStatus(track.index)
|
2023-07-01 13:05:59 +00:00
|
|
|
}))
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
const handleOnAudioMetadataLoad = ({ target }) => {
|
|
|
|
setDuration(target.duration)
|
|
|
|
}
|
|
|
|
|
2023-07-14 13:06:21 +00:00
|
|
|
const handleAudioDescriptionChange = (index: number, field: string, value) => {
|
|
|
|
props.onAudioChange(index, field, value)
|
|
|
|
setTracks(
|
|
|
|
tracks().map((track, idx) => {
|
|
|
|
return idx === index ? { ...track, [field]: value } : track
|
|
|
|
})
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2023-07-01 13:05:59 +00:00
|
|
|
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}
|
2023-07-14 13:06:21 +00:00
|
|
|
ref={(el) => (progressRef.current = el)}
|
2023-07-01 13:05:59 +00:00
|
|
|
onClick={(e) => scrub(e, progressRef, duration(), audioRef)}
|
|
|
|
onMouseMove={(e) => mousedown() && scrub(e, progressRef, duration(), audioRef)}
|
|
|
|
onMouseDown={() => setMousedown(true)}
|
|
|
|
onMouseUp={() => setMousedown(false)}
|
|
|
|
>
|
2023-07-14 13:06:21 +00:00
|
|
|
<div class={styles.progressFilled} ref={(el) => (progressFilledRef.current = el)} />
|
2023-07-01 13:05:59 +00:00
|
|
|
</div>
|
|
|
|
<div class={styles.progressTiming}>
|
|
|
|
<span>{currentTimeContent()}</span>
|
|
|
|
<span>{currentDurationContent()}</span>
|
|
|
|
</div>
|
|
|
|
<audio
|
2023-07-14 13:06:21 +00:00
|
|
|
ref={(el) => (audioRef.current = el)}
|
2023-07-01 13:05:59 +00:00
|
|
|
onTimeUpdate={handleAudioTimeUpdate}
|
|
|
|
onCanPlay={() => {
|
|
|
|
if (getCurrentTrack().isPlaying) {
|
2023-07-14 13:06:21 +00:00
|
|
|
audioRef.current.play()
|
2023-07-01 13:05:59 +00:00
|
|
|
}
|
|
|
|
}}
|
|
|
|
onLoadedMetadata={handleOnAudioMetadataLoad}
|
|
|
|
onEnded={handleAudioEnd}
|
|
|
|
crossorigin="anonymous"
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
</Show>
|
|
|
|
<Show when={tracks()}>
|
|
|
|
<PlayerPlaylist
|
2023-07-14 13:06:21 +00:00
|
|
|
editorMode={props.editorMode}
|
2023-07-01 13:05:59 +00:00
|
|
|
playMedia={playMedia}
|
|
|
|
tracks={tracks()}
|
2023-07-14 13:06:21 +00:00
|
|
|
currentTrack={getCurrentTrack()}
|
2023-07-01 13:05:59 +00:00
|
|
|
articleSlug={props.articleSlug}
|
|
|
|
body={props.body}
|
2023-07-14 13:06:21 +00:00
|
|
|
onAudioChange={handleAudioDescriptionChange}
|
2023-07-01 13:05:59 +00:00
|
|
|
/>
|
|
|
|
</Show>
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
}
|