This repository has been archived on 2025-03-19. You can view files and clone it, but cannot push or open issues or pull requests.
feishin/src/renderer/features/player/hooks/use-center-controls.ts
jeffvli fed96d1fce Additional player adjustments
- Set volume on play
- Explicitly pause/play on set queue
2023-05-21 03:08:25 -07:00

729 lines
20 KiB
TypeScript

// import { write, writeFile } from 'fs';
// import { deflate } from 'zlib';
import { useCallback, useEffect } from 'react';
import isElectron from 'is-electron';
import { PlaybackType, PlayerRepeat, PlayerShuffle, PlayerStatus } from '/@/renderer/types';
import {
useCurrentPlayer,
useCurrentStatus,
useDefaultQueue,
usePlayerControls,
usePlayerStore,
useRepeatStatus,
useSetCurrentTime,
useShuffleStatus,
} from '/@/renderer/store';
import { usePlayerType, useSettingsStore } from '/@/renderer/store/settings.store';
import { useScrobble } from '/@/renderer/features/player/hooks/use-scrobble';
import debounce from 'lodash/debounce';
import { QueueSong } from '/@/renderer/api/types';
import { toast } from '/@/renderer/components';
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
const mpvPlayerListener = isElectron() ? window.electron.mpvPlayerListener : null;
const ipc = isElectron() ? window.electron.ipc : null;
const utils = isElectron() ? window.electron.utils : null;
const mpris = isElectron() && utils?.isLinux() ? window.electron.mpris : null;
export const useCenterControls = (args: { playersRef: any }) => {
const { playersRef } = args;
const settings = useSettingsStore((state) => state.playback);
const currentPlayer = useCurrentPlayer();
const { setShuffle, setRepeat, play, pause, previous, next, setCurrentIndex, autoNext } =
usePlayerControls();
const setCurrentTime = useSetCurrentTime();
const queue = useDefaultQueue();
const playerStatus = useCurrentStatus();
const repeatStatus = useRepeatStatus();
const shuffleStatus = useShuffleStatus();
const playerType = usePlayerType();
const player1Ref = playersRef?.current?.player1;
const player2Ref = playersRef?.current?.player2;
const currentPlayerRef = currentPlayer === 1 ? player1Ref : player2Ref;
const nextPlayerRef = currentPlayer === 1 ? player2Ref : player1Ref;
const { handleScrobbleFromSongRestart, handleScrobbleFromSeek } = useScrobble();
const resetPlayers = useCallback(() => {
if (player1Ref.getInternalPlayer()) {
player1Ref.getInternalPlayer().currentTime = 0;
player1Ref.getInternalPlayer().pause();
}
if (player2Ref.getInternalPlayer()) {
player2Ref.getInternalPlayer().currentTime = 0;
player2Ref.getInternalPlayer().pause();
}
}, [player1Ref, player2Ref]);
const resetNextPlayer = useCallback(() => {
currentPlayerRef.getInternalPlayer().volume = 0.1;
nextPlayerRef.getInternalPlayer().currentTime = 0;
nextPlayerRef.getInternalPlayer().pause();
}, [currentPlayerRef, nextPlayerRef]);
const stopPlayback = useCallback(() => {
player1Ref.getInternalPlayer().pause();
player2Ref.getInternalPlayer().pause();
resetPlayers();
}, [player1Ref, player2Ref, resetPlayers]);
const isMpvPlayer = isElectron() && settings.type === PlaybackType.LOCAL;
const mprisUpdateSong = (args?: {
currentTime?: number;
song?: QueueSong;
status?: PlayerStatus;
}) => {
const { song, currentTime, status } = args || {};
mpris?.updateSong({
currentTime: currentTime || usePlayerStore.getState().current.time,
repeat: usePlayerStore.getState().repeat,
shuffle: usePlayerStore.getState().shuffle,
song: song || usePlayerStore.getState().current.song,
status:
(status || usePlayerStore.getState().current.status) === PlayerStatus.PLAYING
? 'Playing'
: 'Paused',
});
};
const handlePlay = useCallback(() => {
mprisUpdateSong({ status: PlayerStatus.PLAYING });
if (isMpvPlayer) {
mpvPlayer?.volume(usePlayerStore.getState().volume);
mpvPlayer.play();
} else {
currentPlayerRef.getInternalPlayer().play();
}
play();
}, [currentPlayerRef, isMpvPlayer, play]);
const handlePause = useCallback(() => {
mprisUpdateSong({ status: PlayerStatus.PAUSED });
if (isMpvPlayer) {
mpvPlayer.pause();
}
pause();
}, [isMpvPlayer, pause]);
const handleStop = useCallback(() => {
mprisUpdateSong({ status: PlayerStatus.PAUSED });
if (isMpvPlayer) {
mpvPlayer.stop();
} else {
stopPlayback();
}
setCurrentTime(0);
pause();
}, [isMpvPlayer, pause, setCurrentTime, stopPlayback]);
const handleToggleShuffle = useCallback(() => {
if (shuffleStatus === PlayerShuffle.NONE) {
const playerData = setShuffle(PlayerShuffle.TRACK);
mpris?.updateShuffle(true);
return mpvPlayer.setQueueNext(playerData);
}
const playerData = setShuffle(PlayerShuffle.NONE);
mpris?.updateShuffle(false);
return mpvPlayer.setQueueNext(playerData);
}, [setShuffle, shuffleStatus]);
const handleToggleRepeat = useCallback(() => {
if (repeatStatus === PlayerRepeat.NONE) {
const playerData = setRepeat(PlayerRepeat.ALL);
mpris?.updateRepeat('Playlist');
return mpvPlayer.setQueueNext(playerData);
}
if (repeatStatus === PlayerRepeat.ALL) {
const playerData = setRepeat(PlayerRepeat.ONE);
mpris?.updateRepeat('Track');
return mpvPlayer.setQueueNext(playerData);
}
mpris?.updateRepeat('None');
return setRepeat(PlayerRepeat.NONE);
}, [repeatStatus, setRepeat]);
const checkIsLastTrack = useCallback(() => {
return usePlayerStore.getState().actions.checkIsLastTrack();
}, []);
const checkIsFirstTrack = useCallback(() => {
return usePlayerStore.getState().actions.checkIsFirstTrack();
}, []);
const handleAutoNext = useCallback(() => {
const isLastTrack = checkIsLastTrack();
const handleRepeatAll = {
local: () => {
const playerData = autoNext();
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
mpvPlayer.autoNext(playerData);
play();
},
web: () => {
const playerData = autoNext();
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
},
};
const handleRepeatNone = {
local: () => {
if (isLastTrack) {
const playerData = setCurrentIndex(0);
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PAUSED });
mpvPlayer.setQueue(playerData);
mpvPlayer.pause();
pause();
} else {
const playerData = autoNext();
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
mpvPlayer.autoNext(playerData);
play();
}
},
web: () => {
if (isLastTrack) {
resetPlayers();
mprisUpdateSong({ status: PlayerStatus.PAUSED });
pause();
} else {
const playerData = autoNext();
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
resetPlayers();
}
},
};
const handleRepeatOne = {
local: () => {
const playerData = autoNext();
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
mpvPlayer.autoNext(playerData);
play();
},
web: () => {
if (isLastTrack) {
mprisUpdateSong({ status: PlayerStatus.PAUSED });
resetPlayers();
} else {
const playerData = autoNext();
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
resetPlayers();
}
},
};
switch (repeatStatus) {
case PlayerRepeat.NONE:
handleRepeatNone[playerType]();
break;
case PlayerRepeat.ALL:
handleRepeatAll[playerType]();
break;
case PlayerRepeat.ONE:
handleRepeatOne[playerType]();
break;
default:
break;
}
}, [
autoNext,
checkIsLastTrack,
pause,
play,
playerType,
repeatStatus,
resetPlayers,
setCurrentIndex,
]);
const handleNextTrack = useCallback(() => {
const isLastTrack = checkIsLastTrack();
setCurrentTime(0);
const handleRepeatAll = {
local: () => {
const playerData = next();
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
mpvPlayer.setQueue(playerData);
mpvPlayer.next();
},
web: () => {
const playerData = next();
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
},
};
const handleRepeatNone = {
local: () => {
if (isLastTrack) {
const playerData = setCurrentIndex(0);
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PAUSED });
mpvPlayer.setQueue(playerData);
mpvPlayer.pause();
pause();
} else {
const playerData = next();
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
mpvPlayer.setQueue(playerData);
mpvPlayer.next();
}
},
web: () => {
if (isLastTrack) {
const playerData = setCurrentIndex(0);
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
resetPlayers();
pause();
} else {
const playerData = next();
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
resetPlayers();
}
},
};
const handleRepeatOne = {
local: () => {
const playerData = next();
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
mpvPlayer.setQueue(playerData);
mpvPlayer.next();
},
web: () => {
if (!isLastTrack) {
const playerData = next();
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
}
},
};
switch (repeatStatus) {
case PlayerRepeat.NONE:
handleRepeatNone[playerType]();
break;
case PlayerRepeat.ALL:
handleRepeatAll[playerType]();
break;
case PlayerRepeat.ONE:
handleRepeatOne[playerType]();
break;
default:
break;
}
setCurrentTime(0);
}, [
checkIsLastTrack,
next,
pause,
playerType,
repeatStatus,
resetPlayers,
setCurrentIndex,
setCurrentTime,
]);
const handlePrevTrack = useCallback(() => {
const currentTime = isMpvPlayer
? usePlayerStore.getState().current.time
: currentPlayerRef.getCurrentTime();
// Reset the current track more than 10 seconds have elapsed
if (currentTime >= 10) {
setCurrentTime(0);
handleScrobbleFromSongRestart(currentTime);
mpris?.updateSeek(0);
if (isMpvPlayer) {
return mpvPlayer.seekTo(0);
}
return currentPlayerRef.seekTo(0);
}
const isFirstTrack = checkIsFirstTrack();
const handleRepeatAll = {
local: () => {
if (!isFirstTrack) {
const playerData = previous();
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
mpvPlayer.setQueue(playerData);
mpvPlayer.previous();
} else {
const playerData = setCurrentIndex(queue.length - 1);
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
mpvPlayer.setQueue(playerData);
mpvPlayer.previous();
}
},
web: () => {
if (isFirstTrack) {
const playerData = setCurrentIndex(queue.length - 1);
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
resetPlayers();
} else {
const playerData = previous();
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
resetPlayers();
}
},
};
const handleRepeatNone = {
local: () => {
const playerData = previous();
mpris?.updateSong({
currentTime: usePlayerStore.getState().current.time,
song: playerData.current.song,
});
mpvPlayer.setQueue(playerData);
mpvPlayer.previous();
},
web: () => {
if (isFirstTrack) {
resetPlayers();
mprisUpdateSong({ status: PlayerStatus.PAUSED });
pause();
} else {
const playerData = previous();
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
resetPlayers();
}
},
};
const handleRepeatOne = {
local: () => {
if (!isFirstTrack) {
const playerData = previous();
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
mpvPlayer.setQueue(playerData);
mpvPlayer.previous();
} else {
mpvPlayer.stop();
}
},
web: () => {
const playerData = previous();
mprisUpdateSong({ song: playerData.current.song, status: PlayerStatus.PLAYING });
resetPlayers();
},
};
switch (repeatStatus) {
case PlayerRepeat.NONE:
handleRepeatNone[playerType]();
break;
case PlayerRepeat.ALL:
handleRepeatAll[playerType]();
break;
case PlayerRepeat.ONE:
handleRepeatOne[playerType]();
break;
default:
break;
}
return setCurrentTime(0);
}, [
checkIsFirstTrack,
currentPlayerRef,
handleScrobbleFromSongRestart,
isMpvPlayer,
pause,
playerType,
previous,
queue.length,
repeatStatus,
resetPlayers,
setCurrentIndex,
setCurrentTime,
]);
const handlePlayPause = useCallback(() => {
if (queue) {
if (playerStatus === PlayerStatus.PAUSED) {
return handlePlay();
}
return handlePause();
}
return null;
}, [handlePause, handlePlay, playerStatus, queue]);
const handleSkipBackward = (seconds: number) => {
const currentTime = isMpvPlayer
? usePlayerStore.getState().current.time
: currentPlayerRef.getCurrentTime();
const evaluatedTime = currentTime - seconds;
const newTime = evaluatedTime < 0 ? 0 : evaluatedTime;
setCurrentTime(newTime);
mpris?.updateSeek(newTime);
if (isMpvPlayer) {
mpvPlayer.seek(-seconds);
} else {
resetNextPlayer();
currentPlayerRef.seekTo(newTime);
}
};
const handleSkipForward = (seconds: number) => {
const currentTime = isMpvPlayer
? usePlayerStore.getState().current.time
: currentPlayerRef.getCurrentTime();
if (isMpvPlayer) {
const newTime = currentTime + seconds;
mpvPlayer.seek(seconds);
mpris?.updateSeek(newTime);
setCurrentTime(newTime);
} else {
const checkNewTime = currentTime + seconds;
const songDuration = currentPlayerRef.player.player.duration;
const newTime = checkNewTime >= songDuration ? songDuration - 1 : checkNewTime;
mpris?.updateSeek(newTime);
resetNextPlayer();
setCurrentTime(newTime);
currentPlayerRef.seekTo(newTime);
}
};
const debouncedSeek = debounce((e: number) => {
if (isMpvPlayer) {
mpvPlayer.seekTo(e);
} else {
currentPlayerRef.seekTo(e);
}
}, 100);
const handleSeekSlider = useCallback(
(e: number | any) => {
setCurrentTime(e);
handleScrobbleFromSeek(e);
debouncedSeek(e);
mpris?.updateSeek(e);
},
[debouncedSeek, handleScrobbleFromSeek, setCurrentTime],
);
const handleQuit = useCallback(() => {
mpvPlayer.quit();
}, []);
const handleError = useCallback(
(message: string) => {
toast.error({ id: 'mpv-error', message, title: 'An error occurred during playback' });
pause();
mpvPlayer.pause();
},
[pause],
);
useEffect(() => {
if (isElectron()) {
mpvPlayerListener.rendererPlayPause(() => {
handlePlayPause();
});
mpvPlayerListener.rendererNext(() => {
handleNextTrack();
});
mpvPlayerListener.rendererPrevious(() => {
handlePrevTrack();
});
mpvPlayerListener.rendererPlay(() => {
handlePlay();
});
mpvPlayerListener.rendererPause(() => {
handlePause();
});
mpvPlayerListener.rendererStop(() => {
handleStop();
});
mpvPlayerListener.rendererCurrentTime((_event: any, time: number) => {
setCurrentTime(time);
});
mpvPlayerListener.rendererAutoNext(() => {
handleAutoNext();
});
mpvPlayerListener.rendererQuit(() => {
handleQuit();
});
mpvPlayerListener.rendererToggleShuffle(() => {
handleToggleShuffle();
});
mpvPlayerListener.rendererToggleRepeat(() => {
handleToggleRepeat();
});
mpvPlayerListener.rendererError((_event: any, message: string) => {
handleError(message);
});
}
return () => {
ipc?.removeAllListeners('renderer-player-play-pause');
ipc?.removeAllListeners('renderer-player-next');
ipc?.removeAllListeners('renderer-player-previous');
ipc?.removeAllListeners('renderer-player-play');
ipc?.removeAllListeners('renderer-player-pause');
ipc?.removeAllListeners('renderer-player-stop');
ipc?.removeAllListeners('renderer-player-current-time');
ipc?.removeAllListeners('renderer-player-auto-next');
ipc?.removeAllListeners('renderer-player-quit');
ipc?.removeAllListeners('renderer-player-toggle-shuffle');
ipc?.removeAllListeners('renderer-player-toggle-repeat');
ipc?.removeAllListeners('renderer-player-error');
};
}, [
autoNext,
handleAutoNext,
handleError,
handleNextTrack,
handlePause,
handlePlay,
handlePlayPause,
handlePrevTrack,
handleQuit,
handleStop,
handleToggleRepeat,
handleToggleShuffle,
isMpvPlayer,
next,
pause,
play,
previous,
setCurrentTime,
]);
useEffect(() => {
if (utils?.isLinux()) {
const unsubCurrentTime = usePlayerStore.subscribe(
(state) => state.current.time,
(time) => {
mpris?.updatePosition(time);
},
);
return () => {
unsubCurrentTime();
};
}
return () => {};
}, []);
useEffect(() => {
if (utils?.isLinux()) {
mpris.requestPosition((_e: any, data: { position: number }) => {
const newTime = data.position;
handleSeekSlider(newTime);
});
mpris.requestSeek((_e: any, data: { offset: number }) => {
const currentTime = usePlayerStore.getState().current.time;
const currentSongDuration = usePlayerStore.getState().current.song?.duration || 0;
const resultingTime = currentTime + data.offset;
let newTime = resultingTime;
if (resultingTime > currentSongDuration) {
newTime = currentSongDuration - 1;
}
if (resultingTime < 0) {
newTime = 0;
}
handleSeekSlider(newTime);
});
mpris.requestVolume((_e: any, data: { volume: number }) => {
const currentVolume = usePlayerStore.getState().volume;
const resultingVolume = data.volume + currentVolume;
let newVolume = resultingVolume;
if (newVolume > 100) {
newVolume = 100;
} else if (newVolume < 0) {
newVolume = 0;
}
usePlayerStore.getState().actions.setVolume(newVolume);
if (isMpvPlayer) {
mpvPlayer.volume(newVolume);
}
});
mpris.requestToggleRepeat((_e: any, data: { repeat: string }) => {
if (data.repeat === 'Playlist') {
usePlayerStore.getState().actions.setRepeat(PlayerRepeat.ALL);
} else if (data.repeat === 'Track') {
usePlayerStore.getState().actions.setRepeat(PlayerRepeat.ONE);
} else {
usePlayerStore.getState().actions.setRepeat(PlayerRepeat.NONE);
}
});
mpris.requestToggleShuffle((_e: any, data: { shuffle: boolean }) => {
usePlayerStore
.getState()
.actions.setShuffle(data.shuffle ? PlayerShuffle.TRACK : PlayerShuffle.NONE);
});
return () => {
ipc?.removeAllListeners('mpris-request-position');
ipc?.removeAllListeners('mpris-request-seek');
ipc?.removeAllListeners('mpris-request-volume');
ipc?.removeAllListeners('mpris-request-toggle-repeat');
ipc?.removeAllListeners('mpris-request-toggle-shuffle');
};
}
return () => {};
}, [handleSeekSlider, isMpvPlayer]);
return {
handleNextTrack,
handlePause,
handlePlay,
handlePlayPause,
handlePrevTrack,
handleSeekSlider,
handleSkipBackward,
handleSkipForward,
handleStop,
handleToggleRepeat,
handleToggleShuffle,
};
};