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

Fixing the ability to choose default input in browsers when default changes #496

Merged
merged 1 commit into from
Jun 30, 2020
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fix missing uuidv4 import in integration test
- Disable w3c check for Chrome Android webdriver integration tests
- Fix setSinkId() from throwing DOMException in Chromium browsers
- Fixing the ability to choose default input in browsers when default changes

## [1.10.0] - 2020-06-23

Expand Down
6 changes: 3 additions & 3 deletions demos/serverless/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5,144 changes: 2,584 additions & 2,560 deletions docs/assets/js/search.js

Large diffs are not rendered by default.

285 changes: 208 additions & 77 deletions docs/classes/defaultdevicecontroller.html

Large diffs are not rendered by default.

16 changes: 15 additions & 1 deletion docs/classes/deviceselection.html
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ <h2>Index</h2>
<h3>Properties</h3>
<ul class="tsd-index-list">
<li class="tsd-kind-property tsd-parent-kind-class"><a href="deviceselection.html#constraints" class="tsd-kind-icon">constraints</a></li>
<li class="tsd-kind-property tsd-parent-kind-class"><a href="deviceselection.html#groupid" class="tsd-kind-icon">group<wbr>Id</a></li>
<li class="tsd-kind-property tsd-parent-kind-class"><a href="deviceselection.html#stream" class="tsd-kind-icon">stream</a></li>
</ul>
</section>
Expand All @@ -106,6 +107,16 @@ <h3>constraints</h3>
</ul>
</aside>
</section>
<section class="tsd-panel tsd-member tsd-kind-property tsd-parent-kind-class">
<a name="groupid" class="tsd-anchor"></a>
<h3>group<wbr>Id</h3>
<div class="tsd-signature tsd-kind-icon">group<wbr>Id<span class="tsd-signature-symbol">:</span> <span class="tsd-signature-type">string</span><span class="tsd-signature-symbol"> = &quot;&quot;</span></div>
<aside class="tsd-sources">
<ul>
<li>Defined in <a href="https://github.com/aws/amazon-chime-sdk-js/blob/master/src/devicecontroller/DeviceSelection.ts#L7">src/devicecontroller/DeviceSelection.ts:7</a></li>
</ul>
</aside>
</section>
<section class="tsd-panel tsd-member tsd-kind-property tsd-parent-kind-class">
<a name="stream" class="tsd-anchor"></a>
<h3>stream</h3>
Expand All @@ -129,7 +140,7 @@ <h3>matches<wbr>Constraints</h3>
<li class="tsd-description">
<aside class="tsd-sources">
<ul>
<li>Defined in <a href="https://github.com/aws/amazon-chime-sdk-js/blob/master/src/devicecontroller/DeviceSelection.ts#L8">src/devicecontroller/DeviceSelection.ts:8</a></li>
<li>Defined in <a href="https://github.com/aws/amazon-chime-sdk-js/blob/master/src/devicecontroller/DeviceSelection.ts#L9">src/devicecontroller/DeviceSelection.ts:9</a></li>
</ul>
</aside>
<h4 class="tsd-parameters-title">Parameters</h4>
Expand Down Expand Up @@ -162,6 +173,9 @@ <h4 class="tsd-returns-title">Returns <span class="tsd-signature-type">boolean</
<li class=" tsd-kind-property tsd-parent-kind-class">
<a href="deviceselection.html#constraints" class="tsd-kind-icon">constraints</a>
</li>
<li class=" tsd-kind-property tsd-parent-kind-class">
<a href="deviceselection.html#groupid" class="tsd-kind-icon">group<wbr>Id</a>
</li>
<li class=" tsd-kind-property tsd-parent-kind-class">
<a href="deviceselection.html#stream" class="tsd-kind-icon">stream</a>
</li>
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "amazon-chime-sdk-js",
"version": "1.10.9",
"version": "1.10.10",
"description": "Amazon Chime SDK for JavaScript",
"main": "build/index.js",
"types": "build/index.d.ts",
Expand Down
78 changes: 74 additions & 4 deletions src/devicecontroller/DefaultDeviceController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// SPDX-License-Identifier: Apache-2.0

