@stream-io/video-client 1.5.0-0 → 1.5.0
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 +6 -230
- package/dist/index.browser.es.js +1498 -1963
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +1495 -1961
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +1498 -1963
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +9 -93
- package/dist/src/StreamSfuClient.d.ts +56 -72
- package/dist/src/StreamVideoClient.d.ts +10 -2
- package/dist/src/coordinator/connection/client.d.ts +4 -3
- package/dist/src/coordinator/connection/types.d.ts +1 -5
- package/dist/src/devices/InputMediaDeviceManager.d.ts +0 -4
- package/dist/src/devices/MicrophoneManager.d.ts +1 -1
- package/dist/src/events/callEventHandlers.d.ts +3 -1
- package/dist/src/events/internal.d.ts +0 -4
- package/dist/src/gen/video/sfu/event/events.d.ts +4 -106
- package/dist/src/gen/video/sfu/models/models.d.ts +65 -64
- package/dist/src/logger.d.ts +0 -1
- package/dist/src/rpc/createClient.d.ts +0 -2
- package/dist/src/rpc/index.d.ts +0 -1
- package/dist/src/rtc/Dispatcher.d.ts +1 -1
- package/dist/src/rtc/IceTrickleBuffer.d.ts +1 -0
- package/dist/src/rtc/Publisher.d.ts +25 -24
- package/dist/src/rtc/Subscriber.d.ts +11 -12
- package/dist/src/rtc/flows/join.d.ts +20 -0
- package/dist/src/rtc/helpers/tracks.d.ts +3 -3
- package/dist/src/rtc/signal.d.ts +1 -1
- package/dist/src/store/CallState.d.ts +2 -46
- package/package.json +3 -3
- package/src/Call.ts +562 -615
- package/src/StreamSfuClient.ts +246 -277
- package/src/StreamVideoClient.ts +65 -15
- package/src/coordinator/connection/client.ts +8 -25
- package/src/coordinator/connection/connection.ts +0 -1
- package/src/coordinator/connection/token_manager.ts +1 -1
- package/src/coordinator/connection/types.ts +0 -6
- package/src/devices/BrowserPermission.ts +1 -5
- package/src/devices/CameraManager.ts +1 -1
- package/src/devices/InputMediaDeviceManager.ts +3 -12
- package/src/devices/MicrophoneManager.ts +3 -3
- package/src/devices/devices.ts +1 -1
- package/src/events/__tests__/mutes.test.ts +13 -10
- package/src/events/__tests__/participant.test.ts +0 -75
- package/src/events/callEventHandlers.ts +7 -4
- package/src/events/internal.ts +3 -20
- package/src/events/mutes.ts +3 -5
- package/src/events/participant.ts +15 -48
- package/src/gen/video/sfu/event/events.ts +8 -451
- package/src/gen/video/sfu/models/models.ts +204 -211
- package/src/logger.ts +1 -3
- package/src/rpc/createClient.ts +0 -21
- package/src/rpc/index.ts +0 -1
- package/src/rtc/Dispatcher.ts +2 -6
- package/src/rtc/IceTrickleBuffer.ts +2 -2
- package/src/rtc/Publisher.ts +163 -127
- package/src/rtc/Subscriber.ts +155 -94
- package/src/rtc/__tests__/Publisher.test.ts +95 -18
- package/src/rtc/__tests__/Subscriber.test.ts +99 -63
- package/src/rtc/__tests__/videoLayers.test.ts +2 -2
- package/src/rtc/flows/join.ts +65 -0
- package/src/rtc/helpers/tracks.ts +7 -27
- package/src/rtc/signal.ts +3 -3
- package/src/rtc/videoLayers.ts +10 -1
- package/src/stats/SfuStatsReporter.ts +0 -1
- package/src/store/CallState.ts +2 -109
- package/src/store/__tests__/CallState.test.ts +37 -48
- package/dist/src/helpers/ensureExhausted.d.ts +0 -1
- package/dist/src/helpers/withResolvers.d.ts +0 -14
- package/dist/src/rpc/retryable.d.ts +0 -23
- package/dist/src/rtc/helpers/rtcConfiguration.d.ts +0 -2
- package/src/helpers/ensureExhausted.ts +0 -5
- package/src/helpers/withResolvers.ts +0 -43
- package/src/rpc/__tests__/retryable.test.ts +0 -72
- package/src/rpc/retryable.ts +0 -57
- package/src/rtc/helpers/rtcConfiguration.ts +0 -11
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import '../../rtc/__tests__/mocks/webrtc.mocks';
|
|
2
1
|
import { describe, expect, it, vi } from 'vitest';
|
|
3
2
|
import { anyNumber } from 'vitest-mock-extended';
|
|
4
3
|
import { StreamVideoParticipant, VisibilityState } from '../../types';
|
|
5
4
|
import { CallingState, CallState } from '../CallState';
|
|
6
|
-
import {
|
|
5
|
+
import { ConnectionQuality } from '../../gen/video/sfu/models/models';
|
|
7
6
|
import {
|
|
8
7
|
combineComparators,
|
|
9
8
|
conditional,
|
|
@@ -464,17 +463,40 @@ describe('CallState', () => {
|
|
|
464
463
|
describe('Call Permission Events', () => {
|
|
465
464
|
it('handles call.permissions_updated', () => {
|
|
466
465
|
const state = new CallState();
|
|
467
|
-
|
|
468
|
-
|
|
466
|
+
state.setParticipants([
|
|
467
|
+
{
|
|
468
|
+
userId: 'test',
|
|
469
|
+
name: 'test',
|
|
470
|
+
sessionId: 'test',
|
|
471
|
+
isDominantSpeaker: false,
|
|
472
|
+
isSpeaking: false,
|
|
473
|
+
audioLevel: 0,
|
|
474
|
+
image: '',
|
|
475
|
+
publishedTracks: [],
|
|
476
|
+
connectionQuality: ConnectionQuality.EXCELLENT,
|
|
477
|
+
roles: [],
|
|
478
|
+
trackLookupPrefix: '',
|
|
479
|
+
isLocalParticipant: true,
|
|
480
|
+
},
|
|
481
|
+
]);
|
|
469
482
|
|
|
470
483
|
state.updateFromEvent({
|
|
471
484
|
type: 'call.permissions_updated',
|
|
485
|
+
created_at: '',
|
|
486
|
+
call_cid: 'development:12345',
|
|
472
487
|
own_capabilities: [
|
|
473
488
|
OwnCapability.SEND_AUDIO,
|
|
474
489
|
OwnCapability.SEND_VIDEO,
|
|
475
490
|
],
|
|
476
|
-
|
|
477
|
-
|
|
491
|
+
user: {
|
|
492
|
+
id: 'test',
|
|
493
|
+
created_at: '',
|
|
494
|
+
role: '',
|
|
495
|
+
updated_at: '',
|
|
496
|
+
custom: {},
|
|
497
|
+
teams: [],
|
|
498
|
+
language: 'en',
|
|
499
|
+
},
|
|
478
500
|
});
|
|
479
501
|
|
|
480
502
|
expect(state.ownCapabilities).toEqual([
|
|
@@ -487,8 +509,15 @@ describe('CallState', () => {
|
|
|
487
509
|
created_at: '',
|
|
488
510
|
call_cid: 'development:12345',
|
|
489
511
|
own_capabilities: [OwnCapability.SEND_VIDEO],
|
|
490
|
-
|
|
491
|
-
|
|
512
|
+
user: {
|
|
513
|
+
id: 'test',
|
|
514
|
+
created_at: '',
|
|
515
|
+
role: '',
|
|
516
|
+
updated_at: '',
|
|
517
|
+
custom: {},
|
|
518
|
+
teams: [],
|
|
519
|
+
language: 'en',
|
|
520
|
+
},
|
|
492
521
|
});
|
|
493
522
|
expect(state.ownCapabilities).toEqual([OwnCapability.SEND_VIDEO]);
|
|
494
523
|
});
|
|
@@ -852,44 +881,4 @@ describe('CallState', () => {
|
|
|
852
881
|
});
|
|
853
882
|
});
|
|
854
883
|
});
|
|
855
|
-
|
|
856
|
-
describe('orphaned tracks', () => {
|
|
857
|
-
it('registers orphaned tracks', () => {
|
|
858
|
-
const state = new CallState();
|
|
859
|
-
state.registerOrphanedTrack({
|
|
860
|
-
id: '123:TRACK_TYPE_VIDEO',
|
|
861
|
-
track: new MediaStream(),
|
|
862
|
-
trackLookupPrefix: '123',
|
|
863
|
-
trackType: TrackType.AUDIO,
|
|
864
|
-
});
|
|
865
|
-
expect(state['orphanedTracks'].length).toBe(1);
|
|
866
|
-
});
|
|
867
|
-
|
|
868
|
-
it('removes orphaned tracks once assigned', () => {
|
|
869
|
-
const state = new CallState();
|
|
870
|
-
state.registerOrphanedTrack({
|
|
871
|
-
id: '123:TRACK_TYPE_VIDEO',
|
|
872
|
-
track: new MediaStream(),
|
|
873
|
-
trackLookupPrefix: '123',
|
|
874
|
-
trackType: TrackType.VIDEO,
|
|
875
|
-
});
|
|
876
|
-
const orphans = state.takeOrphanedTracks('123');
|
|
877
|
-
expect(orphans.length).toBe(1);
|
|
878
|
-
expect(state['orphanedTracks'].length).toBe(0);
|
|
879
|
-
});
|
|
880
|
-
|
|
881
|
-
it('removes orphaned tracks', () => {
|
|
882
|
-
const state = new CallState();
|
|
883
|
-
const id = '123:TRACK_TYPE_VIDEO';
|
|
884
|
-
state.registerOrphanedTrack({
|
|
885
|
-
id,
|
|
886
|
-
track: new MediaStream(),
|
|
887
|
-
trackLookupPrefix: '123',
|
|
888
|
-
trackType: TrackType.VIDEO,
|
|
889
|
-
});
|
|
890
|
-
expect(state['orphanedTracks'].length).toBe(1);
|
|
891
|
-
state.removeOrphanedTrack(id);
|
|
892
|
-
expect(state['orphanedTracks'].length).toBe(0);
|
|
893
|
-
});
|
|
894
|
-
});
|
|
895
884
|
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare const ensureExhausted: (x: never, message: string) => void;
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
export type PromiseWithResolvers<T> = {
|
|
2
|
-
promise: Promise<T>;
|
|
3
|
-
resolve: (value: T | PromiseLike<T>) => void;
|
|
4
|
-
reject: (reason: any) => void;
|
|
5
|
-
isResolved: boolean;
|
|
6
|
-
isRejected: boolean;
|
|
7
|
-
};
|
|
8
|
-
/**
|
|
9
|
-
* Creates a new promise with resolvers.
|
|
10
|
-
*
|
|
11
|
-
* Based on:
|
|
12
|
-
* - https://github.com/tc39/proposal-promise-with-resolvers/blob/main/polyfills.js
|
|
13
|
-
*/
|
|
14
|
-
export declare const promiseWithResolvers: <T = void>() => PromiseWithResolvers<T>;
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { FinishedUnaryCall, UnaryCall } from '@protobuf-ts/runtime-rpc';
|
|
2
|
-
import { Error as SfuError } from '../gen/video/sfu/models/models';
|
|
3
|
-
/**
|
|
4
|
-
* An internal interface which asserts that "retryable" SFU responses
|
|
5
|
-
* contain a field called "error".
|
|
6
|
-
* Ideally, this should be coming from the Protobuf definitions.
|
|
7
|
-
*/
|
|
8
|
-
export interface SfuResponseWithError {
|
|
9
|
-
/**
|
|
10
|
-
* An optional error field which should be present in all SFU responses.
|
|
11
|
-
*/
|
|
12
|
-
error?: SfuError;
|
|
13
|
-
}
|
|
14
|
-
/**
|
|
15
|
-
* Creates a closure which wraps the given RPC call and retries invoking
|
|
16
|
-
* the RPC until it succeeds or the maximum number of retries is reached.
|
|
17
|
-
*
|
|
18
|
-
* For each retry, there would be a delay to avoid request bursts toward the SFU.
|
|
19
|
-
*
|
|
20
|
-
* @param rpc the closure around the RPC call to execute.
|
|
21
|
-
* @param signal the signal to abort the RPC call and retries loop.
|
|
22
|
-
*/
|
|
23
|
-
export declare const retryable: <I extends object, O extends SfuResponseWithError>(rpc: () => UnaryCall<I, O>, signal?: AbortSignal) => Promise<FinishedUnaryCall<I, O>>;
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
export type PromiseWithResolvers<T> = {
|
|
2
|
-
promise: Promise<T>;
|
|
3
|
-
resolve: (value: T | PromiseLike<T>) => void;
|
|
4
|
-
reject: (reason: any) => void;
|
|
5
|
-
isResolved: boolean;
|
|
6
|
-
isRejected: boolean;
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Creates a new promise with resolvers.
|
|
11
|
-
*
|
|
12
|
-
* Based on:
|
|
13
|
-
* - https://github.com/tc39/proposal-promise-with-resolvers/blob/main/polyfills.js
|
|
14
|
-
*/
|
|
15
|
-
export const promiseWithResolvers = <T = void>(): PromiseWithResolvers<T> => {
|
|
16
|
-
let resolve: (value: T | PromiseLike<T>) => void;
|
|
17
|
-
let reject: (reason: any) => void;
|
|
18
|
-
const promise = new Promise<T>((_resolve, _reject) => {
|
|
19
|
-
resolve = _resolve;
|
|
20
|
-
reject = _reject;
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
let isResolved = false;
|
|
24
|
-
let isRejected = false;
|
|
25
|
-
|
|
26
|
-
const resolver = (value: T | PromiseLike<T>) => {
|
|
27
|
-
isResolved = true;
|
|
28
|
-
resolve(value);
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
const rejecter = (reason: any) => {
|
|
32
|
-
isRejected = true;
|
|
33
|
-
reject(reason);
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
return {
|
|
37
|
-
promise,
|
|
38
|
-
resolve: resolver,
|
|
39
|
-
reject: rejecter,
|
|
40
|
-
isResolved,
|
|
41
|
-
isRejected,
|
|
42
|
-
};
|
|
43
|
-
};
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { RpcError, UnaryCall } from '@protobuf-ts/runtime-rpc';
|
|
3
|
-
import { TwirpErrorCode } from '@protobuf-ts/twirp-transport';
|
|
4
|
-
import { retryable, SfuResponseWithError } from '../retryable';
|
|
5
|
-
|
|
6
|
-
interface TestResponseWithError extends SfuResponseWithError {
|
|
7
|
-
value: number;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
describe('retryable', () => {
|
|
11
|
-
it('retries the RPC when the SFU instructs to do so', async () => {
|
|
12
|
-
// @ts-expect-error incomplete unary call
|
|
13
|
-
const rpc = vi.fn<any[], UnaryCall<{}, TestResponseWithError>>(() => {
|
|
14
|
-
if (rpc.mock.calls.length <= 2) {
|
|
15
|
-
return { response: { error: { shouldRetry: true } } };
|
|
16
|
-
}
|
|
17
|
-
return { response: { value: 10 } };
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
const result = await retryable(rpc);
|
|
21
|
-
expect(result).toBeDefined();
|
|
22
|
-
expect(result.response.value).toBe(10);
|
|
23
|
-
expect(rpc).toHaveBeenCalledTimes(3);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it('retries when the RPC fails', async () => {
|
|
27
|
-
// @ts-expect-error incomplete unary call
|
|
28
|
-
const rpc = vi.fn<any[], UnaryCall<{}, TestResponseWithError>>(() => {
|
|
29
|
-
if (rpc.mock.calls.length === 1) throw new Error('failed');
|
|
30
|
-
return { response: { value: 10 } };
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
const result = await retryable(rpc);
|
|
34
|
-
expect(result).toBeDefined();
|
|
35
|
-
expect(result.response.value).toBe(10);
|
|
36
|
-
expect(rpc).toHaveBeenCalledTimes(2);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('stops retrying when the RPC is rejected with cancellation error', async () => {
|
|
40
|
-
// @ts-expect-error incomplete unary call
|
|
41
|
-
const rpc = vi.fn<any[], UnaryCall<{}, TestResponseWithError>>(() => {
|
|
42
|
-
if (rpc.mock.calls.length <= 1) {
|
|
43
|
-
throw new Error('Generic error, should retry');
|
|
44
|
-
}
|
|
45
|
-
if (rpc.mock.calls.length === 2) {
|
|
46
|
-
throw new RpcError(
|
|
47
|
-
'Request aborted, should not retry',
|
|
48
|
-
TwirpErrorCode[TwirpErrorCode.cancelled],
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
const result = retryable(rpc);
|
|
54
|
-
await expect(result).rejects.toThrow('Request aborted, should not retry');
|
|
55
|
-
expect(rpc).toHaveBeenCalledTimes(2);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('stops retrying when the aborted via signal', async () => {
|
|
59
|
-
const controller = new AbortController();
|
|
60
|
-
const rpc = vi.fn<any[], UnaryCall<{}, TestResponseWithError>>(() => {
|
|
61
|
-
if (rpc.mock.calls.length <= 1) {
|
|
62
|
-
throw new Error('Generic error, should retry');
|
|
63
|
-
}
|
|
64
|
-
controller.abort();
|
|
65
|
-
throw new Error('Request aborted, should not retry');
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
const result = retryable(rpc, controller.signal);
|
|
69
|
-
await expect(result).rejects.toThrow('Request aborted, should not retry');
|
|
70
|
-
expect(rpc).toHaveBeenCalledTimes(2);
|
|
71
|
-
});
|
|
72
|
-
});
|
package/src/rpc/retryable.ts
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
FinishedUnaryCall,
|
|
3
|
-
RpcError,
|
|
4
|
-
UnaryCall,
|
|
5
|
-
} from '@protobuf-ts/runtime-rpc';
|
|
6
|
-
import { TwirpErrorCode } from '@protobuf-ts/twirp-transport';
|
|
7
|
-
import { retryInterval, sleep } from '../coordinator/connection/utils';
|
|
8
|
-
import { Error as SfuError } from '../gen/video/sfu/models/models';
|
|
9
|
-
import { getLogger } from '../logger';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* An internal interface which asserts that "retryable" SFU responses
|
|
13
|
-
* contain a field called "error".
|
|
14
|
-
* Ideally, this should be coming from the Protobuf definitions.
|
|
15
|
-
*/
|
|
16
|
-
export interface SfuResponseWithError {
|
|
17
|
-
/**
|
|
18
|
-
* An optional error field which should be present in all SFU responses.
|
|
19
|
-
*/
|
|
20
|
-
error?: SfuError;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Creates a closure which wraps the given RPC call and retries invoking
|
|
25
|
-
* the RPC until it succeeds or the maximum number of retries is reached.
|
|
26
|
-
*
|
|
27
|
-
* For each retry, there would be a delay to avoid request bursts toward the SFU.
|
|
28
|
-
*
|
|
29
|
-
* @param rpc the closure around the RPC call to execute.
|
|
30
|
-
* @param signal the signal to abort the RPC call and retries loop.
|
|
31
|
-
*/
|
|
32
|
-
export const retryable = async <
|
|
33
|
-
I extends object,
|
|
34
|
-
O extends SfuResponseWithError,
|
|
35
|
-
>(
|
|
36
|
-
rpc: () => UnaryCall<I, O>,
|
|
37
|
-
signal?: AbortSignal,
|
|
38
|
-
): Promise<FinishedUnaryCall<I, O>> => {
|
|
39
|
-
let attempt = 0;
|
|
40
|
-
let result: FinishedUnaryCall<I, O> | undefined = undefined;
|
|
41
|
-
do {
|
|
42
|
-
if (attempt > 0) await sleep(retryInterval(attempt));
|
|
43
|
-
try {
|
|
44
|
-
result = await rpc();
|
|
45
|
-
} catch (err) {
|
|
46
|
-
const isRequestCancelled =
|
|
47
|
-
err instanceof RpcError &&
|
|
48
|
-
err.code === TwirpErrorCode[TwirpErrorCode.cancelled];
|
|
49
|
-
const isAborted = signal?.aborted ?? false;
|
|
50
|
-
if (isRequestCancelled || isAborted) throw err;
|
|
51
|
-
getLogger(['sfu-client', 'rpc'])('debug', `rpc failed (${attempt})`, err);
|
|
52
|
-
attempt++;
|
|
53
|
-
}
|
|
54
|
-
} while (!result || result.response.error?.shouldRetry);
|
|
55
|
-
|
|
56
|
-
return result;
|
|
57
|
-
};
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { ICEServer } from '../../gen/coordinator';
|
|
2
|
-
|
|
3
|
-
export const toRtcConfiguration = (config: ICEServer[]): RTCConfiguration => {
|
|
4
|
-
return {
|
|
5
|
-
iceServers: config.map((ice) => ({
|
|
6
|
-
urls: ice.urls,
|
|
7
|
-
username: ice.username,
|
|
8
|
-
credential: ice.password,
|
|
9
|
-
})),
|
|
10
|
-
};
|
|
11
|
-
};
|