1 Commits

Author SHA1 Message Date
61beadcd06 Fix browser phone connection and audio issues on Android tablets
All checks were successful
Create Release / build (push) Successful in 6s
Resolves issues where browser phone PWA failed to connect and calls would
immediately hang up when answered on Android tablets. Adds proper mobile
audio handling, device connection monitoring, and PWA notifications for
incoming calls.

Key changes:
- Add AudioContext initialization with mobile unlock for autoplay support
- Add Android-specific WebRTC constraints (echo cancellation, ICE restart)
- Add device connection state monitoring and auto-reconnection
- Add incoming call ringtone with vibration fallback
- Add PWA service worker notifications for background calls
- Add Page Visibility API for background call detection
- Improve call answer handler with connection state validation
- Add touch event support for mobile dialpad

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-12 13:21:29 -08:00
4 changed files with 470 additions and 22 deletions

View File

@@ -6996,6 +6996,7 @@ class TWP_Admin {
<div class="phone-interface">
<div class="phone-display">
<div id="phone-status">Ready</div>
<div id="device-connection-status" style="font-size: 12px; color: #999; margin-top: 5px;">Connecting...</div>
<div id="phone-number-display"></div>
<div id="call-timer" style="display: none;">00:00</div>
</div>
@@ -7434,6 +7435,210 @@ class TWP_Admin {
var callStartTime = null;
var tokenRefreshTimer = null;
var tokenExpiry = null;
var audioContext = null;
var ringtoneAudio = null;
var isPageVisible = true;
var deviceConnectionState = 'disconnected'; // disconnected, connecting, connected
var serviceWorkerRegistration = null;
// Initialize AudioContext for mobile audio playback
function initializeAudioContext() {
try {
if (!audioContext) {
// Create AudioContext with compatibility
var AudioContextClass = window.AudioContext || window.webkitAudioContext;
audioContext = new AudioContextClass();
console.log('AudioContext created, state:', audioContext.state);
}
// Resume AudioContext if suspended (required on mobile)
if (audioContext.state === 'suspended') {
audioContext.resume().then(function() {
console.log('AudioContext resumed successfully');
}).catch(function(err) {
console.error('Failed to resume AudioContext:', err);
});
}
return true;
} catch (error) {
console.error('Failed to initialize AudioContext:', error);
return false;
}
}
// Create and setup ringtone audio element
function setupRingtone() {
if (!ringtoneAudio) {
ringtoneAudio = new Audio();
// Use a simple sine wave tone or default ringtone
// For now, we'll use a data URI for a simple beep tone
ringtoneAudio.loop = true;
ringtoneAudio.volume = 0.7;
// Create a simple ringtone using Web Audio API
createRingtone();
}
}
// Create ringtone using Web Audio API for better mobile support
function createRingtone() {
// Use a simple base64-encoded beep tone (short MP3)
// This is a simple 1-second beep at 800Hz
// You can replace this with a custom ringtone file URL if you upload one
// For now, use a simple approach: HTML5 Audio with error fallback
// Note: On mobile, audio playback may be restricted, so we rely heavily on vibration
var ringtoneUrl = '<?php echo plugins_url('assets/sounds/ringtone.mp3', dirname(__FILE__)); ?>';
// Try to load the ringtone file
ringtoneAudio.src = ringtoneUrl;
// Fallback: if ringtone file fails to load, we'll just use vibration
ringtoneAudio.addEventListener('error', function(e) {
console.log('Ringtone file not found (this is normal), using vibration only for mobile');
// Don't show error - vibration is sufficient for mobile
}, { once: true });
// Try to preload
ringtoneAudio.load();
}
// Play ringtone for incoming call
function playRingtone() {
try {
// Initialize AudioContext on user interaction
initializeAudioContext();
if (ringtoneAudio) {
var playPromise = ringtoneAudio.play();
if (playPromise !== undefined) {
playPromise.then(function() {
console.log('Ringtone playing');
}).catch(function(error) {
console.error('Ringtone play failed:', error);
// Fallback: vibrate on mobile
vibrateDevice([300, 200, 300, 200, 300]);
});
}
}
// Always vibrate on mobile for better notification
vibrateDevice([300, 200, 300, 200, 300]);
} catch (error) {
console.error('Error playing ringtone:', error);
}
}
// Stop ringtone
function stopRingtone() {
try {
if (ringtoneAudio) {
ringtoneAudio.pause();
ringtoneAudio.currentTime = 0;
}
} catch (error) {
console.error('Error stopping ringtone:', error);
}
}
// Vibrate device (mobile)
function vibrateDevice(pattern) {
if ('vibrate' in navigator) {
navigator.vibrate(pattern);
}
}
// Register service worker for PWA notifications
function registerServiceWorker() {
if ('serviceWorker' in navigator) {
var swPath = '<?php echo plugins_url('assets/js/twp-service-worker.js', dirname(__FILE__)); ?>';
navigator.serviceWorker.register(swPath)
.then(function(registration) {
console.log('Service Worker registered:', registration);
serviceWorkerRegistration = registration;
// Request notification permission
if ('Notification' in window && Notification.permission === 'default') {
Notification.requestPermission().then(function(permission) {
console.log('Notification permission:', permission);
});
}
})
.catch(function(error) {
console.error('Service Worker registration failed:', error);
});
}
}
// Send notification via service worker
function sendIncomingCallNotification(callerNumber) {
// Try browser notification first
if ('Notification' in window && Notification.permission === 'granted') {
if (serviceWorkerRegistration && serviceWorkerRegistration.active) {
serviceWorkerRegistration.active.postMessage({
type: 'SHOW_NOTIFICATION',
title: 'Incoming Call',
body: 'Call from ' + (callerNumber || 'Unknown Number'),
icon: '<?php echo plugins_url('assets/images/phone-icon.png', dirname(__FILE__)); ?>',
tag: 'incoming-call',
requireInteraction: true
});
} else {
// Fallback: show notification directly
new Notification('Incoming Call', {
body: 'Call from ' + (callerNumber || 'Unknown Number'),
icon: '<?php echo plugins_url('assets/images/phone-icon.png', dirname(__FILE__)); ?>',
tag: 'incoming-call',
requireInteraction: true
});
}
}
}
// Monitor page visibility for background call handling
function setupPageVisibility() {
document.addEventListener('visibilitychange', function() {
isPageVisible = !document.hidden;
console.log('Page visibility changed:', isPageVisible ? 'visible' : 'hidden');
// If page becomes visible, resume audio context
if (isPageVisible && audioContext) {
initializeAudioContext();
}
});
}
// Update device connection status in UI
function updateConnectionStatus(state) {
deviceConnectionState = state;
var statusText = '';
var statusColor = '';
switch(state) {
case 'connected':
statusText = 'Connected';
statusColor = '#4CAF50';
break;
case 'connecting':
statusText = 'Connecting...';
statusColor = '#FF9800';
break;
case 'disconnected':
statusText = 'Disconnected';
statusColor = '#f44336';
break;
default:
statusText = 'Unknown';
statusColor = '#999';
}
// Update status indicator (we'll add this to the UI)
$('#device-connection-status').text(statusText).css('color', statusColor);
}
// Wait for SDK to load
function waitForTwilioSDK(callback) {
@@ -7450,6 +7655,18 @@ class TWP_Admin {
// Initialize the browser phone
function initializeBrowserPhone() {
$('#phone-status').text('Initializing...');
updateConnectionStatus('connecting');
// Initialize audio and PWA features
setupRingtone();
registerServiceWorker();
setupPageVisibility();
// Initialize AudioContext on first user interaction
$(document).one('click touchstart', function() {
console.log('User interaction detected, initializing AudioContext');
initializeAudioContext();
});
// Wait for SDK before proceeding
waitForTwilioSDK(function() {
@@ -7468,9 +7685,11 @@ class TWP_Admin {
// WordPress wp_send_json_error sends the error message as response.data
var errorMsg = response.data || response.error || 'Unknown error';
showError('Failed to initialize: ' + errorMsg);
updateConnectionStatus('disconnected');
}
}).fail(function() {
showError('Failed to connect to server');
updateConnectionStatus('disconnected');
});
});
}
@@ -7518,36 +7737,77 @@ class TWP_Admin {
throw new Error('Twilio Voice SDK not loaded');
}
console.log('Setting up Twilio Device...');
updateConnectionStatus('connecting');
// Request media permissions before setting up device
const hasPermissions = await requestMediaPermissions();
if (!hasPermissions) {
updateConnectionStatus('disconnected');
return; // Stop setup if permissions denied
}
// Clean up existing device if any
if (device) {
console.log('Destroying existing device');
await device.destroy();
}
// Detect if we're on Android/mobile for specific settings
var isAndroid = /Android/i.test(navigator.userAgent);
var isMobile = /Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
console.log('Device detection - Android:', isAndroid, 'Mobile:', isMobile);
// Android-specific audio constraints for better WebRTC performance
var audioConstraints = {
echoCancellation: true,
noiseSuppression: true,
autoGainControl: true
};
// Additional Android-specific settings
if (isAndroid) {
audioConstraints.googEchoCancellation = true;
audioConstraints.googNoiseSuppression = true;
audioConstraints.googAutoGainControl = true;
audioConstraints.googHighpassFilter = true;
}
// Setup Twilio Voice SDK v2 Device
// Note: Voice SDK v2 uses Twilio.Device directly, not Twilio.Voice.Device
device = new Twilio.Device(token, {
logLevel: 1, // 0 = TRACE, 1 = DEBUG
codecPreferences: ['opus', 'pcmu'],
edge: 'sydney' // Or closest edge location
edge: 'sydney', // Or closest edge location
enableIceRestart: true, // Important for mobile network switching
audioConstraints: audioConstraints,
maxCallSignalingTimeoutMs: 30000, // 30 seconds timeout for mobile
closeProtection: true // Warn before closing during call
});
console.log('Twilio Device created with audio constraints:', audioConstraints);
// Set up event handlers BEFORE registering
// Device registered and ready
device.on('registered', function() {
console.log('Device registered successfully');
$('#phone-status').text('Ready').css('color', '#4CAF50');
$('#call-btn').prop('disabled', false);
updateConnectionStatus('connected');
});
// Device unregistered
device.on('unregistered', function() {
console.log('Device unregistered');
updateConnectionStatus('disconnected');
});
// Handle errors
device.on('error', function(error) {
console.error('Twilio Device Error:', error);
console.error('Error code:', error.code, 'Message:', error.message);
updateConnectionStatus('disconnected');
var errorMsg = error.message || error.toString();
@@ -7560,6 +7820,15 @@ class TWP_Admin {
errorMsg = 'Token error: ' + errorMsg + ' - The page will automatically try to refresh the token.';
// Try to reinitialize after token error
setTimeout(initializeBrowserPhone, 5000);
} else if (errorMsg.includes('31005') || errorMsg.includes('Connection error')) {
errorMsg = 'Connection error: Check your internet connection. If on mobile, try switching between WiFi and cellular data.';
// Retry connection
setTimeout(function() {
if (device) {
console.log('Attempting to reconnect device...');
device.register();
}
}, 3000);
}
showError(errorMsg);
@@ -7567,16 +7836,32 @@ class TWP_Admin {
// Handle incoming calls
device.on('incoming', function(call) {
console.log('Incoming call from:', call.parameters.From);
console.log('Call SID:', call.parameters.CallSid);
console.log('Device connection state:', deviceConnectionState);
currentCall = call;
var callerNumber = call.parameters.From || 'Unknown Number';
$('#phone-status').text('Incoming Call').css('color', '#FF9800');
$('#phone-number-display').text(call.parameters.From || 'Unknown Number');
$('#phone-number-display').text(callerNumber);
$('#call-btn').hide();
$('#answer-btn').show();
// Play ringtone and show notification
playRingtone();
// If page is in background, send notification
if (!isPageVisible) {
console.log('Page in background, sending notification');
sendIncomingCallNotification(callerNumber);
}
// Setup call event handlers
setupCallHandlers(call);
if ($('#auto-answer').is(':checked')) {
console.log('Auto-answer enabled, accepting call');
call.accept();
}
});
@@ -7599,6 +7884,8 @@ class TWP_Admin {
function setupCallHandlers(call) {
// Call accepted/connected
call.on('accept', function() {
console.log('Call accepted and connected');
stopRingtone();
$('#phone-status').text('Connected').css('color', '#2196F3');
$('#call-btn').hide();
$('#answer-btn').hide();
@@ -7610,6 +7897,8 @@ class TWP_Admin {
// Call disconnected
call.on('disconnect', function() {
console.log('Call disconnected');
stopRingtone();
currentCall = null;
$('#phone-status').text('Ready').css('color', '#4CAF50');
$('#hangup-btn').hide();
@@ -7627,6 +7916,8 @@ class TWP_Admin {
// Call rejected
call.on('reject', function() {
console.log('Call rejected');
stopRingtone();
currentCall = null;
$('#phone-status').text('Ready').css('color', '#4CAF50');
$('#answer-btn').hide();
@@ -7635,6 +7926,8 @@ class TWP_Admin {
// Call cancelled (by caller before answer)
call.on('cancel', function() {
console.log('Call cancelled by caller');
stopRingtone();
currentCall = null;
$('#phone-status').text('Missed Call').css('color', '#FF9800');
$('#answer-btn').hide();
@@ -7643,6 +7936,26 @@ class TWP_Admin {
$('#phone-status').text('Ready').css('color', '#4CAF50');
}, 3000);
});
// Call error
call.on('error', function(error) {
console.error('Call error:', error);
console.error('Error code:', error.code, 'Message:', error.message);
stopRingtone();
var errorMsg = error.message || error.toString();
// Specific error handling for Android/mobile
if (error.code === 31005) {
errorMsg = 'Connection failed: Check your network connection. Try switching between WiFi and cellular data.';
} else if (error.code === 31201 || error.code === 31204) {
errorMsg = 'Call setup failed: Please try again. If the problem persists, refresh the page.';
} else if (error.code === 31208) {
errorMsg = 'Media connection failed: Check microphone permissions and try again.';
}
showError('Call error: ' + errorMsg);
});
}
function refreshToken() {
@@ -7753,11 +8066,15 @@ class TWP_Admin {
$('#caller-id-select').html('<option value="">Error loading numbers</option>');
});
// Dialpad functionality
$('.dialpad-btn').on('click', function() {
// Dialpad functionality (support both click and touch events)
$('.dialpad-btn').on('click touchend', function(e) {
e.preventDefault(); // Prevent duplicate events
var digit = $(this).data('digit');
var currentVal = $('#phone-number-input').val();
$('#phone-number-input').val(currentVal + digit);
// Initialize AudioContext on user interaction (mobile requirement)
initializeAudioContext();
});
// Call button
@@ -7818,9 +8135,46 @@ class TWP_Admin {
// Answer button
$('#answer-btn').on('click', function() {
console.log('Answer button clicked');
console.log('Device connection state:', deviceConnectionState);
console.log('Current call:', currentCall);
if (!currentCall) {
console.error('No current call to answer');
showError('No incoming call to answer');
return;
}
// Check device connection state
if (deviceConnectionState !== 'connected') {
console.error('Device not connected, state:', deviceConnectionState);
showError('Phone not connected. Reconnecting...');
// Try to reconnect
if (device) {
device.register().then(function() {
console.log('Device reconnected, answering call');
if (currentCall) {
currentCall.accept();
}
}).catch(function(err) {
console.error('Failed to reconnect device:', err);
showError('Failed to reconnect. Please refresh the page.');
});
}
return;
}
// Initialize AudioContext before accepting (important for mobile)
initializeAudioContext();
try {
console.log('Accepting call...');
currentCall.accept();
} catch (error) {
console.error('Error accepting call:', error);
showError('Failed to answer call: ' + error.message);
}
});
// Mute button

42
assets/images/README.md Normal file
View File

@@ -0,0 +1,42 @@
# Browser Phone Images
## Required Images for PWA Notifications
Place the following image files in this directory for browser push notifications:
### phone-icon.png
- **Purpose**: Main notification icon
- **Size**: 192x192 pixels (recommended)
- **Format**: PNG with transparency
- **Usage**: Shown in browser notifications for incoming calls
### phone-badge.png
- **Purpose**: Small badge icon for notifications
- **Size**: 96x96 pixels or 72x72 pixels
- **Format**: PNG with transparency
- **Usage**: Displayed in the notification badge area (Android)
## Fallback Behavior
If these images are not present, the browser will:
- Use a default browser notification icon
- Still display text-based notifications
- Functionality will not be affected
## Image Creation Tips
- Use a simple, recognizable phone icon
- Ensure good contrast for visibility
- Test on both light and dark backgrounds
- Transparent backgrounds work best
- Use a consistent color scheme with your brand
## Example Resources
- Google Material Icons: https://fonts.google.com/icons
- Font Awesome: https://fontawesome.com/
- Flaticon: https://www.flaticon.com/
- Or create custom icons using tools like:
- Figma
- Adobe Illustrator
- Inkscape (free)

View File

@@ -72,7 +72,23 @@ self.addEventListener('push', function(event) {
// Handle messages from the main script
self.addEventListener('message', function(event) {
console.log('TWP Service Worker: Message received', event.data);
if (event.data && event.data.type === 'SHOW_NOTIFICATION') {
self.registration.showNotification(event.data.title, event.data.options);
const options = {
body: event.data.body || 'You have a new call waiting',
icon: event.data.icon || '/wp-content/plugins/twilio-wp-plugin/assets/images/phone-icon.png',
badge: '/wp-content/plugins/twilio-wp-plugin/assets/images/phone-badge.png',
vibrate: [300, 200, 300, 200, 300],
tag: event.data.tag || 'incoming-call',
requireInteraction: event.data.requireInteraction !== undefined ? event.data.requireInteraction : true,
data: event.data.data || {}
};
console.log('TWP Service Worker: Showing notification', event.data.title, options);
event.waitUntil(
self.registration.showNotification(event.data.title || 'Incoming Call', options)
);
}
});

36
assets/sounds/README.md Normal file
View File

@@ -0,0 +1,36 @@
# Ringtone Audio Files
## Custom Ringtone
To add a custom ringtone for incoming calls in the browser phone:
1. Place your ringtone audio file in this directory
2. Name it: `ringtone.mp3`
3. Recommended format: MP3, under 5 seconds, loopable
## Fallback Behavior
If no ringtone file is present, the browser phone will:
- Use device vibration on mobile devices (Android, iOS)
- Rely on browser notifications to alert users
- Display visual indicators in the browser interface
## Supported Audio Formats
- **MP3** (recommended) - Best compatibility across browsers
- **OGG** - Good for Firefox, Chrome
- **WAV** - Larger file size but universal support
## File Requirements
- Maximum file size: 100KB recommended
- Duration: 1-5 seconds (will loop)
- Sample rate: 44.1kHz or 48kHz
- Bitrate: 128kbps or higher
## Example Ringtone Sources
You can find free ringtone files at:
- https://freesound.org/
- https://incompetech.com/
- Or create your own using Audacity or similar tools