player.tsx 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. /* eslint-disable jsx-a11y/media-has-caption */
  2. import { useThrottleCallback } from '@react-hook/throttle';
  3. import React, { useCallback, useEffect, useRef, useState } from 'react';
  4. type Props = {
  5. src: string;
  6. playing: boolean;
  7. seekTime: number;
  8. onTimeUpdate: (time: number) => void;
  9. onEnded: () => void;
  10. timeUpdateFPS: number;
  11. };
  12. type AudioState = {
  13. bufferRange: {
  14. start: number;
  15. end: number;
  16. };
  17. time: number;
  18. duration: number;
  19. paused: boolean;
  20. waiting: boolean;
  21. };
  22. const initialAudioState: AudioState = {
  23. bufferRange: { start: 0, end: 0 },
  24. time: 0,
  25. duration: 0,
  26. paused: true,
  27. waiting: false,
  28. };
  29. function parseBufferRange(bufferRange: TimeRanges): AudioState['bufferRange'] {
  30. if (!bufferRange.length) {
  31. return initialAudioState.bufferRange;
  32. }
  33. return { start: bufferRange.start(0), end: bufferRange.end(0) };
  34. }
  35. export const Player: React.FC<Props> = ({
  36. src,
  37. playing,
  38. seekTime,
  39. onTimeUpdate: reportTimeUpdate,
  40. onEnded,
  41. timeUpdateFPS,
  42. }) => {
  43. const audio = useRef<HTMLAudioElement | null>(null);
  44. const [state, setState] = useState(initialAudioState);
  45. const onPlay = useCallback(() => setState((last) => ({ ...last, paused: false })), []);
  46. const onPause = useCallback(() => setState((last) => ({ ...last, paused: true })), []);
  47. const onWaiting = useCallback(() => setState((last) => ({ ...last, waiting: true })), []);
  48. const onPlaying = useCallback(() => setState((last) => ({ ...last, waiting: false })), []);
  49. const onDurationChange = useCallback(() => {
  50. setState((last) => {
  51. if (!audio.current) {
  52. return last;
  53. }
  54. const { duration, buffered } = audio.current;
  55. return {
  56. ...last,
  57. duration,
  58. bufferRange: parseBufferRange(buffered),
  59. };
  60. });
  61. }, []);
  62. const onTimeUpdate = useCallback(() => {
  63. setState((last) => (audio.current ? { ...last, time: audio.current.currentTime } : last));
  64. }, []);
  65. const onTimeUpdateThrottled = useThrottleCallback(onTimeUpdate, timeUpdateFPS);
  66. useEffect(() => {
  67. reportTimeUpdate(state.time);
  68. }, [state.time, reportTimeUpdate]);
  69. const onProgress = useCallback(() => {
  70. setState((last) =>
  71. audio.current ? { ...last, bufferRange: parseBufferRange(audio.current.buffered) } : last,
  72. );
  73. }, []);
  74. const play = useCallback(async () => {
  75. try {
  76. await audio.current?.play();
  77. } catch {
  78. // pass
  79. }
  80. }, []);
  81. const pause = useCallback(() => {
  82. audio.current?.pause();
  83. }, []);
  84. const shouldSeekTo = useRef<number>(-1);
  85. const onCanPlay = useCallback(() => {
  86. if (audio.current) {
  87. if (state.duration && shouldSeekTo.current !== -1) {
  88. audio.current.currentTime = Math.min(state.duration, Math.max(0, shouldSeekTo.current));
  89. shouldSeekTo.current = -1;
  90. }
  91. }
  92. }, [state.duration]);
  93. const seek = useCallback(
  94. (time: number) => {
  95. if (audio.current && state.duration) {
  96. audio.current.currentTime = Math.min(state.duration, Math.max(0, time));
  97. } else {
  98. shouldSeekTo.current = time;
  99. }
  100. },
  101. [state.duration],
  102. );
  103. const lastSeekTime = useRef<number>(-1);
  104. useEffect(() => {
  105. if (seekTime !== lastSeekTime.current && seekTime !== -1) {
  106. lastSeekTime.current = seekTime;
  107. seek(seekTime);
  108. }
  109. }, [seekTime, seek]);
  110. useEffect(() => {
  111. if (playing) {
  112. play();
  113. } else {
  114. pause();
  115. }
  116. }, [src, playing, play, pause]);
  117. return (
  118. <audio
  119. controls={false}
  120. onCanPlay={onCanPlay}
  121. onDurationChange={onDurationChange}
  122. onPause={onPause}
  123. onPlay={onPlay}
  124. onPlaying={onPlaying}
  125. onProgress={onProgress}
  126. onTimeUpdate={onTimeUpdateThrottled}
  127. onWaiting={onWaiting}
  128. onEnded={onEnded}
  129. ref={audio}
  130. src={src}
  131. />
  132. );
  133. };