import React, { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, } from 'react'; import { View } from 'react-native'; import type { AudioTagHandle, AudioProps, AudioTagPlaybackState, } from './types'; import { AudioComponentContext } from './AudioTagContext'; import { useStableAudioProps } from './utils'; const Audio = React.forwardRef((props, ref) => { const { children } = props; const { autoPlay, controls, loop, muted, preload, source, playbackRate, preservesPitch, volume, onLoadStart, onLoad, onError, onPositionChange, onEnded: onEndedCallback, onPlay, onPause, onVolumeChange, } = useStableAudioProps(props); const audioRef = useRef(null); const [volumeState, setVolumeState] = useState(volume); const [mutedState, setMutedState] = useState(null); const [ready, setReady] = useState(false); const [playbackState, setPlaybackState] = useState('idle'); const [currentTime, setCurrentTime] = useState(0); const [duration, setDuration] = useState(0); const lastElementVolumeRef = useRef(muted ? 0 : volume); const effectiveMutedState = useMemo(() => { return mutedState ?? muted; }, [mutedState, muted]); const effectiveVolumeState = useMemo(() => { return volumeState ?? volume; }, [volumeState, volume]); const reportedVolumeState = useMemo(() => { return effectiveMutedState ? 0 : effectiveVolumeState; }, [effectiveMutedState, effectiveVolumeState]); const path = useMemo(() => { if (!source) { return ''; } if (typeof source === 'string') { return source; } if (typeof source === 'number') { throw new Error('Asset source is not supported on web'); } return source.uri ?? ''; }, [source]); useEffect(() => { const el = audioRef.current; if (!el) { return; } el.loop = loop; el.playbackRate = playbackRate; // `preservesPitch` is non-standard but supported in modern browsers. (el as HTMLAudioElement & { preservesPitch?: boolean }).preservesPitch = preservesPitch; }, [loop, playbackRate, preservesPitch]); useEffect(() => { const el = audioRef.current; if (!el) { return; } el.volume = effectiveVolumeState; }, [effectiveVolumeState]); useEffect(() => { const el = audioRef.current; if (!el) { return; } el.muted = effectiveMutedState; }, [effectiveMutedState]); useEffect(() => { const el = audioRef.current; if (!el) { return; } const handleTimeUpdate = () => { setCurrentTime(el.currentTime); onPositionChange(el.currentTime); }; const handleVolumeChange = () => { const nextMuted = el.muted; const nextVolume = nextMuted ? 0 : el.volume; setMutedState(nextMuted); setVolumeState(el.volume); if (lastElementVolumeRef.current !== nextVolume) { lastElementVolumeRef.current = nextVolume; onVolumeChange(nextVolume); } }; el.addEventListener('timeupdate', handleTimeUpdate); el.addEventListener('volumechange', handleVolumeChange); setDuration(el.duration); setCurrentTime(el.currentTime); lastElementVolumeRef.current = el.muted ? 0 : el.volume; return () => { el.removeEventListener('timeupdate', handleTimeUpdate); el.removeEventListener('volumechange', handleVolumeChange); }; }, [onPositionChange, onVolumeChange, ready]); const play = useCallback(() => { audioRef.current?.play(); setPlaybackState('playing'); onPlay(); }, [onPlay]); const pause = useCallback(() => { audioRef.current?.pause(); setPlaybackState('paused'); onPause(); }, [onPause]); const seekToTime = useCallback( (seconds: number) => { const el = audioRef.current; if (!el) { return; } const nextTime = duration > 0 ? Math.max(0, Math.min(seconds, duration)) : Math.max(0, seconds); el.currentTime = nextTime; setCurrentTime(nextTime); }, [duration] ); const setVolume = useCallback((next: number) => { setVolumeState(next); }, []); const setMuted = useCallback((next: boolean) => { setMutedState(next); }, []); useImperativeHandle( ref, () => ({ play, pause, seekToTime, setVolume, setMuted, }), [pause, play, seekToTime, setMuted, setVolume] ); const ctxValue = useMemo( () => ({ play, pause, seekToTime, setVolume, ready, volume: reportedVolumeState, setMuted, muted: effectiveMutedState, playbackState, currentTime, duration, autoPlay, controls, loop, preload, playbackRate, preservesPitch, audioContext: null, }), [ play, pause, seekToTime, setVolume, ready, reportedVolumeState, setMuted, effectiveMutedState, playbackState, currentTime, duration, autoPlay, controls, loop, preload, playbackRate, preservesPitch, ] ); return ( ); }); export default Audio;