Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge feature branch for VDI support #164

Merged
merged 7 commits into from
May 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we split these up in a future ticket?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's good to combine the build checks (currently include unit/lint/typechecks)
I don't see benefits splitting them up in CircleCI, only downsides - takes more time, duplicates (in this case 3x) all other pre-install routines such as npm install, loading/saving CI cache, etc which takes too much CI minutes.

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