support ytmusic controls on web/mpv players

This commit is contained in:
jeffvli
2026-02-06 21:38:05 -08:00
parent 8e603871b7
commit 8ae29407ec
6 changed files with 243 additions and 75 deletions
@@ -10,8 +10,10 @@ async function searchYoutube(query: string): Promise<Array<{ type: string; video
export const youtubeQueries = { export const youtubeQueries = {
search: (args: { query: string }) => { search: (args: { query: string }) => {
return queryOptions({ return queryOptions({
gcTime: 1000 * 60 * 1,
queryFn: () => searchYoutube(args.query), queryFn: () => searchYoutube(args.query),
queryKey: ['youtube', 'search', args.query], queryKey: ['youtube', 'search', args.query],
staleTime: 1000 * 60 * 1,
}); });
}, },
}; };
@@ -21,8 +21,10 @@ import { PlayerStatus } from '/@/shared/types/types';
export interface MpvPlayerEngineHandle extends AudioPlayer {} export interface MpvPlayerEngineHandle extends AudioPlayer {}
interface MpvPlayerEngineProps { interface MpvPlayerEngineProps {
currentSongUrl: string | undefined;
isMuted: boolean; isMuted: boolean;
isTransitioning: boolean; isTransitioning: boolean;
nextSongUrl: string | undefined;
onEnded: () => void; onEnded: () => void;
onProgress: (e: PlayerOnProgressProps) => void; onProgress: (e: PlayerOnProgressProps) => void;
playerRef: RefObject<MpvPlayerEngineHandle | null>; playerRef: RefObject<MpvPlayerEngineHandle | null>;
@@ -39,8 +41,10 @@ const PROGRESS_UPDATE_INTERVAL = 250;
export const MpvPlayerEngine = (props: MpvPlayerEngineProps) => { export const MpvPlayerEngine = (props: MpvPlayerEngineProps) => {
const { const {
currentSongUrl: currentSongUrlProp,
isMuted, isMuted,
isTransitioning, isTransitioning,
nextSongUrl: nextSongUrlProp,
onEnded, onEnded,
onProgress, onProgress,
playerRef, playerRef,
@@ -56,6 +60,11 @@ export const MpvPlayerEngine = (props: MpvPlayerEngineProps) => {
const isInitializedRef = useRef<boolean>(false); const isInitializedRef = useRef<boolean>(false);
const hasPopulatedQueueRef = useRef<boolean>(false); const hasPopulatedQueueRef = useRef<boolean>(false);
const isMountedRef = useRef<boolean>(true); const isMountedRef = useRef<boolean>(true);
const currentSongUrlRef = useRef<string | undefined>(currentSongUrlProp);
const nextSongUrlRef = useRef<string | undefined>(nextSongUrlProp);
currentSongUrlRef.current = currentSongUrlProp;
nextSongUrlRef.current = nextSongUrlProp;
const { mpvAudioDeviceId, transcode } = usePlaybackSettings(); const { mpvAudioDeviceId, transcode } = usePlaybackSettings();
const mpvExtraParameters = useSettingsStore((store) => store.playback.mpvExtraParameters); const mpvExtraParameters = useSettingsStore((store) => store.playback.mpvExtraParameters);
@@ -124,15 +133,17 @@ export const MpvPlayerEngine = (props: MpvPlayerEngineProps) => {
if (!radioState.currentStreamUrl) { if (!radioState.currentStreamUrl) {
const playerData = usePlayerStore.getState().getPlayerData(); const playerData = usePlayerStore.getState().getPlayerData();
const currentSongUrl = playerData.currentSong const currentResolved =
? getSongUrl(playerData.currentSong, transcode) currentSongUrlProp ??
: undefined; (playerData.currentSong
const nextSongUrl = playerData.nextSong ? getSongUrl(playerData.currentSong, transcode)
? getSongUrl(playerData.nextSong, transcode) : undefined);
: undefined; const nextResolved =
nextSongUrlProp ??
(playerData.nextSong ? getSongUrl(playerData.nextSong, transcode) : undefined);
if (currentSongUrl && nextSongUrl && !hasPopulatedQueueRef.current && mpvPlayer) { if (currentResolved && !hasPopulatedQueueRef.current && mpvPlayer) {
mpvPlayer.setQueue(currentSongUrl, nextSongUrl, true); mpvPlayer.setQueue(currentResolved, nextResolved ?? currentResolved, true);
hasPopulatedQueueRef.current = true; hasPopulatedQueueRef.current = true;
} }
} }
@@ -157,6 +168,30 @@ export const MpvPlayerEngine = (props: MpvPlayerEngineProps) => {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [mpvExtraParameters, mpvProperties, mpvAudioDeviceId, reloadTrigger]); }, [mpvExtraParameters, mpvProperties, mpvAudioDeviceId, reloadTrigger]);
// Sync queue when current/next song URLs change (e.g. user selects song, or external URL resolves from useSongUrl)
useEffect(() => {
if (!mpvPlayer || !isInitializedRef.current) {
return;
}
const radioState = useRadioStore.getState();
if (radioState.currentStreamUrl) {
return;
}
const playerData = usePlayerStore.getState().getPlayerData();
const currentResolved =
currentSongUrlProp ??
(playerData.currentSong ? getSongUrl(playerData.currentSong, transcode) : undefined);
const nextResolved =
nextSongUrlProp ??
(playerData.nextSong ? getSongUrl(playerData.nextSong, transcode) : undefined);
if (currentResolved) {
mpvPlayer.setQueue(currentResolved, nextResolved ?? currentResolved, false);
}
}, [currentSongUrlProp, nextSongUrlProp, currentSong?.id, currentSong?._uniqueId, transcode]);
// Update volume // Update volume
useEffect(() => { useEffect(() => {
if (!mpvPlayer) { if (!mpvPlayer) {
@@ -257,7 +292,7 @@ export const MpvPlayerEngine = (props: MpvPlayerEngineProps) => {
const handleOnAutoNext = () => { const handleOnAutoNext = () => {
mediaAutoNext(); mediaAutoNext();
handleMpvAutoNext(transcode); handleMpvAutoNext(transcode, nextSongUrlRef.current);
}; };
mpvPlayerListener.rendererAutoNext(handleOnAutoNext); mpvPlayerListener.rendererAutoNext(handleOnAutoNext);
@@ -270,10 +305,10 @@ export const MpvPlayerEngine = (props: MpvPlayerEngineProps) => {
usePlayerEvents( usePlayerEvents(
{ {
onMediaNext: () => { onMediaNext: () => {
replaceMpvQueue(transcode); replaceMpvQueue(transcode, currentSongUrlRef.current, nextSongUrlRef.current);
}, },
onMediaPrev: () => { onMediaPrev: () => {
replaceMpvQueue(transcode); replaceMpvQueue(transcode, currentSongUrlRef.current, nextSongUrlRef.current);
}, },
onNextSongInsertion: (song) => { onNextSongInsertion: (song) => {
const radioState = useRadioStore.getState(); const radioState = useRadioStore.getState();
@@ -282,11 +317,12 @@ export const MpvPlayerEngine = (props: MpvPlayerEngineProps) => {
return; return;
} }
const nextSongUrl = song ? getSongUrl(song, transcode) : undefined; const nextSongUrl =
nextSongUrlRef.current ?? (song ? getSongUrl(song, transcode) : undefined);
mpvPlayer?.setQueueNext(nextSongUrl); mpvPlayer?.setQueueNext(nextSongUrl);
}, },
onPlayerPlay: () => { onPlayerPlay: () => {
replaceMpvQueue(transcode); replaceMpvQueue(transcode, currentSongUrlRef.current, nextSongUrlRef.current);
}, },
onQueueCleared: () => {}, onQueueCleared: () => {},
}, },
@@ -337,24 +373,30 @@ export const MpvPlayerEngine = (props: MpvPlayerEngineProps) => {
MpvPlayerEngine.displayName = 'MpvPlayerEngine'; MpvPlayerEngine.displayName = 'MpvPlayerEngine';
function handleMpvAutoNext(transcode: { function handleMpvAutoNext(
bitrate?: number | undefined; transcode: {
enabled: boolean; bitrate?: number | undefined;
format?: string | undefined; enabled: boolean;
}) { format?: string | undefined;
},
nextUrlOverride?: string,
) {
const playerData = usePlayerStore.getState().getPlayerData(); const playerData = usePlayerStore.getState().getPlayerData();
const nextSongUrl = playerData.nextSong const nextSongUrl =
? getSongUrl(playerData.nextSong, transcode) nextUrlOverride ??
: undefined; (playerData.nextSong ? getSongUrl(playerData.nextSong, transcode) : undefined);
mpvPlayer?.autoNext(nextSongUrl); mpvPlayer?.autoNext(nextSongUrl);
} }
function replaceMpvQueue(transcode: { function replaceMpvQueue(
bitrate?: number | undefined; transcode: {
enabled: boolean; bitrate?: number | undefined;
format?: string | undefined; enabled: boolean;
}) { format?: string | undefined;
// Don't override queue if radio is active },
currentUrlOverride?: string,
nextUrlOverride?: string,
) {
const radioState = useRadioStore.getState(); const radioState = useRadioStore.getState();
if (radioState.currentStreamUrl) { if (radioState.currentStreamUrl) {
@@ -362,11 +404,14 @@ function replaceMpvQueue(transcode: {
} }
const playerData = usePlayerStore.getState().getPlayerData(); const playerData = usePlayerStore.getState().getPlayerData();
const currentSongUrl = playerData.currentSong const currentSongUrl =
? getSongUrl(playerData.currentSong, transcode) currentUrlOverride ??
: undefined; (playerData.currentSong ? getSongUrl(playerData.currentSong, transcode) : undefined);
const nextSongUrl = playerData.nextSong const nextSongUrl =
? getSongUrl(playerData.nextSong, transcode) nextUrlOverride ??
: undefined; (playerData.nextSong ? getSongUrl(playerData.nextSong, transcode) : undefined);
mpvPlayer?.setQueue(currentSongUrl, nextSongUrl, false);
if (currentSongUrl) {
mpvPlayer?.setQueue(currentSongUrl, nextSongUrl ?? currentSongUrl, false);
}
} }
@@ -10,14 +10,17 @@ import { logMsg } from '/@/renderer/utils/logger-message';
import { PlayerStatus } from '/@/shared/types/types'; import { PlayerStatus } from '/@/shared/types/types';
export interface WebPlayerEngineHandle extends AudioPlayer { export interface WebPlayerEngineHandle extends AudioPlayer {
player1(): { player1(): WebPlayerEnginePlayerHandle;
ref: null | ReactPlayer; player2(): WebPlayerEnginePlayerHandle;
setVolume: (volume: number) => void; }
};
player2(): { export interface WebPlayerEnginePlayerHandle {
ref: null | ReactPlayer; getCurrentTime: () => number;
setVolume: (volume: number) => void; getDuration: () => number;
}; pause: () => void;
play: () => void;
ref: null | ReactPlayer;
setVolume: (volume: number) => void;
} }
interface WebPlayerEngineProps { interface WebPlayerEngineProps {
@@ -39,6 +42,70 @@ interface WebPlayerEngineProps {
volume: number; volume: number;
} }
interface YouTubePlayer {
getCurrentTime?: () => number;
getDuration?: () => number;
pauseVideo?: () => void;
playVideo?: () => void;
}
function getInternalCurrentTime(ref: null | ReactPlayer): number {
const internal = ref?.getInternalPlayer();
if (!internal) return 0;
if (internal instanceof HTMLMediaElement) {
return (internal as HTMLMediaElement).currentTime ?? 0;
}
if (isYouTubePlayer(internal) && typeof internal.getCurrentTime === 'function') {
return internal.getCurrentTime() ?? 0;
}
return 0;
}
function getInternalDuration(ref: null | ReactPlayer): number {
const internal = ref?.getInternalPlayer();
if (!internal) return 0;
if (internal instanceof HTMLMediaElement) {
return (internal as HTMLMediaElement).duration ?? 0;
}
if (isYouTubePlayer(internal) && typeof internal.getDuration === 'function') {
return internal.getDuration() ?? 0;
}
return 0;
}
function isYouTubePlayer(internal: unknown): internal is YouTubePlayer {
return (
typeof internal === 'object' &&
internal !== null &&
'playVideo' in internal &&
typeof (internal as YouTubePlayer).playVideo === 'function'
);
}
function pauseInternalPlayer(ref: null | ReactPlayer): void {
const internal = ref?.getInternalPlayer();
if (!internal) return;
if (internal instanceof HTMLMediaElement) {
(internal as HTMLMediaElement).pause();
return;
}
if (isYouTubePlayer(internal)) {
internal.pauseVideo?.();
}
}
function playInternalPlayer(ref: null | ReactPlayer): void {
const internal = ref?.getInternalPlayer();
if (!internal) return;
if (internal instanceof HTMLMediaElement) {
void (internal as HTMLMediaElement).play().catch(() => {});
return;
}
if (isYouTubePlayer(internal)) {
internal.playVideo?.();
}
}
// Credits: https://gist.github.com/novwhisky/8a1a0168b94f3b6abfaa?permalink_comment_id=1551393#gistcomment-1551393 // 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 // 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 // player1Source and player2Source are connected BEFORE the user presses play for
@@ -108,25 +175,33 @@ export const WebPlayerEngine = (props: WebPlayerEngineProps) => {
setInternalVolume2(Math.min(1, internalVolume2 + by / 100)); setInternalVolume2(Math.min(1, internalVolume2 + by / 100));
}, },
pause() { pause() {
player1Ref.current?.getInternalPlayer()?.pause(); pauseInternalPlayer(player1Ref.current);
player2Ref.current?.getInternalPlayer()?.pause(); pauseInternalPlayer(player2Ref.current);
}, },
play() { play() {
if (playerNum === 1) { if (playerNum === 1) {
player1Ref.current?.getInternalPlayer()?.play(); playInternalPlayer(player1Ref.current);
} else { } else {
player2Ref.current?.getInternalPlayer()?.play(); playInternalPlayer(player2Ref.current);
} }
}, },
player1() { player1(): WebPlayerEnginePlayerHandle {
return { return {
ref: player1Ref?.current, getCurrentTime: () => getInternalCurrentTime(player1Ref.current),
getDuration: () => getInternalDuration(player1Ref.current),
pause: () => pauseInternalPlayer(player1Ref.current),
play: () => playInternalPlayer(player1Ref.current),
ref: player1Ref?.current ?? null,
setVolume: (volume: number) => setInternalVolume1(volume / 100 || 0), setVolume: (volume: number) => setInternalVolume1(volume / 100 || 0),
}; };
}, },
player2() { player2(): WebPlayerEnginePlayerHandle {
return { return {
ref: player2Ref?.current, getCurrentTime: () => getInternalCurrentTime(player2Ref.current),
getDuration: () => getInternalDuration(player2Ref.current),
pause: () => pauseInternalPlayer(player2Ref.current),
play: () => playInternalPlayer(player2Ref.current),
ref: player2Ref?.current ?? null,
setVolume: (volume: number) => setInternalVolume2(volume / 100 || 0), setVolume: (volume: number) => setInternalVolume2(volume / 100 || 0),
}; };
}, },
@@ -30,6 +30,7 @@ export function useSongUrl(
return prior.current[1]; return prior.current[1];
} }
const url = getYoutubeUrlFromSearchResults(youtubeSearch.data); const url = getYoutubeUrlFromSearchResults(youtubeSearch.data);
if (url) prior.current = [song._uniqueId, url]; if (url) prior.current = [song._uniqueId, url];
return url; return url;
}, [song, isExternal, current, youtubeSearch.data]); }, [song, isExternal, current, youtubeSearch.data]);
@@ -81,12 +82,16 @@ function getYoutubeUrlFromSearchResults(
): string | undefined { ): string | undefined {
if (!results?.length) return undefined; if (!results?.length) return undefined;
const first = results.find((r) => r.type === 'SONG' || r.type === 'VIDEO'); const first = results.find((r) => r.type === 'SONG' || r.type === 'VIDEO');
return first && 'videoId' in first && first.videoId return first && 'videoId' in first && first.videoId
? `${YOUTUBE_WATCH_BASE}${first.videoId}` ? `${YOUTUBE_WATCH_BASE}${first.videoId}`
: undefined; : undefined;
} }
export const getSongUrl = (song: QueueSong, transcode: TranscodingConfig) => { export const getSongUrl = (song: QueueSong, transcode: TranscodingConfig): string => {
if (song._serverType === ServerType.EXTERNAL) {
return '';
}
return api.controller.getStreamUrl({ return api.controller.getStreamUrl({
apiClientProps: { serverId: song._serverId }, apiClientProps: { serverId: song._serverId },
query: { query: {
@@ -97,3 +102,32 @@ export const getSongUrl = (song: QueueSong, transcode: TranscodingConfig) => {
}, },
}); });
}; };
export async function getSongUrlAsync(
song: QueueSong | undefined,
transcode: TranscodingConfig,
): Promise<string | undefined> {
if (!song) {
return undefined;
}
if (song._serverType === ServerType.EXTERNAL) {
if (typeof window === 'undefined' || !window.api?.youtube) {
return undefined;
}
const searchQuery = `${song.artistName ?? ''} ${song.name ?? ''}`.trim();
if (!searchQuery) {
return undefined;
}
try {
const results = await window.api.youtube.search(searchQuery);
console.log('results', results);
return getYoutubeUrlFromSearchResults(results);
} catch {
return undefined;
}
}
const url = getSongUrl(song, transcode);
return url || undefined;
}
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
import { MpvPlayerEngine, MpvPlayerEngineHandle } from './engine/mpv-player-engine'; import { MpvPlayerEngine, MpvPlayerEngineHandle } from './engine/mpv-player-engine';
import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events'; import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events';
import { useSongUrl } from '/@/renderer/features/player/audio-player/hooks/use-stream-url';
import { usePlayer } from '/@/renderer/features/player/context/player-context'; import { usePlayer } from '/@/renderer/features/player/context/player-context';
import { import {
usePlaybackSettings, usePlaybackSettings,
@@ -23,12 +24,15 @@ const mpvPlayer = isElectron() ? window.api.mpvPlayer : null;
export function MpvPlayer() { export function MpvPlayer() {
const playerRef = useRef<MpvPlayerEngineHandle>(null); const playerRef = useRef<MpvPlayerEngineHandle>(null);
const { currentSong, status } = usePlayerData(); const { currentSong, nextSong, status } = usePlayerData();
const { mediaAutoNext, setTimestamp } = usePlayerActions(); const { mediaAutoNext, setTimestamp } = usePlayerActions();
const { speed } = usePlayerProperties(); const { speed } = usePlayerProperties();
const isMuted = usePlayerMuted(); const isMuted = usePlayerMuted();
const volume = usePlayerVolume(); const volume = usePlayerVolume();
const { audioFadeOnStatusChange } = usePlaybackSettings(); const { audioFadeOnStatusChange, transcode } = usePlaybackSettings();
const currentSongUrl = useSongUrl(currentSong, true, transcode);
const nextSongUrl = useSongUrl(nextSong, false, transcode);
const [localPlayerStatus, setLocalPlayerStatus] = useState<PlayerStatus>(status); const [localPlayerStatus, setLocalPlayerStatus] = useState<PlayerStatus>(status);
const [isTransitioning, setIsTransitioning] = useState(false); const [isTransitioning, setIsTransitioning] = useState(false);
@@ -174,8 +178,10 @@ export function MpvPlayer() {
return ( return (
<MpvPlayerEngine <MpvPlayerEngine
currentSongUrl={currentSongUrl}
isMuted={isMuted} isMuted={isMuted}
isTransitioning={isTransitioning} isTransitioning={isTransitioning}
nextSongUrl={nextSongUrl}
onEnded={handleOnEnded} onEnded={handleOnEnded}
onProgress={onProgress} onProgress={onProgress}
playerRef={playerRef} playerRef={playerRef}
@@ -106,7 +106,7 @@ export function WebPlayer() {
currentPlayer: playerRef.current.player1(), currentPlayer: playerRef.current.player1(),
currentPlayerNum: num, currentPlayerNum: num,
currentTime: e.playedSeconds, currentTime: e.playedSeconds,
duration: getDuration(playerRef.current.player1().ref), duration: getDuration(playerRef.current.player1()),
hasNextSong: Boolean(player2), hasNextSong: Boolean(player2),
isTransitioning, isTransitioning,
nextPlayer: playerRef.current.player2(), nextPlayer: playerRef.current.player2(),
@@ -118,7 +118,7 @@ export function WebPlayer() {
case PlayerStyle.GAPLESS: case PlayerStyle.GAPLESS:
gaplessHandler({ gaplessHandler({
currentTime: e.playedSeconds, currentTime: e.playedSeconds,
duration: getDuration(playerRef.current.player1().ref), duration: getDuration(playerRef.current.player1()),
isFlac: false, isFlac: false,
isTransitioning, isTransitioning,
nextPlayer: playerRef.current.player2(), nextPlayer: playerRef.current.player2(),
@@ -144,7 +144,7 @@ export function WebPlayer() {
currentPlayer: playerRef.current.player2(), currentPlayer: playerRef.current.player2(),
currentPlayerNum: num, currentPlayerNum: num,
currentTime: e.playedSeconds, currentTime: e.playedSeconds,
duration: getDuration(playerRef.current.player2().ref), duration: getDuration(playerRef.current.player2()),
hasNextSong: Boolean(player1), hasNextSong: Boolean(player1),
isTransitioning, isTransitioning,
nextPlayer: playerRef.current.player1(), nextPlayer: playerRef.current.player1(),
@@ -156,7 +156,7 @@ export function WebPlayer() {
case PlayerStyle.GAPLESS: case PlayerStyle.GAPLESS:
gaplessHandler({ gaplessHandler({
currentTime: e.playedSeconds, currentTime: e.playedSeconds,
duration: getDuration(playerRef.current.player2().ref), duration: getDuration(playerRef.current.player2()),
isFlac: false, isFlac: false,
isTransitioning, isTransitioning,
nextPlayer: playerRef.current.player1(), nextPlayer: playerRef.current.player1(),
@@ -175,7 +175,7 @@ export function WebPlayer() {
}); });
promise.then(() => { promise.then(() => {
playerRef.current?.player1()?.ref?.getInternalPlayer().pause(); playerRef.current?.player1()?.pause();
playerRef.current?.setVolume(volume); playerRef.current?.setVolume(volume);
setIsTransitioning(false); setIsTransitioning(false);
}); });
@@ -188,7 +188,7 @@ export function WebPlayer() {
}); });
promise.then(() => { promise.then(() => {
playerRef.current?.player2()?.ref?.getInternalPlayer().pause(); playerRef.current?.player2()?.pause();
playerRef.current?.setVolume(volume); playerRef.current?.setVolume(volume);
setIsTransitioning(false); setIsTransitioning(false);
}); });
@@ -213,11 +213,11 @@ export function WebPlayer() {
if (num === 1) { if (num === 1) {
playerRef.current?.player1()?.setVolume(volume); playerRef.current?.player1()?.setVolume(volume);
playerRef.current?.player2()?.setVolume(0); playerRef.current?.player2()?.setVolume(0);
playerRef.current?.player2()?.ref?.getInternalPlayer()?.pause(); playerRef.current?.player2()?.pause();
} else { } else {
playerRef.current?.player2()?.setVolume(volume); playerRef.current?.player2()?.setVolume(volume);
playerRef.current?.player1()?.setVolume(0); playerRef.current?.player1()?.setVolume(0);
playerRef.current?.player1()?.ref?.getInternalPlayer()?.pause(); playerRef.current?.player1()?.pause();
} }
} }
@@ -241,11 +241,11 @@ export function WebPlayer() {
if (num === 1) { if (num === 1) {
playerRef.current?.player1()?.setVolume(volume); playerRef.current?.player1()?.setVolume(volume);
playerRef.current?.player2()?.setVolume(0); playerRef.current?.player2()?.setVolume(0);
playerRef.current?.player2()?.ref?.getInternalPlayer()?.pause(); playerRef.current?.player2()?.pause();
} else { } else {
playerRef.current?.player2()?.setVolume(volume); playerRef.current?.player2()?.setVolume(volume);
playerRef.current?.player1()?.setVolume(0); playerRef.current?.player1()?.setVolume(0);
playerRef.current?.player1()?.ref?.getInternalPlayer()?.pause(); playerRef.current?.player1()?.pause();
} }
} }
@@ -294,14 +294,12 @@ export function WebPlayer() {
const interval = setInterval(() => { const interval = setInterval(() => {
const activePlayer = const activePlayer =
num === 1 ? playerRef.current?.player1() : playerRef.current?.player2(); num === 1 ? playerRef.current?.player1() : playerRef.current?.player2();
const internalPlayer =
activePlayer?.ref?.getInternalPlayer() as HTMLAudioElement | null;
if (!internalPlayer) { if (!activePlayer) {
return; return;
} }
const currentTime = internalPlayer.currentTime; const currentTime = activePlayer.getCurrentTime();
if ( if (
transitionType === PlayerStyle.CROSSFADE || transitionType === PlayerStyle.CROSSFADE ||
@@ -468,6 +466,7 @@ function crossfadeHandler(args: {
crossfadeDuration: number; crossfadeDuration: number;
crossfadeStyle: CrossfadeStyle; crossfadeStyle: CrossfadeStyle;
currentPlayer: { currentPlayer: {
pause: () => void;
ref: null | ReactPlayer; ref: null | ReactPlayer;
setVolume: (volume: number) => void; setVolume: (volume: number) => void;
}; };
@@ -477,6 +476,8 @@ function crossfadeHandler(args: {
hasNextSong: boolean; hasNextSong: boolean;
isTransitioning: boolean | string; isTransitioning: boolean | string;
nextPlayer: { nextPlayer: {
pause: () => void;
play: () => void;
ref: null | ReactPlayer; ref: null | ReactPlayer;
setVolume: (volume: number) => void; setVolume: (volume: number) => void;
}; };
@@ -504,7 +505,7 @@ function crossfadeHandler(args: {
if (!hasNextSong) { if (!hasNextSong) {
currentPlayer.setVolume(volume); currentPlayer.setVolume(volume);
nextPlayer.setVolume(0); nextPlayer.setVolume(0);
nextPlayer.ref?.getInternalPlayer()?.pause(); nextPlayer.pause();
if (isTransitioning) { if (isTransitioning) {
setIsTransitioning(false); setIsTransitioning(false);
@@ -516,7 +517,7 @@ function crossfadeHandler(args: {
if (!isTransitioning) { if (!isTransitioning) {
if (duration > 0 && currentTime > duration - crossfadeDuration) { if (duration > 0 && currentTime > duration - crossfadeDuration) {
nextPlayer.setVolume(0); nextPlayer.setVolume(0);
nextPlayer.ref?.getInternalPlayer().play(); nextPlayer.play();
return setIsTransitioning(player); return setIsTransitioning(player);
} }
@@ -586,6 +587,7 @@ function gaplessHandler(args: {
isFlac: boolean; isFlac: boolean;
isTransitioning: boolean | string; isTransitioning: boolean | string;
nextPlayer: { nextPlayer: {
play: () => void;
ref: null | ReactPlayer; ref: null | ReactPlayer;
setVolume: (volume: number) => void; setVolume: (volume: number) => void;
}; };
@@ -604,10 +606,8 @@ function gaplessHandler(args: {
const durationPadding = getDurationPadding(isFlac); const durationPadding = getDurationPadding(isFlac);
if (currentTime + durationPadding >= duration) { if (currentTime + durationPadding >= duration) {
return nextPlayer.ref nextPlayer.play();
?.getInternalPlayer() return;
?.play()
.catch(() => {});
} }
return null; return null;
@@ -647,8 +647,14 @@ function getCrossfadeEasing(style: CrossfadeStyle): {
} }
} }
function getDuration(ref: null | ReactPlayer | undefined) { function getDuration(
return ref?.getInternalPlayer()?.duration || 0; player:
| undefined
| {
getDuration: () => number;
},
) {
return player?.getDuration?.() ?? 0;
} }
function getDurationPadding(isFlac: boolean) { function getDurationPadding(isFlac: boolean) {