Skip to content

Commit

Permalink
fix errors
Browse files Browse the repository at this point in the history
  • Loading branch information
szuperaz committed Nov 4, 2023
1 parent d526fbc commit 8a29cee
Show file tree
Hide file tree
Showing 9 changed files with 191 additions and 79 deletions.
88 changes: 62 additions & 26 deletions packages/client/src/devices/InputMediaDeviceManager.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { Observable, Subscription, take } from 'rxjs';
import { Observable, Subscription, combineLatest, pairwise, take } from 'rxjs';

Check warning on line 1 in packages/client/src/devices/InputMediaDeviceManager.ts

View workflow job for this annotation

GitHub Actions / test-and-build

'take' is defined but never used
import { Call } from '../Call';
import { CallingState } from '../store';
import { InputMediaDeviceManagerState } from './InputMediaDeviceManagerState';
import { isReactNative } from '../helpers/platforms';
import { Logger } from '../coordinator/connection/types';
import { getLogger } from '../logger';
import { TrackType } from '../gen/video/sfu/models/models';
import { watchForDisconnectedDevice } from './devices';
import { deviceIds$ } from './devices';

export abstract class InputMediaDeviceManager<
T extends InputMediaDeviceManagerState<C>,
Expand All @@ -30,21 +30,11 @@ export abstract class InputMediaDeviceManager<
) {
this.logger = getLogger([`${TrackType[trackType].toLowerCase()} manager`]);
if (
typeof navigator !== 'undefined' &&
typeof navigator.mediaDevices !== 'undefined' &&
deviceIds$ &&
!isReactNative() &&
(this.trackType === TrackType.AUDIO || this.trackType === TrackType.VIDEO)
) {
this.subscriptions.push(
watchForDisconnectedDevice(this.state.selectedDevice$).subscribe(
async (isDisconnected) => {
if (isDisconnected) {
await this.disable();
this.select(undefined);
}
},
),
);
this.handleDisconnectedOrReplacedDevices();
}
}

Expand Down Expand Up @@ -251,20 +241,66 @@ export abstract class InputMediaDeviceManager<
return;
}
await this.disable();

Check warning on line 243 in packages/client/src/devices/InputMediaDeviceManager.ts

View workflow job for this annotation

GitHub Actions / test-and-build

Expected getter 'mediaDeviceKind' to always return a value
if (this.state.selectedDevice) {
this.listDevices()
.pipe(take(1))
.subscribe(async (devices) => {
if (
devices &&
devices.find((d) => d.deviceId === this.state.selectedDevice)
) {
await this.enable();
}
});
}
});
});
}
}

private get mediaDeviceKind() {
if (this.trackType === TrackType.AUDIO) {
return 'audioinput';
}
if (this.trackType === TrackType.VIDEO) {
return 'videoinput';
}
}

private handleDisconnectedOrReplacedDevices() {
this.subscriptions.push(
combineLatest([
deviceIds$!.pipe(pairwise()),
this.state.selectedDevice$,
]).subscribe(async ([[prevDevices, currentDevices], deviceId]) => {
if (!deviceId) {
return;
}
if (this.enablePromise) {
await this.enablePromise;
}
if (this.disablePromise) {
await this.disablePromise;
}

let isDeviceDisconnected = false;
let isDeviceReplaced = false;
const currentDevice = this.findDeviceInList(currentDevices, deviceId);
const prevDevice = this.findDeviceInList(prevDevices, deviceId);
if (!currentDevice && prevDevice) {
isDeviceDisconnected = true;
} else if (
currentDevice &&
prevDevice &&
currentDevice.deviceId === prevDevice.deviceId &&
currentDevice.groupId !== prevDevice.groupId
) {
isDeviceReplaced = true;
}

if (isDeviceDisconnected) {
await this.disable();
this.select(undefined);
}
if (isDeviceReplaced && this.state.status === 'enabled') {
await this.disable();
await this.enable();
}
}),

Check warning on line 297 in packages/client/src/devices/InputMediaDeviceManager.ts

View workflow job for this annotation

GitHub Actions / test-and-build

Expected '===' and instead saw '=='
);
}

private findDeviceInList(devices: MediaDeviceInfo[], deviceId: string) {
return devices.find(
(d) => d.deviceId === deviceId && d.kind == this.mediaDeviceKind,
);
}
}
22 changes: 12 additions & 10 deletions packages/client/src/devices/SpeakerManager.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
import { Subscription } from 'rxjs';
import { Subscription, combineLatest } from 'rxjs';
import { isReactNative } from '../helpers/platforms';
import { SpeakerState } from './SpeakerState';
import { getAudioOutputDevices, watchForDisconnectedDevice } from './devices';
import { deviceIds$, getAudioOutputDevices } from './devices';

