Skip to content

Commit

Permalink
feat: switch to the universal device management api
Browse files Browse the repository at this point in the history
  • Loading branch information
oliverlaz committed Oct 4, 2023
1 parent 231246a commit 04c1c20
Show file tree
Hide file tree
Showing 42 changed files with 328 additions and 1,636 deletions.
71 changes: 7 additions & 64 deletions packages/client/src/Call.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,7 @@ import {
Subscriber,
} from './rtc';
import { muteTypeToTrackType } from './rtc/helpers/tracks';
import {
GoAwayReason,
SdkType,
TrackType,
} from './gen/video/sfu/models/models';
import { GoAwayReason, TrackType } from './gen/video/sfu/models/models';
import {
registerEventHandlers,
registerRingingCallEventHandlers,
Expand Down Expand Up @@ -116,7 +112,7 @@ import {
Logger,
StreamCallEvent,
} from './coordinator/connection/types';
import { getClientDetails, getSdkInfo } from './client-details';
import { getClientDetails } from './client-details';
import { getLogger } from './logger';
import {
CameraDirection,
Expand Down Expand Up @@ -1000,14 +996,11 @@ export class Call {
this.reconnectAttempts = 0; // reset the reconnect attempts counter
this.state.setCallingState(CallingState.JOINED);

// React uses a different device management for now
if (getSdkInfo()?.type !== SdkType.REACT) {
try {
await this.initCamera();
await this.initMic();
} catch (error) {
this.logger('warn', 'Camera and/or mic init failed during join call');
}
try {
await this.initCamera();
await this.initMic();
} catch (error) {
this.logger('warn', 'Camera and/or mic init failed during join call');
}

// 3. once we have the "joinResponse", and possibly reconciled the local state
Expand Down Expand Up @@ -1318,56 +1311,6 @@ export class Call {
return this.statsReporter?.stopReportingStatsFor(sessionId);
};

/**
* Sets the used audio output device (`audioOutputDeviceId` of the [`localParticipant$`](./StreamVideoClient.md/#readonlystatestore).
*
* This method only stores the selection, if you're using custom UI components, you'll have to implement the audio switching, for more information see: https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/sinkId.
*
*
* @param deviceId the selected device, `undefined` means the user wants to use the system's default audio output
*
* @deprecated use `call.speaker` instead
*/
setAudioOutputDevice = (deviceId?: string) => {
if (!this.sfuClient) return;
this.state.updateParticipant(this.sfuClient.sessionId, {
audioOutputDeviceId: deviceId,
});
};

/**
* Sets the `audioDeviceId` property of the [`localParticipant$`](./StreamVideoClient.md/#readonlystatestore)).
*
* This method only stores the selection, if you want to start publishing a media stream call the [`publishAudioStream` method](#publishaudiostream) that will set `audioDeviceId` as well.
*
*
* @param deviceId the selected device, pass `undefined` to clear the device selection
*
* @deprecated use call.microphone.select
*/
setAudioDevice = (deviceId?: string) => {
if (!this.sfuClient) return;
this.state.updateParticipant(this.sfuClient.sessionId, {
audioDeviceId: deviceId,
});
};

/**
* Sets the `videoDeviceId` property of the [`localParticipant$`](./StreamVideoClient.md/#readonlystatestore).
*
* This method only stores the selection, if you want to start publishing a media stream call the [`publishVideoStream` method](#publishvideostream) that will set `videoDeviceId` as well.
*
* @param deviceId the selected device, pass `undefined` to clear the device selection
*
* @deprecated use call.camera.select
*/
setVideoDevice = (deviceId?: string) => {
if (!this.sfuClient) return;
this.state.updateParticipant(this.sfuClient.sessionId, {
videoDeviceId: deviceId,
});
};

/**
* Resets the last sent reaction for the user holding the given `sessionId`. This is a local action, it won't reset the reaction on the backend.
*
Expand Down
2 changes: 1 addition & 1 deletion packages/client/src/devices/InputMediaDeviceManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ export abstract class InputMediaDeviceManager<
}
}

protected abstract getDevices(): Observable<MediaDeviceInfo[]>;
protected abstract getDevices(): Observable<MediaDeviceInfo[] | undefined>;

protected abstract getStream(constraints: C): Promise<MediaStream>;

Expand Down
41 changes: 14 additions & 27 deletions packages/client/src/helpers/DynascaleManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,12 @@ import { Call } from '../Call';
import {
AudioTrackType,
DebounceType,
StreamVideoLocalParticipant,
StreamVideoParticipant,
VideoTrackType,
VisibilityState,
} from '../types';
import { TrackType, VideoDimension } from '../gen/video/sfu/models/models';
import {
SdkType,
TrackType,
VideoDimension,
} from '../gen/video/sfu/models/models';
import {
combineLatest,
distinctUntilChanged,
distinctUntilKeyChanged,
map,
Expand All @@ -22,7 +16,6 @@ import {
} from 'rxjs';
import { ViewportTracker } from './ViewportTracker';
import { getLogger } from '../logger';
import { getSdkInfo } from '../client-details';
import { isFirefox, isSafari } from './browsers';

const DEFAULT_VIEWPORT_VISIBILITY_STATE: Record<
Expand Down Expand Up @@ -174,7 +167,7 @@ export class DynascaleManager {
(participants) =>
participants.find(
(participant) => participant.sessionId === sessionId,
) as StreamVideoLocalParticipant | StreamVideoParticipant,
) as StreamVideoParticipant,
),
takeWhile((participant) => !!participant),
distinctUntilChanged(),
Expand Down Expand Up @@ -339,9 +332,9 @@ export class DynascaleManager {
const participant$ = this.call.state.participants$.pipe(
map(
(participants) =>
participants.find((p) => p.sessionId === sessionId) as
| StreamVideoLocalParticipant
| StreamVideoParticipant,
participants.find(
(p) => p.sessionId === sessionId,
) as StreamVideoParticipant,
),
takeWhile((p) => !!p),
distinctUntilChanged(),
Expand Down Expand Up @@ -373,20 +366,14 @@ export class DynascaleManager {
});
});

const sinkIdSubscription = combineLatest([
this.call.state.localParticipant$,
this.call.speaker.state.selectedDevice$,
]).subscribe(([p, selectedDevice]) => {
const deviceId =
getSdkInfo()?.type === SdkType.REACT
? p?.audioOutputDeviceId
: selectedDevice;

if ('setSinkId' in audioElement && typeof deviceId === 'string') {
// @ts-expect-error setSinkId is not yet in the lib
audioElement.setSinkId(deviceId);
}
});
const sinkIdSubscription = !('setSinkId' in audioElement)
? null
: this.call.speaker.state.selectedDevice$.subscribe((deviceId) => {
if (deviceId) {
// @ts-expect-error setSinkId is not yet in the lib
audioElement.setSinkId(deviceId);
}
});

const volumeSubscription = this.call.speaker.state.volume$.subscribe(
(volume) => {
Expand All @@ -397,7 +384,7 @@ export class DynascaleManager {
audioElement.autoplay = true;

return () => {
sinkIdSubscription.unsubscribe();
sinkIdSubscription?.unsubscribe();
volumeSubscription.unsubscribe();
updateMediaStreamSubscription.unsubscribe();
};
Expand Down
17 changes: 2 additions & 15 deletions packages/client/src/helpers/__tests__/DynascaleManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,14 @@

import '../../rtc/__tests__/mocks/webrtc.mocks';

import { afterEach, beforeEach, describe, expect, it, Mock, vi } from 'vitest';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { DynascaleManager } from '../DynascaleManager';
import { Call } from '../../Call';
import { StreamClient } from '../../coordinator/connection/client';
import { StreamVideoWriteableStateStore } from '../../store';
import { DebounceType, VisibilityState } from '../../types';
import { noopComparator } from '../../sorting';
import { SdkType, TrackType } from '../../gen/video/sfu/models/models';
import { getSdkInfo } from '../../client-details';
import { TrackType } from '../../gen/video/sfu/models/models';

vi.mock('../../client-details.ts', () => {
return {
Expand Down Expand Up @@ -154,18 +153,6 @@ describe('DynascaleManager', () => {
'different-device-id',
);

const mock = getSdkInfo as Mock;
mock.mockImplementation(() => ({
type: SdkType.REACT,
}));

call.state.updateParticipant('session-id-local', {
audioOutputDeviceId: 'new-device-id',
});

// @ts-expect-error setSinkId is not defined in types
expect(audioElement.setSinkId).toHaveBeenCalledWith('new-device-id');

call.speaker.setVolume(0.5);

expect(audioElement.volume).toBe(0.5);
Expand Down
8 changes: 1 addition & 7 deletions packages/client/src/rtc/Publisher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,7 @@ import {
OptimalVideoLayer,
} from './videoLayers';
import { getPreferredCodecs } from './codecs';
import {
trackTypeToDeviceIdKey,
trackTypeToParticipantStreamKey,
} from './helpers/tracks';
import { trackTypeToParticipantStreamKey } from './helpers/tracks';
import { CallState } from '../store';
import { PublishOptions } from '../types';
import { isReactNative } from '../helpers/platforms';
Expand Down Expand Up @@ -387,14 +384,11 @@ export class Publisher {
[audioOrVideoOrScreenShareStream]: undefined,
}));
} else {
const deviceId = track.getSettings().deviceId;
const audioOrVideoDeviceKey = trackTypeToDeviceIdKey(trackType);
this.state.updateParticipant(this.sfuClient.sessionId, (p) => {
return {
publishedTracks: p.publishedTracks.includes(trackType)
? p.publishedTracks
: [...p.publishedTracks, trackType],
...(audioOrVideoDeviceKey && { [audioOrVideoDeviceKey]: deviceId }),
[audioOrVideoOrScreenShareStream]: mediaStream,
};
});
Expand Down
4 changes: 0 additions & 4 deletions packages/client/src/rtc/__tests__/Publisher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,6 @@ describe('Publisher', () => {
// initial publish
await publisher.publishStream(mediaStream, track, TrackType.VIDEO);

expect(state.localParticipant?.videoDeviceId).toEqual('test-device-id');
expect(state.localParticipant?.publishedTracks).toContain(TrackType.VIDEO);
expect(state.localParticipant?.videoStream).toEqual(mediaStream);
expect(transceiver.setCodecPreferences).toHaveBeenCalled();
Expand Down Expand Up @@ -136,15 +135,13 @@ describe('Publisher', () => {
expect.any(Function),
);
expect(transceiver.sender.replaceTrack).toHaveBeenCalledWith(newTrack);
expect(state.localParticipant?.videoDeviceId).toEqual('test-device-id-2');

// stop publishing
await publisher.unpublishStream(TrackType.VIDEO, true);
expect(newTrack.stop).toHaveBeenCalled();
expect(state.localParticipant?.publishedTracks).not.toContain(
TrackType.VIDEO,
);
expect(state.localParticipant?.videoDeviceId).toEqual('test-device-id-2');
});

it('can publish and un-pubish with just enabling and disabling tracks', async () => {
Expand Down Expand Up @@ -178,7 +175,6 @@ describe('Publisher', () => {
// initial publish
await publisher.publishStream(mediaStream, track, TrackType.VIDEO);

expect(state.localParticipant?.videoDeviceId).toEqual('test-device-id');
expect(state.localParticipant?.publishedTracks).toContain(TrackType.VIDEO);
expect(track.enabled).toBe(true);
expect(state.localParticipant?.videoStream).toEqual(mediaStream);
Expand Down
23 changes: 4 additions & 19 deletions packages/client/src/rtc/flows/join.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,7 @@ import {
JoinCallRequest,
JoinCallResponse,
} from '../../gen/coordinator';
import {
isStreamVideoLocalParticipant,
JoinCallData,
StreamVideoLocalParticipant,
StreamVideoParticipant,
} from '../../types';
import { JoinCallData, StreamVideoParticipant } from '../../types';
import { StreamClient } from '../../coordinator/connection/client';

/**
Expand Down Expand Up @@ -107,21 +102,11 @@ const getCascadingModeParams = () => {
* @param source the participant to reconcile from.
*/
export const reconcileParticipantLocalState = (
target: StreamVideoParticipant | StreamVideoLocalParticipant,
source?: StreamVideoParticipant | StreamVideoLocalParticipant,
target: StreamVideoParticipant,
source?: StreamVideoParticipant,
) => {
if (!source) return target;

// copy everything from source to target
Object.assign(target, source);

if (
isStreamVideoLocalParticipant(source) &&
isStreamVideoLocalParticipant(target)
) {
target.audioDeviceId = source.audioDeviceId;
target.videoDeviceId = source.videoDeviceId;
target.audioOutputDeviceId = source.audioOutputDeviceId;
}
return target;
return Object.assign(target, source);
};
23 changes: 1 addition & 22 deletions packages/client/src/rtc/helpers/tracks.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import { TrackType } from '../../gen/video/sfu/models/models';
import type {
StreamVideoLocalParticipant,
StreamVideoParticipant,
} from '../../types';
import type { StreamVideoParticipant } from '../../types';
import { TrackMuteType } from '../../types';

export const trackTypeToParticipantStreamKey = (
Expand All @@ -25,24 +22,6 @@ export const trackTypeToParticipantStreamKey = (
}
};

export const trackTypeToDeviceIdKey = (
trackType: TrackType,
): keyof StreamVideoLocalParticipant | undefined => {
switch (trackType) {
case TrackType.AUDIO:
return 'audioDeviceId';
case TrackType.VIDEO:
return 'videoDeviceId';
case TrackType.SCREEN_SHARE:
case TrackType.SCREEN_SHARE_AUDIO:
case TrackType.UNSPECIFIED:
return undefined;
default:
const exhaustiveTrackTypeCheck: never = trackType;
throw new Error(`Unknown track type: ${exhaustiveTrackTypeCheck}`);
}
};

export const muteTypeToTrackType = (muteType: TrackMuteType): TrackType => {
switch (muteType) {
case 'audio':
Expand Down
Loading

0 comments on commit 04c1c20

Please sign in to comment.