desktop-audio-proxy
Version:
A comprehensive audio streaming solution for Tauri and Electron apps that bypasses CORS and WebKit codec issues
338 lines (292 loc) • 8.78 kB
text/typescript
import { useState, useEffect, useCallback, useMemo } from 'react';
import {
AudioProxyClient,
TauriAudioService,
ElectronAudioService,
} from './index';
import { AudioProxyOptions, StreamInfo, Environment } from './types';
/**
* Hook for managing audio proxy client with automatic URL processing
*/
export function useAudioProxy(url: string | null, options?: AudioProxyOptions) {
const [audioUrl, setAudioUrl] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [streamInfo, setStreamInfo] = useState<StreamInfo | null>(null);
// Memoize client to prevent unnecessary recreations
const client = useMemo(() => new AudioProxyClient(options), [options]);
const processUrl = useCallback(
async (inputUrl: string) => {
setIsLoading(true);
setError(null);
setAudioUrl(null);
setStreamInfo(null);
try {
// Get stream info first
const info = await client.canPlayUrl(inputUrl);
setStreamInfo(info);
// Get playable URL
const playableUrl = await client.getPlayableUrl(inputUrl);
setAudioUrl(playableUrl);
} catch (err) {
const errorMessage =
err instanceof Error ? err.message : 'Unknown error';
setError(errorMessage);
} finally {
setIsLoading(false);
}
},
[client]
);
useEffect(() => {
if (url) {
processUrl(url);
} else {
setAudioUrl(null);
setStreamInfo(null);
setError(null);
setIsLoading(false);
}
}, [url, processUrl]);
const retry = useCallback(() => {
if (url) {
processUrl(url);
}
}, [url, processUrl]);
return {
audioUrl,
isLoading,
error,
streamInfo,
retry,
client,
};
}
/**
* Hook for accessing audio capabilities and system information
*/
export function useAudioCapabilities() {
const [capabilities, setCapabilities] = useState<{
supportedFormats: string[];
missingCodecs: string[];
capabilities: Record<string, string>;
environment: Environment;
electronVersion?: string;
chromiumVersion?: string;
} | null>(null);
const [devices, setDevices] = useState<{
inputDevices: Array<{ id: string; name: string }>;
outputDevices: Array<{ id: string; name: string }>;
} | null>(null);
const [systemSettings, setSystemSettings] = useState<{
defaultInputDevice?: string;
defaultOutputDevice?: string;
masterVolume?: number;
} | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const client = useMemo(() => new AudioProxyClient(), []);
const refreshCapabilities = useCallback(async () => {
setIsLoading(true);
setError(null);
try {
const environment = client.getEnvironment();
let service: TauriAudioService | ElectronAudioService | null = null;
if (environment === 'tauri') {
service = new TauriAudioService();
} else if (environment === 'electron') {
service = new ElectronAudioService();
}
if (service) {
// Get codec capabilities
const codecInfo = await service.checkSystemCodecs();
setCapabilities({
...codecInfo,
environment,
});
// Get audio devices
const deviceInfo = await service.getAudioDevices();
if (deviceInfo) {
setDevices(deviceInfo);
}
// Get system settings (Electron only)
if (environment === 'electron' && 'getSystemAudioSettings' in service) {
const settings = await (
service as ElectronAudioService
).getSystemAudioSettings();
if (settings) {
setSystemSettings(settings);
}
}
} else {
// Basic web environment capabilities
const audio = new Audio();
const formats = ['MP3', 'OGG', 'WAV', 'AAC', 'FLAC', 'WEBM', 'M4A'];
const supportedFormats = formats.filter(format => {
const mimeTypes = {
MP3: 'audio/mpeg',
OGG: 'audio/ogg',
WAV: 'audio/wav',
AAC: 'audio/aac',
FLAC: 'audio/flac',
WEBM: 'audio/webm',
M4A: 'audio/mp4',
};
return (
audio.canPlayType(mimeTypes[format as keyof typeof mimeTypes]) !==
''
);
});
setCapabilities({
supportedFormats,
missingCodecs: formats.filter(f => !supportedFormats.includes(f)),
capabilities: {},
environment,
});
}
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
setError(errorMessage);
} finally {
setIsLoading(false);
}
}, [client]);
useEffect(() => {
refreshCapabilities();
}, [refreshCapabilities]);
return {
capabilities,
devices,
systemSettings,
isLoading,
error,
refresh: refreshCapabilities,
};
}
/**
* Hook for checking proxy server availability
*/
export function useProxyStatus(options?: AudioProxyOptions) {
const [isAvailable, setIsAvailable] = useState<boolean | null>(null);
const [isChecking, setIsChecking] = useState(false);
const [error, setError] = useState<string | null>(null);
const [proxyUrl, setProxyUrl] = useState<string>('');
const client = useMemo(() => new AudioProxyClient(options), [options]);
const checkProxy = useCallback(async () => {
setIsChecking(true);
setError(null);
try {
const available = await client.isProxyAvailable();
setIsAvailable(available);
setProxyUrl(client['options']?.proxyUrl || 'http://localhost:3002');
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
setError(errorMessage);
setIsAvailable(false);
} finally {
setIsChecking(false);
}
}, [client]);
useEffect(() => {
checkProxy();
}, [checkProxy]);
return {
isAvailable,
isChecking,
error,
proxyUrl,
refresh: checkProxy,
};
}
/**
* Hook for audio metadata extraction (Tauri/Electron only)
*/
export function useAudioMetadata(filePath: string | null) {
const [metadata, setMetadata] = useState<{
duration?: number;
bitrate?: number;
sampleRate?: number;
channels?: number;
format?: string;
} | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const client = useMemo(() => new AudioProxyClient(), []);
useEffect(() => {
if (!filePath) {
setMetadata(null);
setError(null);
setIsLoading(false);
return;
}
const getMetadata = async () => {
setIsLoading(true);
setError(null);
setMetadata(null);
try {
const environment = client.getEnvironment();
let service: TauriAudioService | ElectronAudioService | null = null;
if (environment === 'tauri') {
service = new TauriAudioService();
} else if (environment === 'electron') {
service = new ElectronAudioService();
}
if (service) {
const result = await service.getAudioMetadata(filePath);
setMetadata(result);
} else {
setError(
'Audio metadata extraction is only available in Tauri or Electron environments'
);
}
} catch (err) {
const errorMessage =
err instanceof Error ? err.message : 'Unknown error';
setError(errorMessage);
} finally {
setIsLoading(false);
}
};
getMetadata();
}, [filePath, client]);
return {
metadata,
isLoading,
error,
};
}
/**
* Context provider for global audio proxy configuration
*/
import { createContext, useContext, ReactNode, createElement } from 'react';
interface AudioProxyContextValue {
defaultOptions: AudioProxyOptions;
client: AudioProxyClient;
}
const AudioProxyContext = createContext<AudioProxyContextValue | null>(null);
export function AudioProxyProvider({
children,
options = {},
}: {
children: ReactNode;
options?: AudioProxyOptions;
}) {
const client = useMemo(() => new AudioProxyClient(options), [options]);
const value = useMemo(
() => ({
defaultOptions: options,
client,
}),
[options, client]
);
return createElement(AudioProxyContext.Provider, { value }, children);
}
export function useAudioProxyContext() {
const context = useContext(AudioProxyContext);
if (!context) {
throw new Error(
'useAudioProxyContext must be used within an AudioProxyProvider'
);
}
return context;
}