Skip to content

Commit

Permalink
Merge pull request twilio#164 from twilio/feature/vdi
Browse files Browse the repository at this point in the history
Merge feature branch for VDI support
  • Loading branch information
charliesantos authored May 9, 2023
2 parents 6add098 + e029216 commit eee0f98
Show file tree
Hide file tree
Showing 12 changed files with 239 additions and 59 deletions.
20 changes: 10 additions & 10 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,12 @@ commands:
- store_artifacts:
path: ./dist
prefix: ./dist
unit-tests:
build-checks:
steps:
- build
- run:
name: Running unit tests
command: npm run test:unit
name: Running build checks
command: npm run test:typecheck && npm run lint && npm run test:unit
- store_artifacts:
path: coverage
destination: coverage
Expand All @@ -105,9 +105,9 @@ commands:
# Jobs
###
jobs:
run-unit-tests:
run-build-checks:
executor: docker-with-browser
steps: [unit-tests]
steps: [build-checks]
run-network-tests:
parameters:
bver:
Expand Down Expand Up @@ -174,10 +174,10 @@ workflows:
- equal: [true, <<pipeline.parameters.pr_workflow>>]
- equal: [false, <<pipeline.parameters.release_workflow>>]
jobs:
- run-unit-tests:
- run-build-checks:
context:
- vblocks-js
name: Unit Tests
name: Build Checks
- run-integration-tests:
name: Integration Tests <<matrix.browser>> <<matrix.bver>>
context:
Expand All @@ -200,10 +200,10 @@ workflows:
release-workflow:
when: <<pipeline.parameters.release_workflow>>
jobs:
- run-unit-tests:
- run-build-checks:
context:
- vblocks-js
name: Unit Tests
name: Build Checks
- run-integration-tests:
context:
- dockerhub-pulls
Expand All @@ -230,7 +230,7 @@ workflows:
name: Create Release Dry Run
dryRun: true
requires:
- Unit Tests
- Build Checks
# NOTE(mhuynh): Temporarily allow release without these tests passing
# # Chrome integration tests
# - Integration Tests chrome beta
Expand Down
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
2.5.0 (May 9, 2023)
===================

New Features
------------

