2023-07-18 11:26:32 +00:00
|
|
|
import { createEffect, createMemo, 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'
|
|
|
|
|
|
|
|
type Props = {
|
2023-07-18 11:26:32 +00:00
|
|
|
media: MediaItem[]
|
2023-07-14 13:06:21 +00:00
|
|
|
articleSlug?: string
|
|
|
|
body?: string
|
|
|
|
editorMode?: boolean
|
2023-07-18 11:26:32 +00:00
|
|
|
onMediaItemFieldChange?: (index: number, field: keyof MediaItem, value: string) => void
|
2023-07-18 19:11:00 +00:00
|
|
|
onChangeMediaIndex?: (direction: 'up' | 'down', index) => void
|
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 }
|
2023-07-18 11:26:32 +00:00
|
|
|
const gainNodeRef: { current: GainNode } = { current: null }
|
2023-07-14 13:06:21 +00:00
|
|
|
const progressRef: { current: HTMLDivElement } = { current: null }
|
2023-07-18 11:26:32 +00:00
|
|
|
const audioContextRef: { current: AudioContext } = { current: null }
|
|
|
|
const mouseDownRef: { current: boolean } = { current: false }
|
|
|
|
|
|
|
|
const [currentTrackDuration, setCurrentTrackDuration] = createSignal(0)
|
|
|
|
const [currentTime, setCurrentTime] = createSignal(0)
|
|
|
|
const [currentTrackIndex, setCurrentTrackIndex] = createSignal<number>(0)
|
|
|
|
const [isPlaying, setIsPlaying] = createSignal(false)
|
2023-07-01 13:05:59 +00:00
|
|
|
|
2023-07-18 11:26:32 +00:00
|
|
|
const currentTack = createMemo(() => props.media[currentTrackIndex()])
|
2023-07-01 13:05:59 +00:00
|
|
|
|
2023-07-14 13:06:21 +00:00
|
|
|
createEffect(
|
|
|
|
on(
|
2023-07-18 11:26:32 +00:00
|
|
|
() => currentTrackIndex(),
|
2023-07-14 13:06:21 +00:00
|
|
|
() => {
|
2023-07-18 11:26:32 +00:00
|
|
|
setCurrentTrackDuration(0)
|
2023-09-01 14:28:50 +00:00
|
|
|
},
|
|
|
|
{ defer: true }
|
2023-07-14 13:06:21 +00:00
|
|
|
)
|
|
|
|
)
|
2023-07-01 13:05:59 +00:00
|
|
|
|
2023-07-18 11:26:32 +00:00
|
|
|
const handlePlayMedia = async (trackIndex: number) => {
|
|
|
|
setIsPlaying(!isPlaying() || trackIndex !== currentTrackIndex())
|
|
|
|
setCurrentTrackIndex(trackIndex)
|
2023-07-01 13:05:59 +00:00
|
|
|
|
2023-07-18 11:26:32 +00:00
|
|
|
if (audioContextRef.current.state === 'suspended') {
|
|
|
|
await audioContextRef.current.resume()
|
|
|
|
}
|
2023-07-01 13:05:59 +00:00
|
|
|
|
2023-07-18 11:26:32 +00:00
|
|
|
if (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
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-18 11:26:32 +00:00
|
|
|
const handleVolumeChange = (volume: number) => {
|
|
|
|
gainNodeRef.current.gain.value = volume
|
2023-07-01 13:05:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const handleAudioEnd = () => {
|
2023-07-18 11:26:32 +00:00
|
|
|
if (currentTrackIndex() < props.media.length - 1) {
|
|
|
|
playNextTrack()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-07-14 13:06:21 +00:00
|
|
|
audioRef.current.currentTime = 0
|
2023-07-18 11:26:32 +00:00
|
|
|
setIsPlaying(false)
|
|
|
|
setCurrentTrackIndex(0)
|
2023-07-01 13:05:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const handleAudioTimeUpdate = () => {
|
2023-07-18 11:26:32 +00:00
|
|
|
setCurrentTime(audioRef.current.currentTime)
|
2023-07-01 13:05:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
onMount(() => {
|
2023-07-18 11:26:32 +00:00
|
|
|
audioContextRef.current = new AudioContext()
|
|
|
|
gainNodeRef.current = audioContextRef.current.createGain()
|
2023-07-01 13:05:59 +00:00
|
|
|
|
2023-07-18 11:26:32 +00:00
|
|
|
const track = audioContextRef.current.createMediaElementSource(audioRef.current)
|
|
|
|
track.connect(gainNodeRef.current).connect(audioContextRef.current.destination)
|
2023-07-01 13:05:59 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
const playPrevTrack = () => {
|
2023-07-18 11:26:32 +00:00
|
|
|
let newCurrentTrackIndex = currentTrackIndex() - 1
|
|
|
|
if (newCurrentTrackIndex < 0) {
|
|
|
|
newCurrentTrackIndex = 0
|
|
|
|
}
|
|
|
|
|
|
|
|
setCurrentTrackIndex(newCurrentTrackIndex)
|
2023-07-01 13:05:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const playNextTrack = () => {
|
2023-07-18 11:26:32 +00:00
|
|
|
let newCurrentTrackIndex = currentTrackIndex() + 1
|
|
|
|
if (newCurrentTrackIndex > props.media.length - 1) {
|
|
|
|
newCurrentTrackIndex = props.media.length - 1
|
|
|
|
}
|
|
|
|
|
|
|
|
setCurrentTrackIndex(newCurrentTrackIndex)
|
2023-07-01 13:05:59 +00:00
|
|
|
}
|
|
|
|
|
2023-07-18 11:26:32 +00:00
|
|
|
const handleMediaItemFieldChange = (index: number, field: keyof MediaItem, value) => {
|
|
|
|
props.onMediaItemFieldChange(index, field, value)
|
2023-07-01 13:05:59 +00:00
|
|
|
}
|
|
|
|
|
2023-07-18 11:26:32 +00:00
|
|
|
const scrub = (event) => {
|
|
|
|
audioRef.current.currentTime =
|
|
|
|
(event.offsetX / progressRef.current.offsetWidth) * currentTrackDuration()
|
2023-07-14 13:06:21 +00:00
|
|
|
}
|
|
|
|
|
2023-07-01 13:05:59 +00:00
|
|
|
return (
|
|
|
|
<div>
|
2023-07-18 11:26:32 +00:00
|
|
|
<Show when={props.media}>
|
2023-07-01 13:05:59 +00:00
|
|
|
<PlayerHeader
|
2023-07-18 11:26:32 +00:00
|
|
|
onPlayMedia={() => handlePlayMedia(currentTrackIndex())}
|
2023-07-01 13:05:59 +00:00
|
|
|
playPrevTrack={playPrevTrack}
|
|
|
|
playNextTrack={playNextTrack}
|
2023-07-18 11:26:32 +00:00
|
|
|
onVolumeChange={handleVolumeChange}
|
|
|
|
isPlaying={isPlaying()}
|
|
|
|
currentTrack={currentTack()}
|
2023-07-01 13:05:59 +00:00
|
|
|
/>
|
|
|
|
<div class={styles.timeline}>
|
|
|
|
<div
|
|
|
|
class={styles.progress}
|
2023-07-14 13:06:21 +00:00
|
|
|
ref={(el) => (progressRef.current = el)}
|
2023-07-18 11:26:32 +00:00
|
|
|
onClick={(e) => scrub(e)}
|
|
|
|
onMouseMove={(e) => mouseDownRef.current && scrub(e)}
|
|
|
|
onMouseDown={() => (mouseDownRef.current = true)}
|
|
|
|
onMouseUp={() => (mouseDownRef.current = false)}
|
2023-07-01 13:05:59 +00:00
|
|
|
>
|
2023-07-18 11:26:32 +00:00
|
|
|
<div
|
|
|
|
class={styles.progressFilled}
|
|
|
|
style={{
|
|
|
|
width: `${(currentTime() / currentTrackDuration()) * 100 || 0}%`
|
|
|
|
}}
|
|
|
|
/>
|
2023-07-01 13:05:59 +00:00
|
|
|
</div>
|
|
|
|
<div class={styles.progressTiming}>
|
2023-07-18 11:26:32 +00:00
|
|
|
<span>{getFormattedTime(currentTime())}</span>
|
|
|
|
<Show when={currentTrackDuration() > 0}>
|
|
|
|
<span>{getFormattedTime(currentTrackDuration())}</span>
|
|
|
|
</Show>
|
2023-07-01 13:05:59 +00:00
|
|
|
</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}
|
2023-11-13 14:43:08 +00:00
|
|
|
src={currentTack().url}
|
2023-07-01 13:05:59 +00:00
|
|
|
onCanPlay={() => {
|
2023-07-18 11:26:32 +00:00
|
|
|
// start to play the next track on src change
|
|
|
|
if (isPlaying()) {
|
2023-07-14 13:06:21 +00:00
|
|
|
audioRef.current.play()
|
2023-07-01 13:05:59 +00:00
|
|
|
}
|
|
|
|
}}
|
2023-07-18 11:26:32 +00:00
|
|
|
onLoadedMetadata={({ currentTarget }) => setCurrentTrackDuration(currentTarget.duration)}
|
2023-07-01 13:05:59 +00:00
|
|
|
onEnded={handleAudioEnd}
|
|
|
|
crossorigin="anonymous"
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
<PlayerPlaylist
|
2023-07-14 13:06:21 +00:00
|
|
|
editorMode={props.editorMode}
|
2023-07-18 11:26:32 +00:00
|
|
|
onPlayMedia={handlePlayMedia}
|
2023-07-18 19:11:00 +00:00
|
|
|
onChangeMediaIndex={(direction, index) => props.onChangeMediaIndex(direction, index)}
|
2023-07-18 11:26:32 +00:00
|
|
|
isPlaying={isPlaying()}
|
|
|
|
media={props.media}
|
|
|
|
currentTrackIndex={currentTrackIndex()}
|
2023-07-01 13:05:59 +00:00
|
|
|
articleSlug={props.articleSlug}
|
|
|
|
body={props.body}
|
2023-07-18 11:26:32 +00:00
|
|
|
onMediaItemFieldChange={handleMediaItemFieldChange}
|
2023-07-01 13:05:59 +00:00
|
|
|
/>
|
|
|
|
</Show>
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
}
|