2024-02-04 11:25:21 +00:00
|
|
|
import { Show, createEffect, createMemo, createSignal, on, onMount } from 'solid-js'
|
2023-11-14 15:10:00 +00:00
|
|
|
|
2024-06-24 17:50:27 +00:00
|
|
|
import { MediaItem } from '~/types/mediaitem'
|
2023-11-14 15:10:00 +00:00
|
|
|
|
2023-07-01 13:05:59 +00:00
|
|
|
import { PlayerHeader } from './PlayerHeader'
|
|
|
|
import { PlayerPlaylist } from './PlayerPlaylist'
|
2023-11-14 15:10:00 +00:00
|
|
|
|
2023-07-01 13:05:59 +00:00
|
|
|
import styles from './AudioPlayer.module.scss'
|
2023-07-14 13:06:21 +00:00
|
|
|
|
|
|
|
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
|
2024-06-24 17:50:27 +00:00
|
|
|
onMediaItemFieldChange?: (
|
|
|
|
index: number,
|
|
|
|
field: keyof MediaItem | string | number | symbol,
|
|
|
|
value: string,
|
|
|
|
) => void
|
|
|
|
onChangeMediaIndex?: (direction: 'up' | 'down', index: number) => void
|
2023-07-01 13:05:59 +00:00
|
|
|
}
|
|
|
|
|
2024-01-17 12:00:57 +00:00
|
|
|
const getFormattedTime = (point: number) => new Date(point * 1000).toISOString().slice(14, -5)
|
2023-07-01 13:05:59 +00:00
|
|
|
|
2023-07-14 13:06:21 +00:00
|
|
|
export const AudioPlayer = (props: Props) => {
|
2024-06-24 17:50:27 +00:00
|
|
|
let audioRef: HTMLAudioElement | undefined
|
|
|
|
let gainNodeRef: GainNode | undefined
|
|
|
|
let progressRef: HTMLDivElement | undefined
|
|
|
|
let audioContextRef: AudioContext | undefined
|
|
|
|
let mouseDownRef: boolean | undefined
|
2023-07-18 11:26:32 +00:00
|
|
|
|
|
|
|
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()])
|
2024-05-20 23:15:52 +00:00
|
|
|
createEffect(on(currentTrackIndex, () => setCurrentTrackDuration(0), { defer: true }))
|
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
|
|
|
|
2024-06-24 17:50:27 +00:00
|
|
|
if (audioContextRef?.state === 'suspended') {
|
|
|
|
await audioContextRef?.resume()
|
2023-07-18 11:26:32 +00:00
|
|
|
}
|
2023-07-01 13:05:59 +00:00
|
|
|
|
2023-07-18 11:26:32 +00:00
|
|
|
if (isPlaying()) {
|
2024-06-24 17:50:27 +00:00
|
|
|
await audioRef?.play()
|
2023-07-01 13:05:59 +00:00
|
|
|
} else {
|
2024-06-24 17:50:27 +00:00
|
|
|
audioRef?.pause()
|
2023-07-01 13:05:59 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-18 11:26:32 +00:00
|
|
|
const handleVolumeChange = (volume: number) => {
|
2024-06-24 17:50:27 +00:00
|
|
|
if (gainNodeRef) gainNodeRef.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
|
|
|
|
}
|
|
|
|
|
2024-06-24 17:50:27 +00:00
|
|
|
if (audioRef) audioRef.currentTime = 0
|
2023-07-18 11:26:32 +00:00
|
|
|
setIsPlaying(false)
|
|
|
|
setCurrentTrackIndex(0)
|
2023-07-01 13:05:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const handleAudioTimeUpdate = () => {
|
2024-06-24 17:50:27 +00:00
|
|
|
setCurrentTime(audioRef?.currentTime || 0)
|
2023-07-01 13:05:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
onMount(() => {
|
2024-06-24 17:50:27 +00:00
|
|
|
audioContextRef = new AudioContext()
|
|
|
|
gainNodeRef = audioContextRef.createGain()
|
|
|
|
if (audioRef) {
|
|
|
|
const track = audioContextRef?.createMediaElementSource(audioRef)
|
|
|
|
track.connect(gainNodeRef).connect(audioContextRef?.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
|
|
|
}
|
|
|
|
|
2024-06-24 17:50:27 +00:00
|
|
|
const handleMediaItemFieldChange = (
|
|
|
|
index: number,
|
|
|
|
field: keyof MediaItem | string | number | symbol,
|
|
|
|
value: string,
|
|
|
|
) => {
|
|
|
|
props.onMediaItemFieldChange?.(index, field, value)
|
2023-07-01 13:05:59 +00:00
|
|
|
}
|
|
|
|
|
2024-06-24 17:50:27 +00:00
|
|
|
const scrub = (event: MouseEvent | undefined) => {
|
|
|
|
if (progressRef && audioRef) {
|
|
|
|
audioRef.currentTime = (event?.offsetX || 0 / progressRef.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}
|
2024-06-24 17:50:27 +00:00
|
|
|
ref={(el) => (progressRef = el)}
|
|
|
|
onClick={scrub}
|
|
|
|
onMouseMove={(e) => mouseDownRef && scrub(e)}
|
|
|
|
onMouseDown={() => (mouseDownRef = true)}
|
|
|
|
onMouseUp={() => (mouseDownRef = false)}
|
2023-07-01 13:05:59 +00:00
|
|
|
>
|
2023-07-18 11:26:32 +00:00
|
|
|
<div
|
|
|
|
class={styles.progressFilled}
|
|
|
|
style={{
|
2023-11-14 15:10:00 +00:00
|
|
|
width: `${(currentTime() / currentTrackDuration()) * 100 || 0}%`,
|
2023-07-18 11:26:32 +00:00
|
|
|
}}
|
|
|
|
/>
|
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
|
2024-06-24 17:50:27 +00:00
|
|
|
ref={(el) => (audioRef = el)}
|
2023-07-01 13:05:59 +00:00
|
|
|
onTimeUpdate={handleAudioTimeUpdate}
|
2024-01-15 22:36:08 +00:00
|
|
|
src={currentTack().url.replace('images.discours.io', 'cdn.discours.io')}
|
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
|
2024-06-24 17:50:27 +00:00
|
|
|
if (isPlaying() && audioRef) {
|
|
|
|
audioRef.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}
|
2024-06-24 17:50:27 +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>
|
|
|
|
)
|
|
|
|
}
|