@stream-io/video-client 1.53.0 → 1.53.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/dist/index.browser.es.js +21 -3
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +21 -3
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +21 -3
- package/dist/index.es.js.map +1 -1
- package/dist/src/devices/MicrophoneManager.d.ts +6 -0
- package/dist/src/types.d.ts +11 -0
- package/package.json +1 -1
- package/src/devices/MicrophoneManager.ts +16 -0
- package/src/devices/__tests__/MicrophoneManagerRN.test.ts +74 -1
- package/src/reporting/ClientEventReporter.ts +5 -0
- package/src/types.ts +12 -0
|
@@ -71,5 +71,11 @@ export declare class MicrophoneManager extends AudioDeviceManager<MicrophoneMana
|
|
|
71
71
|
private startSpeakingWhileMutedDetection;
|
|
72
72
|
private stopSpeakingWhileMutedDetection;
|
|
73
73
|
private teardownSpeakingWhileMutedDetection;
|
|
74
|
+
/**
|
|
75
|
+
* iOS-only: keep the mic-input chain prepared while muted
|
|
76
|
+
* so the `AVAudioEngine` stays full-duplex and remote audio renders on a
|
|
77
|
+
* muted join.
|
|
78
|
+
*/
|
|
79
|
+
private setMutedRecordingPrepared;
|
|
74
80
|
private hasPermission;
|
|
75
81
|
}
|
package/dist/src/types.d.ts
CHANGED
|
@@ -387,6 +387,17 @@ export type StreamRNVideoSDKGlobals = {
|
|
|
387
387
|
* Stops the in call manager.
|
|
388
388
|
*/
|
|
389
389
|
stop({ isRingingTypeCall }: StreamRNVideoSDKCallManagerRingingParams): void;
|
|
390
|
+
/**
|
|
391
|
+
* iOS-only. Keeps the audio engine's microphone-input (voice-processing)
|
|
392
|
+
* chain prepared while the mic is muted, so the `AVAudioEngine` stays full-duplex
|
|
393
|
+
* meaning: mic going out and speaker coming in, simultaneously
|
|
394
|
+
*
|
|
395
|
+
* Without this, joining muted builds an output-only engine under the
|
|
396
|
+
* `PlayAndRecord`/`VoiceChat` (VPIO) session, which makes the remote audio
|
|
397
|
+
* not audible when joining muted. As echo cancellation expects the mic to be
|
|
398
|
+
* prepared.
|
|
399
|
+
*/
|
|
400
|
+
setMutedRecordingPrepared?(enabled: boolean): void;
|
|
390
401
|
};
|
|
391
402
|
permissions: {
|
|
392
403
|
/**
|
package/package.json
CHANGED
|
@@ -76,6 +76,7 @@ export class MicrophoneManager extends AudioDeviceManager<MicrophoneManagerState
|
|
|
76
76
|
]) => {
|
|
77
77
|
try {
|
|
78
78
|
if (callingState === CallingState.LEFT) {
|
|
79
|
+
this.setMutedRecordingPrepared(false);
|
|
79
80
|
await this.stopSpeakingWhileMutedDetection();
|
|
80
81
|
}
|
|
81
82
|
if (callingState !== CallingState.JOINED) return;
|
|
@@ -84,11 +85,14 @@ export class MicrophoneManager extends AudioDeviceManager<MicrophoneManagerState
|
|
|
84
85
|
if (ownCapabilities.includes(OwnCapability.SEND_AUDIO)) {
|
|
85
86
|
const hasPermission = await this.hasPermission(permissionState);
|
|
86
87
|
if (hasPermission && status !== 'enabled') {
|
|
88
|
+
this.setMutedRecordingPrepared(true);
|
|
87
89
|
await this.startSpeakingWhileMutedDetection(deviceId);
|
|
88
90
|
} else {
|
|
91
|
+
this.setMutedRecordingPrepared(false);
|
|
89
92
|
await this.stopSpeakingWhileMutedDetection();
|
|
90
93
|
}
|
|
91
94
|
} else {
|
|
95
|
+
this.setMutedRecordingPrepared(false);
|
|
92
96
|
await this.stopSpeakingWhileMutedDetection();
|
|
93
97
|
}
|
|
94
98
|
} catch (err) {
|
|
@@ -467,6 +471,18 @@ export class MicrophoneManager extends AudioDeviceManager<MicrophoneManagerState
|
|
|
467
471
|
});
|
|
468
472
|
}
|
|
469
473
|
|
|
474
|
+
/**
|
|
475
|
+
* iOS-only: keep the mic-input chain prepared while muted
|
|
476
|
+
* so the `AVAudioEngine` stays full-duplex and remote audio renders on a
|
|
477
|
+
* muted join.
|
|
478
|
+
*/
|
|
479
|
+
private setMutedRecordingPrepared(enabled: boolean): void {
|
|
480
|
+
if (!isReactNative()) return;
|
|
481
|
+
globalThis.streamRNVideoSDK?.callManager.setMutedRecordingPrepared?.(
|
|
482
|
+
enabled,
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
|
|
470
486
|
private async hasPermission(
|
|
471
487
|
permissionState: BrowserPermissionState,
|
|
472
488
|
): Promise<boolean> {
|
|
@@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
2
2
|
import { MicrophoneManager } from '../MicrophoneManager';
|
|
3
3
|
import { Call } from '../../Call';
|
|
4
4
|
import { StreamClient } from '../../coordinator/connection/client';
|
|
5
|
-
import { StreamVideoWriteableStateStore } from '../../store';
|
|
5
|
+
import { CallingState, StreamVideoWriteableStateStore } from '../../store';
|
|
6
6
|
import {
|
|
7
7
|
mockAudioDevices,
|
|
8
8
|
mockAudioStream,
|
|
@@ -54,11 +54,13 @@ describe('MicrophoneManager React Native', () => {
|
|
|
54
54
|
let manager: MicrophoneManager;
|
|
55
55
|
let checkPermissionMock: ReturnType<typeof vi.fn>;
|
|
56
56
|
let subscribeMock: ReturnType<typeof vi.fn>;
|
|
57
|
+
let setMutedRecordingPreparedMock: ReturnType<typeof vi.fn>;
|
|
57
58
|
|
|
58
59
|
beforeEach(() => {
|
|
59
60
|
speechActivityCallback = null;
|
|
60
61
|
unsubscribeMocks = [];
|
|
61
62
|
checkPermissionMock = vi.fn(async () => true);
|
|
63
|
+
setMutedRecordingPreparedMock = vi.fn();
|
|
62
64
|
subscribeMock = vi.fn((cb) => {
|
|
63
65
|
speechActivityCallback = cb;
|
|
64
66
|
const unsub = vi.fn();
|
|
@@ -71,6 +73,7 @@ describe('MicrophoneManager React Native', () => {
|
|
|
71
73
|
setup: vi.fn(),
|
|
72
74
|
start: vi.fn(),
|
|
73
75
|
stop: vi.fn(),
|
|
76
|
+
setMutedRecordingPrepared: setMutedRecordingPreparedMock,
|
|
74
77
|
},
|
|
75
78
|
permissions: {
|
|
76
79
|
check: checkPermissionMock,
|
|
@@ -210,6 +213,76 @@ describe('MicrophoneManager React Native', () => {
|
|
|
210
213
|
expect(fn).toHaveBeenCalled();
|
|
211
214
|
});
|
|
212
215
|
|
|
216
|
+
it('should prepare muted recording when mic is disabled and user can send audio', async () => {
|
|
217
|
+
await manager.enable();
|
|
218
|
+
await manager.disable();
|
|
219
|
+
|
|
220
|
+
await vi.waitUntil(
|
|
221
|
+
() =>
|
|
222
|
+
setMutedRecordingPreparedMock.mock.calls.some(([arg]) => arg === true),
|
|
223
|
+
{ timeout: 100 },
|
|
224
|
+
);
|
|
225
|
+
expect(setMutedRecordingPreparedMock).toHaveBeenCalledWith(true);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('should release prepared muted recording when mic is enabled', async () => {
|
|
229
|
+
await manager.disable();
|
|
230
|
+
setMutedRecordingPreparedMock.mockClear();
|
|
231
|
+
await manager.enable();
|
|
232
|
+
|
|
233
|
+
await vi.waitUntil(
|
|
234
|
+
() =>
|
|
235
|
+
setMutedRecordingPreparedMock.mock.calls.some(([arg]) => arg === false),
|
|
236
|
+
{ timeout: 100 },
|
|
237
|
+
);
|
|
238
|
+
expect(setMutedRecordingPreparedMock).toHaveBeenCalledWith(false);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('should not prepare muted recording when user cannot send audio', async () => {
|
|
242
|
+
await manager.disable();
|
|
243
|
+
manager['call'].state.setOwnCapabilities([]);
|
|
244
|
+
setMutedRecordingPreparedMock.mockClear();
|
|
245
|
+
// toggle status to re-run the reactive subscription with no send-audio cap
|
|
246
|
+
await manager.enable();
|
|
247
|
+
await manager.disable();
|
|
248
|
+
|
|
249
|
+
await vi.waitUntil(
|
|
250
|
+
() => setMutedRecordingPreparedMock.mock.calls.length > 0,
|
|
251
|
+
{ timeout: 100 },
|
|
252
|
+
);
|
|
253
|
+
expect(setMutedRecordingPreparedMock).not.toHaveBeenCalledWith(true);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('should not prepare muted recording if native microphone permission is denied', async () => {
|
|
257
|
+
checkPermissionMock.mockResolvedValue(false);
|
|
258
|
+
await manager.enable();
|
|
259
|
+
await manager.disable();
|
|
260
|
+
|
|
261
|
+
await vi.waitUntil(() => checkPermissionMock.mock.calls.length > 0, {
|
|
262
|
+
timeout: 100,
|
|
263
|
+
});
|
|
264
|
+
expect(setMutedRecordingPreparedMock).not.toHaveBeenCalledWith(true);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('should release prepared muted recording when the call is left', async () => {
|
|
268
|
+
await manager.disable();
|
|
269
|
+
await vi.waitUntil(
|
|
270
|
+
() =>
|
|
271
|
+
setMutedRecordingPreparedMock.mock.calls.some(([arg]) => arg === true),
|
|
272
|
+
{ timeout: 100 },
|
|
273
|
+
);
|
|
274
|
+
setMutedRecordingPreparedMock.mockClear();
|
|
275
|
+
|
|
276
|
+
manager['call'].state.setCallingState(CallingState.LEFT);
|
|
277
|
+
|
|
278
|
+
await vi.waitUntil(
|
|
279
|
+
() =>
|
|
280
|
+
setMutedRecordingPreparedMock.mock.calls.some(([arg]) => arg === false),
|
|
281
|
+
{ timeout: 100 },
|
|
282
|
+
);
|
|
283
|
+
expect(setMutedRecordingPreparedMock).toHaveBeenCalledWith(false);
|
|
284
|
+
});
|
|
285
|
+
|
|
213
286
|
afterEach(() => {
|
|
214
287
|
globalThis.streamRNVideoSDK = undefined;
|
|
215
288
|
vi.clearAllMocks();
|
|
@@ -390,8 +390,13 @@ export class ClientEventReporter {
|
|
|
390
390
|
const joinAttemptId = this.joinAttemptIds.get(cid);
|
|
391
391
|
if (!joinAttemptId) return;
|
|
392
392
|
const coordinatorConnectId = this.coordinatorConnectId;
|
|
393
|
+
const ctx = this.callContexts.get(cid);
|
|
394
|
+
|
|
393
395
|
this.send({
|
|
394
396
|
user_id: this.streamClient.userID,
|
|
397
|
+
type: ctx?.callType,
|
|
398
|
+
id: ctx?.callId,
|
|
399
|
+
call_cid: cid,
|
|
395
400
|
stage: 'JoinInitiated',
|
|
396
401
|
join_attempt_id: joinAttemptId,
|
|
397
402
|
...(coordinatorConnectId && {
|
package/src/types.ts
CHANGED
|
@@ -480,6 +480,18 @@ export type StreamRNVideoSDKGlobals = {
|
|
|
480
480
|
* Stops the in call manager.
|
|
481
481
|
*/
|
|
482
482
|
stop({ isRingingTypeCall }: StreamRNVideoSDKCallManagerRingingParams): void;
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* iOS-only. Keeps the audio engine's microphone-input (voice-processing)
|
|
486
|
+
* chain prepared while the mic is muted, so the `AVAudioEngine` stays full-duplex
|
|
487
|
+
* meaning: mic going out and speaker coming in, simultaneously
|
|
488
|
+
*
|
|
489
|
+
* Without this, joining muted builds an output-only engine under the
|
|
490
|
+
* `PlayAndRecord`/`VoiceChat` (VPIO) session, which makes the remote audio
|
|
491
|
+
* not audible when joining muted. As echo cancellation expects the mic to be
|
|
492
|
+
* prepared.
|
|
493
|
+
*/
|
|
494
|
+
setMutedRecordingPrepared?(enabled: boolean): void;
|
|
483
495
|
};
|
|
484
496
|
permissions: {
|
|
485
497
|
/**
|