70 lines
2.3 KiB
TypeScript
70 lines
2.3 KiB
TypeScript
import { useEffect, useRef, useState } from 'react';
|
|
|
|
interface AudioSource {
|
|
sourceNode: AudioNode;
|
|
ctx: AudioContext;
|
|
}
|
|
|
|
/**
|
|
* Creates an AudioContext and source node from either a MediaStream (live recording)
|
|
* or an HTMLAudioElement (review playback).
|
|
*
|
|
* Important: `createMediaElementSource` can only be called once per element,
|
|
* so we cache the source per element instance.
|
|
*/
|
|
export function useAudioSource(
|
|
source: MediaStream | HTMLAudioElement | null,
|
|
): AudioSource | null {
|
|
const [audioSource, setAudioSource] = useState<AudioSource | null>(null);
|
|
const elementSourceCache = useRef<
|
|
WeakMap<
|
|
HTMLAudioElement,
|
|
{ sourceNode: MediaElementAudioSourceNode; ctx: AudioContext }
|
|
>
|
|
>(new WeakMap());
|
|
|
|
useEffect(() => {
|
|
// Build the source node (creating an AudioContext as needed) plus optional
|
|
// teardown for contexts we own; the branches converge on one setState.
|
|
let result: AudioSource | null = null;
|
|
let cleanup: (() => void) | undefined;
|
|
|
|
if (source instanceof MediaStream) {
|
|
const ctx = new AudioContext();
|
|
ctx.resume();
|
|
const sourceNode = ctx.createMediaStreamSource(source);
|
|
result = { sourceNode, ctx };
|
|
cleanup = () => ctx.close();
|
|
} else if (source) {
|
|
// HTMLAudioElement — createMediaElementSource can only be called once per
|
|
// element, so reuse a cached context/node when we have one.
|
|
const cached = elementSourceCache.current.get(source);
|
|
if (cached) {
|
|
cached.ctx.resume();
|
|
result = cached;
|
|
} else {
|
|
const ctx = new AudioContext();
|
|
ctx.resume();
|
|
const sourceNode = ctx.createMediaElementSource(source);
|
|
// Connect element source to destination so audio is still audible
|
|
sourceNode.connect(ctx.destination);
|
|
elementSourceCache.current.set(source, { sourceNode, ctx });
|
|
result = { sourceNode, ctx };
|
|
cleanup = () => {
|
|
ctx.close();
|
|
elementSourceCache.current.delete(source);
|
|
};
|
|
}
|
|
}
|
|
|
|
// Publishing an imperatively-created Web Audio node — external-resource
|
|
// sync, not a re-render cascade.
|
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
setAudioSource(result);
|
|
|
|
return cleanup;
|
|
}, [source]);
|
|
|
|
return audioSource;
|
|
}
|