mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-10 04:30:25 +02:00
support ytmusic controls on web/mpv players
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user