desktop-audio-proxy
Version:
A comprehensive audio streaming solution for Tauri and Electron apps that bypasses CORS and WebKit codec issues
245 lines (216 loc) • 7.5 kB
text/typescript
import { AudioProxyClient } from './client';
import { AudioServiceOptions } from './types';
// Type declarations for Electron API
interface ElectronAPI {
getSystemAudioInfo?: () => Promise<{
supportedFormats?: string[];
systemInfo?: string;
}>;
getAudioMetadata?: (_filePath: string) => Promise<{
duration?: number;
bitrate?: number;
sampleRate?: number;
channels?: number;
format?: string;
}>;
getAudioDevices?: () => Promise<{
inputDevices: Array<{ id: string; name: string }>;
outputDevices: Array<{ id: string; name: string }>;
}>;
getSystemAudioSettings?: () => Promise<{
defaultInputDevice?: string;
defaultOutputDevice?: string;
masterVolume?: number;
}>;
}
// Extend the existing Window interface from client.ts - don't redeclare electronAPI
export class ElectronAudioService {
private audioClient: AudioProxyClient;
constructor(options: AudioServiceOptions = {}) {
// Use audioOptions if provided, otherwise use the options directly
const clientOptions = options.audioOptions || options;
this.audioClient = new AudioProxyClient(clientOptions);
}
public async getStreamableUrl(url: string): Promise<string> {
return await this.audioClient.getPlayableUrl(url);
}
public async canPlayStream(url: string) {
return await this.audioClient.canPlayUrl(url);
}
public getEnvironment() {
return this.audioClient.getEnvironment();
}
public async isProxyAvailable(): Promise<boolean> {
return await this.audioClient.isProxyAvailable();
}
public async checkSystemCodecs(): Promise<{
supportedFormats: string[];
missingCodecs: string[];
capabilities: Record<string, string>;
electronVersion?: string;
chromiumVersion?: string;
}> {
const audio = new Audio();
const formats = [
{ name: 'MP3', mime: 'audio/mpeg', codecs: ['mp3'] },
{ name: 'OGG', mime: 'audio/ogg', codecs: ['vorbis', 'opus'] },
{ name: 'WAV', mime: 'audio/wav', codecs: ['pcm'] },
{ name: 'AAC', mime: 'audio/aac', codecs: ['mp4a.40.2'] },
{ name: 'FLAC', mime: 'audio/flac', codecs: ['flac'] },
{ name: 'WEBM', mime: 'audio/webm', codecs: ['vorbis', 'opus'] },
{ name: 'M4A', mime: 'audio/mp4', codecs: ['mp4a.40.2'] },
];
const supportedFormats: string[] = [];
const missingCodecs: string[] = [];
const capabilities: Record<string, string> = {};
for (const format of formats) {
let bestSupport = '';
let isSupported = false;
// Test basic MIME type
const basicSupport = audio.canPlayType(format.mime);
capabilities[format.name + '_basic'] = basicSupport;
if (basicSupport === 'probably' || basicSupport === 'maybe') {
bestSupport = basicSupport;
isSupported = true;
}
// Test with codecs
for (const codec of format.codecs) {
const codecSupport = audio.canPlayType(
`${format.mime}; codecs="${codec}"`
);
capabilities[format.name + '_' + codec] = codecSupport;
if (codecSupport === 'probably') {
bestSupport = 'probably';
isSupported = true;
} else if (codecSupport === 'maybe' && bestSupport !== 'probably') {
bestSupport = 'maybe';
isSupported = true;
}
}
capabilities[format.name] = bestSupport;
if (isSupported) {
supportedFormats.push(format.name);
} else {
missingCodecs.push(format.name);
}
}
const result: {
supportedFormats: string[];
missingCodecs: string[];
capabilities: Record<string, string>;
electronVersion?: string;
chromiumVersion?: string;
} = { supportedFormats, missingCodecs, capabilities };
// Add Electron version info if available
if (this.getEnvironment() === 'electron') {
try {
if (typeof process !== 'undefined' && process.versions) {
result.electronVersion = process.versions.electron;
result.chromiumVersion = process.versions.chrome;
}
// Integrate with Electron main process for system codec detection
const electronAPI = window.electronAPI as ElectronAPI | undefined;
if (electronAPI?.getSystemAudioInfo) {
try {
const systemAudioInfo = await electronAPI.getSystemAudioInfo();
if (systemAudioInfo) {
capabilities['electron_system_info'] =
JSON.stringify(systemAudioInfo);
// Enhanced format support based on system capabilities
if (systemAudioInfo.supportedFormats) {
systemAudioInfo.supportedFormats.forEach((format: string) => {
if (!supportedFormats.includes(format)) {
supportedFormats.push(format);
// Remove from missing codecs if it was there
const missingIndex = missingCodecs.indexOf(format);
if (missingIndex > -1) {
missingCodecs.splice(missingIndex, 1);
}
}
});
}
}
} catch (error) {
console.warn(
'[ElectronAudioService] Failed to get system audio info via IPC:',
error
);
}
}
} catch (error) {
console.warn(
'[ElectronAudioService] Could not access Electron version info:',
error
);
}
}
return result;
}
// Electron-specific method to get audio file metadata via main process
public async getAudioMetadata(filePath: string): Promise<{
duration?: number;
bitrate?: number;
sampleRate?: number;
channels?: number;
format?: string;
} | null> {
const electronAPI = window.electronAPI as ElectronAPI | undefined;
if (
this.getEnvironment() !== 'electron' ||
!electronAPI?.getAudioMetadata
) {
return null;
}
try {
return await electronAPI.getAudioMetadata(filePath);
} catch (error) {
console.warn(
'[ElectronAudioService] Failed to get audio metadata:',
error
);
return null;
}
}
// Electron-specific method to enumerate audio devices via main process
public async getAudioDevices(): Promise<{
inputDevices: Array<{ id: string; name: string }>;
outputDevices: Array<{ id: string; name: string }>;
} | null> {
const electronAPI = window.electronAPI as ElectronAPI | undefined;
if (this.getEnvironment() !== 'electron' || !electronAPI?.getAudioDevices) {
return null;
}
try {
return await electronAPI.getAudioDevices();
} catch (error) {
console.warn(
'[ElectronAudioService] Failed to get audio devices:',
error
);
return null;
}
}
// Electron-specific method to get system audio settings
public async getSystemAudioSettings(): Promise<{
defaultInputDevice?: string;
defaultOutputDevice?: string;
masterVolume?: number;
} | null> {
const electronAPI = window.electronAPI as ElectronAPI | undefined;
if (
this.getEnvironment() !== 'electron' ||
!electronAPI?.getSystemAudioSettings
) {
return null;
}
try {
return await electronAPI.getSystemAudioSettings();
} catch (error) {
console.warn(
'[ElectronAudioService] Failed to get system audio settings:',
error
);
return null;
}
}
}