view react_games/src/API_works/music_player.tsx @ 139:e8f693bece90

test again
author June Park <parkjune1995@gmail.com>
date Fri, 09 Jan 2026 12:29:20 -0800
parents fb9bcd3145cb
children
line wrap: on
line source

import { useCallback, useEffect, useRef, useState } from "react";

const CHAPTERS: string[] = [
  "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3",
  "https://www.learningcontainer.com/wp-content/uploads/2020/02/Kalimba.mp3",
  "https://download.samplelib.com/mp3/sample-3s.mp3",
];

type AudioComponentProp = {
  srcUrl: string;
  onNext?: () => void;
  onPrev?: () => void;
};

function fmt(t: number) {
  if (!isFinite(t)) return "0:00";
  const m = Math.floor(t / 60);
  const s = Math.floor(t % 60);
  return `${m}:${s.toString().padStart(2, "0")}`;
}

const AudioComponent = ({ srcUrl, onNext, onPrev }: AudioComponentProp) => {
  const audioRef = useRef<HTMLAudioElement>(null);

  const [isPlaying, setIsPlaying] = useState(false);
  const [current, setCurrent] = useState(0);
  const [duration, setDuration] = useState(0);

  // If true, auto-play after src changes and can play.
  const autoplayNextRef = useRef(false);

  // Wire up element events once.
  useEffect(() => {
    const el = audioRef.current;
    if (!el) return;

    const onPlay = () => setIsPlaying(true);
    const onPause = () => setIsPlaying(false);
    const onTime = () => setCurrent(el.currentTime || 0);
    const onMeta = () => setDuration(isFinite(el.duration) ? el.duration : 0);
    const onEnded = () => {
      if (!onNext) return;
      autoplayNextRef.current = true;   // keep playing the next track
      onNext();
    };

    el.addEventListener("play", onPlay);
    el.addEventListener("pause", onPause);
    el.addEventListener("timeupdate", onTime);
    el.addEventListener("loadedmetadata", onMeta);
    el.addEventListener("ended", onEnded);

    // Prime initial state
    onMeta();
    onTime();
    setIsPlaying(!el.paused && !el.ended);

    return () => {
      el.removeEventListener("play", onPlay);
      el.removeEventListener("pause", onPause);
      el.removeEventListener("timeupdate", onTime);
      el.removeEventListener("loadedmetadata", onMeta);
      el.removeEventListener("ended", onEnded);
    };
  }, [onNext]);

  // When src changes, load and maybe auto-play when ready.
  useEffect(() => {
    const el = audioRef.current;
    if (!el) return;

    el.load(); // pick up new src

    const tryPlay = () => {
      if (autoplayNextRef.current) {
        autoplayNextRef.current = false;
        el.play().catch(() => {
          // Autoplay might be blocked; leave paused state as-is.
        });
      }
    };

    // Play once ready (covers both cached and freshly loaded cases)
    el.addEventListener("canplay", tryPlay, { once: true });
    // In case it's already buffered:
    tryPlay();

    // Reset progress immediately on new track
    setCurrent(0);
  }, [srcUrl]);

  const toggle = useCallback(() => {
    const el = audioRef.current;
    if (!el) return;
    if (el.paused) el.play().catch(() => {});
    else el.pause();
  }, []);

  const seek = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
    const el = audioRef.current;
    if (!el) return;
    el.currentTime = Number(e.target.value);
  }, []);

  // Button handlers: set the autoplay flag so next track starts immediately.
  const handleNext = useCallback(() => {
    if (!onNext) return;
    autoplayNextRef.current = true; // user gesture → autoplay allowed
    onNext();
  }, [onNext]);

  const handlePrev = useCallback(() => {
    if (!onPrev) return;
    autoplayNextRef.current = true;
    onPrev();
  }, [onPrev]);

  const pct = duration > 0 ? (current / duration) * 100 : 0;

  return (
    <>
      {/* remove "controls" if you want only custom UI */}
      <audio ref={audioRef} src={srcUrl} preload="metadata" />
      <div style={{ display: "flex", gap: 8, alignItems: "center", margin: "8px 0" }}>
        <button onClick={toggle}>{isPlaying ? "Pause" : "Play"}</button>
        <button onClick={handlePrev} disabled={!onPrev}>Prev</button>
        <button onClick={handleNext} disabled={!onNext}>Next</button>
"        <span style={{ marginLeft: 8, width: 56, textAlign: "right" }}>{fmt(current)}</span>",
        <input
          type="range"
          min={0}
          max={duration || 0}
          step={0.01}
          value={current}
          onChange={seek}
          style={{ flex: 1 }}
        />
        <span style={{ width: 56 }}>{fmt(duration)}</span>
      </div>
      {/* Optional thin progress bar */}
      <div style={{ height: 4, width: "100%", background: "#eee", borderRadius: 2 }}>
        <div style={{ height: "100%", width: `${pct}%`, background: "#999", borderRadius: 2 }} />
      </div>
    </>
  );
};

const MusicPlayer = () => {
  const [musicIndex, setMusicIndex] = useState<number>(0);

  const onNext = useCallback(
    () => setMusicIndex((i) => Math.min(i + 1, CHAPTERS.length - 1)),
    []
  );
  const onPrev = useCallback(() => setMusicIndex((i) => Math.max(i - 1, 0)), []);

  return (
    <AudioComponent
      srcUrl={CHAPTERS[musicIndex]}
      onNext={musicIndex === CHAPTERS.length - 1 ? undefined : onNext}
      onPrev={musicIndex === 0 ? undefined : onPrev}
    />
  );
};

export {
  MusicPlayer,
}