UNPKG

desktop-audio-proxy

Version:

A comprehensive audio streaming solution for Tauri and Electron apps that bypasses CORS and WebKit codec issues

811 lines (635 loc) 27.9 kB
import { TauriAudioService } from '../tauri-service'; import { ElectronAudioService } from '../electron-service'; // Mock the AudioProxyClient jest.mock('../client'); describe('Audio Services', () => { describe('TauriAudioService', () => { let service: TauriAudioService; beforeEach(() => { service = new TauriAudioService({ audioOptions: { proxyUrl: 'http://localhost:3001', autoDetect: true, }, }); }); it('should initialize with default options', () => { const defaultService = new TauriAudioService(); expect(defaultService).toBeDefined(); }); it('should initialize with custom options', () => { const customService = new TauriAudioService({ audioOptions: { proxyUrl: 'http://custom:8080', retryAttempts: 5, }, }); expect(customService).toBeDefined(); }); it('should handle legacy options format', () => { const legacyService = new TauriAudioService({ proxyUrl: 'http://legacy:3001', } as any); expect(legacyService).toBeDefined(); }); describe('getStreamableUrl', () => { it('should delegate to AudioProxyClient', async () => { const mockUrl = 'https://example.com/audio.mp3'; const expectedUrl = 'http://localhost:3001/stream?url=' + encodeURIComponent(mockUrl); // Mock the client method const mockGetPlayableUrl = jest.fn().mockResolvedValue(expectedUrl); (service as any).audioClient.getPlayableUrl = mockGetPlayableUrl; const result = await service.getStreamableUrl(mockUrl); expect(mockGetPlayableUrl).toHaveBeenCalledWith(mockUrl); expect(result).toBe(expectedUrl); }); it('should handle errors gracefully', async () => { const mockUrl = 'https://example.com/audio.mp3'; const mockGetPlayableUrl = jest .fn() .mockRejectedValue(new Error('Network error')); (service as any).audioClient.getPlayableUrl = mockGetPlayableUrl; await expect(service.getStreamableUrl(mockUrl)).rejects.toThrow( 'Network error' ); }); }); describe('canPlayStream', () => { it('should delegate to AudioProxyClient', async () => { const mockUrl = 'https://example.com/audio.mp3'; const mockCanPlayUrl = jest.fn().mockResolvedValue(true); (service as any).audioClient.canPlayUrl = mockCanPlayUrl; const result = await service.canPlayStream(mockUrl); expect(mockCanPlayUrl).toHaveBeenCalledWith(mockUrl); expect(result).toBe(true); }); }); describe('Environment detection', () => { it('should expose environment detection', () => { const mockGetEnvironment = jest.fn().mockReturnValue('tauri'); (service as any).audioClient.getEnvironment = mockGetEnvironment; const env = service.getEnvironment(); expect(mockGetEnvironment).toHaveBeenCalled(); expect(env).toBe('tauri'); }); }); describe('isProxyAvailable', () => { it('should check proxy availability', async () => { const mockIsProxyAvailable = jest.fn().mockResolvedValue(true); (service as any).audioClient.isProxyAvailable = mockIsProxyAvailable; const isAvailable = await service.isProxyAvailable(); expect(mockIsProxyAvailable).toHaveBeenCalled(); expect(isAvailable).toBe(true); }); }); describe('checkSystemCodecs', () => { beforeEach(() => { // Mock Audio constructor for codec checking global.Audio = jest.fn().mockImplementation(() => ({ canPlayType: jest.fn((type: string) => { if (type.includes('audio/mpeg') || type.includes('mp3')) return 'probably'; if (type.includes('audio/wav') || type.includes('pcm')) return 'probably'; if (type.includes('audio/ogg') || type.includes('vorbis')) return 'maybe'; return ''; }), })); }); it('should return supported and missing codecs with capabilities', async () => { const codecInfo = await service.checkSystemCodecs(); expect(codecInfo).toHaveProperty('supportedFormats'); expect(codecInfo).toHaveProperty('missingCodecs'); expect(codecInfo).toHaveProperty('capabilities'); expect(Array.isArray(codecInfo.supportedFormats)).toBe(true); expect(Array.isArray(codecInfo.missingCodecs)).toBe(true); expect(typeof codecInfo.capabilities).toBe('object'); }); it('should correctly identify supported formats', async () => { const codecInfo = await service.checkSystemCodecs(); expect(codecInfo.supportedFormats).toContain('MP3'); expect(codecInfo.supportedFormats).toContain('WAV'); expect(codecInfo.supportedFormats).toContain('OGG'); expect(codecInfo.capabilities).toHaveProperty('MP3'); expect(codecInfo.capabilities).toHaveProperty('WAV'); expect(codecInfo.capabilities).toHaveProperty('OGG'); }); it('should handle Tauri environment with system audio info', async () => { // Mock Tauri environment const mockGetEnvironment = jest.fn().mockReturnValue('tauri'); (service as any).audioClient.getEnvironment = mockGetEnvironment; // Mock Tauri API (global as any).window = { __TAURI__: { tauri: { invoke: jest.fn().mockResolvedValue({ supportedFormats: ['MP3', 'FLAC', 'AAC'], systemInfo: 'Linux Audio System', }), }, }, }; const codecInfo = await service.checkSystemCodecs(); expect(codecInfo.supportedFormats).toContain('FLAC'); expect(codecInfo.supportedFormats).toContain('AAC'); expect(codecInfo.capabilities).toHaveProperty('tauri_system_info'); }); it('should handle Tauri environment without system audio info', async () => { // Mock Tauri environment const mockGetEnvironment = jest.fn().mockReturnValue('tauri'); (service as any).audioClient.getEnvironment = mockGetEnvironment; // Mock Tauri API that returns null (caught error case) (global as any).window = { __TAURI__: { tauri: { invoke: jest .fn() .mockReturnValue(Promise.resolve(null).catch(() => null)), }, }, }; const codecInfo = await service.checkSystemCodecs(); expect(codecInfo.supportedFormats).toContain('MP3'); expect(codecInfo.capabilities).not.toHaveProperty('tauri_system_info'); }); it('should handle non-Tauri environment', async () => { // Mock non-Tauri environment const mockGetEnvironment = jest.fn().mockReturnValue('web'); (service as any).audioClient.getEnvironment = mockGetEnvironment; const codecInfo = await service.checkSystemCodecs(); expect(codecInfo.supportedFormats).toContain('MP3'); expect(codecInfo.capabilities).not.toHaveProperty('tauri_system_info'); }); }); describe('getAudioMetadata', () => { it('should return null for non-Tauri environment', async () => { const mockGetEnvironment = jest.fn().mockReturnValue('web'); (service as any).audioClient.getEnvironment = mockGetEnvironment; const metadata = await service.getAudioMetadata('/path/to/audio.mp3'); expect(metadata).toBeNull(); }); it('should return null when Tauri API is not available', async () => { const mockGetEnvironment = jest.fn().mockReturnValue('tauri'); (service as any).audioClient.getEnvironment = mockGetEnvironment; (global as any).window = {}; const metadata = await service.getAudioMetadata('/path/to/audio.mp3'); expect(metadata).toBeNull(); }); it('should return metadata for Tauri environment', async () => { const mockGetEnvironment = jest.fn().mockReturnValue('tauri'); (service as any).audioClient.getEnvironment = mockGetEnvironment; const mockMetadata = { duration: 180.5, bitrate: 320, sampleRate: 44100, channels: 2, format: 'MP3', }; (global as any).window = { __TAURI__: { tauri: { invoke: jest.fn().mockResolvedValue(mockMetadata), }, }, }; const metadata = await service.getAudioMetadata('/path/to/audio.mp3'); expect(metadata).toEqual(mockMetadata); expect( (global as any).window.__TAURI__.tauri.invoke ).toHaveBeenCalledWith('get_audio_metadata', { path: '/path/to/audio.mp3', }); }); it('should handle errors gracefully', async () => { const mockGetEnvironment = jest.fn().mockReturnValue('tauri'); (service as any).audioClient.getEnvironment = mockGetEnvironment; (global as any).window = { __TAURI__: { tauri: { invoke: jest.fn().mockRejectedValue(new Error('File not found')), }, }, }; const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); const metadata = await service.getAudioMetadata('/path/to/invalid.mp3'); expect(metadata).toBeNull(); expect(consoleWarnSpy).toHaveBeenCalledWith( '[TauriAudioService] Failed to get audio metadata:', expect.any(Error) ); consoleWarnSpy.mockRestore(); }); }); describe('getAudioDevices', () => { it('should return null for non-Tauri environment', async () => { const mockGetEnvironment = jest.fn().mockReturnValue('web'); (service as any).audioClient.getEnvironment = mockGetEnvironment; const devices = await service.getAudioDevices(); expect(devices).toBeNull(); }); it('should return null when Tauri API is not available', async () => { const mockGetEnvironment = jest.fn().mockReturnValue('tauri'); (service as any).audioClient.getEnvironment = mockGetEnvironment; (global as any).window = {}; const devices = await service.getAudioDevices(); expect(devices).toBeNull(); }); it('should return audio devices for Tauri environment', async () => { const mockGetEnvironment = jest.fn().mockReturnValue('tauri'); (service as any).audioClient.getEnvironment = mockGetEnvironment; const mockDevices = { inputDevices: [ { id: 'input1', name: 'Built-in Microphone' }, { id: 'input2', name: 'USB Microphone' }, ], outputDevices: [ { id: 'output1', name: 'Built-in Speakers' }, { id: 'output2', name: 'Bluetooth Headphones' }, ], }; (global as any).window = { __TAURI__: { tauri: { invoke: jest.fn().mockResolvedValue(mockDevices), }, }, }; const devices = await service.getAudioDevices(); expect(devices).toEqual(mockDevices); expect( (global as any).window.__TAURI__.tauri.invoke ).toHaveBeenCalledWith('get_audio_devices'); }); it('should handle errors gracefully', async () => { const mockGetEnvironment = jest.fn().mockReturnValue('tauri'); (service as any).audioClient.getEnvironment = mockGetEnvironment; (global as any).window = { __TAURI__: { tauri: { invoke: jest .fn() .mockRejectedValue(new Error('Audio system unavailable')), }, }, }; const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); const devices = await service.getAudioDevices(); expect(devices).toBeNull(); expect(consoleWarnSpy).toHaveBeenCalledWith( '[TauriAudioService] Failed to get audio devices:', expect.any(Error) ); consoleWarnSpy.mockRestore(); }); }); }); describe('ElectronAudioService', () => { let service: ElectronAudioService; beforeEach(() => { service = new ElectronAudioService({ audioOptions: { proxyUrl: 'http://localhost:3001', autoDetect: true, }, }); // Mock Audio constructor for codec checking global.Audio = jest.fn().mockImplementation(() => ({ canPlayType: jest.fn((type: string) => { const supportedTypes = ['audio/mpeg', 'audio/wav', 'audio/ogg']; return supportedTypes.includes(type) ? 'probably' : ''; }), })); }); it('should initialize with default options', () => { const defaultService = new ElectronAudioService(); expect(defaultService).toBeDefined(); }); it('should initialize with custom options', () => { const customService = new ElectronAudioService({ audioOptions: { proxyUrl: 'http://custom:8080', retryAttempts: 5, }, }); expect(customService).toBeDefined(); }); describe('getStreamableUrl', () => { it('should delegate to AudioProxyClient', async () => { const mockUrl = 'https://example.com/audio.mp3'; const expectedUrl = 'http://localhost:3001/stream?url=' + encodeURIComponent(mockUrl); const mockGetPlayableUrl = jest.fn().mockResolvedValue(expectedUrl); (service as any).audioClient.getPlayableUrl = mockGetPlayableUrl; const result = await service.getStreamableUrl(mockUrl); expect(mockGetPlayableUrl).toHaveBeenCalledWith(mockUrl); expect(result).toBe(expectedUrl); }); }); describe('checkSystemCodecs', () => { it('should return supported and missing codecs with capabilities', async () => { const codecInfo = await service.checkSystemCodecs(); expect(codecInfo).toHaveProperty('supportedFormats'); expect(codecInfo).toHaveProperty('missingCodecs'); expect(codecInfo).toHaveProperty('capabilities'); expect(Array.isArray(codecInfo.supportedFormats)).toBe(true); expect(Array.isArray(codecInfo.missingCodecs)).toBe(true); expect(typeof codecInfo.capabilities).toBe('object'); }); it('should correctly identify supported formats', async () => { const codecInfo = await service.checkSystemCodecs(); expect(codecInfo.supportedFormats).toContain('MP3'); expect(codecInfo.supportedFormats).toContain('WAV'); expect(codecInfo.supportedFormats).toContain('OGG'); expect(codecInfo.capabilities).toHaveProperty('MP3'); expect(codecInfo.capabilities).toHaveProperty('WAV'); expect(codecInfo.capabilities).toHaveProperty('OGG'); }); it('should correctly identify missing codecs', async () => { const codecInfo = await service.checkSystemCodecs(); expect(codecInfo.missingCodecs).toContain('AAC'); expect(codecInfo.missingCodecs).toContain('FLAC'); expect(codecInfo.missingCodecs).toContain('WEBM'); expect(codecInfo.capabilities).toHaveProperty('AAC'); expect(codecInfo.capabilities).toHaveProperty('FLAC'); expect(codecInfo.capabilities).toHaveProperty('WEBM'); }); it('should include electron version info when available', async () => { // Mock the getEnvironment method to return 'electron' const mockGetEnvironment = jest.fn().mockReturnValue('electron'); (service as any).getEnvironment = mockGetEnvironment; // Mock process.versions for electron (global as any).process = { versions: { electron: '25.0.0', chrome: '114.0.0', }, }; const codecInfo = await service.checkSystemCodecs(); expect(codecInfo).toHaveProperty('electronVersion'); expect(codecInfo).toHaveProperty('chromiumVersion'); expect(codecInfo).toHaveProperty('capabilities'); expect(codecInfo.electronVersion).toBe('25.0.0'); expect(codecInfo.chromiumVersion).toBe('114.0.0'); }); }); describe('canPlayStream', () => { it('should delegate to AudioProxyClient', async () => { const mockUrl = 'https://example.com/audio.mp3'; const mockCanPlayUrl = jest.fn().mockResolvedValue(true); (service as any).audioClient.canPlayUrl = mockCanPlayUrl; const result = await service.canPlayStream(mockUrl); expect(mockCanPlayUrl).toHaveBeenCalledWith(mockUrl); expect(result).toBe(true); }); }); describe('Environment detection', () => { it('should expose environment detection', () => { const mockGetEnvironment = jest.fn().mockReturnValue('electron'); (service as any).audioClient.getEnvironment = mockGetEnvironment; const env = service.getEnvironment(); expect(mockGetEnvironment).toHaveBeenCalled(); expect(env).toBe('electron'); }); }); describe('isProxyAvailable', () => { it('should check proxy availability', async () => { const mockIsProxyAvailable = jest.fn().mockResolvedValue(true); (service as any).audioClient.isProxyAvailable = mockIsProxyAvailable; const isAvailable = await service.isProxyAvailable(); expect(mockIsProxyAvailable).toHaveBeenCalled(); expect(isAvailable).toBe(true); }); }); describe('checkSystemCodecs - enhanced features', () => { it('should include system info when electronAPI is available', async () => { // Mock Electron environment const mockGetEnvironment = jest.fn().mockReturnValue('electron'); (service as any).audioClient.getEnvironment = mockGetEnvironment; // Mock electronAPI (global as any).window = { electronAPI: { getSystemAudioInfo: jest.fn().mockResolvedValue({ supportedFormats: ['FLAC', 'OPUS'], systemInfo: 'Windows Audio System', }), }, }; const codecInfo = await service.checkSystemCodecs(); expect(codecInfo.supportedFormats).toContain('FLAC'); expect(codecInfo.supportedFormats).toContain('OPUS'); expect(codecInfo.capabilities).toHaveProperty('electron_system_info'); }); it('should handle electronAPI errors gracefully', async () => { // Mock Electron environment const mockGetEnvironment = jest.fn().mockReturnValue('electron'); (service as any).audioClient.getEnvironment = mockGetEnvironment; // Mock electronAPI that fails (global as any).window = { electronAPI: { getSystemAudioInfo: jest .fn() .mockRejectedValue(new Error('IPC error')), }, }; const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); const codecInfo = await service.checkSystemCodecs(); expect(codecInfo.supportedFormats).toContain('MP3'); expect(consoleWarnSpy).toHaveBeenCalledWith( '[ElectronAudioService] Failed to get system audio info via IPC:', expect.any(Error) ); consoleWarnSpy.mockRestore(); }); it('should handle missing electronAPI gracefully', async () => { // Mock Electron environment const mockGetEnvironment = jest.fn().mockReturnValue('electron'); (service as any).audioClient.getEnvironment = mockGetEnvironment; // No electronAPI available (global as any).window = {}; const codecInfo = await service.checkSystemCodecs(); expect(codecInfo.supportedFormats).toContain('MP3'); expect(codecInfo.capabilities).not.toHaveProperty( 'electron_system_info' ); }); it('should handle non-electron environment', async () => { // Mock non-electron environment const mockGetEnvironment = jest.fn().mockReturnValue('web'); (service as any).audioClient.getEnvironment = mockGetEnvironment; const codecInfo = await service.checkSystemCodecs(); expect(codecInfo).not.toHaveProperty('electronVersion'); expect(codecInfo).not.toHaveProperty('chromiumVersion'); }); }); describe('getAudioMetadata', () => { it('should return null for non-Electron environment', async () => { const mockGetEnvironment = jest.fn().mockReturnValue('web'); (service as any).audioClient.getEnvironment = mockGetEnvironment; const metadata = await service.getAudioMetadata('/path/to/audio.mp3'); expect(metadata).toBeNull(); }); it('should return null when electronAPI is not available', async () => { const mockGetEnvironment = jest.fn().mockReturnValue('electron'); (service as any).audioClient.getEnvironment = mockGetEnvironment; (global as any).window = {}; const metadata = await service.getAudioMetadata('/path/to/audio.mp3'); expect(metadata).toBeNull(); }); it('should return metadata for Electron environment', async () => { const mockGetEnvironment = jest.fn().mockReturnValue('electron'); (service as any).audioClient.getEnvironment = mockGetEnvironment; const mockMetadata = { duration: 240.5, bitrate: 256, sampleRate: 48000, channels: 2, format: 'FLAC', }; (global as any).window = { electronAPI: { getAudioMetadata: jest.fn().mockResolvedValue(mockMetadata), }, }; const metadata = await service.getAudioMetadata('/path/to/audio.flac'); expect(metadata).toEqual(mockMetadata); expect( (global as any).window.electronAPI.getAudioMetadata ).toHaveBeenCalledWith('/path/to/audio.flac'); }); it('should handle errors gracefully', async () => { const mockGetEnvironment = jest.fn().mockReturnValue('electron'); (service as any).audioClient.getEnvironment = mockGetEnvironment; (global as any).window = { electronAPI: { getAudioMetadata: jest .fn() .mockRejectedValue(new Error('File access denied')), }, }; const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); const metadata = await service.getAudioMetadata( '/path/to/protected.mp3' ); expect(metadata).toBeNull(); expect(consoleWarnSpy).toHaveBeenCalledWith( '[ElectronAudioService] Failed to get audio metadata:', expect.any(Error) ); consoleWarnSpy.mockRestore(); }); }); describe('getAudioDevices', () => { it('should return null for non-Electron environment', async () => { const mockGetEnvironment = jest.fn().mockReturnValue('web'); (service as any).audioClient.getEnvironment = mockGetEnvironment; const devices = await service.getAudioDevices(); expect(devices).toBeNull(); }); it('should return null when electronAPI is not available', async () => { const mockGetEnvironment = jest.fn().mockReturnValue('electron'); (service as any).audioClient.getEnvironment = mockGetEnvironment; (global as any).window = {}; const devices = await service.getAudioDevices(); expect(devices).toBeNull(); }); it('should return audio devices for Electron environment', async () => { const mockGetEnvironment = jest.fn().mockReturnValue('electron'); (service as any).audioClient.getEnvironment = mockGetEnvironment; const mockDevices = { inputDevices: [ { id: 'input1', name: 'Built-in Microphone' }, { id: 'input2', name: 'External USB Mic' }, ], outputDevices: [ { id: 'output1', name: 'Built-in Speakers' }, { id: 'output2', name: 'HDMI Audio' }, ], }; (global as any).window = { electronAPI: { getAudioDevices: jest.fn().mockResolvedValue(mockDevices), }, }; const devices = await service.getAudioDevices(); expect(devices).toEqual(mockDevices); expect( (global as any).window.electronAPI.getAudioDevices ).toHaveBeenCalled(); }); it('should handle errors gracefully', async () => { const mockGetEnvironment = jest.fn().mockReturnValue('electron'); (service as any).audioClient.getEnvironment = mockGetEnvironment; (global as any).window = { electronAPI: { getAudioDevices: jest .fn() .mockRejectedValue(new Error('Audio subsystem error')), }, }; const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); const devices = await service.getAudioDevices(); expect(devices).toBeNull(); expect(consoleWarnSpy).toHaveBeenCalledWith( '[ElectronAudioService] Failed to get audio devices:', expect.any(Error) ); consoleWarnSpy.mockRestore(); }); }); describe('getSystemAudioSettings', () => { it('should return null for non-Electron environment', async () => { const mockGetEnvironment = jest.fn().mockReturnValue('web'); (service as any).audioClient.getEnvironment = mockGetEnvironment; const settings = await service.getSystemAudioSettings(); expect(settings).toBeNull(); }); it('should return null when electronAPI is not available', async () => { const mockGetEnvironment = jest.fn().mockReturnValue('electron'); (service as any).audioClient.getEnvironment = mockGetEnvironment; (global as any).window = {}; const settings = await service.getSystemAudioSettings(); expect(settings).toBeNull(); }); it('should return system audio settings for Electron environment', async () => { const mockGetEnvironment = jest.fn().mockReturnValue('electron'); (service as any).audioClient.getEnvironment = mockGetEnvironment; const mockSettings = { defaultInputDevice: 'input1', defaultOutputDevice: 'output1', masterVolume: 0.75, }; (global as any).window = { electronAPI: { getSystemAudioSettings: jest.fn().mockResolvedValue(mockSettings), }, }; const settings = await service.getSystemAudioSettings(); expect(settings).toEqual(mockSettings); expect( (global as any).window.electronAPI.getSystemAudioSettings ).toHaveBeenCalled(); }); it('should handle errors gracefully', async () => { const mockGetEnvironment = jest.fn().mockReturnValue('electron'); (service as any).audioClient.getEnvironment = mockGetEnvironment; (global as any).window = { electronAPI: { getSystemAudioSettings: jest .fn() .mockRejectedValue(new Error('Settings unavailable')), }, }; const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); const settings = await service.getSystemAudioSettings(); expect(settings).toBeNull(); expect(consoleWarnSpy).toHaveBeenCalledWith( '[ElectronAudioService] Failed to get system audio settings:', expect.any(Error) ); consoleWarnSpy.mockRestore(); }); }); }); });