> ## Documentation Index
> Fetch the complete documentation index at: https://docs.openhome.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Websocket Streaming

> How to use our websocket in your application to call your agent

Use the desired agent id under your account in the WebSocket URL.

```wss theme={"system"}

wss://app.openhome.com/websocket/voice-stream/OPENHOME_API_KEY/PERSONALITY_ID

```

Here is an example:

```wss theme={"system"}

wss://app.openhome.com/websocket/voice-stream/xyzsdsadasannfma/4727

```

### OPENHOME\_API\_KEY

Checkout <a href="/api-sdk/api-reference">API Docs</a> to get OPENHOME\_API\_KEY

### AGENT\_ID

Checkout <a href="/api-sdk/api-reference">Get Agents</a> section to get your account's agent ids.

* **4727**: Represents the agent ID.
* Set the agent ID to `0` to skip this part. This will start the call with the default agent, OpenHome.

***

## WebSocket Flow

### Audio Data Format

Audio data sent to the WebSocket must adhere to the following specifications:

* Format: **16-bit PCM**
* Sample Rate: **16000 Hz**
* Encoding: **Base64**

Ensure that the audio is converted to this format before sending it to the WebSocket.

***

### WebSocket Message Structure

#### Client to Server Messages

1. **User to Server Text Messages**
   ```json theme={"system"}
   {
     "data": "MESSAGE_CONTEXT",
     "type": "transcribed"
   }

   ```
2. **User to Server Audio Messages**
   ```json theme={"system"}
   {
   "data": "BASE64_ENCODED_AUDIO_MESSAGES",
   "type": "audio"
   }
   ```

### Server to Client Messages

1. Server to User Text Messages

   ```json theme={"system"}
   {
       "data": {
           "content": "MESSAGE CONTENT",
           "live": true,
           "role": "assistant"
       },
       "type": "message"
   }
   ```

   **live: true** indicates live transcription or response logs.

   **final: true** provides finalized transcription and response messages.

2. Server to User Audio Messages
   ```json theme={"system"}
   {
       "data": "BASE64_ENCODED_AUDIO_MESSAGES",
       "type": "audio"
   }
   ```

## EXAMPLES

Find More Examples on <a href="https://github.com/OpenHome-AI/examples">GitHub</a>

