| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150 |
- /* eslint-disable jsx-a11y/media-has-caption */
- import { useThrottleCallback } from '@react-hook/throttle';
- import React, { useCallback, useEffect, useRef, useState } from 'react';
- type Props = {
- src: string;
- playing: boolean;
- seekTime: number;
- onTimeUpdate: (time: number) => void;
- onEnded: () => void;
- timeUpdateFPS: number;
- };
- type AudioState = {
- bufferRange: {
- start: number;
- end: number;
- };
- time: number;
- duration: number;
- paused: boolean;
- waiting: boolean;
- };
- const initialAudioState: AudioState = {
- bufferRange: { start: 0, end: 0 },
- time: 0,
- duration: 0,
- paused: true,
- waiting: false,
- };
- function parseBufferRange(bufferRange: TimeRanges): AudioState['bufferRange'] {
- if (!bufferRange.length) {
- return initialAudioState.bufferRange;
- }
- return { start: bufferRange.start(0), end: bufferRange.end(0) };
- }
- export const Player: React.FC<Props> = ({
- src,
- playing,
- seekTime,
- onTimeUpdate: reportTimeUpdate,
- onEnded,
- timeUpdateFPS,
- }) => {
- const audio = useRef<HTMLAudioElement | null>(null);
- const [state, setState] = useState(initialAudioState);
- const onPlay = useCallback(() => setState((last) => ({ ...last, paused: false })), []);
- const onPause = useCallback(() => setState((last) => ({ ...last, paused: true })), []);
- const onWaiting = useCallback(() => setState((last) => ({ ...last, waiting: true })), []);
- const onPlaying = useCallback(() => setState((last) => ({ ...last, waiting: false })), []);
- const onDurationChange = useCallback(() => {
- setState((last) => {
- if (!audio.current) {
- return last;
- }
- const { duration, buffered } = audio.current;
- return {
- ...last,
- duration,
- bufferRange: parseBufferRange(buffered),
- };
- });
- }, []);
- const onTimeUpdate = useCallback(() => {
- setState((last) => (audio.current ? { ...last, time: audio.current.currentTime } : last));
- }, []);
- const onTimeUpdateThrottled = useThrottleCallback(onTimeUpdate, timeUpdateFPS);
- useEffect(() => {
- reportTimeUpdate(state.time);
- }, [state.time, reportTimeUpdate]);
- const onProgress = useCallback(() => {
- setState((last) =>
- audio.current ? { ...last, bufferRange: parseBufferRange(audio.current.buffered) } : last,
- );
- }, []);
- const play = useCallback(async () => {
- try {
- await audio.current?.play();
- } catch {
- // pass
- }
- }, []);
- const pause = useCallback(() => {
- audio.current?.pause();
- }, []);
- const shouldSeekTo = useRef<number>(-1);
- const onCanPlay = useCallback(() => {
- if (audio.current) {
- if (state.duration && shouldSeekTo.current !== -1) {
- audio.current.currentTime = Math.min(state.duration, Math.max(0, shouldSeekTo.current));
- shouldSeekTo.current = -1;
- }
- }
- }, [state.duration]);
- const seek = useCallback(
- (time: number) => {
- if (audio.current && state.duration) {
- audio.current.currentTime = Math.min(state.duration, Math.max(0, time));
- } else {
- shouldSeekTo.current = time;
- }
- },
- [state.duration],
- );
- const lastSeekTime = useRef<number>(-1);
- useEffect(() => {
- if (seekTime !== lastSeekTime.current && seekTime !== -1) {
- lastSeekTime.current = seekTime;
- seek(seekTime);
- }
- }, [seekTime, seek]);
- useEffect(() => {
- if (playing) {
- play();
- } else {
- pause();
- }
- }, [src, playing, play, pause]);
- return (
- <audio
- controls={false}
- onCanPlay={onCanPlay}
- onDurationChange={onDurationChange}
- onPause={onPause}
- onPlay={onPlay}
- onPlaying={onPlaying}
- onProgress={onProgress}
- onTimeUpdate={onTimeUpdateThrottled}
- onWaiting={onWaiting}
- onEnded={onEnded}
- ref={audio}
- src={src}
- />
- );
- };
|