mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-06 20:10:12 +02:00
fix MPV visualizer on macOS and handle exclusive mode UX (#1930)
This commit is contained in:
@@ -47,6 +47,7 @@ mac:
|
|||||||
gatekeeperAssess: false
|
gatekeeperAssess: false
|
||||||
notarize: false
|
notarize: false
|
||||||
extendInfo:
|
extendInfo:
|
||||||
|
NSAudioCaptureUsageDescription: "System audio access is required for mpv visualizer capture in Feishin"
|
||||||
NSLocalNetworkUsageDescription: 'Local network is necessary for accessing servers hosted on the same system as Feishin'
|
NSLocalNetworkUsageDescription: 'Local network is necessary for accessing servers hosted on the same system as Feishin'
|
||||||
|
|
||||||
dmg:
|
dmg:
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ mac:
|
|||||||
gatekeeperAssess: false
|
gatekeeperAssess: false
|
||||||
notarize: false
|
notarize: false
|
||||||
extendInfo:
|
extendInfo:
|
||||||
|
NSAudioCaptureUsageDescription: "System audio access is required for mpv visualizer capture in Feishin"
|
||||||
NSLocalNetworkUsageDescription: 'Local network is necessary for accessing servers hosted on the same system as Feishin'
|
NSLocalNetworkUsageDescription: 'Local network is necessary for accessing servers hosted on the same system as Feishin'
|
||||||
|
|
||||||
dmg:
|
dmg:
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ mac:
|
|||||||
gatekeeperAssess: false
|
gatekeeperAssess: false
|
||||||
notarize: false
|
notarize: false
|
||||||
extendInfo:
|
extendInfo:
|
||||||
|
NSAudioCaptureUsageDescription: 'System audio access is required for mpv visualizer capture in Feishin'
|
||||||
NSLocalNetworkUsageDescription: 'Local network is necessary for accessing servers hosted on the same system as Feishin'
|
NSLocalNetworkUsageDescription: 'Local network is necessary for accessing servers hosted on the same system as Feishin'
|
||||||
|
|
||||||
dmg:
|
dmg:
|
||||||
|
|||||||
@@ -761,7 +761,7 @@
|
|||||||
"artistReleaseTypeConfiguration_description": "configure what release types are shown, and in what order, on the album artist page",
|
"artistReleaseTypeConfiguration_description": "configure what release types are shown, and in what order, on the album artist page",
|
||||||
"audioDevice_description": "select the audio device to use for playback",
|
"audioDevice_description": "select the audio device to use for playback",
|
||||||
"audioDevice": "audio device",
|
"audioDevice": "audio device",
|
||||||
"audioExclusiveMode_description": "enable exclusive output mode. In this mode, the system is usually locked out, and only mpv will be able to output audio",
|
"audioExclusiveMode_description": "enable exclusive output mode. in this mode, the system is usually locked out, and only mpv will be able to output audio. visualizer system audio capture will not work while this is enabled",
|
||||||
"audioExclusiveMode": "audio exclusive mode",
|
"audioExclusiveMode": "audio exclusive mode",
|
||||||
"audioPlayer_description": "select the audio player to use for playback",
|
"audioPlayer_description": "select the audio player to use for playback",
|
||||||
"audioPlayer": "audio player",
|
"audioPlayer": "audio player",
|
||||||
@@ -1220,6 +1220,7 @@
|
|||||||
"systemAudioConsentTitle": "Allow access to system audio?",
|
"systemAudioConsentTitle": "Allow access to system audio?",
|
||||||
"systemAudioCaptureFailed": "Could not start capture: {{message}}",
|
"systemAudioCaptureFailed": "Could not start capture: {{message}}",
|
||||||
"systemAudioNoAudioTrack": "No audio track was returned. Ensure audio capture is enabled when prompted.",
|
"systemAudioNoAudioTrack": "No audio track was returned. Ensure audio capture is enabled when prompted.",
|
||||||
|
"systemAudioExclusiveModeNotSupported": "Visualizer is unavailable while audio exclusive mode is enabled. Disable Audio Exclusive Mode in MPV settings and try again.",
|
||||||
"visualizerType": "Visualizer Type",
|
"visualizerType": "Visualizer Type",
|
||||||
"cyclePresets": "Cycle Presets",
|
"cyclePresets": "Cycle Presets",
|
||||||
"cycleTime": "Cycle Time (seconds)",
|
"cycleTime": "Cycle Time (seconds)",
|
||||||
|
|||||||
+21
-1
@@ -5,6 +5,7 @@ import {
|
|||||||
app,
|
app,
|
||||||
BrowserWindow,
|
BrowserWindow,
|
||||||
BrowserWindowConstructorOptions,
|
BrowserWindowConstructorOptions,
|
||||||
|
desktopCapturer,
|
||||||
globalShortcut,
|
globalShortcut,
|
||||||
ipcMain,
|
ipcMain,
|
||||||
Menu,
|
Menu,
|
||||||
@@ -733,7 +734,26 @@ async function createWindow(first = true): Promise<void> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
mainWindow.webContents.session.setDisplayMediaRequestHandler((_request, callback) => {
|
mainWindow.webContents.session.setDisplayMediaRequestHandler((_request, callback) => {
|
||||||
callback({ audio: 'loopback' });
|
if (!isMacOS()) {
|
||||||
|
callback({ audio: 'loopback' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
desktopCapturer
|
||||||
|
.getSources({ thumbnailSize: { height: 0, width: 0 }, types: ['screen'] })
|
||||||
|
.then((sources) => {
|
||||||
|
const source = sources[0];
|
||||||
|
if (!source) {
|
||||||
|
callback({});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
callback({ audio: 'loopback', video: source });
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
log.warn('desktopCapturer.getSources failed', err);
|
||||||
|
callback({});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!disableAutoUpdates() && store.get('disable_auto_updates') !== true) {
|
if (!disableAutoUpdates() && store.get('disable_auto_updates') !== true) {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export function useVisualizerSystemAudio(options: {
|
|||||||
onDeniedRef.current = onSystemAudioCaptureDenied;
|
onDeniedRef.current = onSystemAudioCaptureDenied;
|
||||||
onSuccessRef.current = onSystemAudioCaptureSuccess;
|
onSuccessRef.current = onSystemAudioCaptureSuccess;
|
||||||
const playbackType = usePlaybackType();
|
const playbackType = usePlaybackType();
|
||||||
|
const isMacOS = Boolean(window.api?.utils?.isMacOS?.());
|
||||||
const { setWebAudio, webAudio } = useWebAudio();
|
const { setWebAudio, webAudio } = useWebAudio();
|
||||||
const webAudioRef = useRef(webAudio);
|
const webAudioRef = useRef(webAudio);
|
||||||
const streamRef = useRef<MediaStream | null>(null);
|
const streamRef = useRef<MediaStream | null>(null);
|
||||||
@@ -80,7 +81,7 @@ export function useVisualizerSystemAudio(options: {
|
|||||||
try {
|
try {
|
||||||
const stream = await navigator.mediaDevices.getDisplayMedia({
|
const stream = await navigator.mediaDevices.getDisplayMedia({
|
||||||
audio: true,
|
audio: true,
|
||||||
video: false,
|
video: isMacOS, // On macOS, getDisplayMedia requires video to be requested in order to capture system audio
|
||||||
});
|
});
|
||||||
|
|
||||||
const audioTracks = stream.getAudioTracks();
|
const audioTracks = stream.getAudioTracks();
|
||||||
@@ -124,7 +125,7 @@ export function useVisualizerSystemAudio(options: {
|
|||||||
} finally {
|
} finally {
|
||||||
connectInFlightRef.current = false;
|
connectInFlightRef.current = false;
|
||||||
}
|
}
|
||||||
}, [disconnect, setWebAudio]);
|
}, [disconnect, isMacOS, setWebAudio]);
|
||||||
|
|
||||||
const connectRef = useRef(connect);
|
const connectRef = useRef(connect);
|
||||||
connectRef.current = connect;
|
connectRef.current = connect;
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
import isElectron from 'is-electron';
|
import isElectron from 'is-electron';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { useIsLocalVisualizerSurfaceVisible } from '/@/renderer/features/player/hooks/use-is-local-visualizer-surface-visible';
|
import { useIsLocalVisualizerSurfaceVisible } from '/@/renderer/features/player/hooks/use-is-local-visualizer-surface-visible';
|
||||||
import { useVisualizerSystemAudio } from '/@/renderer/features/player/hooks/use-visualizer-system-audio';
|
import { useVisualizerSystemAudio } from '/@/renderer/features/player/hooks/use-visualizer-system-audio';
|
||||||
import { closeLocalVisualizerSurfaces } from '/@/renderer/features/player/utils/close-local-visualizer-surfaces';
|
import { closeLocalVisualizerSurfaces } from '/@/renderer/features/player/utils/close-local-visualizer-surfaces';
|
||||||
import { usePlaybackType } from '/@/renderer/store';
|
import { useMpvSettings, usePlaybackType } from '/@/renderer/store';
|
||||||
import { Button } from '/@/shared/components/button/button';
|
import { Button } from '/@/shared/components/button/button';
|
||||||
import { Group } from '/@/shared/components/group/group';
|
import { Group } from '/@/shared/components/group/group';
|
||||||
import { Modal } from '/@/shared/components/modal/modal';
|
import { Modal } from '/@/shared/components/modal/modal';
|
||||||
import { Stack } from '/@/shared/components/stack/stack';
|
import { Stack } from '/@/shared/components/stack/stack';
|
||||||
import { Text } from '/@/shared/components/text/text';
|
import { Text } from '/@/shared/components/text/text';
|
||||||
|
import { toast } from '/@/shared/components/toast/toast';
|
||||||
import { useDisclosure } from '/@/shared/hooks/use-disclosure';
|
import { useDisclosure } from '/@/shared/hooks/use-disclosure';
|
||||||
import { PlayerType } from '/@/shared/types/types';
|
import { PlayerType } from '/@/shared/types/types';
|
||||||
|
|
||||||
@@ -31,12 +32,21 @@ export function VisualizerSystemAudioBridgeHook() {
|
|||||||
function VisualizerSystemAudioBridge() {
|
function VisualizerSystemAudioBridge() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const playbackType = usePlaybackType();
|
const playbackType = usePlaybackType();
|
||||||
|
const { audioExclusiveMode } = useMpvSettings();
|
||||||
const isVisualizerSurfaceVisible = useIsLocalVisualizerSurfaceVisible();
|
const isVisualizerSurfaceVisible = useIsLocalVisualizerSurfaceVisible();
|
||||||
const [promptState, setPromptState] = useState<PromptState>('loading');
|
const [promptState, setPromptState] = useState<PromptState>('loading');
|
||||||
const [sessionAllowCapture, setSessionAllowCapture] = useState(false);
|
const [sessionAllowCapture, setSessionAllowCapture] = useState(false);
|
||||||
|
const wasBlockedByExclusiveModeRef = useRef(false);
|
||||||
const [isPromptOpen, { close: closePrompt, open: openPrompt, toggle: togglePrompt }] =
|
const [isPromptOpen, { close: closePrompt, open: openPrompt, toggle: togglePrompt }] =
|
||||||
useDisclosure(false);
|
useDisclosure(false);
|
||||||
|
|
||||||
|
const isExclusiveModeEnabled = audioExclusiveMode === 'yes';
|
||||||
|
const isVisualizerBlockedByExclusiveMode =
|
||||||
|
isElectron() &&
|
||||||
|
playbackType === PlayerType.LOCAL &&
|
||||||
|
isVisualizerSurfaceVisible &&
|
||||||
|
isExclusiveModeEnabled;
|
||||||
|
|
||||||
const persistConsent = useCallback((granted: boolean) => {
|
const persistConsent = useCallback((granted: boolean) => {
|
||||||
if (!isElectron() || !window.api.localSettings) {
|
if (!isElectron() || !window.api.localSettings) {
|
||||||
return;
|
return;
|
||||||
@@ -67,6 +77,7 @@ function VisualizerSystemAudioBridge() {
|
|||||||
const eligibleForPrompt =
|
const eligibleForPrompt =
|
||||||
isElectron() &&
|
isElectron() &&
|
||||||
playbackType === PlayerType.LOCAL &&
|
playbackType === PlayerType.LOCAL &&
|
||||||
|
!isExclusiveModeEnabled &&
|
||||||
isVisualizerSurfaceVisible &&
|
isVisualizerSurfaceVisible &&
|
||||||
promptState !== 'loading' &&
|
promptState !== 'loading' &&
|
||||||
!promptState.consent &&
|
!promptState.consent &&
|
||||||
@@ -80,9 +91,25 @@ function VisualizerSystemAudioBridge() {
|
|||||||
}
|
}
|
||||||
}, [eligibleForPrompt, closePrompt, openPrompt]);
|
}, [eligibleForPrompt, closePrompt, openPrompt]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isVisualizerBlockedByExclusiveMode && !wasBlockedByExclusiveModeRef.current) {
|
||||||
|
toast.error({
|
||||||
|
message: t('visualizer.systemAudioExclusiveModeNotSupported', {
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
setSessionAllowCapture(false);
|
||||||
|
closePrompt();
|
||||||
|
closeLocalVisualizerSurfaces();
|
||||||
|
}
|
||||||
|
|
||||||
|
wasBlockedByExclusiveModeRef.current = isVisualizerBlockedByExclusiveMode;
|
||||||
|
}, [closePrompt, isVisualizerBlockedByExclusiveMode, t]);
|
||||||
|
|
||||||
const shouldAttemptConnection =
|
const shouldAttemptConnection =
|
||||||
isElectron() &&
|
isElectron() &&
|
||||||
playbackType === PlayerType.LOCAL &&
|
playbackType === PlayerType.LOCAL &&
|
||||||
|
!isExclusiveModeEnabled &&
|
||||||
isVisualizerSurfaceVisible &&
|
isVisualizerSurfaceVisible &&
|
||||||
promptState !== 'loading' &&
|
promptState !== 'loading' &&
|
||||||
(promptState.consent || sessionAllowCapture);
|
(promptState.consent || sessionAllowCapture);
|
||||||
|
|||||||
Reference in New Issue
Block a user