import AudioVideoController from '../audiovideocontroller/AudioVideoController';
import BrowserBehavior from '../browserbehavior/BrowserBehavior';
import DefaultBrowserBehavior from '../browserbehavior/DefaultBrowserBehavior';
import DeviceChangeObserver from '../devicechangeobserver/DeviceChangeObserver';
import Logger from '../logger/Logger';
import Maybe from '../maybe/Maybe';
Expand All @@ -24,6 +26,7 @@ export default class DefaultDeviceController implements DeviceControllerBasedMed
private static defaultSampleRate = 48000;
private static defaultSampleSize = 16;
private static defaultChannelCount = 1;
private browserBehavior: BrowserBehavior = new DefaultBrowserBehavior();
private deviceInfoCache: MediaDeviceInfo[] | null = null;
private activeDevices: { [kind: string]: DeviceSelection | null } = { audio: null, video: null };
private audioOutputDeviceId: string | null = null;
Expand All @@ -42,7 +45,6 @@ export default class DefaultDeviceController implements DeviceControllerBasedMed
private videoMaxBandwidthKbps: number = DefaultDeviceController.defaultVideoMaxBandwidthKbps;

private useWebAudio: boolean = false;

private isAndroid: boolean = false;
private isPixel3: boolean = false;

Expand Down Expand Up @@ -482,6 +484,65 @@ export default class DefaultDeviceController implements DeviceControllerBasedMed
return device && device.id ? device : null;
}

private hasSameGroupId(groupId: string, kind: string, device: Device): boolean {
device = this.getDeviceIdStr(device);
if (groupId === this.getGroupIdFromDeviceId(kind, device) || groupId === '') {
return true;
}
return false;
}

private getGroupIdFromDeviceId(kind: string, device: string): string {
if (this.deviceInfoCache !== null) {
const cachedDeviceInfo = this.listCachedDevicesOfKind(`${kind}input`).find(
(cachedDevice: MediaDeviceInfo) => cachedDevice.deviceId === device
);
if (cachedDeviceInfo && cachedDeviceInfo.groupId) {
return cachedDeviceInfo.groupId;
}
}
return '';
}

private getDeviceIdStr(device: Device): string | null {
if (typeof device === 'string') {
return device;
}
if (device === null) {
return null;
}
const deviceMediaTrackConstraints = device as MediaTrackConstraints;
if ((device as MediaStream).id) {
return (device as MediaStream).id;
} else if (deviceMediaTrackConstraints.deviceId) {
/* istanbul ignore else */
if ((deviceMediaTrackConstraints.deviceId as ConstrainDOMStringParameters).exact) {
/* istanbul ignore else */
if (
(deviceMediaTrackConstraints.deviceId as ConstrainDOMStringParameters).exact as string
) {
return (deviceMediaTrackConstraints.deviceId as ConstrainDOMStringParameters)
.exact as string;
}
}
}
return '';
}

getActiveDeviceId(kind: string): string | null {
if (this.activeDevices[kind] && this.activeDevices[kind].constraints) {
const activeDeviceMediaTrackConstraints =
this.activeDevices[kind].constraints.audio || this.activeDevices[kind].constraints.video;
const activeDeviceConstrainDOMStringParameters = (activeDeviceMediaTrackConstraints as MediaTrackConstraints)
.deviceId;
const activeDeviceId = (activeDeviceConstrainDOMStringParameters as ConstrainDOMStringParameters)
.exact;
/* istanbul ignore else */
if (activeDeviceId as string) return activeDeviceId as string;
}
return null;
}

private async chooseInputDevice(
kind: string,
device: Device,
Expand All @@ -503,15 +564,17 @@ export default class DefaultDeviceController implements DeviceControllerBasedMed
kind,
device
);

if (
this.activeDevices[kind] &&
this.activeDevices[kind].matchesConstraints(proposedConstraints) &&
this.activeDevices[kind].stream.active
this.activeDevices[kind].stream.active &&
this.activeDevices[kind].groupId !== null &&
RidipDe marked this conversation as resolved.
Show resolved Hide resolved
this.hasSameGroupId(this.activeDevices[kind].groupId, kind, device)
) {
this.logger.info(`reusing existing ${kind} device`);
return DevicePermission.PermissionGrantedPreviously;
}

