feishin/src/renderer/components/audio-player/index.tsx

347 lines
12 KiB
TypeScript
Raw Normal View History

2022-12-19 15:59:14 -08:00
import { useImperativeHandle, forwardRef, useRef, useState, useCallback, useEffect } from 'react';
import isElectron from 'is-electron';
import type { ReactPlayerProps } from 'react-player';
import ReactPlayer from 'react-player/lazy';
2022-12-19 15:59:14 -08:00
import type { Song } from '/@/renderer/api/types';
import {
2023-07-01 19:10:05 -07:00
crossfadeHandler,
gaplessHandler,
2022-12-19 15:59:14 -08:00
} from '/@/renderer/components/audio-player/utils/list-handlers';
import { useSettingsStore } from '/@/renderer/store/settings.store';
import type { CrossfadeStyle } from '/@/renderer/types';
import { PlaybackStyle, PlayerStatus } from '/@/renderer/types';
import { useSpeed } from '/@/renderer/store';
2022-12-19 15:59:14 -08:00
interface AudioPlayerProps extends ReactPlayerProps {
2023-07-01 19:10:05 -07:00
crossfadeDuration: number;
crossfadeStyle: CrossfadeStyle;
currentPlayer: 1 | 2;
playbackStyle: PlaybackStyle;
player1: Song;
player2: Song;
status: PlayerStatus;
volume: number;
2022-12-19 15:59:14 -08:00
}
export type AudioPlayerProgress = {
2023-07-01 19:10:05 -07:00
loaded: number;
loadedSeconds: number;
played: number;
playedSeconds: number;
2022-12-19 15:59:14 -08:00
};
const getDuration = (ref: any) => {
2023-07-01 19:10:05 -07:00
return ref.current?.player?.player?.player?.duration;
2022-12-19 15:59:14 -08:00
};
type WebAudio = {
context: AudioContext;
gain: GainNode;
};
2022-12-19 15:59:14 -08:00
export const AudioPlayer = forwardRef(
2023-07-01 19:10:05 -07:00
(
{
status,
playbackStyle,
crossfadeStyle,
crossfadeDuration,
currentPlayer,
autoNext,
player1,
player2,
muted,
volume,
}: AudioPlayerProps,
ref: any,
) => {
const player1Ref = useRef<ReactPlayer>(null);
const player2Ref = useRef<ReactPlayer>(null);
2023-07-01 19:10:05 -07:00
const [isTransitioning, setIsTransitioning] = useState(false);
const audioDeviceId = useSettingsStore((state) => state.playback.audioDeviceId);
const playback = useSettingsStore((state) => state.playback.mpvProperties);
const playbackSpeed = useSpeed();
const [webAudio, setWebAudio] = useState<WebAudio | null>(null);
const [player1Source, setPlayer1Source] = useState<MediaElementAudioSourceNode | null>(
null,
);
const [player2Source, setPlayer2Source] = useState<MediaElementAudioSourceNode | null>(
null,
);
const calculateReplayGain = useCallback(
(song: Song): number => {
if (playback.replayGainMode === 'no') {
return 1;
}
let gain: number | undefined;
let peak: number | undefined;
if (playback.replayGainMode === 'track') {
gain = song.gain?.track ?? song.gain?.album;
peak = song.peak?.track ?? song.peak?.album;
} else {
gain = song.gain?.album ?? song.gain?.track;
peak = song.peak?.album ?? song.peak?.track;
}
if (gain === undefined) {
gain = playback.replayGainFallbackDB;
if (!gain) {
return 1;
}
}
if (peak === undefined) {
peak = 1;
}
const preAmp = playback.replayGainPreampDB ?? 0;
// https://wiki.hydrogenaud.io/index.php?title=ReplayGain_1.0_specification&section=19
// Normalized to max gain
const expectedGain = 10 ** ((gain + preAmp) / 20);
if (playback.replayGainClip) {
return Math.min(expectedGain, 1 / peak);
}
return expectedGain;
},
[
playback.replayGainClip,
playback.replayGainFallbackDB,
playback.replayGainMode,
playback.replayGainPreampDB,
],
);
useEffect(() => {
if ('AudioContext' in window) {
const context = new AudioContext({
latencyHint: 'playback',
sampleRate: playback.audioSampleRateHz || undefined,
});
const gain = context.createGain();
gain.connect(context.destination);
setWebAudio({ context, gain });
return () => {
return context.close();
};
}
return () => {};
// Intentionally ignore the sample rate dependency, as it makes things really messy
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
2022-12-19 15:59:14 -08:00
2023-07-01 19:10:05 -07:00
useImperativeHandle(ref, () => ({
get player1() {
return player1Ref?.current;
},
get player2() {
return player2Ref?.current;
},
}));
2022-12-19 15:59:14 -08:00
2023-07-01 19:10:05 -07:00
const handleOnEnded = () => {
autoNext();
setIsTransitioning(false);
};
2022-12-19 15:59:14 -08:00
2023-07-01 19:10:05 -07:00
useEffect(() => {
if (status === PlayerStatus.PLAYING) {
if (currentPlayer === 1) {
player1Ref.current?.getInternalPlayer()?.play();
} else {
player2Ref.current?.getInternalPlayer()?.play();
}
} else {
player1Ref.current?.getInternalPlayer()?.pause();
player2Ref.current?.getInternalPlayer()?.pause();
}
}, [currentPlayer, status]);
2022-12-19 15:59:14 -08:00
2023-07-01 19:10:05 -07:00
const handleCrossfade1 = useCallback(
(e: AudioPlayerProgress) => {
return crossfadeHandler({
currentPlayer,
currentPlayerRef: player1Ref,
currentTime: e.playedSeconds,
duration: getDuration(player1Ref),
fadeDuration: crossfadeDuration,
fadeType: crossfadeStyle,
isTransitioning,
nextPlayerRef: player2Ref,
player: 1,
setIsTransitioning,
volume,
});
},
[crossfadeDuration, crossfadeStyle, currentPlayer, isTransitioning, volume],
);
2022-12-19 15:59:14 -08:00
2023-07-01 19:10:05 -07:00
const handleCrossfade2 = useCallback(
(e: AudioPlayerProgress) => {
return crossfadeHandler({
currentPlayer,
currentPlayerRef: player2Ref,
currentTime: e.playedSeconds,
duration: getDuration(player2Ref),
fadeDuration: crossfadeDuration,
fadeType: crossfadeStyle,
isTransitioning,
nextPlayerRef: player1Ref,
player: 2,
setIsTransitioning,
volume,
});
},
[crossfadeDuration, crossfadeStyle, currentPlayer, isTransitioning, volume],
);
2022-12-19 15:59:14 -08:00
2023-07-01 19:10:05 -07:00
const handleGapless1 = useCallback(
(e: AudioPlayerProgress) => {
return gaplessHandler({
currentTime: e.playedSeconds,
duration: getDuration(player1Ref),
isFlac: player1?.container === 'flac',
isTransitioning,
nextPlayerRef: player2Ref,
setIsTransitioning,
});
},
[isTransitioning, player1?.container],
);
2022-12-19 15:59:14 -08:00
2023-07-01 19:10:05 -07:00
const handleGapless2 = useCallback(
(e: AudioPlayerProgress) => {
return gaplessHandler({
currentTime: e.playedSeconds,
duration: getDuration(player2Ref),
isFlac: player2?.container === 'flac',
isTransitioning,
nextPlayerRef: player1Ref,
setIsTransitioning,
});
},
[isTransitioning, player2?.container],
);
2022-12-19 15:59:14 -08:00
2023-07-01 19:10:05 -07:00
useEffect(() => {
if (isElectron()) {
if (audioDeviceId) {
player1Ref.current?.getInternalPlayer()?.setSinkId(audioDeviceId);
player2Ref.current?.getInternalPlayer()?.setSinkId(audioDeviceId);
} else {
player1Ref.current?.getInternalPlayer()?.setSinkId('');
player2Ref.current?.getInternalPlayer()?.setSinkId('');
}
}
}, [audioDeviceId]);
2022-12-19 15:59:14 -08:00
useEffect(() => {
if (webAudio && player1Source) {
if (player1 === undefined) {
player1Source.disconnect();
setPlayer1Source(null);
} else if (currentPlayer === 1) {
webAudio.gain.gain.setValueAtTime(calculateReplayGain(player1), 0);
}
}
}, [calculateReplayGain, currentPlayer, player1, player1Source, webAudio]);
useEffect(() => {
if (webAudio && player2Source) {
if (player2 === undefined) {
player2Source.disconnect();
setPlayer2Source(null);
} else if (currentPlayer === 2) {
webAudio.gain.gain.setValueAtTime(calculateReplayGain(player2), 0);
}
}
}, [calculateReplayGain, currentPlayer, player2, player2Source, webAudio]);
const handlePlayer1Start = useCallback(
async (player: ReactPlayer) => {
if (!webAudio || player1Source) return;
if (webAudio.context.state !== 'running') {
await webAudio.context.resume();
}
const internal = player.getInternalPlayer() as HTMLMediaElement | undefined;
if (internal) {
const { context, gain } = webAudio;
const source = context.createMediaElementSource(internal);
source.connect(gain);
setPlayer1Source(source);
}
},
[player1Source, webAudio],
);
const handlePlayer2Start = useCallback(
async (player: ReactPlayer) => {
if (!webAudio || player2Source) return;
if (webAudio.context.state !== 'running') {
await webAudio.context.resume();
}
const internal = player.getInternalPlayer() as HTMLMediaElement | undefined;
if (internal) {
const { context, gain } = webAudio;
const source = context.createMediaElementSource(internal);
source.connect(gain);
setPlayer2Source(source);
}
},
[player2Source, webAudio],
);
2023-07-01 19:10:05 -07:00
return (
<>
<ReactPlayer
ref={player1Ref}
config={{
file: { attributes: { crossOrigin: 'anonymous' }, forceAudio: true },
}}
2023-07-01 19:10:05 -07:00
height={0}
muted={muted}
playbackRate={playbackSpeed}
2023-07-01 19:10:05 -07:00
playing={currentPlayer === 1 && status === PlayerStatus.PLAYING}
progressInterval={isTransitioning ? 10 : 250}
url={player1?.streamUrl}
volume={volume}
width={0}
onEnded={handleOnEnded}
onProgress={
playbackStyle === PlaybackStyle.GAPLESS ? handleGapless1 : handleCrossfade1
}
onReady={handlePlayer1Start}
2023-07-01 19:10:05 -07:00
/>
<ReactPlayer
ref={player2Ref}
config={{
file: { attributes: { crossOrigin: 'anonymous' }, forceAudio: true },
}}
2023-07-01 19:10:05 -07:00
height={0}
muted={muted}
playbackRate={playbackSpeed}
2023-07-01 19:10:05 -07:00
playing={currentPlayer === 2 && status === PlayerStatus.PLAYING}
progressInterval={isTransitioning ? 10 : 250}
url={player2?.streamUrl}
volume={volume}
width={0}
onEnded={handleOnEnded}
onProgress={
playbackStyle === PlaybackStyle.GAPLESS ? handleGapless2 : handleCrossfade2
}
onReady={handlePlayer2Start}
2023-07-01 19:10:05 -07:00
/>
</>
);
},
2022-12-19 15:59:14 -08:00
);