Skip to content

Commit

Permalink
Merge branch 'feature/WD-1079' into fast-reconnects
Browse files Browse the repository at this point in the history
  • Loading branch information
oliverlaz committed Dec 8, 2023
2 parents 14d794d + e6becb9 commit ac14d70
Show file tree
Hide file tree
Showing 82 changed files with 1,011 additions and 422 deletions.
7 changes: 7 additions & 0 deletions packages/client/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver).

### [0.5.1](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-0.5.0...@stream-io/video-client-0.5.1) (2023-12-05)


### Features

* **client:** speaking while muted in React Native using temporary peer connection ([#1207](https://github.com/GetStream/stream-video-js/issues/1207)) ([9093006](https://github.com/GetStream/stream-video-js/commit/90930063503b6dfb83572dad8a31e45b16bf1685))

## [0.5.0](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-0.4.10...@stream-io/video-client-0.5.0) (2023-11-29)


Expand Down
2 changes: 1 addition & 1 deletion packages/client/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@stream-io/video-client",
"version": "0.5.0",
"version": "0.5.1",
"packageManager": "[email protected]",
"main": "dist/index.cjs.js",
"module": "dist/index.es.js",
Expand Down
34 changes: 24 additions & 10 deletions packages/client/src/devices/MicrophoneManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import { createSoundDetector } from '../helpers/sound-detector';
import { isReactNative } from '../helpers/platforms';
import { OwnCapability } from '../gen/coordinator';
import { CallingState } from '../store';
import { RNSpeechDetector } from '../helpers/RNSpeechDetector';

export class MicrophoneManager extends InputMediaDeviceManager<MicrophoneManagerState> {
private soundDetectorCleanup?: Function;
private rnSpeechDetector: RNSpeechDetector | undefined;

constructor(call: Call) {
super(call, new MicrophoneManagerState(), TrackType.AUDIO);
Expand Down Expand Up @@ -58,21 +60,33 @@ export class MicrophoneManager extends InputMediaDeviceManager<MicrophoneManager
}

private async startSpeakingWhileMutedDetection(deviceId?: string) {
await this.stopSpeakingWhileMutedDetection();
if (isReactNative()) {
return;
this.rnSpeechDetector = new RNSpeechDetector();
await this.rnSpeechDetector.start();
const unsubscribe = this.rnSpeechDetector?.onSpeakingDetectedStateChange(
(event) => {
this.state.setSpeakingWhileMuted(event.isSoundDetected);
},
);
this.soundDetectorCleanup = () => {
unsubscribe();
this.rnSpeechDetector?.stop();
this.rnSpeechDetector = undefined;
};
} else {
// Need to start a new stream that's not connected to publisher
const stream = await this.getStream({
deviceId,
});
this.soundDetectorCleanup = createSoundDetector(stream, (event) => {
this.state.setSpeakingWhileMuted(event.isSoundDetected);
});
}
await this.stopSpeakingWhileMutedDetection();
// Need to start a new stream that's not connected to publisher
const stream = await this.getStream({
deviceId,
});
this.soundDetectorCleanup = createSoundDetector(stream, (event) => {
this.state.setSpeakingWhileMuted(event.isSoundDetected);
});
}

private async stopSpeakingWhileMutedDetection() {
if (isReactNative() || !this.soundDetectorCleanup) {
if (!this.soundDetectorCleanup) {
return;
}
this.state.setSpeakingWhileMuted(false);
Expand Down
126 changes: 126 additions & 0 deletions packages/client/src/devices/__tests__/MicrophoneManagerRN.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { MicrophoneManager } from '../MicrophoneManager';
import { Call } from '../../Call';
import { StreamClient } from '../../coordinator/connection/client';
import { StreamVideoWriteableStateStore } from '../../store';
import { mockAudioDevices, mockAudioStream, mockCall } from './mocks';
import { of } from 'rxjs';
import '../../rtc/__tests__/mocks/webrtc.mocks';
import { OwnCapability } from '../../gen/coordinator';

let handler;

vi.mock('../../helpers/platforms.ts', () => {
return {
isReactNative: vi.fn(() => true),
};
});

vi.mock('../devices.ts', () => {
console.log('MOCKING devices API');
return {
disposeOfMediaStream: vi.fn(),
getAudioDevices: vi.fn(() => {
return of(mockAudioDevices);
}),
getAudioStream: vi.fn(() => Promise.resolve(mockAudioStream())),
deviceIds$: {},
};
});

vi.mock('../../Call.ts', () => {
console.log('MOCKING Call');
return {
Call: vi.fn(() => mockCall()),
};
});

vi.mock('../../helpers/RNSpeechDetector.ts', () => {
console.log('MOCKING RNSpeechDetector');
return {
RNSpeechDetector: vi.fn().mockImplementation(() => ({
start: vi.fn(),
stop: vi.fn(),
onSpeakingDetectedStateChange: vi.fn((callback) => {
handler = callback;
return vi.fn();
}),
})),
};
});

describe('MicrophoneManager React Native', () => {
let manager: MicrophoneManager;
beforeEach(() => {
manager = new MicrophoneManager(
new Call({
id: '',
type: '',
streamClient: new StreamClient('abc123'),
clientStore: new StreamVideoWriteableStateStore(),
}),
);
});

it(`should start sound detection if mic is disabled`, async () => {
await manager.enable();
// @ts-expect-error
vi.spyOn(manager, 'startSpeakingWhileMutedDetection');
await manager.disable();

expect(manager['startSpeakingWhileMutedDetection']).toHaveBeenCalled();
expect(manager['rnSpeechDetector']?.start).toHaveBeenCalled();
});

it(`should stop sound detection if mic is enabled`, async () => {
manager.state.setSpeakingWhileMuted(true);
manager['soundDetectorCleanup'] = () => {};

await manager.enable();

expect(manager.state.speakingWhileMuted).toBe(false);
});

it('should update speaking while muted state', async () => {
await manager['startSpeakingWhileMutedDetection']();

expect(manager.state.speakingWhileMuted).toBe(false);

handler!({ isSoundDetected: true, audioLevel: 2 });

expect(manager.state.speakingWhileMuted).toBe(true);

handler!({ isSoundDetected: false, audioLevel: 0 });

expect(manager.state.speakingWhileMuted).toBe(false);
});

it('should stop speaking while muted notifications if user loses permission to send audio', async () => {
await manager.enable();
await manager.disable();

// @ts-expect-error
vi.spyOn(manager, 'stopSpeakingWhileMutedDetection');
manager['call'].state.setOwnCapabilities([]);

expect(manager['stopSpeakingWhileMutedDetection']).toHaveBeenCalled();
});

it('should start speaking while muted notifications if user gains permission to send audio', async () => {
await manager.enable();
await manager.disable();

manager['call'].state.setOwnCapabilities([]);

// @ts-expect-error
vi.spyOn(manager, 'stopSpeakingWhileMutedDetection');
manager['call'].state.setOwnCapabilities([OwnCapability.SEND_AUDIO]);

expect(manager['stopSpeakingWhileMutedDetection']).toHaveBeenCalled();
});

afterEach(() => {
vi.clearAllMocks();
vi.resetModules();
});
});
112 changes: 112 additions & 0 deletions packages/client/src/helpers/RNSpeechDetector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { BaseStats } from '../stats/types';
import { SoundStateChangeHandler } from './sound-detector';

/**
* Flatten the stats report into an array of stats objects.
*
* @param report the report to flatten.
*/
const flatten = (report: RTCStatsReport) => {
const stats: RTCStats[] = [];
report.forEach((s) => {
stats.push(s);
});
return stats;
};

const AUDIO_LEVEL_THRESHOLD = 0.2;

export class RNSpeechDetector {
private pc1 = new RTCPeerConnection({});
private pc2 = new RTCPeerConnection({});
private intervalId: NodeJS.Timer | undefined;

/**
* Starts the speech detection.
*/
public async start() {
try {
const audioStream = await navigator.mediaDevices.getUserMedia({
audio: true,
});

this.pc1.addEventListener('icecandidate', async (e) => {
await this.pc2.addIceCandidate(
e.candidate as RTCIceCandidateInit | undefined,
);
});
this.pc2.addEventListener('icecandidate', async (e) => {
await this.pc1.addIceCandidate(
e.candidate as RTCIceCandidateInit | undefined,
);
});

audioStream
.getTracks()
.forEach((track) => this.pc1.addTrack(track, audioStream));
const offer = await this.pc1.createOffer({});
await this.pc2.setRemoteDescription(offer);
await this.pc1.setLocalDescription(offer);
const answer = await this.pc2.createAnswer();
await this.pc1.setRemoteDescription(answer);
await this.pc2.setLocalDescription(answer);
const audioTracks = audioStream.getAudioTracks();
// We need to mute the audio track for this temporary stream, or else you will hear yourself twice while in the call.
audioTracks.forEach((track) => (track.enabled = false));
} catch (error) {
console.error(
'Error connecting and negotiating between PeerConnections:',
error,
);
}
}

/**
* Stops the speech detection and releases all allocated resources.
*/
public stop() {
this.pc1.close();
this.pc2.close();
if (this.intervalId) {
clearInterval(this.intervalId);
}
}

/**
* Public method that detects the audio levels and returns the status.
*/
public onSpeakingDetectedStateChange(
onSoundDetectedStateChanged: SoundStateChangeHandler,
) {
this.intervalId = setInterval(async () => {
const stats = (await this.pc1.getStats()) as RTCStatsReport;
const report = flatten(stats);
// Audio levels are present inside stats of type `media-source` and of kind `audio`
const audioMediaSourceStats = report.find(
(stat) =>
stat.type === 'media-source' &&
(stat as RTCRtpStreamStats).kind === 'audio',
) as BaseStats;
if (audioMediaSourceStats) {
const { audioLevel } = audioMediaSourceStats;
if (audioLevel) {
if (audioLevel >= AUDIO_LEVEL_THRESHOLD) {
onSoundDetectedStateChanged({
isSoundDetected: true,
audioLevel,
});
} else {
onSoundDetectedStateChanged({
isSoundDetected: false,
audioLevel: 0,
});
}
}
}
}, 1000);

return () => {
clearInterval(this.intervalId);
};
}
}
1 change: 1 addition & 0 deletions packages/client/src/stats/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export type BaseStats = {
audioLevel?: number;
bytesSent?: number;
bytesReceived?: number;
codec?: string;
Expand Down
5 changes: 5 additions & 0 deletions packages/react-bindings/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver).

### [0.3.12](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-react-bindings-0.3.11...@stream-io/video-react-bindings-0.3.12) (2023-12-05)

### Dependency Updates

* `@stream-io/video-client` updated to version `0.5.1`
### [0.3.11](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-react-bindings-0.3.10...@stream-io/video-react-bindings-0.3.11) (2023-11-29)

### Dependency Updates
Expand Down
2 changes: 1 addition & 1 deletion packages/react-bindings/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@stream-io/video-react-bindings",
"version": "0.3.11",
"version": "0.3.12",
"packageManager": "[email protected]",
"main": "./dist/index.cjs.js",
"module": "./dist/index.es.js",
Expand Down
18 changes: 18 additions & 0 deletions packages/react-native-sdk/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,24 @@

This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver).

### [0.3.4](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-react-native-sdk-0.3.3...@stream-io/video-react-native-sdk-0.3.4) (2023-12-06)


### Bug Fixes

* **react-native:** unnecessary setState in initial device management ([#1211](https://github.com/GetStream/stream-video-js/issues/1211)) ([c9a10c3](https://github.com/GetStream/stream-video-js/commit/c9a10c3938aeddcae0008d4de84a604c873dcbde))

### [0.3.3](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-react-native-sdk-0.3.2...@stream-io/video-react-native-sdk-0.3.3) (2023-12-05)

### Dependency Updates

* `@stream-io/video-client` updated to version `0.5.1`
* `@stream-io/video-react-bindings` updated to version `0.3.12`

### Features

* **client:** speaking while muted in React Native using temporary peer connection ([#1207](https://github.com/GetStream/stream-video-js/issues/1207)) ([9093006](https://github.com/GetStream/stream-video-js/commit/90930063503b6dfb83572dad8a31e45b16bf1685))

### [0.3.2](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-react-native-sdk-0.3.1...@stream-io/video-react-native-sdk-0.3.2) (2023-12-04)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ Welcome to the Stream Video React Native SDK - a comprehensive toolkit designed

Our React Native SDK comes with user-friendly UI components, easy-to-use React hooks, and context providers/wrappers, making your development process seamless. Moreover, all calls are routed through Stream's global edge network, ensuring lower latency and higher reliability due to proximity to end users.

If you're new to Stream React Native Video SDK, we recommend starting with the following three tutorials, depending upon your requirements:
If you're new to Stream React Native Video SDK, we recommend starting with the following three tutorials, depending on your requirements:

- [Video & Audio Calling Tutorial](https://getstream.io/video/sdk/reactnative/tutorial/video-calling/)
- [Audio Room Tutorial](https://getstream.io/video/sdk/reactnative/tutorial/livestreaming/)
- [Livestream Tutorial](https://getstream.io/video/sdk/reactnative/tutorial/audio-room/)
- [Audio Room Tutorial](https://getstream.io/video/sdk/reactnative/tutorial/audio-room/)
- [Livestream Tutorial](https://getstream.io/video/sdk/reactnative/tutorial/livestreaming/)

After the tutorials, the documentation explains how to use:

Expand All @@ -23,7 +23,7 @@ After the tutorials, the documentation explains how to use:
It also explains advanced features such as:

- [Ringing/Calls](./core/joining-and-creating-calls/)
- [Requesting & Granting permissions](./ui-cookbook/permission-requests/)
- [Requesting and Granting permissions](./ui-cookbook/permission-requests/)
- [Participants Layout Switching](./ui-cookbook/runtime-layout-switching/)
- Friendly support for [Push notifications](./advanced/push-notifications/setup), [deep linking](./advanced/deeplinking/) and Reconnection of calls.

Expand Down
Loading

0 comments on commit ac14d70

Please sign in to comment.