/** * RustDesk Standalone Web Client - Audio Decoder * * Handles Opus audio decoding using libopus WASM and playback * through the Web Audio API. */ const RDAudio = (() => { let _audioContext = null; let _opusDecoder = null; let _sampleRate = 48000; let _channels = 2; let _gainNode = null; let _isMuted = false; let _volume = 1.0; let _isEnabled = true; let _nextPlayTime = 0; let _initialized = false; /** * Initialize audio subsystem */ async function init() { if (_initialized) return true; try { _audioContext = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: _sampleRate, latencyHint: 'interactive' }); _gainNode = _audioContext.createGain(); _gainNode.connect(_audioContext.destination); _gainNode.gain.value = _volume; // Initialize libopus decoder await initOpusDecoder(); _initialized = true; console.log('[RDAudio] Initialized. Sample rate:', _sampleRate, 'Channels:', _channels); return true; } catch (e) { console.error('[RDAudio] Initialization failed:', e); return false; } } /** * Initialize the Opus decoder from libopus WASM */ async function initOpusDecoder() { if (typeof OpusDecoder === 'undefined') { console.warn('[RDAudio] OpusDecoder not available, audio disabled'); _isEnabled = false; return; } try { _opusDecoder = new OpusDecoder(); // Initialize with sample rate and channels const initResult = await _opusDecoder.init(_sampleRate, _channels); if (initResult !== 0) { console.error('[RDAudio] Opus decoder init failed:', initResult); _opusDecoder = null; _isEnabled = false; return; } console.log('[RDAudio] Opus decoder initialized'); } catch (e) { console.warn('[RDAudio] Opus decoder creation failed:', e); _opusDecoder = null; _isEnabled = false; } } /** * Handle incoming AudioFrame from the session * @param {Object} audioFrame - Decoded protobuf AudioFrame */ function handleAudioFrame(audioFrame) { if (!_isEnabled || !_audioContext) return; if (_audioContext.state === 'suspended') { _audioContext.resume(); } if (!audioFrame.data || audioFrame.data.byteLength === 0) return; try { if (_opusDecoder) { decodeAndPlayOpus(audioFrame.data); } else { // Fallback: try to play raw PCM data playPCMData(audioFrame.data); } } catch (e) { console.error('[RDAudio] Audio decode error:', e); } } /** * Decode Opus data and play through Web Audio API */ function decodeAndPlayOpus(opusData) { if (!_opusDecoder || !_audioContext) return; const inputData = new Uint8Array(opusData.buffer || opusData); // Decode Opus to PCM const pcmBuffer = _opusDecoder.decode(inputData); if (pcmBuffer && pcmBuffer.length > 0) { playPCMData(pcmBuffer); } } /** * Play PCM data through Web Audio API * @param {Float32Array|Uint8Array} pcmData - PCM audio data */ function playPCMData(pcmData) { if (!_audioContext || _isMuted) return; let float32Data; if (pcmData instanceof Float32Array) { float32Data = pcmData; } else { // Convert bytes to Float32 const dataView = new DataView(pcmData.buffer || pcmData); float32Data = new Float32Array(dataView.byteLength / 4); for (let i = 0; i < float32Data.length; i++) { float32Data[i] = dataView.getFloat32(i * 4, true); // Little-endian } } const frameCount = float32Data.length / _channels; if (frameCount === 0) return; // Create AudioBuffer const audioBuffer = _audioContext.createBuffer(_channels, frameCount, _sampleRate); for (let ch = 0; ch < _channels; ch++) { const channelData = audioBuffer.getChannelData(ch); for (let i = 0; i < frameCount; i++) { channelData[i] = float32Data[i * _channels + ch]; } } // Schedule playback schedulePlayback(audioBuffer); } /** * Schedule audio buffer playback with gapless transition */ function schedulePlayback(audioBuffer) { const currentTime = _audioContext.currentTime; const startTime = Math.max(currentTime, _nextPlayTime); const source = _audioContext.createBufferSource(); source.buffer = audioBuffer; source.connect(_gainNode); source.onended = () => { source.disconnect(); }; source.start(startTime); _nextPlayTime = startTime + audioBuffer.duration; } /** * Set audio format from server's AudioFormat message */ function setFormat(sampleRate, channels) { if (sampleRate > 0) _sampleRate = sampleRate; if (channels > 0) _channels = channels; console.log('[RDAudio] Format updated:', _sampleRate, 'Hz,', _channels, 'channels'); } /** * Set volume (0.0 to 1.0) */ function setVolume(volume) { _volume = Math.max(0, Math.min(1, volume)); if (_gainNode) { _gainNode.gain.value = _volume; } } /** * Mute/unmute audio */ function setMuted(muted) { _isMuted = muted; if (_gainNode) { _gainNode.gain.value = muted ? 0 : _volume; } } /** * Toggle mute */ function toggleMute() { setMuted(!_isMuted); return _isMuted; } /** * Get mute state */ function isMuted() { return _isMuted; } /** * Check if audio is enabled and initialized */ function isEnabled() { return _isEnabled; } /** * Resume audio context (needed after user gesture) */ async function resume() { if (_audioContext && _audioContext.state === 'suspended') { await _audioContext.resume(); } } /** * Clean up audio resources */ function cleanup() { if (_opusDecoder) { try { _opusDecoder.destroy(); } catch (e) {} _opusDecoder = null; } if (_audioContext) { _audioContext.close().catch(() => {}); _audioContext = null; } _initialized = false; _nextPlayTime = 0; } return { init, handleAudioFrame, setFormat, setVolume, setMuted, toggleMute, isMuted, isEnabled, resume, cleanup }; })();