Files
llink/js/desktop/src/components/audio/use-audio-source.ts
T
2026-06-01 14:17:58 -07:00

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;
}