### WebRTC API Overrides (Beta)

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](https://www.citrix.com/solutions/vdi-and-daas/hdx/what-is-hdx.html)'s WebRTC [redirection technologies](https://www.citrix.com/blogs/2019/01/15/hdx-a-webrtc-manifesto/), your application can use this new *beta* feature for improved audio quality in those environments.

- [Device.Options.enumerateDevices](https://twilio.github.io/twilio-voice.js/interfaces/voice.device.options.html#enumeratedevices)
- [Device.Options.getUserMedia](https://twilio.github.io/twilio-voice.js/interfaces/voice.device.options.html#getusermedia)
- [Device.Options.RTCPeerConnection](https://twilio.github.io/twilio-voice.js/interfaces/voice.device.options.html#rtcpeerconnection)
- [call.on('audio', handler(remoteAudio))](https://twilio.github.io/twilio-voice.js/classes/voice.call.html#audioevent)

2.4.0 (April 6, 2023)
===================

Expand Down
33 changes: 26 additions & 7 deletions lib/twilio/audiohelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@ class AudioHelper extends EventEmitter {
[Device.SoundName.Outgoing]: true,
};

/**
* The enumerateDevices method to use
*/
private _enumerateDevices: any;

/**
* The `getUserMedia()` function to use.
*/
Expand Down Expand Up @@ -174,9 +179,13 @@ class AudioHelper extends EventEmitter {
this._getUserMedia = getUserMedia;
this._mediaDevices = options.mediaDevices || getMediaDevicesInstance();
this._onActiveInputChanged = onActiveInputChanged;
this._enumerateDevices = typeof options.enumerateDevices === 'function'
? options.enumerateDevices
: this._mediaDevices && this._mediaDevices.enumerateDevices;

const isAudioContextSupported: boolean = !!(options.AudioContext || options.audioContext);
const isEnumerationSupported: boolean = !!(this._mediaDevices && this._mediaDevices.enumerateDevices);
const isEnumerationSupported: boolean = !!this._enumerateDevices;

const isSetSinkSupported: boolean = typeof options.setSinkId === 'function';
this.isOutputSelectionSupported = isEnumerationSupported && isSetSinkSupported;
this.isVolumeSupported = isAudioContextSupported;
Expand Down Expand Up @@ -282,7 +291,7 @@ class AudioHelper extends EventEmitter {
* @private
*/
_unbind(): void {
if (!this._mediaDevices) {
if (!this._mediaDevices || !this._enumerateDevices) {
throw new NotSupportedError('Enumeration is not supported');
}

Expand Down Expand Up @@ -400,7 +409,7 @@ class AudioHelper extends EventEmitter {
* Initialize output device enumeration.
*/
private _initializeEnumeration(): void {
if (!this._mediaDevices) {
if (!this._mediaDevices || !this._enumerateDevices) {
throw new NotSupportedError('Enumeration is not supported');
}

Expand Down Expand Up @@ -526,11 +535,11 @@ class AudioHelper extends EventEmitter {
* Update the available input and output devices
*/
private _updateAvailableDevices = (): Promise<void> => {
if (!this._mediaDevices) {
if (!this._mediaDevices || !this._enumerateDevices) {
return Promise.reject('Enumeration not supported');
}

return this._mediaDevices.enumerateDevices().then((devices: MediaDeviceInfo[]) => {
return this._enumerateDevices().then((devices: MediaDeviceInfo[]) => {
this._updateDevices(devices.filter((d: MediaDeviceInfo) => d.kind === 'audiooutput'),
this.availableOutputDevices,
this._removeLostOutput);
Expand Down Expand Up @@ -618,8 +627,13 @@ class AudioHelper extends EventEmitter {
this._inputVolumeSource.disconnect();
}

this._inputVolumeSource = this._audioContext.createMediaStreamSource(this._inputStream);
this._inputVolumeSource.connect(this._inputVolumeAnalyser);
try {
this._inputVolumeSource = this._audioContext.createMediaStreamSource(this._inputStream);
this._inputVolumeSource.connect(this._inputVolumeAnalyser);
} catch (ex) {
this._log.warn('Unable to update volume source', ex);
delete this._inputVolumeSource;
}
}

/**
Expand Down Expand Up @@ -696,6 +710,11 @@ namespace AudioHelper {
*/
audioContext?: AudioContext;

/**
* Overrides the native MediaDevices.enumerateDevices API.
*/
enumerateDevices?: any;

/**
* A custom MediaDevices instance to use.
*/
Expand Down
19 changes: 19 additions & 0 deletions lib/twilio/call.ts
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,7 @@ class Call extends EventEmitter {

this._mediaHandler = new (this._options.MediaHandler)
(config.audioHelper, config.pstream, config.getUserMedia, {
RTCPeerConnection: this._options.RTCPeerConnection,
codecPreferences: this._options.codecPreferences,
dscp: this._options.dscp,
forceAggressiveIceNomination: this._options.forceAggressiveIceNomination,
Expand All @@ -395,6 +396,11 @@ class Call extends EventEmitter {
this._latestOutputVolume = outputVolume;
});

this._mediaHandler.onaudio = (remoteAudio: typeof Audio) => {
this._log.info('Remote audio created');
this.emit('audio', remoteAudio);
};

this._mediaHandler.onvolume = (inputVolume: number, outputVolume: number,
internalInputVolume: number, internalOutputVolume: number) => {
// (rrowland) These values mock the 0 -> 32767 format used by legacy getStats. We should look into
Expand Down Expand Up @@ -1510,6 +1516,14 @@ namespace Call {
*/
declare function acceptEvent(call: Call): void;

/**
* Emitted after the HTMLAudioElement for the remote audio is created.
* @param remoteAudio - The HTMLAudioElement.
* @example `call.on('audio', handler(remoteAudio))`
* @event
*/
declare function audioEvent(remoteAudio: HTMLAudioElement): void;

/**
* Emitted when the {@link Call} is canceled.
* @example `call.on('cancel', () => { })`
Expand Down Expand Up @@ -1875,6 +1889,11 @@ namespace Call {
*/
rtcConstraints?: MediaStreamConstraints;

/**
* The RTCPeerConnection passed to {@link Device} on setup.
*/
RTCPeerConnection?: any;

/**
* Whether the disconnect sound should be played.
*/
Expand Down
72 changes: 66 additions & 6 deletions lib/twilio/device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -940,7 +940,7 @@ class Device extends EventEmitter {

const config: Call.Config = {
audioHelper: this._audio,
getUserMedia,
getUserMedia: this._options.getUserMedia || getUserMedia,
isUnifiedPlanDefault: Device._isUnifiedPlanDefault,
onIgnore: (): void => {
this._soundcache.get(Device.SoundName.Incoming).stop();
Expand All @@ -952,6 +952,7 @@ class Device extends EventEmitter {

options = Object.assign({
MediaStream: this._options.MediaStream || rtc.PeerConnection,
RTCPeerConnection: this._options.RTCPeerConnection,
beforeAccept: (currentCall: Call) => {
if (!this._activeCall || this._activeCall === currentCall) {
return;
Expand All @@ -969,7 +970,7 @@ class Device extends EventEmitter {
maxAverageBitrate: this._options.maxAverageBitrate,
preflight: this._options.preflight,
rtcConstraints: this._options.rtcConstraints,
shouldPlayDisconnect: () => this.audio?.disconnect(),
shouldPlayDisconnect: () => this._audio?.disconnect(),
twimlParams,
voiceEventSidGenerator: this._options.voiceEventSidGenerator,
}, options);
Expand All @@ -986,6 +987,12 @@ class Device extends EventEmitter {

const call = new (this._options.Call || Call)(config, options);

this._publisher.info('settings', 'init', {
RTCPeerConnection: !!this._options.RTCPeerConnection,
enumerateDevices: !!this._options.enumerateDevices,
getUserMedia: !!this._options.getUserMedia,
}, call);

call.once('accept', () => {
this._stream.updatePreferredURI(this._preferredURI);
this._removeCall(call);
Expand All @@ -994,7 +1001,7 @@ class Device extends EventEmitter {
this._audio._maybeStartPollingVolume();
}

if (call.direction === Call.CallDirection.Outgoing && this.audio?.outgoing()) {
if (call.direction === Call.CallDirection.Outgoing && this._audio?.outgoing()) {
this._soundcache.get(Device.SoundName.Outgoing).play();
}

Expand Down Expand Up @@ -1204,7 +1211,7 @@ class Device extends EventEmitter {
this._publishNetworkChange();
});

const play = (this.audio?.incoming() && !wasBusy)
const play = (this._audio?.incoming() && !wasBusy)
? () => this._soundcache.get(Device.SoundName.Incoming).play()
: () => Promise.resolve();

Expand Down Expand Up @@ -1310,8 +1317,11 @@ class Device extends EventEmitter {
this._audio = new (this._options.AudioHelper || AudioHelper)(
this._updateSinkIds,
this._updateInputStream,
getUserMedia,
{ audioContext: Device.audioContext },
this._options.getUserMedia || getUserMedia,
{
audioContext: Device.audioContext,
enumerateDevices: this._options.enumerateDevices,
},
);

this._audio.on('deviceChange', (lostActiveDevices: MediaDeviceInfo[]) => {
Expand Down Expand Up @@ -1687,12 +1697,22 @@ namespace Device {
*/
edge?: string[] | string;

/**
* Overrides the native MediaDevices.enumerateDevices API.
*/
enumerateDevices?: any;

/**
* Experimental feature.
* Whether to use ICE Aggressive nomination.
*/
forceAggressiveIceNomination?: boolean;

/**
* Overrides the native MediaDevices.getUserMedia API.
*/
getUserMedia?: any;

/**
* Log level.
*/
Expand Down Expand Up @@ -1724,6 +1744,46 @@ namespace Device {
*/
maxCallSignalingTimeoutMs?: number;

/**
* Overrides the native RTCPeerConnection class.
*
* By default, the SDK will use the `unified-plan` SDP format if the browser supports it.
* Unexpected behavior may happen if the `RTCPeerConnection` parameter uses an SDP format
* that is different than what the SDK uses.
*
* For example, if the browser supports `unified-plan` and the `RTCPeerConnection`
* parameter uses `plan-b` by default, the SDK will use `unified-plan`
* which will cause conflicts with the usage of the `RTCPeerConnection`.
*
* In order to avoid this issue, you need to explicitly set the SDP format that you want
* the SDK to use with the `RTCPeerConnection` via [[Device.ConnectOptions.rtcConfiguration]] for outgoing calls.
* Or [[Call.AcceptOptions.rtcConfiguration]] for incoming calls.
*
* See the example below. Assuming the `RTCPeerConnection` you provided uses `plan-b` by default, the following
* code sets the SDP format to `unified-plan` instead.
*
* ```ts
* // Outgoing calls
* const call = await device.connect({
* rtcConfiguration: {
* sdpSemantics: 'unified-plan'
* }
* // Other options
* });
*
* // Incoming calls
* device.on('incoming', call => {
* call.accept({
* rtcConfiguration: {
* sdpSemantics: 'unified-plan'
* }
* // Other options
* });
* });
* ```
*/
RTCPeerConnection?: any;

/**
* A mapping of custom sound URLs by sound name.
*/
Expand Down
15 changes: 13 additions & 2 deletions lib/twilio/log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,13 @@ class Log {
* @param [options] - Optional settings
*/
constructor(options?: LogOptions) {
this._log = (options && options.LogLevelModule ? options.LogLevelModule : LogLevelModule).getLogger(PACKAGE_NAME);
try {
this._log = (options && options.LogLevelModule ? options.LogLevelModule : LogLevelModule).getLogger(PACKAGE_NAME);
} catch {
// tslint:disable-next-line
console.warn('Cannot create custom logger');
this._log = console as any;
}
}

/**
Expand Down Expand Up @@ -93,7 +99,12 @@ class Log {
* Set a default log level to disable all logging below the given level
*/
setDefaultLevel(level: LogLevelModule.LogLevelDesc): void {
this._log.setDefaultLevel(level);
if (this._log.setDefaultLevel) {
this._log.setDefaultLevel(level);
} else {
// tslint:disable-next-line
console.warn('Logger cannot setDefaultLevel');
}
}

/**
Expand Down
Loading

0 comments on commit eee0f98

Please sign in to comment.