Push-to-Talk Guide

Build robust PTT functionality with proper state handling, UI feedback, and error recovery.

Basic Implementation

The simplest PTT implementation binds mouse/touch events to start and stop talking:

JavaScript
const pttButton = document.getElementById('ptt-button');

// Start talking on press
pttButton.addEventListener('mousedown', async () => {
  await talker.startTalking(channelId);
});

// Stop talking on release
pttButton.addEventListener('mouseup', async () => {
  await talker.stopTalking();
});

// Handle mouse leaving button while pressed
pttButton.addEventListener('mouseleave', async () => {
  if (talker.isTalking()) {
    await talker.stopTalking();
  }
});

Handling PTT Results

The startTalking() method returns a result object that tells you exactly what happened:

TypeScript
async function handlePTTStart(channelId: string) {
  const result = await talker.startTalking(channelId);

  if (result.success) {
    // PTT started or already active
    updateUI('talking');
    return;
  }

  // Handle failure cases
  switch (result.code) {
    case 'not_connected':
      updateUI('connecting');
      // SDK will auto-reconnect, wait for connection_change event
      break;

    case 'channel_busy':
      // Someone else is talking on this channel
      showToast('Channel is busy. Please wait...');
      break;

    case 'no_user_id':
      console.error('Developer error:', result.message);
      break;

    case 'error':
      showError(result.message);
      break;
  }
}

Connection-Aware PTT

Build a PTT button that responds to connection state:

TypeScript
class PTTController {
  private pendingChannel: string | null = null;

  constructor(private talker: TalkerClient) {
    // Listen for connection changes
    talker.on('connection_change', ({ connected }) => {
      if (connected && this.pendingChannel) {
        // Retry PTT after reconnection
        this.startTalking(this.pendingChannel);
        this.pendingChannel = null;
      }
    });
  }

  async startTalking(channelId: string) {
    const result = await this.talker.startTalking(channelId);

    if (!result.success && result.code === 'not_connected') {
      // Save channel for retry after connection
      this.pendingChannel = channelId;
      this.updateUI('connecting');
    } else if (result.success) {
      this.updateUI('talking');
    }
  }

  async stopTalking() {
    this.pendingChannel = null;
    await this.talker.stopTalking();
    this.updateUI('idle');
  }

  private updateUI(state: 'idle' | 'connecting' | 'talking') {
    // Update your UI based on state
  }
}

Audio Level Visualization

Display real-time audio levels while the user is talking:

TypeScript
let animationFrame: number;

function startVisualization() {
  function update() {
    const level = talker.getAudioLevel();

    // level is 0-1, update your visualization
    const bar = document.getElementById('audio-level');
    bar.style.width = `${level * 100}%`;

    if (talker.isTalking()) {
      animationFrame = requestAnimationFrame(update);
    }
  }

  update();
}

function stopVisualization() {
  cancelAnimationFrame(animationFrame);
  document.getElementById('audio-level').style.width = '0%';
}

// Start/stop with PTT
pttButton.addEventListener('mousedown', async () => {
  await talker.startTalking(channelId);
  startVisualization();
});

pttButton.addEventListener('mouseup', async () => {
  await talker.stopTalking();
  stopVisualization();
});

Receiving Broadcasts

Show who is currently speaking in a channel:

TypeScript
interface ActiveBroadcast {
  senderId: string;
  senderName: string;
  channelId: string;
  startTime: number;
}

let activeBroadcast: ActiveBroadcast | null = null;

talker.on('broadcast_start', (event) => {
  activeBroadcast = {
    senderId: event.senderId,
    senderName: event.senderName,
    channelId: event.channelId,
    startTime: event.timestamp,
  };

  showSpeakingIndicator(event.senderName);
});

talker.on('broadcast_end', (event) => {
  if (activeBroadcast?.senderId === event.senderId) {
    activeBroadcast = null;
    hideSpeakingIndicator();
  }
});

// Track playback progress
talker.on('playback_progress', (event) => {
  updatePlaybackDuration(event.duration);
});

Managing the Playback Queue

Control how incoming broadcasts are played:

TypeScript
// Show queue status
function updateQueueStatus() {
  const queueLength = talker.getPlaybackQueueLength();
  const current = talker.getCurrentlyPlaying();

  if (current) {
    showPlaying(current.senderName);
  }

  if (queueLength > 0) {
    showQueueBadge(queueLength);
  }
}

// Pause queue during important actions
function onImportantAction() {
  talker.pauseQueue();

  // Resume after action completes
  setTimeout(() => {
    talker.resumeQueue();
  }, 5000);
}

// Skip current and pause (e.g., for urgent announcement)
function onUrgentInterrupt() {
  talker.stopPlaybackAndPause();

  // Play urgent audio, then resume
  playUrgentAudio().then(() => {
    talker.resumeQueue();
  });
}