export class SpeakerManager {
public readonly state = new SpeakerState();
private subscriptions: Subscription[] = [];

constructor() {
if (
typeof navigator !== 'undefined' &&
typeof navigator.mediaDevices !== 'undefined' &&
!isReactNative()
) {
if (deviceIds$ && !isReactNative()) {
this.subscriptions.push(
watchForDisconnectedDevice(this.state.selectedDevice$).subscribe(
async (isDisconnected) => {
if (isDisconnected) {
combineLatest([deviceIds$!, this.state.selectedDevice$]).subscribe(
([devices, deviceId]) => {
if (!deviceId) {
return;
}
const device = devices.find(
(d) => d.deviceId === deviceId && d.kind === 'audiooutput',
);
if (!device) {
this.select('');
}
},
Expand Down
8 changes: 7 additions & 1 deletion packages/client/src/devices/__tests__/CameraManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ import { StreamClient } from '../../coordinator/connection/client';
import { CallingState, StreamVideoWriteableStateStore } from '../../store';

import { afterEach, beforeEach, describe, expect, it, Mock, vi } from 'vitest';
import { mockCall, mockVideoDevices, mockVideoStream } from './mocks';
import {
mockCall,
mockDeviceIds$,
mockVideoDevices,
mockVideoStream,
} from './mocks';
import { getVideoStream } from '../devices';
import { TrackType } from '../../gen/video/sfu/models/models';
import { CameraManager } from '../CameraManager';
Expand All @@ -17,6 +22,7 @@ vi.mock('../devices.ts', () => {
return of(mockVideoDevices);
}),
getVideoStream: vi.fn(() => Promise.resolve(mockVideoStream())),
deviceIds$: mockDeviceIds$(),
};
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import { CallingState, StreamVideoWriteableStateStore } from '../../store';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
MockTrack,
disconnectDevice,
emitDeviceIds,
mockCall,
mockDeviceDisconnectWatcher,
mockDeviceIds$,
mockVideoDevices,
mockVideoStream,
} from './mocks';
Expand All @@ -26,7 +26,7 @@ vi.mock('../../Call.ts', () => {
vi.mock('../devices.ts', () => {
console.log('MOCKING devices API');
return {
watchForDisconnectedDevice: mockDeviceDisconnectWatcher(),
deviceIds$: mockDeviceIds$(),
};
});

Expand Down Expand Up @@ -250,30 +250,70 @@ describe('InputMediaDeviceManager.test', () => {
expect(manager.enable).not.toHaveBeenCalled();
});

it('should restart track if a new default device is connected', async () => {
it('should restart track if the default device is replaced and status is enabled', async () => {
vi.useFakeTimers();
emitDeviceIds(mockVideoDevices);

await manager.enable();
const device = mockVideoDevices[0];
await manager.select(device.deviceId);

vi.spyOn(manager, 'enable');
await (
(manager.state.mediaStream?.getTracks()[0] as MockTrack).eventHandlers[
'ended'
] as Function
)();
vi.spyOn(manager, 'disable');

expect(manager.enable).toHaveBeenCalled();
emitDeviceIds([
{ ...device, groupId: device.groupId + 'new' },
...mockVideoDevices.slice(1),
]);

await vi.runAllTimersAsync();

expect(manager.enable).toHaveBeenCalledOnce();
expect(manager.disable).toHaveBeenCalledOnce();
expect(manager.state.status).toBe('enabled');

vi.useRealTimers();
});

it('should do nothing if default device is replaced and status is disabled', async () => {
vi.useFakeTimers();
emitDeviceIds(mockVideoDevices);

const device = mockVideoDevices[0];
await manager.select(device.deviceId);
await manager.disable();

vi.spyOn(manager, 'enable');
vi.spyOn(manager, 'disable');

emitDeviceIds([
{ ...device, groupId: device.groupId + 'new' },
...mockVideoDevices.slice(1),
]);

await vi.runAllTimersAsync();

expect(manager.enable).not.toHaveBeenCalledOnce();
expect(manager.disable).not.toHaveBeenCalledOnce();
expect(manager.state.status).toBe('disabled');
expect(manager.state.selectedDevice).toBe(device.deviceId);

vi.useRealTimers();
});

it('should disable stream and deselect device if selected device is disconnected', async () => {
vi.useFakeTimers();
emitDeviceIds(mockVideoDevices);

await manager.enable();
const deviceId = mockVideoDevices[1].deviceId;
await manager.select(deviceId);
const device = mockVideoDevices[0];
await manager.select(device.deviceId);

disconnectDevice();
emitDeviceIds(mockVideoDevices.slice(1));

await vi.runAllTimersAsync();

expect(manager.state.selectedDevice).toBe(undefined);
expect(manager.state.status).toBe('disabled');

vi.useRealTimers();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ import { StreamClient } from '../../coordinator/connection/client';
import { CallingState, StreamVideoWriteableStateStore } from '../../store';

import { afterEach, beforeEach, describe, expect, it, Mock, vi } from 'vitest';
import { mockAudioDevices, mockAudioStream, mockCall } from './mocks';
import {
mockAudioDevices,
mockAudioStream,
mockCall,
mockDeviceIds$,
} from './mocks';
import { getAudioStream } from '../devices';
import { TrackType } from '../../gen/video/sfu/models/models';
import { MicrophoneManager } from '../MicrophoneManager';
Expand All @@ -22,6 +27,7 @@ vi.mock('../devices.ts', () => {
return of(mockAudioDevices);
}),
getAudioStream: vi.fn(() => Promise.resolve(mockAudioStream())),
deviceIds$: mockDeviceIds$(),
};
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Call } from '../../Call';
import { StreamClient } from '../../coordinator/connection/client';
import { CallingState, StreamVideoWriteableStateStore } from '../../store';
import * as RxUtils from '../../store/rxUtils';
import { mockCall, mockScreenShareStream } from './mocks';
import { mockCall, mockDeviceIds$, mockScreenShareStream } from './mocks';
import { getScreenShareStream } from '../devices';
import { TrackType } from '../../gen/video/sfu/models/models';

Expand All @@ -14,6 +14,7 @@ vi.mock('../devices.ts', () => {
disposeOfMediaStream: vi.fn(),
getScreenShareStream: vi.fn(() => Promise.resolve(mockScreenShareStream())),
checkIfAudioOutputChangeSupported: vi.fn(() => Promise.resolve(true)),
deviceIds$: () => mockDeviceIds$(),
};
});

Expand Down
11 changes: 4 additions & 7 deletions packages/client/src/devices/__tests__/SpeakerManager.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import { afterEach, beforeEach, describe, vi, it, expect } from 'vitest';
import {
disconnectDevice,
mockAudioDevices,
mockDeviceDisconnectWatcher,
} from './mocks';
import { emitDeviceIds, mockAudioDevices, mockDeviceIds$ } from './mocks';
import { of } from 'rxjs';
import { SpeakerManager } from '../SpeakerManager';
import { checkIfAudioOutputChangeSupported } from '../devices';
Expand All @@ -13,7 +9,7 @@ vi.mock('../devices.ts', () => {
return {
getAudioOutputDevices: vi.fn(() => of(mockAudioDevices)),
checkIfAudioOutputChangeSupported: vi.fn(() => true),
watchForDisconnectedDevice: mockDeviceDisconnectWatcher(),
deviceIds$: mockDeviceIds$(),
};
});

Expand Down Expand Up @@ -65,10 +61,11 @@ describe('SpeakerManager.test', () => {
});

it('should disable device if selected device is disconnected', () => {
emitDeviceIds(mockAudioDevices);
const deviceId = mockAudioDevices[1].deviceId;
manager.select(deviceId);

disconnectDevice();
emitDeviceIds(mockAudioDevices.slice(2));

expect(manager.state.selectedDevice).toBe('');
});
Expand Down
15 changes: 8 additions & 7 deletions packages/client/src/devices/__tests__/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { vi } from 'vitest';
import { CallingState, CallState } from '../../store';
import { OwnCapability } from '../../gen/coordinator';
import { Call } from '../../Call';
import { Subject } from 'rxjs';
import { BehaviorSubject, Subject } from 'rxjs';

Check warning on line 5 in packages/client/src/devices/__tests__/mocks.ts

View workflow job for this annotation

GitHub Actions / test-and-build

'BehaviorSubject' is defined but never used

export const mockVideoDevices = [
{
Expand Down Expand Up @@ -184,15 +184,16 @@ export const mockScreenShareStream = (includeAudio: boolean = true) => {
} as any as MediaStream;
};

const deviceDisconnectSubject = new Subject<boolean>();
export const mockDeviceDisconnectWatcher = () => {
let deviceIds: Subject<MediaDeviceInfo[]>;
export const mockDeviceIds$ = () => {
global.navigator = {
// @ts-expect-error
//@ts-expect-error
mediaDevices: {},
};
return () => deviceDisconnectSubject;
deviceIds = new Subject();
return deviceIds;
};

export const disconnectDevice = () => {
deviceDisconnectSubject.next(true);
export const emitDeviceIds = (values: MediaDeviceInfo[]) => {
deviceIds.next(values);
};
Loading

0 comments on commit 8a29cee

Please sign in to comment.