mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-09 20:29:36 +02:00
add new web player implementation
This commit is contained in:
@@ -0,0 +1,171 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import {
|
||||||
|
QueueData,
|
||||||
|
subscribeCurrentTrack,
|
||||||
|
subscribePlayerMute,
|
||||||
|
subscribePlayerProgress,
|
||||||
|
subscribePlayerQueue,
|
||||||
|
subscribePlayerSeekToTimestamp,
|
||||||
|
subscribePlayerSpeed,
|
||||||
|
subscribePlayerStatus,
|
||||||
|
subscribePlayerVolume,
|
||||||
|
} from '/@/renderer/store';
|
||||||
|
import { QueueSong } from '/@/shared/types/domain-types';
|
||||||
|
import { PlayerStatus } from '/@/shared/types/types';
|
||||||
|
|
||||||
|
export interface PlayerEvents {
|
||||||
|
cleanup: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlayerEventsCallbacks {
|
||||||
|
onCurrentTrackChange?: (
|
||||||
|
track: { index: number; track: QueueSong | undefined },
|
||||||
|
prevTrack: { index: number; track: QueueSong | undefined },
|
||||||
|
) => void;
|
||||||
|
onPlayerMute?: (muted: boolean, prevMuted: boolean) => void;
|
||||||
|
onPlayerProgress?: (timestamp: number, prevTimestamp: number) => void;
|
||||||
|
onPlayerQueueChange?: (queue: QueueData, prevQueue: QueueData) => void;
|
||||||
|
onPlayerSeek?: (seconds: number, prevSeconds: number) => void;
|
||||||
|
onPlayerSeekToTimestamp?: (timestamp: number, prevTimestamp: number) => void;
|
||||||
|
onPlayerSpeed?: (speed: number, prevSpeed: number) => void;
|
||||||
|
onPlayerStatus?: (status: PlayerStatus, prevStatus: PlayerStatus) => void;
|
||||||
|
onPlayerVolume?: (volume: number, prevVolume: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createPlayerEvents(callbacks: PlayerEventsCallbacks): PlayerEvents {
|
||||||
|
const unsubscribers: (() => void)[] = [];
|
||||||
|
|
||||||
|
// Subscribe to current track changes
|
||||||
|
if (callbacks.onCurrentTrackChange) {
|
||||||
|
const unsubscribe = subscribeCurrentTrack(callbacks.onCurrentTrackChange);
|
||||||
|
unsubscribers.push(unsubscribe);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe to player progress
|
||||||
|
if (callbacks.onPlayerProgress) {
|
||||||
|
const unsubscribe = subscribePlayerProgress(callbacks.onPlayerProgress);
|
||||||
|
unsubscribers.push(unsubscribe);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe to queue changes
|
||||||
|
if (callbacks.onPlayerQueueChange) {
|
||||||
|
const unsubscribe = subscribePlayerQueue(callbacks.onPlayerQueueChange);
|
||||||
|
unsubscribers.push(unsubscribe);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe to seek events
|
||||||
|
if (callbacks.onPlayerSeekToTimestamp) {
|
||||||
|
const unsubscribe = subscribePlayerSeekToTimestamp(callbacks.onPlayerSeekToTimestamp);
|
||||||
|
unsubscribers.push(unsubscribe);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe to player status changes
|
||||||
|
if (callbacks.onPlayerStatus) {
|
||||||
|
const unsubscribe = subscribePlayerStatus(callbacks.onPlayerStatus);
|
||||||
|
unsubscribers.push(unsubscribe);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe to volume changes
|
||||||
|
if (callbacks.onPlayerVolume) {
|
||||||
|
const unsubscribe = subscribePlayerVolume(callbacks.onPlayerVolume);
|
||||||
|
unsubscribers.push(unsubscribe);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe to mute changes
|
||||||
|
if (callbacks.onPlayerMute) {
|
||||||
|
const unsubscribe = subscribePlayerMute(callbacks.onPlayerMute);
|
||||||
|
unsubscribers.push(unsubscribe);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe to speed changes
|
||||||
|
if (callbacks.onPlayerSpeed) {
|
||||||
|
const unsubscribe = subscribePlayerSpeed(callbacks.onPlayerSpeed);
|
||||||
|
unsubscribers.push(unsubscribe);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
cleanup: () => {
|
||||||
|
unsubscribers.forEach((unsubscribe) => unsubscribe());
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
import { createPlayerEvents } from '/@/renderer/components/audio-player/listener/player-events';
|
||||||
|
import { QueueData } from '/@/renderer/store';
|
||||||
|
import { QueueSong } from '/@/shared/types/domain-types';
|
||||||
|
import { PlayerStatus } from '/@/shared/types/types';
|
||||||
|
|
||||||
|
export interface PlayerEvents {
|
||||||
|
onCurrentTrackChange?: (
|
||||||
|
track: { index: number; track: QueueSong | undefined },
|
||||||
|
prevTrack: { index: number; track: QueueSong | undefined },
|
||||||
|
) => void;
|
||||||
|
onPlayerMute?: (muted: boolean, prevMuted: boolean) => void;
|
||||||
|
onPlayerProgress?: (timestamp: number, prevTimestamp: number) => void;
|
||||||
|
onPlayerQueueChange?: (queue: QueueData, prevQueue: QueueData) => void;
|
||||||
|
onPlayerSeekToTimestamp?: (timestamp: number, prevTimestamp: number) => void;
|
||||||
|
onPlayerSpeed?: (speed: number, prevSpeed: number) => void;
|
||||||
|
onPlayerStatus?: (status: PlayerStatus, prevStatus: PlayerStatus) => void;
|
||||||
|
onPlayerVolume?: (volume: number, prevVolume: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePlayerEvents(callbacks: PlayerEvents, deps: React.DependencyList) {
|
||||||
|
useEffect(() => {
|
||||||
|
const engine = createPlayerEvents(callbacks);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
engine.cleanup();
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [...deps]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export interface AudioPlayer {
|
||||||
|
decreaseVolume(by: number): void;
|
||||||
|
increaseVolume(by: number): void;
|
||||||
|
pause(): void;
|
||||||
|
play(): void;
|
||||||
|
seekTo(seekTo: number): void;
|
||||||
|
setVolume(volume: number): void;
|
||||||
|
}
|
||||||
@@ -0,0 +1,322 @@
|
|||||||
|
import type { Dispatch } from 'react';
|
||||||
|
import type ReactPlayer from 'react-player';
|
||||||
|
|
||||||
|
import { useCallback, useRef, useState } from 'react';
|
||||||
|
import { OnProgressProps } from 'react-player/base';
|
||||||
|
|
||||||
|
import { WebPlayerEngine, WebPlayerEngineHandle } from './engine/web-player-engine';
|
||||||
|
|
||||||
|
import { usePlayerEvents } from '/@/renderer/components/audio-player/listener/use-player-events';
|
||||||
|
import {
|
||||||
|
usePlayerActions,
|
||||||
|
usePlayerData,
|
||||||
|
usePlayerMuted,
|
||||||
|
usePlayerProperties,
|
||||||
|
usePlayerVolume,
|
||||||
|
} from '/@/renderer/store';
|
||||||
|
import { PlayerStatus, PlayerStyle } from '/@/shared/types/types';
|
||||||
|
|
||||||
|
const PLAY_PAUSE_FADE_DURATION = 300;
|
||||||
|
const PLAY_PAUSE_FADE_INTERVAL = 10;
|
||||||
|
|
||||||
|
export function AudiolingWebPlayer() {
|
||||||
|
const playerRef = useRef<WebPlayerEngineHandle>(null);
|
||||||
|
const { player, player1, player2 } = usePlayerData();
|
||||||
|
const { mediaAutoNext, setProgress } = usePlayerActions();
|
||||||
|
const { crossfadeDuration, speed, transitionType } = usePlayerProperties();
|
||||||
|
const isMuted = usePlayerMuted();
|
||||||
|
const volume = usePlayerVolume();
|
||||||
|
|
||||||
|
const [localPlayerStatus, setLocalPlayerStatus] = useState<PlayerStatus>(player.status);
|
||||||
|
const [isTransitioning, setIsTransitioning] = useState<boolean | string>(false);
|
||||||
|
|
||||||
|
const fadeAndSetStatus = useCallback(
|
||||||
|
async (startVolume: number, endVolume: number, duration: number, status: PlayerStatus) => {
|
||||||
|
if (isTransitioning) {
|
||||||
|
return setLocalPlayerStatus(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
const steps = duration / PLAY_PAUSE_FADE_INTERVAL;
|
||||||
|
const volumeStep = (endVolume - startVolume) / steps;
|
||||||
|
let currentStep = 0;
|
||||||
|
|
||||||
|
const promise = new Promise((resolve) => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
currentStep++;
|
||||||
|
const newVolume = startVolume + volumeStep * currentStep;
|
||||||
|
|
||||||
|
playerRef.current?.setVolume(newVolume);
|
||||||
|
|
||||||
|
if (currentStep >= steps) {
|
||||||
|
clearInterval(interval);
|
||||||
|
setIsTransitioning(false);
|
||||||
|
resolve(true);
|
||||||
|
}
|
||||||
|
}, PLAY_PAUSE_FADE_INTERVAL);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (status === PlayerStatus.PAUSED) {
|
||||||
|
await promise;
|
||||||
|
setLocalPlayerStatus(status);
|
||||||
|
} else if (status === PlayerStatus.PLAYING) {
|
||||||
|
setLocalPlayerStatus(status);
|
||||||
|
await promise;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[isTransitioning],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onProgressPlayer1 = useCallback(
|
||||||
|
(e: OnProgressProps) => {
|
||||||
|
if (transitionType === 'crossfade' && player.playerNum === 1) {
|
||||||
|
setProgress(Number(e.playedSeconds.toFixed(0)));
|
||||||
|
} else if (transitionType === 'gapless') {
|
||||||
|
setProgress(Number(e.playedSeconds.toFixed(0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!playerRef.current?.player1()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (transitionType) {
|
||||||
|
case PlayerStyle.CROSSFADE:
|
||||||
|
crossfadeHandler({
|
||||||
|
crossfadeDuration: crossfadeDuration,
|
||||||
|
currentPlayer: playerRef.current.player1(),
|
||||||
|
currentPlayerNum: player.playerNum,
|
||||||
|
currentTime: e.playedSeconds,
|
||||||
|
duration: getDuration(playerRef.current.player1().ref),
|
||||||
|
isTransitioning,
|
||||||
|
nextPlayer: playerRef.current.player2(),
|
||||||
|
playerNum: 1,
|
||||||
|
setIsTransitioning,
|
||||||
|
volume,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case PlayerStyle.GAPLESS:
|
||||||
|
gaplessHandler({
|
||||||
|
currentTime: e.playedSeconds,
|
||||||
|
duration: getDuration(playerRef.current.player1().ref),
|
||||||
|
isFlac: false,
|
||||||
|
isTransitioning,
|
||||||
|
nextPlayer: playerRef.current.player2(),
|
||||||
|
setIsTransitioning,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[crossfadeDuration, isTransitioning, player.playerNum, setProgress, transitionType, volume],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onProgressPlayer2 = useCallback(
|
||||||
|
(e: OnProgressProps) => {
|
||||||
|
if (transitionType === PlayerStyle.CROSSFADE && player.playerNum === 2) {
|
||||||
|
setProgress(Number(e.playedSeconds.toFixed(0)));
|
||||||
|
} else if (transitionType === PlayerStyle.GAPLESS) {
|
||||||
|
setProgress(Number(e.playedSeconds.toFixed(0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!playerRef.current?.player2()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (transitionType) {
|
||||||
|
case PlayerStyle.CROSSFADE:
|
||||||
|
crossfadeHandler({
|
||||||
|
crossfadeDuration: crossfadeDuration,
|
||||||
|
currentPlayer: playerRef.current.player2(),
|
||||||
|
currentPlayerNum: player.playerNum,
|
||||||
|
currentTime: e.playedSeconds,
|
||||||
|
duration: getDuration(playerRef.current.player2().ref),
|
||||||
|
isTransitioning,
|
||||||
|
nextPlayer: playerRef.current.player1(),
|
||||||
|
playerNum: 2,
|
||||||
|
setIsTransitioning,
|
||||||
|
volume,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case PlayerStyle.GAPLESS:
|
||||||
|
gaplessHandler({
|
||||||
|
currentTime: e.playedSeconds,
|
||||||
|
duration: getDuration(playerRef.current.player2().ref),
|
||||||
|
isFlac: false,
|
||||||
|
isTransitioning,
|
||||||
|
nextPlayer: playerRef.current.player1(),
|
||||||
|
setIsTransitioning,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[crossfadeDuration, isTransitioning, player.playerNum, setProgress, transitionType, volume],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleOnEndedPlayer1 = useCallback(() => {
|
||||||
|
const promise = new Promise((resolve) => {
|
||||||
|
mediaAutoNext();
|
||||||
|
resolve(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
promise.then(() => {
|
||||||
|
playerRef.current?.player1()?.ref?.getInternalPlayer().pause();
|
||||||
|
playerRef.current?.setVolume(volume);
|
||||||
|
setIsTransitioning(false);
|
||||||
|
});
|
||||||
|
}, [mediaAutoNext, volume]);
|
||||||
|
|
||||||
|
const handleOnEndedPlayer2 = useCallback(() => {
|
||||||
|
const promise = new Promise((resolve) => {
|
||||||
|
mediaAutoNext();
|
||||||
|
resolve(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
promise.then(() => {
|
||||||
|
playerRef.current?.player2()?.ref?.getInternalPlayer().pause();
|
||||||
|
playerRef.current?.setVolume(volume);
|
||||||
|
setIsTransitioning(false);
|
||||||
|
});
|
||||||
|
}, [mediaAutoNext, volume]);
|
||||||
|
|
||||||
|
usePlayerEvents(
|
||||||
|
{
|
||||||
|
onPlayerSeekToTimestamp: (timestamp) => {
|
||||||
|
if (player.playerNum === 1) {
|
||||||
|
playerRef.current?.player1()?.ref?.seekTo(timestamp);
|
||||||
|
} else {
|
||||||
|
playerRef.current?.player2()?.ref?.seekTo(timestamp);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onPlayerStatus: async (status) => {
|
||||||
|
if (status === PlayerStatus.PAUSED) {
|
||||||
|
fadeAndSetStatus(volume, 0, PLAY_PAUSE_FADE_DURATION, PlayerStatus.PAUSED);
|
||||||
|
} else if (status === PlayerStatus.PLAYING) {
|
||||||
|
fadeAndSetStatus(0, volume, PLAY_PAUSE_FADE_DURATION, PlayerStatus.PLAYING);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onPlayerVolume: (volume) => {
|
||||||
|
playerRef.current?.setVolume(volume);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[volume, player.playerNum, isTransitioning],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WebPlayerEngine
|
||||||
|
isMuted={isMuted}
|
||||||
|
isTransitioning={Boolean(isTransitioning)}
|
||||||
|
onEndedPlayer1={handleOnEndedPlayer1}
|
||||||
|
onEndedPlayer2={handleOnEndedPlayer2}
|
||||||
|
onProgressPlayer1={onProgressPlayer1}
|
||||||
|
onProgressPlayer2={onProgressPlayer2}
|
||||||
|
playerNum={player.playerNum}
|
||||||
|
playerRef={playerRef}
|
||||||
|
playerStatus={localPlayerStatus}
|
||||||
|
speed={speed}
|
||||||
|
src1={player1?.streamUrl}
|
||||||
|
src2={player2?.streamUrl}
|
||||||
|
volume={volume}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function crossfadeHandler(args: {
|
||||||
|
crossfadeDuration: number;
|
||||||
|
currentPlayer: {
|
||||||
|
ref: null | ReactPlayer;
|
||||||
|
setVolume: (volume: number) => void;
|
||||||
|
};
|
||||||
|
currentPlayerNum: number;
|
||||||
|
currentTime: number;
|
||||||
|
duration: number;
|
||||||
|
isTransitioning: boolean | string;
|
||||||
|
nextPlayer: {
|
||||||
|
ref: null | ReactPlayer;
|
||||||
|
setVolume: (volume: number) => void;
|
||||||
|
};
|
||||||
|
playerNum: number;
|
||||||
|
setIsTransitioning: Dispatch<boolean | string>;
|
||||||
|
volume: number;
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
crossfadeDuration,
|
||||||
|
currentPlayer,
|
||||||
|
currentPlayerNum,
|
||||||
|
currentTime,
|
||||||
|
duration,
|
||||||
|
isTransitioning,
|
||||||
|
nextPlayer,
|
||||||
|
playerNum,
|
||||||
|
setIsTransitioning,
|
||||||
|
volume,
|
||||||
|
} = args;
|
||||||
|
const player = `player${playerNum}`;
|
||||||
|
|
||||||
|
if (!isTransitioning) {
|
||||||
|
if (currentTime > duration - crossfadeDuration) {
|
||||||
|
nextPlayer.setVolume(0);
|
||||||
|
nextPlayer.ref?.getInternalPlayer().play();
|
||||||
|
return setIsTransitioning(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isTransitioning !== player && currentPlayerNum !== playerNum) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeLeft = duration - currentTime;
|
||||||
|
|
||||||
|
// Calculate the volume levels based on time remaining
|
||||||
|
const currentPlayerVolume = (timeLeft / crossfadeDuration) * volume;
|
||||||
|
const nextPlayerVolume = ((crossfadeDuration - timeLeft) / crossfadeDuration) * volume;
|
||||||
|
|
||||||
|
// Set volumes for both players
|
||||||
|
currentPlayer.setVolume(currentPlayerVolume);
|
||||||
|
nextPlayer.setVolume(nextPlayerVolume);
|
||||||
|
}
|
||||||
|
|
||||||
|
function gaplessHandler(args: {
|
||||||
|
currentTime: number;
|
||||||
|
duration: number;
|
||||||
|
isFlac: boolean;
|
||||||
|
isTransitioning: boolean | string;
|
||||||
|
nextPlayer: {
|
||||||
|
ref: null | ReactPlayer;
|
||||||
|
setVolume: (volume: number) => void;
|
||||||
|
};
|
||||||
|
setIsTransitioning: Dispatch<boolean | string>;
|
||||||
|
}) {
|
||||||
|
const { currentTime, duration, isFlac, isTransitioning, nextPlayer, setIsTransitioning } = args;
|
||||||
|
|
||||||
|
if (!isTransitioning) {
|
||||||
|
if (currentTime > duration - 2) {
|
||||||
|
return setIsTransitioning(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const durationPadding = getDurationPadding(isFlac);
|
||||||
|
|
||||||
|
if (currentTime + durationPadding >= duration) {
|
||||||
|
return nextPlayer.ref
|
||||||
|
?.getInternalPlayer()
|
||||||
|
?.play()
|
||||||
|
.catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDuration(ref: null | ReactPlayer | undefined) {
|
||||||
|
return ref?.getInternalPlayer()?.duration || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDurationPadding(isFlac: boolean) {
|
||||||
|
switch (isFlac) {
|
||||||
|
case false:
|
||||||
|
return 0.116;
|
||||||
|
case true:
|
||||||
|
return 0.065;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user