UNPKG

react-native-audio-api

Version:

react-native-audio-api provides system for controlling audio in React Native environment compatible with Web Audio API specification

288 lines (251 loc) • 7.43 kB
import { Dispatch, RefObject, SetStateAction, useCallback, useEffect, useRef, useState, } from 'react'; import { Platform } from 'react-native'; import type BaseAudioContext from '../core/BaseAudioContext'; import { probeDuration } from '../core/AudioFileUtils'; import { NotSupportedError } from '../errors'; import { NativeAudioAPIModule } from '../specs'; import { base64ToArrayBuffer, isFfmpegEnabled } from '../utils'; import { isRemoteHttpUrl, loadRemoteHttpSource, } from '../utils/remoteHttpSource'; import { AudioFileSourceNode } from './AudioFileSourceNode'; import type { AudioSource, PreloadType } from './types'; import { getSourceHeaders } from './utils'; type UseAudioSourceLoaderParams = { context: BaseAudioContext | undefined; path: string; source: AudioSource; preloadMode: PreloadType; forceDownload: boolean; loop: boolean; playbackRate: number; preservesPitch: boolean; autoPlay: boolean; effectiveVolumeRef: RefObject<number>; onLoadStart: () => void; onLoad: () => void; onError: (error: Error) => void; onEnded: (duration: number) => void; onAutoPlay: () => void; }; type UseAudioSourceLoaderResult = { fileSourceRef: RefObject<AudioFileSourceNode | null>; ready: boolean; duration: number; setDuration: Dispatch<SetStateAction<number>>; loadForPlayback: () => Promise<boolean>; disposeSource: () => void; }; export function useAudioSourceLoader({ context, path, source, preloadMode, forceDownload, loop, playbackRate, preservesPitch, autoPlay, effectiveVolumeRef, onLoadStart, onLoad, onError, onEnded, onAutoPlay, }: UseAudioSourceLoaderParams): UseAudioSourceLoaderResult { const [ready, setReady] = useState(false); const [duration, setDuration] = useState(0); const fileSourceRef = useRef<AudioFileSourceNode>(null); const sourceRef = useRef<ArrayBuffer | string | null>(null); const isFetchingCancelled = useRef(false); const fullDataFetched = useRef(false); const onLoadStartRef = useRef(onLoadStart); onLoadStartRef.current = onLoadStart; const onLoadRef = useRef(onLoad); onLoadRef.current = onLoad; const onErrorRef = useRef(onError); onErrorRef.current = onError; const onEndedRef = useRef(onEnded); onEndedRef.current = onEnded; const onAutoPlayRef = useRef(onAutoPlay); onAutoPlayRef.current = onAutoPlay; const playbackRateRef = useRef(playbackRate); playbackRateRef.current = playbackRate; const preservesPitchRef = useRef(preservesPitch); preservesPitchRef.current = preservesPitch; const disposeSource = useCallback(() => { fileSourceRef.current?.stopPositionTracking(); fileSourceRef.current?.dispose(); sourceRef.current = null; fullDataFetched.current = false; }, []); const spawnFileSource = useCallback((): boolean => { const nextSource = sourceRef.current; if (!context || !nextSource) { return false; } fileSourceRef.current?.dispose(); const initialVolume = effectiveVolumeRef.current; const initialPlaybackRate = playbackRateRef.current; const initialPreservesPitch = preservesPitchRef.current; const node = context.context.createFileSource({ source: nextSource, headers: getSourceHeaders(source), loop, volume: initialVolume, playbackRate: initialPlaybackRate, preservesPitch: initialPreservesPitch, }); if (!node) { onErrorRef.current( new NotSupportedError('This file format requires FFmpeg build') ); return false; } const fileSource = new AudioFileSourceNode(context, node); const { duration: nextDuration } = fileSource.attach({ loop, onEnded: () => { onEndedRef.current(nextDuration); spawnFileSource(); }, }); fileSource.setVolume(initialVolume); fileSource.setPlaybackRate(initialPlaybackRate); fileSource.setPreservesPitch(initialPreservesPitch); fileSourceRef.current = fileSource; setDuration(nextDuration); onLoadRef.current(); if (autoPlay) { onAutoPlayRef.current(); } return true; }, [autoPlay, context, effectiveVolumeRef, loop, source]); const loadPlaybackSource = useCallback(async (): Promise<boolean> => { if (!path) { return false; } isFetchingCancelled.current = false; setReady(false); onLoadStartRef.current(); const headers = getSourceHeaders(source); try { if (isRemoteHttpUrl(path)) { sourceRef.current = await loadRemoteHttpSource( path, headers, forceDownload ); } else if ( Platform.OS === 'android' && !__DEV__ && !path.startsWith('file://') ) { const base64Payload = await NativeAudioAPIModule.readAndroidReleaseAssetBytesAsBase64(path); sourceRef.current = base64ToArrayBuffer(base64Payload); } else if (path.startsWith('file://')) { sourceRef.current = path.replace('file://', ''); } else { sourceRef.current = path; } fullDataFetched.current = true; if (!isFetchingCancelled.current && spawnFileSource()) { setReady(true); return true; } return false; } catch (error) { if (!isFetchingCancelled.current) { onErrorRef.current(error as Error); } setReady(false); return false; } }, [forceDownload, path, source, spawnFileSource]); const probeMetadataOnly = useCallback(async (): Promise<void> => { if (!isRemoteHttpUrl(path)) { setReady(true); return; } isFetchingCancelled.current = false; setReady(false); onLoadStartRef.current(); const headers = getSourceHeaders(source); try { const probedDuration = await probeDuration( path, context?.sampleRate, headers ); if (probedDuration && !isFetchingCancelled.current) { setDuration(probedDuration); } setReady(true); } catch (error) { if (!isFetchingCancelled.current) { onErrorRef.current(error as Error); } setReady(false); } }, [context?.sampleRate, path, source]); const loadForPlayback = useCallback(async (): Promise<boolean> => { if (fullDataFetched.current) { return fileSourceRef.current != null; } return loadPlaybackSource(); }, [loadPlaybackSource]); useEffect(() => { isFetchingCancelled.current = false; fullDataFetched.current = false; if (!path) { setReady(false); setDuration(0); disposeSource(); return () => { isFetchingCancelled.current = true; disposeSource(); }; } if (preloadMode === 'none') { setReady(true); return () => { isFetchingCancelled.current = true; disposeSource(); }; } if (preloadMode === 'metadata') { if (!isFfmpegEnabled()) { setReady(true); } else { probeMetadataOnly(); } return () => { isFetchingCancelled.current = true; disposeSource(); }; } loadPlaybackSource(); return () => { isFetchingCancelled.current = true; disposeSource(); }; }, [disposeSource, loadPlaybackSource, path, preloadMode, probeMetadataOnly]); return { fileSourceRef, ready, duration, setDuration, loadForPlayback, disposeSource, }; }