mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-17 06:00:20 +02:00
fix audio context breaking on source change
This commit is contained in:
@@ -46,6 +46,26 @@ export function WebPlayer() {
|
|||||||
const [player1Source, setPlayer1Source] = useState<MediaElementAudioSourceNode | null>(null);
|
const [player1Source, setPlayer1Source] = useState<MediaElementAudioSourceNode | null>(null);
|
||||||
const [player2Source, setPlayer2Source] = useState<MediaElementAudioSourceNode | null>(null);
|
const [player2Source, setPlayer2Source] = useState<MediaElementAudioSourceNode | null>(null);
|
||||||
|
|
||||||
|
// `react-player` may swap its underlying internal player when switching URLs
|
||||||
|
// (e.g. file/http streams => HTMLMediaElement, YouTube => iframe player). A
|
||||||
|
// MediaElementAudioSourceNode is permanently bound to a specific element, so we
|
||||||
|
// must recreate the node when the element changes (or disconnect when it stops
|
||||||
|
// being a media element).
|
||||||
|
const player1InternalRef = useRef<HTMLMediaElement | null>(null);
|
||||||
|
const player2InternalRef = useRef<HTMLMediaElement | null>(null);
|
||||||
|
const player1SourceRef = useRef<MediaElementAudioSourceNode | null>(null);
|
||||||
|
const player2SourceRef = useRef<MediaElementAudioSourceNode | null>(null);
|
||||||
|
const player1ConnectInFlightRef = useRef<null | Promise<void>>(null);
|
||||||
|
const player2ConnectInFlightRef = useRef<null | Promise<void>>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
player1SourceRef.current = player1Source;
|
||||||
|
}, [player1Source]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
player2SourceRef.current = player2Source;
|
||||||
|
}, [player2Source]);
|
||||||
|
|
||||||
const fadeAndSetStatus = useCallback(
|
const fadeAndSetStatus = useCallback(
|
||||||
async (startVolume: number, endVolume: number, duration: number, status: PlayerStatus) => {
|
async (startVolume: number, endVolume: number, duration: number, status: PlayerStatus) => {
|
||||||
// Cancel any in-progress fade
|
// Cancel any in-progress fade
|
||||||
@@ -398,46 +418,110 @@ export function WebPlayer() {
|
|||||||
const player1Url = useSongUrl(player1, num === 1, transcode);
|
const player1Url = useSongUrl(player1, num === 1, transcode);
|
||||||
const player2Url = useSongUrl(player2, num === 2, transcode);
|
const player2Url = useSongUrl(player2, num === 2, transcode);
|
||||||
|
|
||||||
const handlePlayer1Start = useCallback(
|
const disconnectPlayerSource = useCallback(
|
||||||
async (player: ReactPlayer) => {
|
(playerNum: 1 | 2) => {
|
||||||
if (!webAudio || player1Source) return;
|
const sourceRef = playerNum === 1 ? player1SourceRef : player2SourceRef;
|
||||||
if (player1Url) {
|
const setSource = playerNum === 1 ? setPlayer1Source : setPlayer2Source;
|
||||||
// This should fire once, only if the source is real (meaning we
|
const internalRef = playerNum === 1 ? player1InternalRef : player2InternalRef;
|
||||||
// saw the dummy source) and the context is not ready
|
|
||||||
if (webAudio.context.state !== 'running') {
|
if (sourceRef.current) {
|
||||||
await webAudio.context.resume();
|
try {
|
||||||
|
sourceRef.current.disconnect();
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const internal = player.getInternalPlayer() as HTMLMediaElement | undefined;
|
sourceRef.current = null;
|
||||||
if (internal) {
|
internalRef.current = null;
|
||||||
const { context, gains } = webAudio;
|
setSource(null);
|
||||||
const source = context.createMediaElementSource(internal);
|
|
||||||
source.connect(gains[0]);
|
|
||||||
setPlayer1Source(source);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[player1Source, player1Url, webAudio],
|
[setPlayer1Source, setPlayer2Source],
|
||||||
|
);
|
||||||
|
|
||||||
|
const connectPlayerToWebAudio = useCallback(
|
||||||
|
async (playerNum: 1 | 2, player: ReactPlayer) => {
|
||||||
|
if (!webAudio) return;
|
||||||
|
|
||||||
|
const inFlightRef =
|
||||||
|
playerNum === 1 ? player1ConnectInFlightRef : player2ConnectInFlightRef;
|
||||||
|
if (inFlightRef.current) {
|
||||||
|
await inFlightRef.current;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const internalRef = playerNum === 1 ? player1InternalRef : player2InternalRef;
|
||||||
|
const sourceRef = playerNum === 1 ? player1SourceRef : player2SourceRef;
|
||||||
|
const setSource = playerNum === 1 ? setPlayer1Source : setPlayer2Source;
|
||||||
|
const gain = webAudio.gains[playerNum === 1 ? 0 : 1];
|
||||||
|
|
||||||
|
const task = (async () => {
|
||||||
|
const internal = player.getInternalPlayer() as unknown;
|
||||||
|
|
||||||
|
// YouTube (and some other sources) are not HTMLMediaElements, so WebAudio
|
||||||
|
// can't attach; ensure we drop any stale node from a prior media element.
|
||||||
|
if (!(internal instanceof HTMLMediaElement)) {
|
||||||
|
disconnectPlayerSource(playerNum);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (webAudio.context.state !== 'running') {
|
||||||
|
try {
|
||||||
|
await webAudio.context.resume();
|
||||||
|
} catch {
|
||||||
|
// ignore resume failures; we'll try again on next ready
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the internal media element changed, we must recreate the source node.
|
||||||
|
if (internalRef.current === internal && sourceRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sourceRef.current) {
|
||||||
|
try {
|
||||||
|
sourceRef.current.disconnect();
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internalRef.current = internal;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const source = webAudio.context.createMediaElementSource(internal);
|
||||||
|
source.connect(gain);
|
||||||
|
sourceRef.current = source;
|
||||||
|
setSource(source);
|
||||||
|
} catch (error) {
|
||||||
|
// Most commonly: trying to create another MediaElementSourceNode for the
|
||||||
|
// same element, or attempting to attach a tainted/cross-origin element.
|
||||||
|
console.error('Error connecting WebAudio source', { error, playerNum });
|
||||||
|
disconnectPlayerSource(playerNum);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
inFlightRef.current = task.finally(() => {
|
||||||
|
inFlightRef.current = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
await inFlightRef.current;
|
||||||
|
},
|
||||||
|
[disconnectPlayerSource, webAudio],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handlePlayer1Start = useCallback(
|
||||||
|
async (player: ReactPlayer) => {
|
||||||
|
await connectPlayerToWebAudio(1, player);
|
||||||
|
},
|
||||||
|
[connectPlayerToWebAudio],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handlePlayer2Start = useCallback(
|
const handlePlayer2Start = useCallback(
|
||||||
async (player: ReactPlayer) => {
|
async (player: ReactPlayer) => {
|
||||||
if (!webAudio || player2Source) return;
|
await connectPlayerToWebAudio(2, player);
|
||||||
if (player2Url) {
|
|
||||||
if (webAudio.context.state !== 'running') {
|
|
||||||
await webAudio.context.resume();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const internal = player.getInternalPlayer() as HTMLMediaElement | undefined;
|
|
||||||
if (internal) {
|
|
||||||
const { context, gains } = webAudio;
|
|
||||||
const source = context.createMediaElementSource(internal);
|
|
||||||
source.connect(gains[1]);
|
|
||||||
setPlayer2Source(source);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[player2Source, player2Url, webAudio],
|
[connectPlayerToWebAudio],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user