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

178 lines
5.4 KiB
TypeScript
Raw Normal View History

2024-02-04 11:25:21 +00:00
import { Show, createEffect, createMemo, createSignal, on, onMount } from 'solid-js'
2024-06-24 17:50:27 +00:00
import { MediaItem } from '~/types/mediaitem'
import { PlayerHeader } from './PlayerHeader'
import { PlayerPlaylist } from './PlayerPlaylist'
import styles from './AudioPlayer.module.scss'
type Props = {
media: MediaItem[]
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
}
2024-01-17 12:00:57 +00:00
const getFormattedTime = (point: number) => new Date(point * 1000).toISOString().slice(14, -5)
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
const [currentTrackDuration, setCurrentTrackDuration] = createSignal(0)
const [currentTime, setCurrentTime] = createSignal(0)
const [currentTrackIndex, setCurrentTrackIndex] = createSignal<number>(0)
const [isPlaying, setIsPlaying] = createSignal(false)
const currentTack = createMemo(() => props.media[currentTrackIndex()])
2024-05-20 23:15:52 +00:00
createEffect(on(currentTrackIndex, () => setCurrentTrackDuration(0), { defer: true }))
const handlePlayMedia = async (trackIndex: number) => {
setIsPlaying(!isPlaying() || trackIndex !== currentTrackIndex())
setCurrentTrackIndex(trackIndex)
2024-06-24 17:50:27 +00:00
if (audioContextRef?.state === 'suspended') {
await audioContextRef?.resume()
}
if (isPlaying()) {
2024-06-24 17:50:27 +00:00
await audioRef?.play()
} else {
2024-06-24 17:50:27 +00:00
audioRef?.pause()
}
}
const handleVolumeChange = (volume: number) => {
2024-06-24 17:50:27 +00:00
if (gainNodeRef) gainNodeRef.gain.value = volume
}
const handleAudioEnd = () => {
if (currentTrackIndex() < props.media.length - 1) {
playNextTrack()
return
}
2024-06-24 17:50:27 +00:00
if (audioRef) audioRef.currentTime = 0
setIsPlaying(false)
setCurrentTrackIndex(0)
}
const handleAudioTimeUpdate = () => {
2024-06-24 17:50:27 +00:00
setCurrentTime(audioRef?.currentTime || 0)
}
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)
}
})
const playPrevTrack = () => {
let newCurrentTrackIndex = currentTrackIndex() - 1
if (newCurrentTrackIndex < 0) {
newCurrentTrackIndex = 0
}
setCurrentTrackIndex(newCurrentTrackIndex)
}
const playNextTrack = () => {
let newCurrentTrackIndex = currentTrackIndex() + 1
if (newCurrentTrackIndex > props.media.length - 1) {
newCurrentTrackIndex = props.media.length - 1
}
setCurrentTrackIndex(newCurrentTrackIndex)
}
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)
}
2024-06-24 17:50:27 +00:00
const scrub = (event: MouseEvent | undefined) => {
if (progressRef && audioRef) {
audioRef.currentTime = (event?.offsetX || 0 / progressRef.offsetWidth) * currentTrackDuration()
}
}
return (
<div>
<Show when={props.media}>
<PlayerHeader
onPlayMedia={() => handlePlayMedia(currentTrackIndex())}
playPrevTrack={playPrevTrack}
playNextTrack={playNextTrack}
onVolumeChange={handleVolumeChange}
isPlaying={isPlaying()}
currentTrack={currentTack()}
/>
<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)}
>
<div
class={styles.progressFilled}
style={{
width: `${(currentTime() / currentTrackDuration()) * 100 || 0}%`,
}}
/>
</div>
<div class={styles.progressTiming}>
<span>{getFormattedTime(currentTime())}</span>
<Show when={currentTrackDuration() > 0}>
<span>{getFormattedTime(currentTrackDuration())}</span>
</Show>
</div>
<audio
2024-06-24 17:50:27 +00:00
ref={(el) => (audioRef = el)}
onTimeUpdate={handleAudioTimeUpdate}
2024-01-15 22:36:08 +00:00
src={currentTack().url.replace('images.discours.io', 'cdn.discours.io')}
onCanPlay={() => {
// start to play the next track on src change
2024-06-24 17:50:27 +00:00
if (isPlaying() && audioRef) {
audioRef.play()
}
}}
onLoadedMetadata={({ currentTarget }) => setCurrentTrackDuration(currentTarget.duration)}
onEnded={handleAudioEnd}
crossorigin="anonymous"
/>
</div>
<PlayerPlaylist
editorMode={props.editorMode}
onPlayMedia={handlePlayMedia}
2024-06-24 17:50:27 +00:00
onChangeMediaIndex={(direction, index) => props.onChangeMediaIndex?.(direction, index)}
isPlaying={isPlaying()}
media={props.media}
currentTrackIndex={currentTrackIndex()}
articleSlug={props.articleSlug}
body={props.body}
onMediaItemFieldChange={handleMediaItemFieldChange}
/>
</Show>
</div>
)
}