<CodeGroup>
  ```HTML HTML theme={"system"}
  <!DOCTYPE html>
  <html>
  <head>
      <title>Audio Stream</title>
      <style>
          .container {
              max-width: 800px;
              margin: 0 auto;
              padding: 20px;
          }
          .status {
              padding: 10px;
              margin-bottom: 10px;
              background-color: #f0f0f0;
          }
          .active { background-color: #90EE90; }
          .error { background-color: #ffcccb; }
          #retryButton {
              padding: 10px;
              margin: 10px 0;
              display: none;
          }
      </style>
  </head>
  <body>
      <div class="container">
          <div id="connectionStatus" class="status">Connection Status: Disconnected</div>
          <div id="micStatus" class="status">Microphone Status: Off</div>
          <div class="call-controls">
              <button id="startCall" class="call-button">Start Call</button>
              <button id="endCall" class="call-button">End Call</button>
          </div>
          <button id="retryButton" onclick="retryMicrophoneAccess()">Retry Microphone Access</button>
          <audio id="audioPlayer" controls></audio>
      </div>
      <script>
          const api_key = "0603b422xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxd0e87"
          const wsUrl = 'wss://app.openhome.com/websocket/voice-stream/' + api_key + '/0';
          
          const BUFFER_SIZE = 4096;
          const FRAMES_PER_BUFFER = 3200;
          const CHANNELS = 1;
          const RATE = 16000;
          
          let ws;
          let audioContext;
          let mediaStream;
          let reconnectAttempts = 0;
          let microphoneSource;
          let audioBuffer = new Float32Array();
          
          // Audio playback setup
          let audioElement = document.getElementById('audioPlayer');
          let mediaSource = null;
          let sourceBuffer = null;
          let audioQueue = [];
          let isSourceOpen = false;

          async function initAudio() {
              try {
                  if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
                      throw new Error('Your browser does not support audio recording');
                  }

                  audioContext = new (window.AudioContext || window.webkitAudioContext)({
                      sampleRate: RATE
                  });

                  mediaStream = await navigator.mediaDevices.getUserMedia({
                      audio: {
                          channelCount: CHANNELS,
                          sampleRate: RATE,
                          echoCancellation: true,
                          noiseSuppression: true,
                          autoGainControl: true
                      }
                  });

                  if (!mediaStream || !mediaStream.active) {
                      throw new Error('Failed to get media stream');
                  }

                  const tracks = mediaStream.getAudioTracks();
                  if (!tracks || tracks.length === 0) {
                      throw new Error('No audio tracks available');
                  }

                  microphoneSource = audioContext.createMediaStreamSource(mediaStream);
                  const processor = audioContext.createScriptProcessor(BUFFER_SIZE, CHANNELS, CHANNELS);

                  microphoneSource.connect(processor);
                  processor.connect(audioContext.destination);

                  processor.onaudioprocess = (e) => {
                      const inputData = e.inputBuffer.getChannelData(0);
                      
                      const newBuffer = new Float32Array(audioBuffer.length + inputData.length);
                      newBuffer.set(audioBuffer);
                      newBuffer.set(inputData, audioBuffer.length);
                      audioBuffer = newBuffer;

                      while (audioBuffer.length >= FRAMES_PER_BUFFER) {
                          const samplesTo = audioBuffer.slice(0, FRAMES_PER_BUFFER);
                          const pcmData = convertFloatTo16BitPCM(samplesTo);
                          
                          if (ws && ws.readyState === WebSocket.OPEN) {
                              const base64Audio = btoa(String.fromCharCode(...new Uint8Array(pcmData.buffer)));
                              ws.send(JSON.stringify({
                                  type: 'audio',
                                  data: base64Audio
                              }));
                          }
                          
                          audioBuffer = audioBuffer.slice(FRAMES_PER_BUFFER);
                      }
                  };

                  document.getElementById('micStatus').textContent = 'Microphone Status: Active';
                  document.getElementById('micStatus').classList.add('active');
                  document.getElementById('micStatus').classList.remove('error');
                  document.getElementById('retryButton').style.display = 'none';

                  if (audioContext.state === 'suspended') {
                      await audioContext.resume();
                  }

              } catch (error) {
                  console.error('Error initializing audio:', error);
                  document.getElementById('micStatus').textContent = 'Microphone Status: Error - ' + error.message;
                  document.getElementById('micStatus').classList.add('error');
                  document.getElementById('micStatus').classList.remove('active');
                  document.getElementById('retryButton').style.display = 'block';
                  throw error;
              }
          }

          function convertFloatTo16BitPCM(float32Array) {
              const int16Array = new Int16Array(float32Array.length);
              for (let i = 0; i < float32Array.length; i++) {
                  const s = Math.max(-1, Math.min(1, float32Array[i]));
                  int16Array[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;
              }
              return int16Array;
          }

          async function retryMicrophoneAccess() {
              try {
                  if (mediaStream) {
                      mediaStream.getTracks().forEach(track => track.stop());
                  }
                  
                  if (audioContext) {
                      await audioContext.close();
                  }

                  audioBuffer = new Float32Array();
                  await initAudio();
              } catch (error) {
                  console.error('Error retrying microphone access:', error);
                  document.getElementById('micStatus').textContent = 'Microphone Status: Error - ' + error.message;
                  document.getElementById('micStatus').classList.add('error');
              }
          }

          function createMediaSource() {
              return new Promise((resolve, reject) => {
                  try {
                      if (!isCallActive) {
                          reject(new Error('Call is not active'));
                          return;
                      }

                      if (mediaSource) {
                          if (mediaSource.readyState !== 'closed') {
                              try {
                                  mediaSource.endOfStream();
                              } catch (e) {
                                  console.error('Error ending previous media source:', e);
                              }
                          }
                          mediaSource = null;
                          sourceBuffer = null;
                      }

                      mediaSource = new MediaSource();
                      audioElement.src = URL.createObjectURL(mediaSource);
                      // Set up automatic play when data is available
                      audioElement.addEventListener('canplay', () => {
                          audioElement.play().catch(e => console.error('Error playing audio:', e));
                      });

                      // Handle audio ending
                      audioElement.addEventListener('ended', () => {
                          if (audioQueue.length > 0) {
                              audioElement.play().catch(e => console.error('Error playing audio:', e));
                          }
                      });
                      
                      mediaSource.addEventListener('sourceopen', () => {
                          try {
                              console.log('MediaSource opened');
                              isSourceOpen = true;
                              
                              if (!sourceBuffer && mediaSource.readyState === 'open') {
                                  sourceBuffer = mediaSource.addSourceBuffer('audio/mpeg');
                                  sourceBuffer.mode = 'sequence';
                                  sourceBuffer.addEventListener('updateend', () => {
                                      if (audioQueue.length > 0 && !sourceBuffer.updating) {
                                          processAudioQueue();
                                      }
                                  });
                              }
                              
                              resolve();
                          } catch (e) {
                              console.error('Error in sourceopen:', e);
                              reject(e);
                          }
                      });

                      mediaSource.addEventListener('sourceended', () => {
                          console.log('MediaSource ended');
                          isSourceOpen = false;
                      });

                      mediaSource.addEventListener('sourceclose', () => {
                          console.log('MediaSource closed');
                          isSourceOpen = false;
                          if (isCallActive) {
                              setTimeout(() => createMediaSource().catch(console.error), 1000);
                          }
                      });

                  } catch (e) {
                      console.error('Error creating MediaSource:', e);
                      reject(e);
                  }
              });
          }

          function processAudioQueue() {
              if (!sourceBuffer || !isSourceOpen) return;
              
              while (audioQueue.length > 0 && !sourceBuffer.updating) {
                  try {
                      const data = audioQueue.shift();
                      sourceBuffer.appendBuffer(data);
                      return;
                  } catch (e) {
                      console.error('Error appending buffer:', e);
                      if (e.name === 'QuotaExceededError') {
                          if (sourceBuffer.buffered.length > 0) {
                              const start = sourceBuffer.buffered.start(0);
                              const end = sourceBuffer.buffered.end(0);
                              if (end - start > 10) {
                                  sourceBuffer.remove(start, end - 10);
                              }
                          }
                          audioQueue.unshift(data);
                          return;
                      }
                      createMediaSource().catch(console.error);
                      return;
                  }
              }
          }

          async function initWebSocket() {
              try {
                  ws = new WebSocket(wsUrl);
                  
                  ws.onopen = async () => {
                      console.log('WebSocket connected');
                      document.getElementById('connectionStatus').textContent = 'Connection Status: Connected';
                      document.getElementById('connectionStatus').classList.add('active');
                      document.getElementById('connectionStatus').classList.remove('error');
                      reconnectAttempts = 0;
                      
                      await createMediaSource();
                  };

                  ws.onmessage = (event) => {
                      try {
                          const message = JSON.parse(event.data);
                          if (message.type === 'audio' && message.data) {
                              ws.send(JSON.stringify({
                                  type: "ack",
                                  data: "audio-received"
                              }));

                              const audioData = base64ToUint8Array(message.data);
                              audioQueue.push(audioData);
                              
                              if (sourceBuffer && !sourceBuffer.updating) {
                                  processAudioQueue();
                              }
                          }
                      } catch (error) {
                          console.error('Error processing message:', error);
                      }
                  };

                  ws.onclose = () => {
                      console.log('WebSocket closed');
                      document.getElementById('connectionStatus').textContent = 'Connection Status: Disconnected';
                      document.getElementById('connectionStatus').classList.remove('active');
                      document.getElementById('connectionStatus').classList.add('error');
                      
                      const timeout = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
                      reconnectAttempts++;
                      setTimeout(() => {
                          initWebSocket();
                      }, timeout);
                  };

                  ws.onerror = (error) => {
                      console.error('WebSocket error:', error);
                      document.getElementById('connectionStatus').textContent = 'Connection Status: Error';
                      document.getElementById('connectionStatus').classList.remove('active');
                      document.getElementById('connectionStatus').classList.add('error');
                  };
              } catch (error) {
                  console.error('Error initializing WebSocket:', error);
                  document.getElementById('connectionStatus').textContent = 'Connection Status: Error - ' + error.message;
                  document.getElementById('connectionStatus').classList.add('error');
              }
          }

          function base64ToUint8Array(base64) {
              const binaryString = atob(base64);
              const bytes = new Uint8Array(binaryString.length);
              for (let i = 0; i < binaryString.length; i++) {
                  bytes[i] = binaryString.charCodeAt(i);
              }
              return bytes;
          }

          let isCallActive = false;
          const startCallButton = document.getElementById('startCall');
          const endCallButton = document.getElementById('endCall');

          // Add event listeners for the buttons
          startCallButton.addEventListener('click', startCall);
          endCallButton.addEventListener('click', endCall);

          async function startCall() {
              if (isCallActive) return;
              
              try {
                  startCallButton.disabled = true;
                  // Reset all states before starting new call
                  await resetStates();
                  await init();
                  isCallActive = true;
                  startCallButton.style.display = 'none';
                  endCallButton.style.display = 'block';
                  audioElement.play().catch(e => console.error('Error playing audio:', e));
              } catch (error) {
                  console.error('Error starting call:', error);
                  startCallButton.disabled = false;
              }
          }

          async function endCall() {
              if (!isCallActive) return;

              try {
                  // Stop the microphone
                  if (mediaStream) {
                      mediaStream.getTracks().forEach(track => track.stop());
                      mediaStream = null;
                  }

                  // Close audio context
                  if (audioContext) {
                      await audioContext.close();
                      audioContext = null;
                  }

                  // Close WebSocket connection
                  if (ws) {
                      if (ws.readyState === WebSocket.OPEN) {
                          ws.close();
                      }
                      ws = null;
                  }

                  await resetStates();

                  // Update UI
                  startCallButton.style.display = 'block';
                  startCallButton.disabled = false;
                  endCallButton.style.display = 'none';
                  document.getElementById('connectionStatus').textContent = 'Connection Status: Disconnected';
                  document.getElementById('connectionStatus').classList.remove('active');
                  document.getElementById('micStatus').textContent = 'Microphone Status: Off';
                  document.getElementById('micStatus').classList.remove('active');

              } catch (error) {
                  console.error('Error ending call:', error);
              }
          }
          async function resetStates() {
              // Reset MediaSource
              if (mediaSource) {
                  if (mediaSource.readyState !== 'closed') {
                      try {
                          mediaSource.endOfStream();
                      } catch (e) {
                          console.error('Error ending media stream:', e);
                      }
                  }
                  mediaSource = null;
              }

              // Clear source buffer
              if (sourceBuffer) {
                  try {
                      if (!sourceBuffer.updating) {
                          sourceBuffer.abort();
                      }
                  } catch (e) {
                      console.error('Error aborting source buffer:', e);
                  }
                  sourceBuffer = null;
              }

              // Reset audio element
              try {
                  audioElement.pause();
                  audioElement.src = '';
                  audioElement.load(); // Important: properly reset the audio element
              } catch (e) {
                  console.error('Error resetting audio element:', e);
              }

              // Reset other states
              audioQueue = [];
              isSourceOpen = false;
              audioBuffer = new Float32Array();
              isCallActive = false;
              reconnectAttempts = 0;

              // Return a promise that resolves after a short delay
              return new Promise(resolve => setTimeout(resolve, 100));
          }

          // Modify the init function to not auto-start
          async function init() {
              try {
                  await initWebSocket();
                  await initAudio();
                  
                  setInterval(() => {
                      if (ws && ws.readyState === WebSocket.OPEN) {
                          ws.send(JSON.stringify({ type: 'ping' }));
                      }
                  }, 30000);

                  document.addEventListener('click', async () => {
                      if (audioContext && audioContext.state === 'suspended') {
                          await audioContext.resume();
                      }
                  });

              } catch (error) {
                  console.error('Initialization error:', error);
                  throw error; // Propagate the error to handle it in startCall
              }
          }

          //init().catch(console.error);
      </script>
  </body>
  </html>
  ```

  ```py Python theme={"system"}
  import websockets
  import asyncio
  import base64
  import json
  import subprocess
  import pyaudio
  import os
  import numpy as np

  OPENHOME_API_KEY = "abcxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxyz"

  class VoiceStreamer:
      def __init__(self, speaker_type="mpv"):
          self.api_key = OPENHOME_API_KEY
          self.server_url = f"https://app.openhome.com/websocket/voice-stream/{self.api_key}/0"
          self.frames_per_buffer = 3200
          self.format = pyaudio.paInt16
          self.channels = 1
          self.rate = 16000
          self.speaker_type = speaker_type
          self.websocket = None
          self.should_send_audio = True

          # Noise reduction parameters
          self.alpha = 0.95  # Smoothing factor
          self.prev_noise_power = None

          # INITIALIZE PYAUDIO
          self.py_audio_obj = pyaudio.PyAudio()
          self.stream = self._create_stream()
          
          # INITIALIZE PLACEHOLDER FOR MPV
          self.mpv_process = None
          self.speaker = None
          # self.playback_complete = asyncio.Event()
          self.is_speaking = False

      def _create_stream(self):
          """Create the microphone audio stream with better error handling."""
          try:
              # Get default input device info
              device_info = self.py_audio_obj.get_default_input_device_info()
              
              # Adjust parameters based on device capabilities
              supported_rate = min(int(device_info['defaultSampleRate']), self.rate)
              
              stream = self.py_audio_obj.open(
                  format=self.format,
                  channels=self.channels,
                  rate=supported_rate,
                  input=True,
                  frames_per_buffer=self.frames_per_buffer,
                  stream_callback=None,
                  start=False  # Don't start immediately
              )
              
              # Test the stream
              stream.start_stream()
              test_data = stream.read(self.frames_per_buffer)
              if not test_data:
                  raise IOError("No audio data received")
                  
              return stream
              
          except Exception as e:
              print(f"Error creating audio stream: {e}")
              # Try fallback parameters
              return self.py_audio_obj.open(
                  format=self.format,
                  channels=1,
                  rate=16000,
                  input=True,
                  frames_per_buffer=1024
              )

      def process_audio(self, audio_data):
          audio_array = np.frombuffer(audio_data, dtype=np.int16)
          audio_float = audio_array.astype(np.float32) / 32768.0
          
          # RMS energy calculation
          rms = np.sqrt(np.mean(audio_float**2))
          
          # Initialize state variables if they don't exist
          if not hasattr(self, 'energy_history'):
              self.energy_history = []
              self.baseline_energy = None
              self.speaking_threshold = 0.03
              self.last_voice_activity = False
              self.voice_holdoff_counter = 0
          
          # Update energy history
          self.energy_history.append(rms)
          if len(self.energy_history) > 30:
              self.energy_history.pop(0)
          
          # Calculate dynamic threshold with different bases for speaking/not speaking
          if self.is_speaking:
              # Much higher threshold when bot is speaking
              dynamic_threshold = np.mean(self.energy_history) * 1.8
          else:
              # Normal threshold when bot is not speaking
              dynamic_threshold = np.mean(self.energy_history) * 1.2
          
          # Significant audio detection with hysteresis
          if not self.last_voice_activity:
              # Higher threshold to start detection
              significant_audio = rms > dynamic_threshold * 1.2
          else:
              # Lower threshold to continue detection
              significant_audio = rms > dynamic_threshold * 0.8
          
          # Voice activity state machine with holdoff
          if significant_audio:
              self.last_voice_activity = True
              self.voice_holdoff_counter = 0
          else:
              self.voice_holdoff_counter += 1
              if self.voice_holdoff_counter > 10:  # Adjust holdoff period as needed
                  self.last_voice_activity = False
          
          # If bot is speaking, require much stronger user audio
          if self.is_speaking and rms < dynamic_threshold * 2.0:
              return b'\x00' * len(audio_data)
          
          # If no significant audio, return silence
          if not self.last_voice_activity:
              return b'\x00' * len(audio_data)
          
          # Noise reduction
          if self.prev_noise_power is None:
              self.prev_noise_power = rms**2
              
          noise_power = self.alpha * self.prev_noise_power + (1 - self.alpha) * rms**2
          self.prev_noise_power = noise_power
          
          gain = np.maximum(1 - (noise_power / (rms**2 + 1e-10)), 0.1)
          
          # Additional gain reduction when bot is speaking
          if self.is_speaking:
              gain *= 0.3  # Stronger gain reduction when bot is speaking
          
          processed_audio = audio_float * gain
          
          return (processed_audio * 32768).astype(np.int16).tobytes()

      def pause_mic(self):
          """Pause the microphone stream to prevent capturing speaker's audio."""
          self.should_send_audio = False
          if self.stream.is_active():
              self.stream.stop_stream()
              print("[+] Microphone stream paused")

      def resume_mic(self):
          """Resume the microphone stream after speaker finishes."""
          if not self.stream.is_active():
              self.stream.start_stream()
          self.should_send_audio = True
          print("[+] Microphone stream resumed")

      async def handle_mpv(self):
          """Handle MPV cleanup after audio playback ends."""
          if self.mpv_process and self.mpv_process.stdin:
              try:
                  self.mpv_process.stdin.close()
              except BrokenPipeError:
                  print("[-] Broken pipe error while writing to MPV")
              await self.mpv_process.communicate()
              self.mpv_process = None

          # self.playback_complete.set()
          self.is_speaking = False
          print("[+] MPV playback completed")

          message = {"type": "text", "data": "bot-speak-end"}
          await self.websocket.send(json.dumps(message))

      async def send_data(self):
          buffer_size = 5  # Number of frames to buffer
          audio_buffer = []
          
          while True:
              try:
                  if not self.should_send_audio:
                      await asyncio.sleep(0.01)
                      continue
                      
                  audio_bytes = self.stream.read(self.frames_per_buffer)
                  processed_audio = self.process_audio(audio_bytes)
                  
                  # Buffer the processed audio
                  audio_buffer.append(processed_audio)
                  if len(audio_buffer) < buffer_size:
                      continue
                      
                  # Check if any frame in buffer has significant audio
                  has_audio = any(np.frombuffer(frame, dtype=np.int16).any() for frame in audio_buffer)
                  
                  if has_audio:
                      # Send all buffered frames
                      if self.is_speaking:
                          print("Interrupting")
                          self.mpv_process.stdin.write(b"m\n")
                          message = {"type": "text", "data": "interrupt-event"}
                          await self.websocket.send(json.dumps(message))
                          self.mpv_process.stdin.write(b"q\n")
                          await self.mpv_process.stdin.drain()
                          self.is_speaking = False
                          print("[+] STOPPED MPV...")

                      for frame in audio_buffer:
                          encoded_bytes = base64.b64encode(frame).decode("utf-8")
                          json_data = json.dumps({"type": "audio", "data": encoded_bytes})
                          await self.websocket.send(json_data)
                  else:
                      print("NO AUDIO")
                  # Clear buffer
                  audio_buffer = []
                  
              except websockets.exceptions.ConnectionClosedError:
                  print("[!] Connection is closed")
                  break
              except Exception as e:
                  print("[!] Error in send_data:", e)
                  self.stream = self._create_stream()
              await asyncio.sleep(0.01)

      async def receive_data(self):
          """Receive data from server and handle it based on speaker type."""
          while True:
              try:
                  server_response = await self.websocket.recv()
                  data = json.loads(server_response)

                  if data["type"] == "text":
                      await self.handle_text_message(data)
                  elif data["type"] == "audio":
                      await self.handle_audio_message(data)
                  elif data["type"] == "message":
                      await self.handle_chat_message(data["data"])

              except websockets.exceptions.ConnectionClosedError:
                  print("[!] Connection is closed")
                  break
              except Exception as e:
                  print("[!] Error in receive_data:", e)

      async def handle_chat_message(self, data):
          """Process 'chat' type messages from server."""
          if data.get("final",False):
              print("%s: FINAL: %s"%(data.get("role").upper(), data.get("content","")))
          else:
              print("%s: LIVE: %s..."%(data.get("role").upper(), data.get("content","")))

      async def handle_text_message(self, data):
          """Process 'text' type messages from server based on speaker type."""
          if data["data"] == "audio-init":
              print("[+] BOT SPEAKING EVENT IS SET...")
              # self.pause_mic()
              # self.playback_complete.clear()
              self.is_speaking = True

              self.mpv_process = await asyncio.create_subprocess_exec(
                      "mpv", "--no-cache", "--no-terminal", "--", "fd://0",
                      stdin=subprocess.PIPE, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
                  )
              message = {"type": "text", "data": "bot-speaking"}
              await self.websocket.send(json.dumps(message))
              print("[+] SENT BOT SPEAKING EVENT...")

          elif data["data"] == "interrupt":
              print("[+] INTERRUPTION RECEIVED...")
              if self.speaker_type == "mpv" and self.mpv_process and self.mpv_process.stdin:
                  self.mpv_process.stdin.write(b"q\n")
                  await self.mpv_process.stdin.drain()
                  self.is_speaking = False
                  print("[+] STOPPED MPV...")

          elif data["data"] == "audio-end":
              print("[+] AUDIO END RECEIVED...")
              if self.speaker_type == "mpv":
                  asyncio.get_event_loop().create_task(self.handle_mpv())

              # await self.playback_complete.wait()
              await asyncio.sleep(0.5)
              # self.resume_mic()

      async def handle_audio_message(self, data):
          """Process 'audio' type messages from server."""
          message = {"type": "ack", "data": "audio-received"}
          await self.websocket.send(json.dumps(message))

          audio_bytes = base64.b64decode(data["data"])
          
          if self.speaker_type == "mpv" and self.mpv_process and self.mpv_process.stdin and self.is_speaking:
              self.mpv_process.stdin.write(audio_bytes)
              await self.mpv_process.stdin.drain()

      async def run(self):
          """Establish a connection to the server and start sending/receiving data."""
          try:
              async with websockets.connect(self.server_url) as websocket:
                  self.websocket = websocket
                  await asyncio.gather(self.send_data(), self.receive_data())
          except Exception as e:
              print(e)

      def __del__(self):
          """Cleanup resources."""
          if self.stream:
              self.stream.stop_stream()
              self.stream.close()
          if self.py_audio_obj:
              self.py_audio_obj.terminate()

  if __name__ == "__main__":
      voice_streamer = VoiceStreamer(speaker_type=os.getenv("SPEAKER_TYPE"))
      asyncio.run(voice_streamer.run())
  ```

  ```python RaspberryPi theme={"system"}
  import websockets
  import asyncio
  import base64
  import json
  import subprocess
  import pyaudio
  import os
  from dotenv import load_dotenv
  from time import time

  load_dotenv(dotenv_path=".env")

  class VoiceStreamer:
      def __init__(self):
          self.api_key = os.getenv('API_KEY')
          self.default_agent = os.getenv('DEFAULT_AGENT')
          self.server_url = f"{os.getenv('STREAM_SERVER_URL_WS')}/websocket/voice-stream/{self.api_key}/{self.default_agent}?devkit=true"
          self.frames_per_buffer = 1024
          self.format = pyaudio.paInt16
          self.channels = 1
          self.rate = 16000
          self.websocket = None
          self.last_live_transcription = time()
          self.py_audio_obj = pyaudio.PyAudio()
          self.stream = None
          self.stream = self._create_stream()
          self.mpv_process = None
          self.bot_speaking = True

      def _create_stream(self):
          if self.stream is not None:
              self.stream.stop_stream()
              self.stream.close()
          return self.py_audio_obj.open(
              format=self.format,
              channels=self.channels,
              rate=self.rate,
              input=True,
              frames_per_buffer=self.frames_per_buffer,
              start=True
          )
      
      def pause_mic(self):
          if self.stream.is_active():
              self.stream.stop_stream()

      def resume_mic(self):
          if not self.stream.is_active():
              self.stream.start_stream()

      async def handle_mpv(self):
          if self.mpv_process and self.mpv_process.stdin:
              try:
                  self.mpv_process.stdin.close()
                  await self.mpv_process.wait()
              except BrokenPipeError:
                  pass
              self.mpv_process = None

          self.bot_speaking = False
          message = {"type": "text", "data": "bot-speak-end"}
          await self.websocket.send(json.dumps(message))

      async def send_data(self):
          while True:
              try:
                  audio_bytes = self.stream.read(self.frames_per_buffer, exception_on_overflow=False)
                  if audio_bytes and not self.bot_speaking:
                      encoded_bytes = base64.b64encode(audio_bytes).decode("utf-8")
                      await self.websocket.send(json.dumps({"type": "audio", "data": encoded_bytes}))

              except Exception:
                  self.stream = self._create_stream()
              await asyncio.sleep(0.01)

      async def receive_data(self):
          while True:
              try:
                  server_response = await self.websocket.recv()
                  data = json.loads(server_response)

                  if data["type"] == "text":
                      await self.handle_text_message(data)
                  elif data["type"] == "audio":
                      await self.handle_audio_message(data)
                  elif data["type"] == "message" and data["data"].get("final",False):
                      print(data["data"], flush=True)

              except websockets.exceptions.ConnectionClosedError:
                  break
              except Exception:
                  continue

      async def handle_text_message(self, data):
          if data["data"] == "audio-init":
              self.mpv_process = await asyncio.create_subprocess_exec(
                  "mpv", "--no-cache", "--no-terminal", "--", "fd://0",
                  stdin=subprocess.PIPE, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
              )

              await self.websocket.send(json.dumps({"type": "text", "data": "bot-speaking"}))
              self.bot_speaking = True

          elif data["data"] == "interrupt" and self.mpv_process:
              self.mpv_process.stdin.write(b"q\n")
              await self.mpv_process.stdin.drain()

          elif data["data"] == "audio-end":
              await self.handle_mpv()

      async def handle_audio_message(self, data):
          while not self.bot_speaking:
              await asyncio.sleep(0.2)
          if self.mpv_process and self.mpv_process.stdin:
              self.mpv_process.stdin.write(base64.b64decode(data["data"]))
              await self.mpv_process.stdin.drain()

      async def run(self):
          try:
              async with websockets.connect(self.server_url) as websocket:
                  self.websocket = websocket
                  await asyncio.gather(self.send_data(), self.receive_data())
          except Exception:
              pass

  if __name__ == "__main__":
      voice_streamer = VoiceStreamer()
      asyncio.run(voice_streamer.run())
  ```
</CodeGroup>