Keyboard PTT

Add keyboard shortcuts for PTT:

TypeScript
const PTT_KEY = ' '; // Spacebar

document.addEventListener('keydown', async (e) => {
  // Ignore if typing in input field
  if (e.target instanceof HTMLInputElement ||
      e.target instanceof HTMLTextAreaElement) {
    return;
  }

  if (e.key === PTT_KEY && !e.repeat) {
    e.preventDefault();
    await talker.startTalking(currentChannelId);
  }
});

document.addEventListener('keyup', async (e) => {
  if (e.key === PTT_KEY) {
    await talker.stopTalking();
  }
});

Touch Device Support

Handle touch events for mobile devices:

TypeScript
const pttButton = document.getElementById('ptt-button');

pttButton.addEventListener('touchstart', async (e) => {
  e.preventDefault(); // Prevent mouse events
  await talker.startTalking(channelId);
}, { passive: false });

pttButton.addEventListener('touchend', async (e) => {
  e.preventDefault();
  await talker.stopTalking();
});

pttButton.addEventListener('touchcancel', async () => {
  await talker.stopTalking();
});

// Prevent context menu on long press
pttButton.addEventListener('contextmenu', (e) => {
  e.preventDefault();
});

iOS Audio Unlock

iOS Safari requires user interaction to play audio. Call unlockAudio() on first touch:

TypeScript
// Call on any user interaction
document.addEventListener('touchstart', () => {
  talker.unlockAudio();
}, { once: true });

// Or on a specific "Enable Audio" button
enableAudioButton.addEventListener('click', async () => {
  await talker.unlockAudio();
  enableAudioButton.style.display = 'none';
});
iOS Audio Policy

On iOS, audio will not play until unlockAudio() is called during a user gesture. Without this, users won't hear incoming broadcasts.

Complete Example

Here's a complete PTT implementation with all features:

TypeScript
class PTTManager {
  private isPressed = false;
  private pendingChannel: string | null = null;

  constructor(
    private talker: TalkerClient,
    private button: HTMLElement,
    private channelId: string
  ) {
    this.setupEventListeners();
    this.setupTalkerEvents();
  }

  private setupEventListeners() {
    // Mouse events
    this.button.addEventListener('mousedown', () => this.onPress());
    this.button.addEventListener('mouseup', () => this.onRelease());
    this.button.addEventListener('mouseleave', () => this.onRelease());

    // Touch events
    this.button.addEventListener('touchstart', (e) => {
      e.preventDefault();
      this.onPress();
    }, { passive: false });
    this.button.addEventListener('touchend', (e) => {
      e.preventDefault();
      this.onRelease();
    });
    this.button.addEventListener('touchcancel', () => this.onRelease());

    // iOS audio unlock
    document.addEventListener('touchstart', () => {
      this.talker.unlockAudio();
    }, { once: true });
  }

  private setupTalkerEvents() {
    this.talker.on('connection_change', ({ connected }) => {
      this.updateButtonState();

      if (connected && this.pendingChannel) {
        this.startTalking();
        this.pendingChannel = null;
      }
    });

    this.talker.on('broadcast_start', (event) => {
      this.showSpeaker(event.senderName);
    });

    this.talker.on('broadcast_end', () => {
      this.hideSpeaker();
    });
  }

  private async onPress() {
    if (this.isPressed) return;
    this.isPressed = true;
    await this.startTalking();
  }

  private async onRelease() {
    if (!this.isPressed) return;
    this.isPressed = false;
    this.pendingChannel = null;
    await this.talker.stopTalking();
    this.updateButtonState();
  }

  private async startTalking() {
    const result = await this.talker.startTalking(this.channelId);

    if (!result.success) {
      if (result.code === 'not_connected') {
        this.pendingChannel = this.channelId;
      } else if (result.code === 'channel_busy') {
        this.showBusyFeedback();
      }
    }

    this.updateButtonState();
  }

  private showBusyFeedback() {
    // Show "Channel busy" feedback to user
  }

  private updateButtonState() {
    const { status } = this.talker.getConnectionStatus();

    this.button.classList.remove('idle', 'connecting', 'talking', 'disconnected');

    if (!this.talker.isFullyConnected()) {
      this.button.classList.add('disconnected');
    } else if (this.talker.isTalking()) {
      this.button.classList.add('talking');
    } else if (this.pendingChannel) {
      this.button.classList.add('connecting');
    } else {
      this.button.classList.add('idle');
    }
  }

  private showSpeaker(name: string) {
    // Show who is speaking
  }

  private hideSpeaker() {
    // Hide speaker indicator
  }
}