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:
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:
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:
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:
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:
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:
// 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:
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:
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:
// 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';
});
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:
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
}
}