const startTimeMs = Date.now();
RidipDe marked this conversation as resolved.
Show resolved Hide resolved
const newDevice: DeviceSelection = new DeviceSelection();
try {
Expand All @@ -527,9 +590,15 @@ export default class DefaultDeviceController implements DeviceControllerBasedMed
newDevice.stream = stream;
newDevice.constraints = proposedConstraints;
} else {
if (
this.browserBehavior.hasChromiumWebRTC() &&
this.getDeviceIdStr(device) === this.getActiveDeviceId(kind) &&
this.getDeviceIdStr(device) === 'default'
RidipDe marked this conversation as resolved.
Show resolved Hide resolved
) {
this.releaseMediaStream(this.activeDevices[kind].stream);
}
newDevice.stream = await navigator.mediaDevices.getUserMedia(proposedConstraints);
newDevice.constraints = proposedConstraints;

if (kind === 'video' && this.lastNoVideoInputDeviceCount > callCount) {
this.logger.warn(
`ignored to get video device for constraints ${JSON.stringify(
Expand Down Expand Up @@ -557,6 +626,7 @@ export default class DefaultDeviceController implements DeviceControllerBasedMed
}
});
}
newDevice.groupId = this.getGroupIdFromDeviceId(kind, this.getDeviceIdStr(device));
} catch (error) {
this.logger.error(
`failed to get ${kind} device for constraints ${JSON.stringify(proposedConstraints)}: ${
Expand Down
3 changes: 2 additions & 1 deletion src/devicecontroller/DeviceSelection.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// Copyright 2019-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

export default class DeviceSelection {
constraints: MediaStreamConstraints;
stream: MediaStream;
groupId: string = '';

matchesConstraints(constraints: MediaStreamConstraints): boolean {
return JSON.stringify(this.constraints) === JSON.stringify(constraints);
Expand Down
2 changes: 1 addition & 1 deletion src/versioning/Versioning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export default class Versioning {
* Return string representation of SDK version
*/
static get sdkVersion(): string {
return '1.10.9';
return '1.10.10';
}

/**
Expand Down
48 changes: 45 additions & 3 deletions test/devicecontroller/DefaultDeviceController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,15 @@ describe('DefaultDeviceController', () => {
function getMediaDeviceInfo(
deviceId: string,
kind: MediaDeviceKind,
label: string
label: string,
groupId?: string
): MediaDeviceInfo {
// @ts-ignore
return {
deviceId,
kind,
label,
groupId,
};
}

Expand Down Expand Up @@ -185,7 +187,7 @@ describe('DefaultDeviceController', () => {
expect(called).to.be.true;
});

it('cathces an error from restarting the local audio', async () => {
it('catches an error from restarting the local audio', async () => {
let called = false;

class TestAudioVideoController extends NoOpAudioVideoController {
Expand Down Expand Up @@ -218,6 +220,38 @@ describe('DefaultDeviceController', () => {
expect(permission).to.equal(DevicePermission.PermissionGrantedByBrowser);
});

it('releases audio media stream when requesting default device and default is already active in chromium based browser', async () => {
deviceController.enableWebAudio(true);
domMockBehavior.browserName = 'chrome';
domMockBuilder = new DOMMockBuilder(domMockBehavior);
deviceController = new DefaultDeviceController(logger);
domMockBehavior.enumerateDeviceList = [
getMediaDeviceInfo('default', 'audioinput', 'label', 'group-id-1'),
];
await deviceController.listAudioInputDevices();
let permission = await deviceController.chooseAudioInputDevice('default');
expect(permission).to.equal(DevicePermission.PermissionGrantedByBrowser);
domMockBehavior.enumerateDeviceList.pop();
// add external default device
domMockBehavior.enumerateDeviceList = [
getMediaDeviceInfo('default', 'audioinput', 'default - label2', 'group-id-2'),
];
domMockBuilder = new DOMMockBuilder(domMockBehavior);
await deviceController.listAudioInputDevices();
permission = await deviceController.chooseAudioInputDevice('default');
expect(permission).to.equal(DevicePermission.PermissionGrantedByBrowser);
});

it('sets to null device when an external device disconnects', async () => {
deviceController.enableWebAudio(true);
let permission = await deviceController.chooseAudioInputDevice(stringDeviceId);
expect(permission).to.equal(DevicePermission.PermissionGrantedByBrowser);

// The previous audio source node will be disconneted.
permission = await deviceController.chooseAudioInputDevice(null);
expect(permission).to.equal(DevicePermission.PermissionGrantedByBrowser);
});

it('releases all previously-acquired audio streams', done => {
const stringDeviceIds: Device[] = [
'device-id-1',
Expand Down Expand Up @@ -445,7 +479,7 @@ describe('DefaultDeviceController', () => {
expect(spy.called).to.be.false;
});

it("disconnects the audio input source node instead of the given stream 's tracks", async () => {
it("disconnects the audio input source node instead of the given stream's tracks", async () => {
deviceController.enableWebAudio(true);
await deviceController.chooseAudioInputDevice(stringDeviceId);
const stream = await deviceController.acquireAudioInputStream();
Expand Down Expand Up @@ -801,4 +835,12 @@ describe('DefaultDeviceController', () => {
await new Promise(resolve => new TimeoutScheduler(100).start(resolve));
});
});

describe('getActiveDeviceId', () => {
it('calls getActiveDeviceId to get the avtive device for the video', async () => {
let permission = await deviceController.chooseVideoInputDevice('default');
expect(permission).to.equal(DevicePermission.PermissionGrantedByBrowser);
expect(deviceController.getActiveDeviceId('video')).to.equal('default');
});
});
});