audio-player

Next

A customizable audio player component with multiple variants and features including progress bars, playback controls, and metadata display.

Docs
austin-uiblock

Preview

Loading preview…
registry/hooks/use-audio-playback.ts
// ./registry/hooks/use-audio-playback.ts

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

export interface UseAudioPlaybackOptions {
  onPlay?: () => void;
  onPause?: () => void;
  onEnd?: () => void;
  onError?: (error: Error) => void;
}

export interface UseAudioPlaybackReturn {
  isPlaying: boolean;
  audioUrl: string;
  playbackTime: number;
  duration: number;
  audioLoaded: boolean;
  audioRef: React.RefObject<HTMLAudioElement | null>;
  togglePlayback: () => Promise<void>;
  stop: () => void;
  seek: (time: number) => void;
  setPlaybackRate: (rate: number) => void;
  resetAudio: () => void;
}

export const useAudioPlayback = (
  source: File | Blob | string | null,
  options?: UseAudioPlaybackOptions
): UseAudioPlaybackReturn => {
  const [isPlaying, setIsPlaying] = useState(false);
  const [audioUrl, setAudioUrl] = useState<string>("");
  const [playbackTime, setPlaybackTime] = useState(0);
  const [duration, setDuration] = useState(0);
  const [audioLoaded, setAudioLoaded] = useState(false);

  const audioRef = useRef<HTMLAudioElement>(null);
  const animationRef = useRef<number | null>(null);
  const objectUrlRef = useRef<string | null>(null);

  // Update audio URL when source changes
  useEffect(() => {
    // Clean up previous object URL
    if (objectUrlRef.current) {
      URL.revokeObjectURL(objectUrlRef.current);
      objectUrlRef.current = null;
    }

    setAudioLoaded(false);
    setDuration(0);
    setPlaybackTime(0);
    setIsPlaying(false);

    if (source instanceof File || source instanceof Blob) {
      const url = URL.createObjectURL(source);
      objectUrlRef.current = url;
      setAudioUrl(url);
    } else if (typeof source === "string") {
      setAudioUrl(source);
    } else {
      setAudioUrl("");
    }
  }, [source]);

  // Cleanup on unmount
  useEffect(() => {
    return () => {
      if (animationRef.current) {
        cancelAnimationFrame(animationRef.current);
      }
      if (objectUrlRef.current) {
        URL.revokeObjectURL(objectUrlRef.current);
      }
    };
  }, []);

  const updatePlaybackTime = () => {
    if (audioRef.current && isPlaying && !audioRef.current.paused) {
      setPlaybackTime(audioRef.current.currentTime);
      animationRef.current = requestAnimationFrame(updatePlaybackTime);
    }
  };

  const togglePlayback = async () => {
    if (!audioRef.current || !audioLoaded) return;

    try {
      if (isPlaying) {
        audioRef.current.pause();
        setIsPlaying(false);
        if (animationRef.current) {
          cancelAnimationFrame(animationRef.current);
          animationRef.current = null;
        }
        options?.onPause?.();
      } else {
        await audioRef.current.play();
        setIsPlaying(true);
        updatePlaybackTime();
        options?.onPlay?.();
      }
    } catch (error) {
      console.error("Error playing audio:", error);
      setIsPlaying(false);
      options?.onError?.(error as Error);
    }
  };

  const stop = () => {
    if (!audioRef.current) return;
    
    audioRef.current.pause();
    audioRef.current.currentTime = 0;
    setIsPlaying(false);
    setPlaybackTime(0);
    
    if (animationRef.current) {
      cancelAnimationFrame(animationRef.current);
      animationRef.current = null;
    }
  };

  const seek = (time: number) => {
    if (!audioRef.current) return;
    audioRef.current.currentTime = Math.max(0, Math.min(time, duration));
    setPlaybackTime(audioRef.current.currentTime);
  };

  const setPlaybackRate = (rate: number) => {
    if (!audioRef.current) return;
    audioRef.current.playbackRate = rate;
  };

  const resetAudio = () => {
    stop();
    
    if (objectUrlRef.current) {
      URL.revokeObjectURL(objectUrlRef.current);
      objectUrlRef.current = null;
    }

    setDuration(0);
    setAudioLoaded(false);
    setAudioUrl("");
  };

  // Audio event handlers
  const handleLoadedMetadata = () => {
    if (
      audioRef.current &&
      !isNaN(audioRef.current.duration) &&
      isFinite(audioRef.current.duration)
    ) {
      setDuration(audioRef.current.duration);
      setAudioLoaded(true);
    }
  };

  const handleCanPlay = () => {
    if (
      audioRef.current &&
      !isNaN(audioRef.current.duration) &&
      isFinite(audioRef.current.duration)
    ) {
      setDuration(audioRef.current.duration);
      setAudioLoaded(true);
    }
  };

  const handleTimeUpdate = () => {
    if (audioRef.current && isPlaying) {
      setPlaybackTime(audioRef.current.currentTime);
    }
  };

  const handleEnded = () => {
    setIsPlaying(false);
    setPlaybackTime(0);
    if (animationRef.current) {
      cancelAnimationFrame(animationRef.current);
      animationRef.current = null;
    }
    if (audioRef.current) {
      audioRef.current.currentTime = 0;
    }
    options?.onEnd?.();
  };

  const handleError = (e: React.SyntheticEvent<HTMLAudioElement>) => {
    console.error("Audio error:", e);
    setAudioLoaded(false);
    setIsPlaying(false);
    options?.onError?.(new Error("Error loading audio file"));
  };

  // Attach event handlers
  useEffect(() => {
    const audio = audioRef.current;
    if (!audio) return;

    audio.addEventListener("loadedmetadata", handleLoadedMetadata);
    audio.addEventListener("canplay", handleCanPlay);
    audio.addEventListener("timeupdate", handleTimeUpdate);
    audio.addEventListener("ended", handleEnded);
    audio.addEventListener("error", handleError as any);

    return () => {
      audio.removeEventListener("loadedmetadata", handleLoadedMetadata);
      audio.removeEventListener("canplay", handleCanPlay);
      audio.removeEventListener("timeupdate", handleTimeUpdate);
      audio.removeEventListener("ended", handleEnded);
      audio.removeEventListener("error", handleError as any);
    };
  }, [isPlaying, options]);

  return {
    isPlaying,
    audioUrl,
    playbackTime,
    duration,
    audioLoaded,
    audioRef,
    togglePlayback,
    stop,
    seek,
    setPlaybackRate,
    resetAudio,
  };
};

Installation

npx shadcn@latest add @austin-ui/audio-player

Usage

import { AudioPlayer } from "@/components/audio-player"
<AudioPlayer />