desktop-audio-proxy
Version:
A comprehensive audio streaming solution for Tauri and Electron apps that bypasses CORS and WebKit codec issues
356 lines (312 loc) • 9 kB
text/typescript
import {
ref,
computed,
watch,
onMounted,
inject,
type Ref,
type ComputedRef,
} from 'vue';
import {
AudioProxyClient,
TauriAudioService,
ElectronAudioService,
} from './index';
import { AudioProxyOptions, StreamInfo, Environment } from './types';
/**
* Vue composable for managing audio proxy client with automatic URL processing
*/
export function useAudioProxy(
url: Ref<string | null> | string | null,
options?: AudioProxyOptions
) {
const audioUrl = ref<string | null>(null);
const isLoading = ref(false);
const error = ref<string | null>(null);
const streamInfo = ref<StreamInfo | null>(null);
// Create reactive URL ref if needed
const urlRef = ref(url);
const client = new AudioProxyClient(options);
const processUrl = async (inputUrl: string) => {
isLoading.value = true;
error.value = null;
audioUrl.value = null;
streamInfo.value = null;
try {
// Get stream info first
const info = await client.canPlayUrl(inputUrl);
streamInfo.value = info;
// Get playable URL
const playableUrl = await client.getPlayableUrl(inputUrl);
audioUrl.value = playableUrl;
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
error.value = errorMessage;
} finally {
isLoading.value = false;
}
};
const retry = () => {
if (urlRef.value) {
processUrl(urlRef.value);
}
};
// Watch for URL changes
watch(
urlRef,
(newUrl: string) => {
if (newUrl) {
processUrl(newUrl);
} else {
audioUrl.value = null;
streamInfo.value = null;
error.value = null;
isLoading.value = false;
}
},
{ immediate: true }
);
return {
audioUrl: readonly(audioUrl),
isLoading: readonly(isLoading),
error: readonly(error),
streamInfo: readonly(streamInfo),
retry,
client,
};
}
/**
* Vue composable for accessing audio capabilities and system information
*/
export function useAudioCapabilities() {
const capabilities = ref<{
supportedFormats: string[];
missingCodecs: string[];
capabilities: Record<string, string>;
environment: Environment;
electronVersion?: string;
chromiumVersion?: string;
} | null>(null);
const devices = ref<{
inputDevices: Array<{ id: string; name: string }>;
outputDevices: Array<{ id: string; name: string }>;
} | null>(null);
const systemSettings = ref<{
defaultInputDevice?: string;
defaultOutputDevice?: string;
masterVolume?: number;
} | null>(null);
const isLoading = ref(true);
const error = ref<string | null>(null);
const client = new AudioProxyClient();
const refresh = async () => {
isLoading.value = true;
error.value = 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();
capabilities.value = {
...codecInfo,
environment,
};
// Get audio devices
const deviceInfo = await service.getAudioDevices();
if (deviceInfo) {
devices.value = deviceInfo;
}
// Get system settings (Electron only)
if (environment === 'electron' && 'getSystemAudioSettings' in service) {
const settings = await (
service as ElectronAudioService
).getSystemAudioSettings();
if (settings) {
systemSettings.value = 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]) !==
''
);
});
capabilities.value = {
supportedFormats,
missingCodecs: formats.filter(f => !supportedFormats.includes(f)),
capabilities: {},
environment,
};
}
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
error.value = errorMessage;
} finally {
isLoading.value = false;
}
};
onMounted(() => {
refresh();
});
return {
capabilities: readonly(capabilities),
devices: readonly(devices),
systemSettings: readonly(systemSettings),
isLoading: readonly(isLoading),
error: readonly(error),
refresh,
};
}
/**
* Vue composable for checking proxy server availability
*/
export function useProxyStatus(options?: AudioProxyOptions) {
const isAvailable = ref<boolean | null>(null);
const isChecking = ref(false);
const error = ref<string | null>(null);
const proxyUrl = ref<string>('');
const client = new AudioProxyClient(options);
const refresh = async () => {
isChecking.value = true;
error.value = null;
try {
const available = await client.isProxyAvailable();
isAvailable.value = available;
proxyUrl.value =
(client as any).options?.proxyUrl || 'http://localhost:3002';
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
error.value = errorMessage;
isAvailable.value = false;
} finally {
isChecking.value = false;
}
};
onMounted(() => {
refresh();
});
return {
isAvailable: readonly(isAvailable),
isChecking: readonly(isChecking),
error: readonly(error),
proxyUrl: readonly(proxyUrl),
refresh,
};
}
/**
* Vue composable for audio metadata extraction (Tauri/Electron only)
*/
export function useAudioMetadata(filePath: Ref<string | null> | string | null) {
const metadata = ref<{
duration?: number;
bitrate?: number;
sampleRate?: number;
channels?: number;
format?: string;
} | null>(null);
const isLoading = ref(false);
const error = ref<string | null>(null);
const filePathRef = ref(filePath);
const client = new AudioProxyClient();
const getMetadata = async (path: string) => {
isLoading.value = true;
error.value = null;
metadata.value = 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(path);
metadata.value = result;
} else {
error.value =
'Audio metadata extraction is only available in Tauri or Electron environments';
}
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
error.value = errorMessage;
} finally {
isLoading.value = false;
}
};
watch(
filePathRef,
(newPath: string) => {
if (newPath) {
getMetadata(newPath);
} else {
metadata.value = null;
error.value = null;
isLoading.value = false;
}
},
{ immediate: true }
);
return {
metadata: readonly(metadata),
isLoading: readonly(isLoading),
error: readonly(error),
};
}
/**
* Helper function to create readonly refs
*/
function readonly<T>(ref: Ref<T>): ComputedRef<T> {
return computed(() => ref.value);
}
/**
* Vue plugin for global audio proxy configuration
*/
export interface AudioProxyGlobalOptions {
defaultOptions?: AudioProxyOptions;
}
export function createAudioProxy(globalOptions: AudioProxyGlobalOptions = {}) {
return {
install(app: any) {
const client = new AudioProxyClient(globalOptions.defaultOptions);
app.config.globalProperties.$audioProxy = client;
app.provide('audioProxy', client);
app.provide('audioProxyOptions', globalOptions.defaultOptions || {});
},
};
}
/**
* Injection key for dependency injection
*/
export const audioProxyInjectionKey = Symbol('audioProxy');
/**
* Composable to inject the global audio proxy client
*/
export function useGlobalAudioProxy() {
const client = inject(audioProxyInjectionKey);
if (!client) {
throw new Error(
'AudioProxy plugin must be installed to use useGlobalAudioProxy'
);
}
return client as AudioProxyClient;
}