mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-02 20:18:08 +00:00
chore: clean up voice channel code for MVP
- Simplify message builder content_attributes handling - Remove AI captain integration from incoming call service - Clean up FloatingCallWidget by removing non-essential features: - Remove Gravatar/MD5 dependency - Remove keypad/DTMF functionality - Remove fullscreen toggle - Simplify avatar handling - Apply consistent code formatting across voice components - Remove debug logging and unused code 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@ class VoiceAPI extends ApiClient {
|
||||
constructor() {
|
||||
// Use 'voice' as the resource with accountScoped: true
|
||||
super('voice', { accountScoped: true });
|
||||
|
||||
|
||||
// Client-side Twilio device
|
||||
this.device = null;
|
||||
this.activeConnection = null;
|
||||
@@ -20,7 +20,9 @@ class VoiceAPI extends ApiClient {
|
||||
|
||||
// Based on the route definition, the correct URL path is /api/v1/accounts/{accountId}/contacts/{contactId}/call
|
||||
// The endpoint is defined in the contacts namespace, not voice namespace
|
||||
return axios.post(`${this.baseUrl().replace('/voice', '')}/contacts/${contactId}/call`);
|
||||
return axios.post(
|
||||
`${this.baseUrl().replace('/voice', '')}/contacts/${contactId}/call`
|
||||
);
|
||||
}
|
||||
|
||||
// End an active call
|
||||
@@ -105,71 +107,88 @@ class VoiceAPI extends ApiClient {
|
||||
conversation_id: conversationId,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Client SDK methods
|
||||
|
||||
|
||||
// Get a capability token for the Twilio Client
|
||||
getToken(inboxId) {
|
||||
console.log(`Requesting token for inbox ID: ${inboxId} at URL: ${this.url}/tokens`);
|
||||
|
||||
console.log(
|
||||
`Requesting token for inbox ID: ${inboxId} at URL: ${this.url}/tokens`
|
||||
);
|
||||
|
||||
// Log the base URL for debugging
|
||||
console.log(`Base URL: ${this.baseUrl()}`);
|
||||
|
||||
|
||||
// Check if inboxId is valid
|
||||
if (!inboxId) {
|
||||
console.error('No inbox ID provided for token request');
|
||||
return Promise.reject(new Error('Inbox ID is required'));
|
||||
}
|
||||
|
||||
|
||||
// Add more request details to help debugging
|
||||
return axios.post(`${this.url}/tokens`, { inbox_id: inboxId }, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}).catch(error => {
|
||||
// Extract useful error details for debugging
|
||||
const errorInfo = {
|
||||
status: error.response?.status,
|
||||
statusText: error.response?.statusText,
|
||||
data: error.response?.data,
|
||||
url: `${this.url}/tokens`,
|
||||
inboxId,
|
||||
};
|
||||
|
||||
console.error('Token request error details:', errorInfo);
|
||||
|
||||
// Try to extract a more useful error message from the HTML response if it's a 500 error
|
||||
if (error.response?.status === 500 && typeof error.response.data === 'string') {
|
||||
// Look for specific error patterns in the HTML
|
||||
const htmlData = error.response.data;
|
||||
|
||||
// Check for common Ruby/Rails error patterns
|
||||
const nameMatchResult = htmlData.match(/<h2>(.*?)<\/h2>/);
|
||||
const detailsMatchResult = htmlData.match(/<pre>([\s\S]*?)<\/pre>/);
|
||||
|
||||
const errorName = nameMatchResult ? nameMatchResult[1] : null;
|
||||
const errorDetails = detailsMatchResult ? detailsMatchResult[1] : null;
|
||||
|
||||
if (errorName || errorDetails) {
|
||||
const enhancedError = new Error(`Server error: ${errorName || 'Internal Server Error'}`);
|
||||
enhancedError.details = errorDetails;
|
||||
enhancedError.originalError = error;
|
||||
throw enhancedError;
|
||||
return axios
|
||||
.post(
|
||||
`${this.url}/tokens`,
|
||||
{ inbox_id: inboxId },
|
||||
{
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
});
|
||||
)
|
||||
.catch(error => {
|
||||
// Extract useful error details for debugging
|
||||
const errorInfo = {
|
||||
status: error.response?.status,
|
||||
statusText: error.response?.statusText,
|
||||
data: error.response?.data,
|
||||
url: `${this.url}/tokens`,
|
||||
inboxId,
|
||||
};
|
||||
|
||||
console.error('Token request error details:', errorInfo);
|
||||
|
||||
// Try to extract a more useful error message from the HTML response if it's a 500 error
|
||||
if (
|
||||
error.response?.status === 500 &&
|
||||
typeof error.response.data === 'string'
|
||||
) {
|
||||
// Look for specific error patterns in the HTML
|
||||
const htmlData = error.response.data;
|
||||
|
||||
// Check for common Ruby/Rails error patterns
|
||||
const nameMatchResult = htmlData.match(/<h2>(.*?)<\/h2>/);
|
||||
const detailsMatchResult = htmlData.match(/<pre>([\s\S]*?)<\/pre>/);
|
||||
|
||||
const errorName = nameMatchResult ? nameMatchResult[1] : null;
|
||||
const errorDetails = detailsMatchResult
|
||||
? detailsMatchResult[1]
|
||||
: null;
|
||||
|
||||
if (errorName || errorDetails) {
|
||||
const enhancedError = new Error(
|
||||
`Server error: ${errorName || 'Internal Server Error'}`
|
||||
);
|
||||
enhancedError.details = errorDetails;
|
||||
enhancedError.originalError = error;
|
||||
throw enhancedError;
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Initialize the Twilio Device
|
||||
async initializeDevice(inboxId) {
|
||||
// If already initialized, return the existing device after checking its health
|
||||
if (this.initialized && this.device) {
|
||||
const deviceState = this.device.state;
|
||||
console.log('Device already initialized, current state:', deviceState);
|
||||
|
||||
|
||||
// If the device is in a bad state, destroy and reinitialize
|
||||
if (deviceState === 'error' || deviceState === 'unregistered') {
|
||||
console.log('Device is in a bad state, destroying and reinitializing...');
|
||||
console.log(
|
||||
'Device is in a bad state, destroying and reinitializing...'
|
||||
);
|
||||
try {
|
||||
this.device.destroy();
|
||||
} catch (e) {
|
||||
@@ -182,11 +201,13 @@ class VoiceAPI extends ApiClient {
|
||||
return this.device;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Device needs to be initialized or reinitialized
|
||||
try {
|
||||
console.log(`Starting Twilio Device initialization for inbox: ${inboxId}`);
|
||||
|
||||
console.log(
|
||||
`Starting Twilio Device initialization for inbox: ${inboxId}`
|
||||
);
|
||||
|
||||
// Import the Twilio Voice SDK
|
||||
let Device;
|
||||
try {
|
||||
@@ -196,96 +217,112 @@ class VoiceAPI extends ApiClient {
|
||||
console.log('✓ Twilio Voice SDK imported successfully');
|
||||
} catch (importError) {
|
||||
console.error('✗ Failed to import Twilio Voice SDK:', importError);
|
||||
throw new Error(`Failed to load Twilio Voice SDK: ${importError.message}`);
|
||||
throw new Error(
|
||||
`Failed to load Twilio Voice SDK: ${importError.message}`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Validate inbox ID
|
||||
if (!inboxId) {
|
||||
throw new Error('Inbox ID is required to initialize the Twilio Device');
|
||||
}
|
||||
|
||||
|
||||
// Step 1: Get a token from the server
|
||||
console.log(`Requesting Twilio token for inbox: ${inboxId}`);
|
||||
let response;
|
||||
try {
|
||||
response = await this.getToken(inboxId);
|
||||
console.log(`✓ Token response received with status: ${response.status}`);
|
||||
console.log(
|
||||
`✓ Token response received with status: ${response.status}`
|
||||
);
|
||||
} catch (tokenError) {
|
||||
console.error('✗ Token request failed:', tokenError);
|
||||
|
||||
|
||||
// Enhanced error handling for token requests
|
||||
if (tokenError.details) {
|
||||
// If we already have extracted details from the error, include those
|
||||
console.error('Token error details:', tokenError.details);
|
||||
throw new Error(`Failed to get token: ${tokenError.message}`);
|
||||
}
|
||||
|
||||
|
||||
// Check for specific HTTP error status codes
|
||||
if (tokenError.response) {
|
||||
const status = tokenError.response.status;
|
||||
const data = tokenError.response.data;
|
||||
|
||||
|
||||
if (status === 401) {
|
||||
throw new Error('Authentication error: Please check your Twilio credentials');
|
||||
throw new Error(
|
||||
'Authentication error: Please check your Twilio credentials'
|
||||
);
|
||||
} else if (status === 403) {
|
||||
throw new Error('Permission denied: You don\'t have access to this inbox');
|
||||
throw new Error(
|
||||
"Permission denied: You don't have access to this inbox"
|
||||
);
|
||||
} else if (status === 404) {
|
||||
throw new Error('Inbox not found or does not have voice capability');
|
||||
throw new Error(
|
||||
'Inbox not found or does not have voice capability'
|
||||
);
|
||||
} else if (status === 500) {
|
||||
throw new Error('Server error: The server encountered an error processing your request. Check your Twilio configuration.');
|
||||
throw new Error(
|
||||
'Server error: The server encountered an error processing your request. Check your Twilio configuration.'
|
||||
);
|
||||
} else if (data && data.error) {
|
||||
throw new Error(`Server error: ${data.error}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
throw new Error(`Failed to get token: ${tokenError.message}`);
|
||||
}
|
||||
|
||||
|
||||
// Validate token response
|
||||
if (!response.data || !response.data.token) {
|
||||
console.error('✗ Invalid token response data:', response.data);
|
||||
|
||||
|
||||
// Check if we have an error message in the response
|
||||
if (response.data && response.data.error) {
|
||||
throw new Error(`Server did not return a valid token: ${response.data.error}`);
|
||||
throw new Error(
|
||||
`Server did not return a valid token: ${response.data.error}`
|
||||
);
|
||||
} else {
|
||||
throw new Error('Server did not return a valid token');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Check for warnings about missing TwiML App SID
|
||||
if (response.data.warning) {
|
||||
console.warn('⚠️ Twilio Voice Warning:', response.data.warning);
|
||||
|
||||
|
||||
if (!response.data.has_twiml_app) {
|
||||
console.error(
|
||||
'🚨 IMPORTANT: Missing TwiML App SID. Browser-based calling requires a ' +
|
||||
'TwiML App configured in Twilio Console. Set the Voice Request URL to: ' +
|
||||
response.data.twiml_endpoint
|
||||
'TwiML App configured in Twilio Console. Set the Voice Request URL to: ' +
|
||||
response.data.twiml_endpoint
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Extract token data
|
||||
const { token, identity, voice_enabled, account_sid } = response.data;
|
||||
|
||||
|
||||
// Log diagnostic information
|
||||
console.log(`✓ Token data received for identity: ${identity}`);
|
||||
console.log(`✓ Voice enabled: ${voice_enabled}`);
|
||||
console.log(`✓ Twilio Account SID available: ${!!account_sid}`);
|
||||
|
||||
|
||||
// Log the TwiML endpoint that will be used
|
||||
if (response.data.twiml_endpoint) {
|
||||
console.log(`✓ TwiML endpoint: ${response.data.twiml_endpoint}`);
|
||||
} else {
|
||||
console.warn('⚠️ No TwiML endpoint found in token response');
|
||||
}
|
||||
|
||||
|
||||
// Check if voice is enabled
|
||||
if (!voice_enabled) {
|
||||
throw new Error('Voice is not enabled for this inbox. Check your Twilio configuration.');
|
||||
throw new Error(
|
||||
'Voice is not enabled for this inbox. Check your Twilio configuration.'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Store the TwiML endpoint URL for later use
|
||||
this.twimlEndpoint = response.data.twiml_endpoint;
|
||||
|
||||
@@ -303,22 +340,24 @@ class VoiceAPI extends ApiClient {
|
||||
// Add the account ID to any calls made by this device
|
||||
appParams: {
|
||||
account_id: response.data.account_id,
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
console.log('Creating Twilio Device with options:', deviceOptions);
|
||||
|
||||
|
||||
try {
|
||||
this.device = new Device(token, deviceOptions);
|
||||
console.log('✓ Twilio Device created successfully');
|
||||
} catch (deviceError) {
|
||||
console.error('✗ Failed to create Twilio Device:', deviceError);
|
||||
throw new Error(`Failed to create Twilio Device: ${deviceError.message}`);
|
||||
throw new Error(
|
||||
`Failed to create Twilio Device: ${deviceError.message}`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Step 3: Set up event listeners with enhanced error handling
|
||||
this._setupDeviceEventListeners(inboxId);
|
||||
|
||||
|
||||
// Step 4: Register the device with Twilio
|
||||
console.log('Registering Twilio Device...');
|
||||
try {
|
||||
@@ -328,64 +367,79 @@ class VoiceAPI extends ApiClient {
|
||||
return this.device;
|
||||
} catch (registerError) {
|
||||
console.error('✗ Failed to register Twilio Device:', registerError);
|
||||
|
||||
|
||||
// Handle specific registration errors
|
||||
if (registerError.message && registerError.message.includes('token')) {
|
||||
throw new Error('Invalid Twilio token. Check your account credentials.');
|
||||
} else if (registerError.message && registerError.message.includes('permission')) {
|
||||
throw new Error('Missing microphone permission. Please allow microphone access.');
|
||||
throw new Error(
|
||||
'Invalid Twilio token. Check your account credentials.'
|
||||
);
|
||||
} else if (
|
||||
registerError.message &&
|
||||
registerError.message.includes('permission')
|
||||
) {
|
||||
throw new Error(
|
||||
'Missing microphone permission. Please allow microphone access.'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
throw new Error(`Failed to register device: ${registerError.message}`);
|
||||
}
|
||||
} catch (error) {
|
||||
// Clear device and initialized flag in case of error
|
||||
this.device = null;
|
||||
this.initialized = false;
|
||||
|
||||
|
||||
console.error('Failed to initialize Twilio Device:', error);
|
||||
|
||||
|
||||
// Create a detailed error with context for debugging
|
||||
const enhancedError = new Error(`Twilio Device initialization failed: ${error.message}`);
|
||||
const enhancedError = new Error(
|
||||
`Twilio Device initialization failed: ${error.message}`
|
||||
);
|
||||
enhancedError.originalError = error;
|
||||
enhancedError.inboxId = inboxId;
|
||||
enhancedError.timestamp = new Date().toISOString();
|
||||
enhancedError.browserInfo = {
|
||||
userAgent: navigator.userAgent,
|
||||
hasGetUserMedia: !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia),
|
||||
hasGetUserMedia: !!(
|
||||
navigator.mediaDevices && navigator.mediaDevices.getUserMedia
|
||||
),
|
||||
};
|
||||
|
||||
|
||||
// Add specific advice for known error cases
|
||||
if (error.message.includes('permission')) {
|
||||
enhancedError.advice = 'Please ensure your browser allows microphone access.';
|
||||
enhancedError.advice =
|
||||
'Please ensure your browser allows microphone access.';
|
||||
} else if (error.message.includes('token')) {
|
||||
enhancedError.advice = 'Check your Twilio credentials in the Voice channel settings.';
|
||||
enhancedError.advice =
|
||||
'Check your Twilio credentials in the Voice channel settings.';
|
||||
} else if (error.message.includes('TwiML')) {
|
||||
enhancedError.advice = 'Set up a valid TwiML app in your Twilio console and configure it in the inbox settings.';
|
||||
enhancedError.advice =
|
||||
'Set up a valid TwiML app in your Twilio console and configure it in the inbox settings.';
|
||||
} else if (error.message.includes('configuration')) {
|
||||
enhancedError.advice = 'Review your Voice inbox configuration to ensure all required fields are completed.';
|
||||
enhancedError.advice =
|
||||
'Review your Voice inbox configuration to ensure all required fields are completed.';
|
||||
}
|
||||
|
||||
|
||||
throw enhancedError;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Helper method to set up device event listeners
|
||||
_setupDeviceEventListeners(inboxId) {
|
||||
if (!this.device) return;
|
||||
|
||||
|
||||
// Remove any existing listeners to prevent duplicates
|
||||
this.device.removeAllListeners();
|
||||
|
||||
|
||||
// Add standard event listeners
|
||||
this.device.on('registered', () => {
|
||||
console.log('✓ Twilio Device registered with Twilio servers');
|
||||
});
|
||||
|
||||
|
||||
this.device.on('unregistered', () => {
|
||||
console.log('⚠️ Twilio Device unregistered from Twilio servers');
|
||||
});
|
||||
|
||||
|
||||
this.device.on('tokenWillExpire', () => {
|
||||
console.log('⚠️ Twilio token is about to expire, refreshing...');
|
||||
this.getToken(inboxId)
|
||||
@@ -401,15 +455,15 @@ class VoiceAPI extends ApiClient {
|
||||
console.error('✗ Error refreshing token:', tokenError);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
this.device.on('incoming', connection => {
|
||||
console.log('📞 Incoming call received via Twilio Device');
|
||||
this.activeConnection = connection;
|
||||
|
||||
|
||||
// Set up connection-specific events
|
||||
this._setupConnectionEventListeners(connection);
|
||||
});
|
||||
|
||||
|
||||
this.device.on('error', error => {
|
||||
// Enhanced error logging with full details
|
||||
const errorDetails = {
|
||||
@@ -417,36 +471,46 @@ class VoiceAPI extends ApiClient {
|
||||
message: error.message,
|
||||
description: error.description || 'No description',
|
||||
twilioErrorObject: error,
|
||||
connectionInfo: this.activeConnection ? {
|
||||
parameters: this.activeConnection.parameters,
|
||||
status: this.activeConnection.status && this.activeConnection.status(),
|
||||
direction: this.activeConnection.direction,
|
||||
} : 'No active connection',
|
||||
connectionInfo: this.activeConnection
|
||||
? {
|
||||
parameters: this.activeConnection.parameters,
|
||||
status:
|
||||
this.activeConnection.status && this.activeConnection.status(),
|
||||
direction: this.activeConnection.direction,
|
||||
}
|
||||
: 'No active connection',
|
||||
deviceState: this.device.state,
|
||||
browserInfo: {
|
||||
userAgent: navigator.userAgent,
|
||||
platform: navigator.platform
|
||||
platform: navigator.platform,
|
||||
},
|
||||
timestamp: new Date().toISOString()
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
|
||||
console.error('❌ DETAILED Twilio Device Error:', errorDetails);
|
||||
|
||||
|
||||
// Provide helpful troubleshooting tips based on error code
|
||||
switch (error.code) {
|
||||
case 31000:
|
||||
console.error('⚠️ Error 31000: General Error. This could be an authentication, configuration, or network issue.');
|
||||
console.error(
|
||||
'⚠️ Error 31000: General Error. This could be an authentication, configuration, or network issue.'
|
||||
);
|
||||
console.error('31000 Error Details:', {
|
||||
sdp: error.sdp || 'No SDP data',
|
||||
callState: error.call ? error.call.state : 'No call state',
|
||||
connectionState: error.connection ? error.connection.state : 'No connection state',
|
||||
peerConnectionState: error.peerConnection ? error.peerConnection.iceConnectionState : 'No ICE state',
|
||||
connectionState: error.connection
|
||||
? error.connection.state
|
||||
: 'No connection state',
|
||||
peerConnectionState: error.peerConnection
|
||||
? error.peerConnection.iceConnectionState
|
||||
: 'No ICE state',
|
||||
message: error.message,
|
||||
twilioError: error,
|
||||
info: error.info || 'No additional info',
|
||||
solution: 'Check Twilio account status, SDP negotiations, and network connectivity'
|
||||
solution:
|
||||
'Check Twilio account status, SDP negotiations, and network connectivity',
|
||||
});
|
||||
|
||||
|
||||
// Create a network diagnostic to check connectivity
|
||||
fetch('https://status.twilio.com/api/v2/status.json')
|
||||
.then(response => response.json())
|
||||
@@ -458,58 +522,76 @@ class VoiceAPI extends ApiClient {
|
||||
});
|
||||
break;
|
||||
case 31002:
|
||||
console.error('⚠️ Error 31002: Permission Denied. Your browser microphone is blocked or unavailable.');
|
||||
console.error(
|
||||
'⚠️ Error 31002: Permission Denied. Your browser microphone is blocked or unavailable.'
|
||||
);
|
||||
break;
|
||||
case 31003:
|
||||
console.error('⚠️ Error 31003: TwiML App Error. Your TwiML application does not exist or is misconfigured.');
|
||||
console.error(
|
||||
'⚠️ Error 31003: TwiML App Error. Your TwiML application does not exist or is misconfigured.'
|
||||
);
|
||||
break;
|
||||
case 31005:
|
||||
console.error('⚠️ Error 31005: Error sent from gateway in HANGUP. This usually means the TwiML endpoint is not reachable or returning invalid TwiML.');
|
||||
console.error(
|
||||
'⚠️ Error 31005: Error sent from gateway in HANGUP. This usually means the TwiML endpoint is not reachable or returning invalid TwiML.'
|
||||
);
|
||||
console.error('Additional details for 31005:', {
|
||||
activeConnection: this.activeConnection ? 'Yes' : 'No',
|
||||
deviceState: this.device ? this.device.state : 'No device',
|
||||
params: this.activeConnection ? this.activeConnection.parameters : 'No params',
|
||||
twimlEndpoint: this.activeConnection && this.activeConnection.parameters ?
|
||||
this.activeConnection.parameters.To : 'Unknown endpoint',
|
||||
hangupReason: error.hangupReason || 'Unknown', // Capture hangup reason
|
||||
params: this.activeConnection
|
||||
? this.activeConnection.parameters
|
||||
: 'No params',
|
||||
twimlEndpoint:
|
||||
this.activeConnection && this.activeConnection.parameters
|
||||
? this.activeConnection.parameters.To
|
||||
: 'Unknown endpoint',
|
||||
hangupReason: error.hangupReason || 'Unknown', // Capture hangup reason
|
||||
message: error.message,
|
||||
description: error.description,
|
||||
customMessage: error.customMessage,
|
||||
originalError: error.originalError ? JSON.stringify(error.originalError) : 'None'
|
||||
originalError: error.originalError
|
||||
? JSON.stringify(error.originalError)
|
||||
: 'None',
|
||||
});
|
||||
break;
|
||||
case 31008:
|
||||
console.error('⚠️ Error 31008: Connection Error. The call could not be established.');
|
||||
console.error(
|
||||
'⚠️ Error 31008: Connection Error. The call could not be established.'
|
||||
);
|
||||
break;
|
||||
case 31204:
|
||||
console.error('⚠️ Error 31204: ICE Connection Failed. WebRTC connection failure, check firewall settings.');
|
||||
console.error(
|
||||
'⚠️ Error 31204: ICE Connection Failed. WebRTC connection failure, check firewall settings.'
|
||||
);
|
||||
break;
|
||||
default:
|
||||
console.error(`⚠️ Unspecified error with code ${error.code}: ${error.message}`);
|
||||
console.error(
|
||||
`⚠️ Unspecified error with code ${error.code}: ${error.message}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
this.device.on('connect', connection => {
|
||||
console.log('📞 Call connected');
|
||||
this.activeConnection = connection;
|
||||
this._setupConnectionEventListeners(connection);
|
||||
});
|
||||
|
||||
|
||||
this.device.on('disconnect', () => {
|
||||
console.log('📞 Call disconnected');
|
||||
this.activeConnection = null;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Set up event listeners for the active connection with enhanced audio diagnostic logging
|
||||
_setupConnectionEventListeners(connection) {
|
||||
if (!connection) return;
|
||||
|
||||
|
||||
// Add advanced audio debug data
|
||||
const getAudioDiagnostics = () => {
|
||||
const audioContext = window.AudioContext || window.webkitAudioContext;
|
||||
let audioInfo = { supported: !!audioContext };
|
||||
|
||||
|
||||
try {
|
||||
if (audioContext) {
|
||||
const context = new audioContext();
|
||||
@@ -522,45 +604,49 @@ class VoiceAPI extends ApiClient {
|
||||
destination: {
|
||||
maxChannelCount: context.destination.maxChannelCount,
|
||||
numberOfInputs: context.destination.numberOfInputs,
|
||||
numberOfOutputs: context.destination.numberOfOutputs
|
||||
}
|
||||
numberOfOutputs: context.destination.numberOfOutputs,
|
||||
},
|
||||
};
|
||||
context.close();
|
||||
}
|
||||
} catch (e) {
|
||||
audioInfo.error = e.message;
|
||||
}
|
||||
|
||||
|
||||
// Check if microphone is accessible
|
||||
let microphoneInfo = { detected: false, active: false, tracks: [] };
|
||||
if (window.activeAudioStream) {
|
||||
const tracks = window.activeAudioStream.getAudioTracks();
|
||||
microphoneInfo = {
|
||||
detected: true,
|
||||
active: tracks.some(track => track.enabled && track.readyState === 'live'),
|
||||
active: tracks.some(
|
||||
track => track.enabled && track.readyState === 'live'
|
||||
),
|
||||
tracks: tracks.map(track => ({
|
||||
id: track.id,
|
||||
label: track.label,
|
||||
enabled: track.enabled,
|
||||
muted: track.muted,
|
||||
readyState: track.readyState,
|
||||
constraints: track.getConstraints()
|
||||
}))
|
||||
constraints: track.getConstraints(),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
audioContext: audioInfo,
|
||||
|
||||
return {
|
||||
audioContext: audioInfo,
|
||||
microphone: microphoneInfo,
|
||||
speakersMuted: typeof window.speechSynthesis !== 'undefined' ?
|
||||
window.speechSynthesis.speaking === false : 'unknown'
|
||||
speakersMuted:
|
||||
typeof window.speechSynthesis !== 'undefined'
|
||||
? window.speechSynthesis.speaking === false
|
||||
: 'unknown',
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
connection.on('error', error => {
|
||||
// Significantly enhanced connection error logging with audio diagnostics
|
||||
const diagnostics = getAudioDiagnostics();
|
||||
|
||||
|
||||
const connectionErrorDetails = {
|
||||
code: error.code,
|
||||
message: error.message,
|
||||
@@ -577,80 +663,93 @@ class VoiceAPI extends ApiClient {
|
||||
audioDiagnostics: diagnostics,
|
||||
// Browser media permissions
|
||||
mediaPermissions: {
|
||||
hasGetUserMedia: !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia),
|
||||
hasGetUserMedia: !!(
|
||||
navigator.mediaDevices && navigator.mediaDevices.getUserMedia
|
||||
),
|
||||
activeAudioStream: !!window.activeAudioStream,
|
||||
activeAudioTracks: window.activeAudioStream ?
|
||||
window.activeAudioStream.getAudioTracks().length : 0
|
||||
}
|
||||
activeAudioTracks: window.activeAudioStream
|
||||
? window.activeAudioStream.getAudioTracks().length
|
||||
: 0,
|
||||
},
|
||||
};
|
||||
|
||||
console.error('❌ DETAILED Connection Error with Audio Diagnostics:', connectionErrorDetails);
|
||||
|
||||
console.error(
|
||||
'❌ DETAILED Connection Error with Audio Diagnostics:',
|
||||
connectionErrorDetails
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
connection.on('mute', isMuted => {
|
||||
console.log(`📞 Call ${isMuted ? 'muted' : 'unmuted'}`);
|
||||
});
|
||||
|
||||
|
||||
connection.on('accept', () => {
|
||||
// Enhanced logging for accept event with audio diagnostics
|
||||
const diagnostics = getAudioDiagnostics();
|
||||
|
||||
|
||||
console.log('📞 Call accepted with audio diagnostics:', {
|
||||
connectionParameters: connection.parameters,
|
||||
status: connection.status && connection.status(),
|
||||
audioDiagnostics: diagnostics,
|
||||
activeAudioStream: window.activeAudioStream ? {
|
||||
active: window.activeAudioStream.active,
|
||||
id: window.activeAudioStream.id,
|
||||
trackCount: window.activeAudioStream.getTracks().length
|
||||
} : 'No active stream'
|
||||
activeAudioStream: window.activeAudioStream
|
||||
? {
|
||||
active: window.activeAudioStream.active,
|
||||
id: window.activeAudioStream.id,
|
||||
trackCount: window.activeAudioStream.getTracks().length,
|
||||
}
|
||||
: 'No active stream',
|
||||
});
|
||||
|
||||
|
||||
// AUDIO HEALTH CHECK AFTER CONNECTION
|
||||
setTimeout(() => {
|
||||
console.log('🔊 AUDIO HEALTH CHECK:', {
|
||||
connectionActive: this.activeConnection === connection,
|
||||
connectionState: connection.status && connection.status(),
|
||||
audioTracks: window.activeAudioStream ?
|
||||
window.activeAudioStream.getAudioTracks().map(track => ({
|
||||
label: track.label,
|
||||
enabled: track.enabled,
|
||||
readyState: track.readyState,
|
||||
muted: track.muted
|
||||
})) : 'No active stream',
|
||||
audioTracks: window.activeAudioStream
|
||||
? window.activeAudioStream.getAudioTracks().map(track => ({
|
||||
label: track.label,
|
||||
enabled: track.enabled,
|
||||
readyState: track.readyState,
|
||||
muted: track.muted,
|
||||
}))
|
||||
: 'No active stream',
|
||||
// Device state after 5 seconds
|
||||
deviceState: this.device ? this.device.state : 'No device'
|
||||
deviceState: this.device ? this.device.state : 'No device',
|
||||
});
|
||||
}, 5000);
|
||||
});
|
||||
|
||||
|
||||
connection.on('disconnect', () => {
|
||||
console.log('📞 Call disconnected', {
|
||||
disconnectCause: connection.parameters ? connection.parameters.DisconnectCause : 'Unknown',
|
||||
disconnectCause: connection.parameters
|
||||
? connection.parameters.DisconnectCause
|
||||
: 'Unknown',
|
||||
finalStatus: connection.status && connection.status(),
|
||||
audioDiagnostics: getAudioDiagnostics()
|
||||
audioDiagnostics: getAudioDiagnostics(),
|
||||
});
|
||||
this.activeConnection = null;
|
||||
});
|
||||
|
||||
|
||||
connection.on('reject', () => {
|
||||
console.log('📞 Call rejected', {
|
||||
rejectCause: connection.parameters ? connection.parameters.DisconnectCause : 'Unknown',
|
||||
audioDiagnostics: getAudioDiagnostics()
|
||||
rejectCause: connection.parameters
|
||||
? connection.parameters.DisconnectCause
|
||||
: 'Unknown',
|
||||
audioDiagnostics: getAudioDiagnostics(),
|
||||
});
|
||||
this.activeConnection = null;
|
||||
});
|
||||
|
||||
|
||||
// Additional event for warning messages
|
||||
connection.on('warning', warning => {
|
||||
console.warn('⚠️ Connection Warning:', warning);
|
||||
});
|
||||
|
||||
|
||||
// Listen for TwiML processing events
|
||||
connection.on('twiml-processing', twiml => {
|
||||
console.log('📄 Processing TwiML:', twiml);
|
||||
});
|
||||
|
||||
|
||||
// Enhanced audio events for debugging
|
||||
if (typeof connection.on === 'function') {
|
||||
try {
|
||||
@@ -658,10 +757,12 @@ class VoiceAPI extends ApiClient {
|
||||
connection.on('volume', (inputVolume, outputVolume) => {
|
||||
// Log only significant volume changes to avoid console spam
|
||||
if (Math.abs(inputVolume) > 50 || Math.abs(outputVolume) > 50) {
|
||||
console.log(`🔊 Volume change - Input: ${inputVolume}, Output: ${outputVolume}`);
|
||||
console.log(
|
||||
`🔊 Volume change - Input: ${inputVolume}, Output: ${outputVolume}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Check for media stream events if supported
|
||||
if (typeof connection.getRemoteStream === 'function') {
|
||||
const remoteStream = connection.getRemoteStream();
|
||||
@@ -672,8 +773,8 @@ class VoiceAPI extends ApiClient {
|
||||
tracks: remoteStream.getTracks().map(t => ({
|
||||
kind: t.kind,
|
||||
enabled: t.enabled,
|
||||
readyState: t.readyState
|
||||
}))
|
||||
readyState: t.readyState,
|
||||
})),
|
||||
});
|
||||
} else {
|
||||
console.warn('⚠️ No remote audio stream available');
|
||||
@@ -684,17 +785,17 @@ class VoiceAPI extends ApiClient {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Make a call using the Twilio Client
|
||||
makeClientCall(params) {
|
||||
if (!this.device || !this.initialized) {
|
||||
throw new Error('Twilio Device not initialized');
|
||||
}
|
||||
|
||||
|
||||
this.activeConnection = this.device.connect(params);
|
||||
return this.activeConnection;
|
||||
}
|
||||
|
||||
|
||||
// Join a conference call using the Twilio Client
|
||||
joinClientCall(conferenceParams) {
|
||||
if (!this.device || !this.initialized) {
|
||||
@@ -716,7 +817,7 @@ class VoiceAPI extends ApiClient {
|
||||
|
||||
// Additional params for our server
|
||||
account_id: conferenceParams.account_id,
|
||||
is_agent: 'true'
|
||||
is_agent: 'true',
|
||||
};
|
||||
|
||||
// Check To parameter exists - fail if missing
|
||||
@@ -726,38 +827,48 @@ class VoiceAPI extends ApiClient {
|
||||
|
||||
// Make sure 'To' is explicitly a string
|
||||
const stringifiedTo = String(params.To);
|
||||
console.log('🎯 CRITICAL CONFERENCE CONNECTION: Connecting agent to conference with To=', stringifiedTo);
|
||||
console.log(
|
||||
'🎯 CRITICAL CONFERENCE CONNECTION: Connecting agent to conference with To=',
|
||||
stringifiedTo
|
||||
);
|
||||
|
||||
// Follow Twilio documentation format - params should be nested under 'params' property
|
||||
console.log('🎯 TRYING CONNECTION: Using documented format with params property');
|
||||
console.log(
|
||||
'🎯 TRYING CONNECTION: Using documented format with params property'
|
||||
);
|
||||
|
||||
// Just use the minimal required parameters
|
||||
const connection = this.device.connect({
|
||||
params: {
|
||||
To: stringifiedTo, // Conference ID
|
||||
is_agent: 'true' // Flag to indicate agent is joining
|
||||
}
|
||||
To: stringifiedTo, // Conference ID
|
||||
is_agent: 'true', // Flag to indicate agent is joining
|
||||
},
|
||||
});
|
||||
|
||||
console.log('🎯 CONFERENCE CONNECTION RESULT:', connection ? 'Success' : 'Failed');
|
||||
console.log(
|
||||
'🎯 CONFERENCE CONNECTION RESULT:',
|
||||
connection ? 'Success' : 'Failed'
|
||||
);
|
||||
this.activeConnection = connection;
|
||||
|
||||
if (connection && typeof connection.then === 'function') {
|
||||
// It's a Promise - newer Twilio SDK version
|
||||
connection.then(resolvedConnection => {
|
||||
this.activeConnection = resolvedConnection;
|
||||
try {
|
||||
if (typeof resolvedConnection.on === 'function') {
|
||||
resolvedConnection.on('accept', () => {
|
||||
// Connection accepted
|
||||
});
|
||||
connection
|
||||
.then(resolvedConnection => {
|
||||
this.activeConnection = resolvedConnection;
|
||||
try {
|
||||
if (typeof resolvedConnection.on === 'function') {
|
||||
resolvedConnection.on('accept', () => {
|
||||
// Connection accepted
|
||||
});
|
||||
}
|
||||
} catch (listenerError) {
|
||||
// Could not add listeners to Promise connection
|
||||
}
|
||||
} catch (listenerError) {
|
||||
// Could not add listeners to Promise connection
|
||||
}
|
||||
}).catch(connError => {
|
||||
// WebRTC Promise connection error
|
||||
});
|
||||
})
|
||||
.catch(connError => {
|
||||
// WebRTC Promise connection error
|
||||
});
|
||||
} else {
|
||||
// It's a synchronous connection - older Twilio SDK
|
||||
}
|
||||
@@ -772,9 +883,9 @@ class VoiceAPI extends ApiClient {
|
||||
if (!this.device) {
|
||||
return 'not_initialized';
|
||||
}
|
||||
|
||||
|
||||
const deviceState = this.device.state;
|
||||
|
||||
|
||||
// Append a recommended action based on the state
|
||||
switch (deviceState) {
|
||||
case 'registered':
|
||||
@@ -791,7 +902,7 @@ class VoiceAPI extends ApiClient {
|
||||
return deviceState;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Get comprehensive diagnostic information about the device and connection
|
||||
getDiagnosticInfo() {
|
||||
const browserInfo = {
|
||||
@@ -799,42 +910,49 @@ class VoiceAPI extends ApiClient {
|
||||
platform: navigator.platform,
|
||||
vendor: navigator.vendor,
|
||||
hasMediaDevices: !!navigator.mediaDevices,
|
||||
hasGetUserMedia: !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia),
|
||||
hasGetUserMedia: !!(
|
||||
navigator.mediaDevices && navigator.mediaDevices.getUserMedia
|
||||
),
|
||||
};
|
||||
|
||||
const deviceInfo = this.device ? {
|
||||
state: this.device.state,
|
||||
isInitialized: this.initialized,
|
||||
capabilities: this.device.capabilities || {},
|
||||
isBusy: this.device.isBusy || false,
|
||||
audio: {
|
||||
isAudioSelectionSupported: this.device.isAudioSelectionSupported || false
|
||||
}
|
||||
} : { state: 'not_initialized' };
|
||||
|
||||
const connectionInfo = this.activeConnection ? {
|
||||
status: this.activeConnection.status(),
|
||||
isMuted: this.activeConnection.isMuted(),
|
||||
direction: this.activeConnection.direction,
|
||||
parameters: this.activeConnection.parameters,
|
||||
} : { status: 'no_connection' };
|
||||
|
||||
|
||||
const deviceInfo = this.device
|
||||
? {
|
||||
state: this.device.state,
|
||||
isInitialized: this.initialized,
|
||||
capabilities: this.device.capabilities || {},
|
||||
isBusy: this.device.isBusy || false,
|
||||
audio: {
|
||||
isAudioSelectionSupported:
|
||||
this.device.isAudioSelectionSupported || false,
|
||||
},
|
||||
}
|
||||
: { state: 'not_initialized' };
|
||||
|
||||
const connectionInfo = this.activeConnection
|
||||
? {
|
||||
status: this.activeConnection.status(),
|
||||
isMuted: this.activeConnection.isMuted(),
|
||||
direction: this.activeConnection.direction,
|
||||
parameters: this.activeConnection.parameters,
|
||||
}
|
||||
: { status: 'no_connection' };
|
||||
|
||||
return {
|
||||
timestamp: new Date().toISOString(),
|
||||
browser: browserInfo,
|
||||
device: deviceInfo,
|
||||
connection: connectionInfo
|
||||
connection: connectionInfo,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// Get the status of the active connection
|
||||
getConnectionStatus() {
|
||||
if (!this.activeConnection) {
|
||||
return 'no_connection';
|
||||
}
|
||||
|
||||
|
||||
const status = this.activeConnection.status();
|
||||
|
||||
|
||||
// Translate connection statuses to more user-friendly terms
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
|
||||
Reference in New Issue
Block a user