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
|
||||
notarize: false
|
||||
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'
|
||||
|
||||
dmg:
|
||||
|
||||
@@ -47,6 +47,7 @@ mac:
|
||||
gatekeeperAssess: false
|
||||
notarize: false
|
||||
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'
|
||||
|
||||
dmg:
|
||||
|
||||
@@ -47,6 +47,7 @@ mac:
|
||||
gatekeeperAssess: false
|
||||
notarize: false
|
||||
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'
|
||||
|
||||
dmg:
|
||||
|
||||
@@ -761,7 +761,7 @@
|
||||
"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": "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",
|
||||
"audioPlayer_description": "select the audio player to use for playback",
|
||||
"audioPlayer": "audio player",
|
||||
@@ -1220,6 +1220,7 @@
|
||||
"systemAudioConsentTitle": "Allow access to system audio?",
|
||||
"systemAudioCaptureFailed": "Could not start capture: {{message}}",
|
||||
"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",
|
||||
"cyclePresets": "Cycle Presets",
|
||||
"cycleTime": "Cycle Time (seconds)",
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
app,
|
||||
BrowserWindow,
|
||||
BrowserWindowConstructorOptions,
|
||||
desktopCapturer,
|
||||
globalShortcut,
|
||||
ipcMain,
|
||||
Menu,
|
||||
@@ -733,7 +734,26 @@ async function createWindow(first = true): Promise<void> {
|
||||
});
|
||||
|
||||
mainWindow.webContents.session.setDisplayMediaRequestHandler((_request, callback) => {
|
||||
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) {
|
||||
|
||||
@@ -19,6 +19,7 @@ export function useVisualizerSystemAudio(options: {
|
||||
onDeniedRef.current = onSystemAudioCaptureDenied;
|
||||
onSuccessRef.current = onSystemAudioCaptureSuccess;
|
||||
const playbackType = usePlaybackType();
|
||||
const isMacOS = Boolean(window.api?.utils?.isMacOS?.());
|
||||
const { setWebAudio, webAudio } = useWebAudio();
|
||||
const webAudioRef = useRef(webAudio);
|
||||
const streamRef = useRef<MediaStream | null>(null);
|
||||
@@ -80,7 +81,7 @@ export function useVisualizerSystemAudio(options: {
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getDisplayMedia({
|
||||
audio: true,
|
||||
video: false,
|
||||
video: isMacOS, // On macOS, getDisplayMedia requires video to be requested in order to capture system audio
|
||||
});
|
||||
|
||||
const audioTracks = stream.getAudioTracks();
|
||||
@@ -124,7 +125,7 @@ export function useVisualizerSystemAudio(options: {
|
||||
} finally {
|
||||
connectInFlightRef.current = false;
|
||||
}
|
||||
}, [disconnect, setWebAudio]);
|
||||
}, [disconnect, isMacOS, setWebAudio]);
|
||||
|
||||
const connectRef = useRef(connect);
|
||||
connectRef.current = connect;
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import isElectron from 'is-electron';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
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 { 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 { Group } from '/@/shared/components/group/group';
|
||||
import { Modal } from '/@/shared/components/modal/modal';
|
||||
import { Stack } from '/@/shared/components/stack/stack';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
import { toast } from '/@/shared/components/toast/toast';
|
||||
import { useDisclosure } from '/@/shared/hooks/use-disclosure';
|
||||
import { PlayerType } from '/@/shared/types/types';
|
||||
|
||||
@@ -31,12 +32,21 @@ export function VisualizerSystemAudioBridgeHook() {
|
||||
function VisualizerSystemAudioBridge() {
|
||||
const { t } = useTranslation();
|
||||
const playbackType = usePlaybackType();
|
||||
const { audioExclusiveMode } = useMpvSettings();
|
||||
const isVisualizerSurfaceVisible = useIsLocalVisualizerSurfaceVisible();
|
||||
const [promptState, setPromptState] = useState<PromptState>('loading');
|
||||
const [sessionAllowCapture, setSessionAllowCapture] = useState(false);
|
||||
const wasBlockedByExclusiveModeRef = useRef(false);
|
||||
const [isPromptOpen, { close: closePrompt, open: openPrompt, toggle: togglePrompt }] =
|
||||
useDisclosure(false);
|
||||
|
||||
const isExclusiveModeEnabled = audioExclusiveMode === 'yes';
|
||||
const isVisualizerBlockedByExclusiveMode =
|
||||
isElectron() &&
|
||||
playbackType === PlayerType.LOCAL &&
|
||||
isVisualizerSurfaceVisible &&
|
||||
isExclusiveModeEnabled;
|
||||
|
||||
const persistConsent = useCallback((granted: boolean) => {
|
||||
if (!isElectron() || !window.api.localSettings) {
|
||||
return;
|
||||
@@ -67,6 +77,7 @@ function VisualizerSystemAudioBridge() {
|
||||
const eligibleForPrompt =
|
||||
isElectron() &&
|
||||
playbackType === PlayerType.LOCAL &&
|
||||
!isExclusiveModeEnabled &&
|
||||
isVisualizerSurfaceVisible &&
|
||||
promptState !== 'loading' &&
|
||||
!promptState.consent &&
|
||||
@@ -80,9 +91,25 @@ function VisualizerSystemAudioBridge() {
|
||||
}
|
||||
}, [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 =
|
||||
isElectron() &&
|
||||
playbackType === PlayerType.LOCAL &&
|
||||
!isExclusiveModeEnabled &&
|
||||
isVisualizerSurfaceVisible &&
|
||||
promptState !== 'loading' &&
|
||||
(promptState.consent || sessionAllowCapture);
|
||||
|
||||
Reference in New Issue
Block a user