This is the Changelog for v2 of the Voice JavaScript SDK. Click here to see the v1 Changelog.
If you are upgrading to version 2.3.0 or later and have firewall rules or network configuration that blocks any unknown traffic by default, you need to update your configuration to allow connections to the new DNS names and IP addresses. Please refer to this changelog for more details.
device.connect()
without waiting for the promise to get resolved, then calling
device.audio.setInputDevice()
right away results in an
AcquisitionFailedError
.
The Call Message Events, originally released in 2.2.0, has been promoted to GA. This release includes the following breaking changes.
Call.MessageType
enum to
string
.
31209
is raised if the payload size of a Call Message Event exceeds the authorized limit. With this release,
31212
is raised instead.
PreflightTest
throws an error when
RTCIceCandidateStatsReport
is not available. Thanks to @phi-line for your
contribution
.
AcquisitionFailedError
is raised when making a call while a
setInputDevice
invocation is still in progress. The following snippet will reproduce the issue in versions <2.11.2:
_10// Call setInputDevice without waiting for it to resolve e.g. using 'await'_10device.audio.setInputDevice(id);_10_10// Calling device.connect immediately raises an AcquisitionFailedError error_10device.connect(...);
In Manifest V2, Chrome Extensions had the capability to run the Voice JS SDK in the background for making calls. However, with the introduction of Manifest V3, running the Voice JS SDK in the background is now possible only through service workers. Service workers lack access to certain features, such as DOM, getUserMedia
, and audio playback, making it impossible to make calls with previous versions of the SDK.
This new release enables the SDK to run in a service worker context, allowing it to listen for incoming calls or initiate outgoing calls. Once a call object is created, it can be forwarded to an offscreen document, where the SDK gains access to all necessary APIs to fully establish and interact with the call. For implementation details, refer to our example.
Previous versions of the SDK supported simultaneous outgoing and incoming calls using different identities. If an incoming call was received while the Device
with the same identity was busy, the active call had to be disconnected before accepting the incoming call. With this SDK release, multiple incoming calls for the same identity can be accepted, muted, or put on hold, without disconnecting any existing active calls. This is achieved by forwarding the incoming call to a different Device
instance. Below are the new APIs and an example for more details.
_21// Create a Device instance that handles receiving all incoming calls for the same identity._21const receiverDevice = new Device(token, options);_21await receiverDevice.register();_21_21receiverDevice.on('incoming', (call) => {_21 // Forward this call to a new Device instance using the call.connectToken string._21 forwardCall(call.connectToken);_21});_21_21// The forwardCall function may look something like the following._21async function forwardCall(connectToken) {_21 // For each incoming call, create a new Device instance for interaction_21 // without affecting other calls._21 // IMPORTANT: The token for this new Device needs to have the same identity_21 // as the token used in the receiverDevice._21 const device = new Device(token, options);_21 const call = await device.connect({ connectToken });_21_21 // Destroy the Device after the call is completed_21 call.on('disconnect', () => device.destroy());_21}
device.register()
does not return a promise rejection when the WebSocket fails to connect. Thank you @kamalbennani for your
contribution
.
Device.Options.logLevel
is only accepting a
number
type. With this release,
strings
are now also allowed. See
Device.Options.logLevel
for a list of possible values.
call.mute()
does not have an effect while the
call.status()
is either
ringing
or
connecting
. Thank you @zyzmoz for your
contribution
.
The SDK now includes Audio Processor APIs, enabling access to raw audio input and the ability to modify audio data before sending it to Twilio. With this new feature, the following use cases can now be achieved on the client side:
Please visit this page for more details about the Audio Processor APIs.
Added a new feature flag enableImprovedSignalingErrorPrecision
to enhance the precision of errors emitted by Device
and Call
objects.
_10const token = ...;_10const device = new Device(token, {_10 enableImprovedSignalingErrorPrecision: true,_10});
The default value of this option is false
.
When this flag is enabled, some errors that would have been described with a generic error code are now described with a more precise error code. With this feature, the following errors now have their own error codes. Please see this page for more details about each error.
Device Error Changes
_10const device = new Device(token, {_10 enableImprovedSignalingErrorPrecision: true,_10});_10device.on('error', (deviceError) => {_10 // the following table describes how deviceError will change with this feature flag_10});
Device Error Name | Device Error Code with Feature Flag Enabled | Device Error Code with Feature Flag Disabled |
---|---|---|
GeneralErrors.ApplicationNotFoundError | 31001 | 53000 |
GeneralErrors.ConnectionDeclinedError | 31002 | 53000 |
GeneralErrors.ConnectionTimeoutError | 31003 | 53000 |
MalformedRequestErrors.MissingParameterArrayError | 31101 | 53000 |
MalformedRequestErrors.AuthorizationTokenMissingError | 31102 | 53000 |
MalformedRequestErrors.MaxParameterLengthExceededError | 31103 | 53000 |
MalformedRequestErrors.InvalidBridgeTokenError | 31104 | 53000 |
MalformedRequestErrors.InvalidClientNameError | 31105 | 53000 |
MalformedRequestErrors.ReconnectParameterInvalidError | 31107 | 53000 |
SignatureValidationErrors.AccessTokenSignatureValidationFailed | 31202 | 53000 |
AuthorizationErrors.NoValidAccountError | 31203 | 53000 |
AuthorizationErrors.JWTTokenExpirationTooLongError | 31207 | 53000 |
ClientErrors.NotFound | 31404 | 53000 |
ClientErrors.TemporarilyUnavilable | 31480 | 53000 |
ClientErrors.BusyHere | 31486 | 53000 |
SIPServerErrors.Decline | 31603 | 53000 |
Call Error Changes
_10const device = new Device(token, {_10 enableImprovedSignalingErrorPrecision: true,_10});_10const call = device.connect(...);_10call.on('error', (callError) => {_10 // the following table describes how callError will change with this feature flag_10});
Call Error Name | Call Error Code with Feature Flag Enabled | Call Error Code with Feature Flag Disabled |
---|---|---|
GeneralErrors.ConnectionDeclinedError | 31002 | 31005 |
AuthorizationErrors.InvalidJWTTokenError | 31204 | 31005 |
AuthorizationErrors.JWTTokenExpiredError | 31205 | 31005 |
IMPORTANT: If your application logic currently relies on listening to the
generic error code 53000
or 31005
, and you opt into enabling the feature
flag, then your application logic needs to be updated to anticipate the new error code when any of the above errors happen.
Updated November 1, 2023
We have identified an issue on Chromium-based browsers running on MacOS 14 (Sonoma) where the audio deteriorates during a call. This issue happens due to the excessive calls to MediaDevices: enumerateDevices() API. With this release, the SDK calls this API only when necessary to avoid audio deterioration.
call.on('ringing', handler)
call.on('warning', handler)
call.on('warning-cleared', handler)
device.on('destroyed', handler)
call.sendMessage()
API throws an error if the SDK is imported as an
ECMAScript Module (ESM)
using the
@twilio/voice-sdk/esm
path.
Currently, the SDK is imported as a CommonJS Module (CJS) using the root path @twilio/voice-sdk
. With this release, the SDK contains an experimental feature that allows it to be imported as an ECMAScript Module (ESM) using the @twilio/voice-sdk/esm
path. As this is an experimental feature, some frameworks using bundlers like Vite
and Rollup
may not work. Full support for ESM will be available in a future release and will become the default import behavior of the SDK.
Example:
_10import { Device } from '@twilio/voice-sdk/esm';
npm audit
.
device.updateOptions
would reset the
device.audio._enabledSounds
state.
_10const device = new Device(token, {_10 sounds: {_10 dtmf8: 'http://mysite.com/8_button.mp3',_10 // Other custom sounds_10 },_10 // Other options_10});
--legacy-peer-deps
flag.
ws
package has been moved to
devDependencies
.
xmlhttprequest
npm package.
Updated: This is now GA as of December 14, 2023
The SDK now allows you to override WebRTC APIs using the following options and events. If your environment supports WebRTC redirection, such as Citrix HDX's WebRTC redirection technologies, your application can use this new beta feature for improved audio quality in those environments.
RTCIceCandidateStats
-
ip
and
deleted
.
TypeError
is thrown after rejecting a call then invoking
updateToken
.
PeerConnection
object is not properly disposed.
device.audio.disconnect
,
device.audio.incoming
and
device.audio.outgoing
do not have the correct type definitions.
deviceinfochange
event is being emitted indefinitely, causing high cpu usage.
This release includes updated DNS names for Twilio Edge Locations. The Voice JS SDK uses these Edge Locations to connect to Twilio's infrastructure via the parameter Device.Options.edge
. The current usage of this parameter does not change as the SDK automatically maps the edge value to the new DNS names.
Additionally, you need to update your Content Security Policies (CSP) if you have it enabled for your application. You also need to update your network configuration such as firewalls, if necessary, to allow connections to the new DNS names and IP addresses.
The SDK can now send and receive custom messages to and from Twilio's backend via the following new Call
APIs.
Please visit this page for more details about this feature. Additionally, please see the following for more information on how to send and receive messages on the server.
NOTE: This feature should not be used with PII.
Example
_20const device = new Device(token, options);_20_20const setupCallHandlers = call => {_20 call.on('messageReceived', message => messageReceivedHandler(message));_20 call.on('messageSent', message => messageSentHandler(message));_20};_20_20// For outgoing calls_20const call = await device.connect();_20setupCallHandlers(call);_20_20// For incoming calls_20device.on('incoming', call => setupCallHandlers(call));_20await device.register();_20_20// For sending a message_20const eventSid = call.sendMessage({_20 content: { foo: 'foo' },_20 messageType: Call.MessageType.UserDefinedMessage,_20});
device.updateOptions
.
The Voice JavaScript SDK now fully supports Call reconnection. If the media connection or signaling websocket is lost, the SDK is able to attempt to reconnect the Call. A Call can now potentially be recovered up to 30 seconds after a media or signaling connection loss.
The Twilio.Device
will emit a 'reconnecting' event when a connectivity loss occurs, and a 'reconnected' event upon successful reconnection.
There exists a limitation such that Signaling Reconnection and Edge Fallback are mutually exclusive. To opt-in to the Signaling Reconnection feature, a new option can be passed to the SDK: maxCallSignalingTimeoutMs
. If this value is not present in the options object passed to the Device
constructor, the default value will be 0
. Reconnection can only happen with an up-to-date AccessToken.
Customers relying on edge fallback, along with a small subset of customers using the 'roaming'
edge, will not automatically benefit from this feature without additional configuration. Go to the Edge Locations page for more information.
The Voice JavaScript SDK now provides two additional features to help keep your AccessTokens up to date:
'tokenWillExpire'
event, which will be emitted by the Twilio.Device before its associated AccessToken is set to expire. By default, it will be emitted 10 seconds before the AccessToken's expiration.
DeviceOptions.tokenRefreshMs
property that can configure the timing of the
'tokenWillExpire'
event.
You can use these new features in conjunction with the device.updateToken()
method to automatically keep an AccessToken up to date.
In the following example, the 'tokenWillExpire'
event will be emitted 30 seconds (3000 milliseconds) before the AccessToken is set to expire, and the event listener for the 'tokenWillExpire'
event will retrieve a new AccessToken and update the Device's AccessToken with the device.updateToken()
method.
_10const device = new Device(token, {_10 // 'tokenWillExpire' event will be emitted 30 seconds before the AccessToken expires_10 tokenRefreshMs: 30000,_10});_10_10device.on('tokenWillExpire', () => {_10 return getTokenViaAjax().then(token => dev.updateToken(token));_10});
The Twilio Voice JavaScript SDK now supports Twilio Regions.
If you are part of the Twilio Regions Pilot and wish to specify a home region when using the Voice JavaScript SDK, you will need to:
Twilio.Device
.
Below is an example of how you would use the Node.js Helper Library to create AccessTokens for the Voice JavaScript SDK for a Region.
_16const accessToken = const accessToken = new twilio.jwt.AccessToken(_16 credentials.accountSid,_16 credentials.apiKeySid,_16 credentials.apiKeySecret, {_16 identity,_16 ttl,_16 region: 'au1',_16 },_16);_16_16const grant = new VoiceGrant({_16 outgoingApplicationSid: credentials.twimlAppSid,_16 incomingAllow: true,_16});_16_16accessToken.addGrant(grant);
Note: The API Key and Secret above must be created within the au1
region. It's recommended that the TwiML App used in the Voice Grant is also created in the same Region.
The example below shows how to pass the au1
-related edge location to the Twilio.Device
constructor.
_10const device = new Device(accessToken, {_10 edge: 'sydney',_10});
The new Twilio.Device.home
accessor will return a string value of the home region of the device instance, given that it successfully connected with Twilio.
Existing EU customers can now migrate their Voice use-cases to the data center in Ireland to establish data residency within the region. In addition, new customers may now select Ireland as their region of choice for Voice related use cases. There is no additional cost to use the new data center in Ireland. To learn more about Regional Voice in Ireland, check out our blog post or head over to our Global Infrastructure docs to get started.
The Voice JavaScript SDK now exposes a Twilio.Device.identity
accessor.
Given that a Twilio.Device
has registered successfully with Twilio, the Twilio.Device.identity
accessor will return a read-only string containing the identity
that was passed to the AccessToken used to instantiate the Twilio.Device
.
ws
version to fix a potential security vulnerability
Twilio.Device.destroy()
Device must now be instantiated before it can be used. Calling Device.setup()
will no longer
work; instead, a new Device
must be instantiated via new Device(token, options?)
.
As Connection is an overloaded and ambiguous term, the class has been renamed Call to better indicate what the object represents and be more consistent with Mobile SDKs and our REST APIs.
Device.setup()
has been removed, and new Device(...)
will not automatically begin
connecting to signaling. There is no need to listen for Device.on('ready')
. Instead,
the signaling connection will automatically be acquired in one of two scenarios:
Device.connect()
, creating an outbound Call. In this case,
the state of the signaling connection will be represented in the Call.
Device.register()
, which will register the client to listen
for incoming calls at the identity specified in the AccessToken.
As long as outgoing calls are expected to be made, or incoming calls are expected to be received,
the token supplied to Device
should be fresh and not expired. This can be done by setting a
timer in the application to call updateToken
with the new token shortly before the prior
token expires. This is important, because signaling connection is lazy loaded and will fail if
the token is not valid at the time of creation.
Example:
_10const TTL = 600000; // Assuming our endpoint issues tokens for 600 seconds (10 minutes)_10const REFRESH_TIMER = TTL - 30000; // We update our token 30 seconds before expiration;_10const interval = setInterval(async () => {_10 const newToken = await getNewTokenViaAjax();_10 device.updateToken(newToken);_10}, REFRESH_TIMER);
The Device states have changed. The states were: [Ready, Busy, Offline]
. These
have been changed to more accurately and clearly represent the states of the
Device. There are two changes to Device state:
[Registered, Registering, Unregistered, Destroyed]
. This
removes the idea of "Busy" from the state, as technically the Device can have an active
Call whether it is registered or not, depending on the use case. The Device will always
start as
Unregistered
. In this state, it can still make outbound Calls. Once
Device.register()
has been called, this state will change to
Registering
and finally
Registered
. If
Device.unregister()
is called the state will revert to
Unregistered
. If the signaling
connection is lost, the state will transition to
Registering
or `Unregistered' depending
on whether or not the connection can be re-established.
The destroyed
state represents a Device
that has been "destroyed" by calling
Device.destroy
. The device should be considered unusable at this point and a
new one should be constructed for further use.
Device.isBusy
. This is a very basic
shortcut for the logic
return !!device.activeConnection
.
The events emitted by the Device
are represented by the Device.EventName
enum and represent the new Device states:
_10export enum EventName {_10 Destroyed = 'destroyed',_10 Error = 'error',_10 Incoming = 'incoming',_10 Unregistered = 'unregistered',_10 Registering = 'registering',_10 Registered = 'registered',_10}
Note that unregistered
, registering
, and registered
have replaced
offline
and ready
. Although frequently used to represent connected or disconnected,
ready
and offline
actually were meant to represent registered
and unregistered
,
which was quite ambiguous and a primary reason for the change.
When the device is destroyed using Device.destroy
, a "destroyed"
event will
be emitted.
The construction signature and usage of Device
has changed. These are the new API signatures:
_22/**_22 * Create a new Device. This is synchronous and will not open a signaling socket immediately._22 */_22new Device(token: string, options?: Device.Options): Device;_22_22/**_22 * Promise resolves when the Device has successfully registered._22 * Replaces Device.registerPresence()_22 * Can reject if the Device is unusable, i.e. "destroyed"._22 */_22async Device.register(): Promise<void>;_22/**_22 * Promise resolves when the Device has successfully unregistered._22 * Replaces Device.unregisterPresence()_22 * Can reject if the Device is unusable, i.e. "destroyed"._22 */_22async Device.unregister(): Promise<void>;_22/**_22 * Promise resolves when signaling is established and a Call has been created._22 * Can reject if the Device is unusable, i.e. "destroyed"._22 */_22async Device.connect(options?: Device.ConnectOptions): Promise<Call>;
_10const device = new Device(token, { edge: 'ashburn' });_10_10device.on(Device.EventName.Incoming, call => { /* use `call` here */ });_10await device.register();
_10const device = new Device(token, { edge: 'ashburn' });_10const call = await device.connect({ To: 'alice' });
The arguments for Device.connect()
and Call.accept()
have been standardized
to the following options objects:
_11interface Call.AcceptOptions {_11 /**_11 * An RTCConfiguration to pass to the RTCPeerConnection constructor._11 */_11 rtcConfiguration?: RTCConfiguration;_11_11 /**_11 * MediaStreamConstraints to pass to getUserMedia when making or accepting a Call._11 */_11 rtcConstraints?: MediaStreamConstraints;_11}
_10interface Device.ConnectOptions extends Call.AcceptOptions {_10 /**_10 * A flat object containing key\:value pairs to be sent to the TwiML app._10 */_10 params?: Record<string, string>;_10}
Note that these now take a MediaStreamConstraints rather than just the audio constraints. For example:
_10device.connect({ To: 'client:alice' }, { deviceId: 'default' });
might be re-written as:
_10device.connect({_10 params: { To: 'client:alice' },_10 rtcConstraints: { audio: { deviceId: 'default' } },_10});
For backward compatibility, the new error format was attached to the old format under error.twilioError
:
_10class oldError extends Error {_10 //..._10 code: number;_10 message: string;_10 twilioError: TwilioError;_10}
The new Error format is:
_41class TwilioError extends Error {_41 /**_41 * A list of possible causes for the Error._41 */_41 causes: string[];_41_41 /**_41 * The numerical code associated with this Error._41 */_41 code: number;_41_41 /**_41 * A description of what the Error means._41 */_41 description: string;_41_41 /**_41 * An explanation of when the Error may be observed._41 */_41 explanation: string;_41_41 /**_41 * Any further information discovered and passed along at run-time._41 */_41 message: string;_41_41 /**_41 * The name of this Error._41 */_41 name: string;_41_41 /**_41 * The original Error received from the external system, if any._41 */_41 originalError?: Error;_41_41 /**_41 * A list of potential solutions for the Error._41 */_41 solutions: string[];_41}
With the transition, the following error codes have changed:
Previously, Device.setup()
could only be used the set options once. Now, we've added
Device.updateOptions(options: Device.Options)
which will allow changing
the Device options without instantiating a new Device. Note that the edge
cannot be changed
during an active Call.
Example usage:
_10const options = { edge: 'ashburn' };_10const device = new Device(token, options);_10_10// Later..._10_10device.updateOptions({ allowIncomingWhileBusy: true });
The resulting (non-default) options would now be:
_10{_10 allowIncomingWhileBusy: true,_10 edge: 'ashburn',_10}
This function will throw with an InvalidStateError
if the Device has been
destroyed beforehand.
The SDK now uses the loglevel
module. This exposes
several new features for the SDK, including the ability to intercept log messages with custom
handlers and the ability to set logging levels after instantiating a Device
. To get an instance
of the loglevel
Logger
class used internally by the SDK:
_10import { Logger as TwilioClientLogger } from '@twilio/voice-client-sdk';_10..._10TwilioClientLogger.setLogLevel('DEBUG');
Please see the original loglevel
project for more
documentation on usage.
Connection.mediaStream
. To access the MediaStreams, use
Connection.getRemoteStream()
and
Connection.getLocalStream()
Connection.message
in favor of the newer
Connection.customParameters
. Where
.message
was an Object,
.customParameters
is a
Map
.
Connection.options
Connection.pstream
Connection.sendHangup
Connection.on('cancel')
logic so that we no longer emit
cancel
in response to
Connection.ignore()
.
Some deprecated Device
options have been removed. This includes:
enableIceRestart
enableRingingState
fakeLocalDtmf
The above three removed options are now assumed true
. The new Device.Options
interface is now:
_15export interface Options {_15 allowIncomingWhileBusy?: boolean;_15 appName?: string;_15 appVersion?: string;_15 audioConstraints?: MediaTrackConstraints | boolean;_15 closeProtection?: boolean | string;_15 codecPreferences?: Connection.Codec[];_15 disableAudioContextSounds?: boolean;_15 dscp?: boolean;_15 edge?: string[] | string;_15 forceAggressiveIceNomination?: boolean;_15 maxAverageBitrate?: number;_15 rtcConfiguration?: RTCConfiguration;_15 sounds?: Partial<Record<Device.SoundName, string>>;_15}
The formula used to calculate the mean-opinion score (MOS) has been fixed for extreme network conditions. These fixes will not affect scores for nominal network conditions.