Files
feishin/src/renderer/components/audio-player/engine/web-player-engine.tsx
T
2025-11-29 19:30:46 -08:00

172 lines
6.2 KiB
TypeScript

import type { RefObject } from 'react';
import { useImperativeHandle, useRef, useState } from 'react';
import ReactPlayer from 'react-player';
import { AudioPlayer } from '/@/renderer/components/audio-player/types';
import { PlayerStatus } from '/@/shared/types/types';
export interface OnProgressProps {
loaded: number;
loadedSeconds: number;
played: number;
playedSeconds: number;
}
export interface WebPlayerEngineHandle extends AudioPlayer {
player1(): {
ref: null | ReactPlayer;
setVolume: (volume: number) => void;
};
player2(): {
ref: null | ReactPlayer;
setVolume: (volume: number) => void;
};
}
interface WebPlayerEngineProps {
isMuted: boolean;
isTransitioning: boolean;
onEndedPlayer1: () => void;
onEndedPlayer2: () => void;
onProgressPlayer1: (e: OnProgressProps) => void;
onProgressPlayer2: (e: OnProgressProps) => void;
playerNum: number;
playerRef: RefObject<WebPlayerEngineHandle>;
playerStatus: PlayerStatus;
speed?: number;
src1: string | undefined;
src2: string | undefined;
volume: number;
}
// Credits: https://gist.github.com/novwhisky/8a1a0168b94f3b6abfaa?permalink_comment_id=1551393#gistcomment-1551393
// This is used so that the player will always have an <audio> element. This means that
// player1Source and player2Source are connected BEFORE the user presses play for
// the first time. This workaround is important for Safari, which seems to require the
// source to be connected PRIOR to resuming audio context
const EMPTY_SOURCE =
'data:audio/mp3;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU2LjM2LjEwMAAAAAAAAAAAAAAA//OEAAAAAAAAAAAAAAAAAAAAAAAASW5mbwAAAA8AAAAEAAABIADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV6urq6urq6urq6urq6urq6urq6urq6urq6v////////////////////////////////8AAAAATGF2YzU2LjQxAAAAAAAAAAAAAAAAJAAAAAAAAAAAASDs90hvAAAAAAAAAAAAAAAAAAAA//MUZAAAAAGkAAAAAAAAA0gAAAAATEFN//MUZAMAAAGkAAAAAAAAA0gAAAAARTMu//MUZAYAAAGkAAAAAAAAA0gAAAAAOTku//MUZAkAAAGkAAAAAAAAA0gAAAAANVVV';
export const WebPlayerEngine = (props: WebPlayerEngineProps) => {
const {
isMuted,
isTransitioning,
onEndedPlayer1,
onEndedPlayer2,
onProgressPlayer1,
onProgressPlayer2,
playerNum,
playerRef,
playerStatus,
speed,
src1,
src2,
volume,
} = props;
const player1Ref = useRef<null | ReactPlayer>(null);
const player2Ref = useRef<null | ReactPlayer>(null);
const [internalVolume1, setInternalVolume1] = useState(volume / 100 || 0);
const [internalVolume2, setInternalVolume2] = useState(volume / 100 || 0);
useImperativeHandle<WebPlayerEngineHandle, WebPlayerEngineHandle>(playerRef, () => ({
decreaseVolume(by: number) {
setInternalVolume1(Math.max(0, internalVolume1 - by / 100));
setInternalVolume2(Math.max(0, internalVolume2 - by / 100));
},
increaseVolume(by: number) {
setInternalVolume1(Math.min(1, internalVolume1 + by / 100));
setInternalVolume2(Math.min(1, internalVolume2 + by / 100));
},
pause() {
player1Ref.current?.getInternalPlayer()?.pause();
player2Ref.current?.getInternalPlayer()?.pause();
},
play() {
if (playerNum === 1) {
player1Ref.current?.getInternalPlayer()?.play();
} else {
player2Ref.current?.getInternalPlayer()?.play();
}
},
player1() {
return {
ref: player1Ref?.current,
setVolume: (volume: number) => setInternalVolume1(volume / 100 || 0),
};
},
player2() {
return {
ref: player2Ref?.current,
setVolume: (volume: number) => setInternalVolume2(volume / 100 || 0),
};
},
seekTo(seekTo: number) {
playerNum === 1
? player1Ref.current?.seekTo(seekTo)
: player2Ref.current?.seekTo(seekTo);
},
setVolume(volume: number) {
setInternalVolume1(volume / 100 || 0);
setInternalVolume2(volume / 100 || 0);
},
setVolume1(volume: number) {
setInternalVolume1(volume / 100 || 0);
},
setVolume2(volume: number) {
setInternalVolume2(volume / 100 || 0);
},
}));
return (
<>
{Boolean(src1) && (
<ReactPlayer
config={{
file: { attributes: { crossOrigin: 'anonymous' }, forceAudio: true },
}}
controls={false}
height={0}
muted={isMuted}
onEnded={src1 ? () => onEndedPlayer1() : undefined}
onProgress={onProgressPlayer1}
playbackRate={speed || 1}
playing={playerNum === 1 && playerStatus === PlayerStatus.PLAYING}
progressInterval={isTransitioning ? 10 : 250}
ref={player1Ref}
url={src1 || EMPTY_SOURCE}
volume={convertToLogVolume(internalVolume1)}
width={0}
/>
)}
{Boolean(src2) && (
<ReactPlayer
config={{
file: { attributes: { crossOrigin: 'anonymous' }, forceAudio: true },
}}
controls={false}
height={0}
muted={isMuted}
onEnded={src2 ? () => onEndedPlayer2() : undefined}
onProgress={onProgressPlayer2}
playbackRate={speed || 1}
playing={playerNum === 2 && playerStatus === PlayerStatus.PLAYING}
progressInterval={isTransitioning ? 10 : 250}
ref={player2Ref}
url={src2 || EMPTY_SOURCE}
volume={convertToLogVolume(internalVolume2)}
width={0}
/>
)}
</>
);
};
WebPlayerEngine.displayName = 'WebPlayerEngine';
function convertToLogVolume(linearVolume: number) {
return Math.pow(linearVolume, 2.0);
}