@stream-io/video-client 0.0.1-alpha.7

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.
Files changed (157) hide show
  1. package/LICENSE +219 -0
  2. package/README.md +14 -0
  3. package/dist/index.d.ts +23 -0
  4. package/dist/index.js +14663 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/src/Batcher.d.ts +12 -0
  7. package/dist/src/CallDropScheduler.d.ts +44 -0
  8. package/dist/src/StreamSfuClient.d.ts +25 -0
  9. package/dist/src/StreamVideoClient.d.ts +145 -0
  10. package/dist/src/__tests__/StreamVideoClient.test.d.ts +1 -0
  11. package/dist/src/config/defaultConfigs.d.ts +2 -0
  12. package/dist/src/config/types.d.ts +29 -0
  13. package/dist/src/coordinator/StreamCoordinatorClient.d.ts +19 -0
  14. package/dist/src/coordinator/connection/base64.d.ts +2 -0
  15. package/dist/src/coordinator/connection/client.d.ts +174 -0
  16. package/dist/src/coordinator/connection/connection.d.ts +139 -0
  17. package/dist/src/coordinator/connection/connection_fallback.d.ts +38 -0
  18. package/dist/src/coordinator/connection/errors.d.ts +16 -0
  19. package/dist/src/coordinator/connection/events.d.ts +7 -0
  20. package/dist/src/coordinator/connection/insights.d.ts +58 -0
  21. package/dist/src/coordinator/connection/signing.d.ts +30 -0
  22. package/dist/src/coordinator/connection/token_manager.d.ts +39 -0
  23. package/dist/src/coordinator/connection/types.d.ts +96 -0
  24. package/dist/src/coordinator/connection/utils.d.ts +25 -0
  25. package/dist/src/devices.d.ts +79 -0
  26. package/dist/src/events/call.d.ts +26 -0
  27. package/dist/src/events/internal.d.ts +8 -0
  28. package/dist/src/events/participant.d.ts +21 -0
  29. package/dist/src/events/speaker.d.ts +10 -0
  30. package/dist/src/gen/coordinator/index.d.ts +1664 -0
  31. package/dist/src/gen/google/protobuf/descriptor.d.ts +1650 -0
  32. package/dist/src/gen/google/protobuf/duration.d.ts +113 -0
  33. package/dist/src/gen/google/protobuf/struct.d.ts +184 -0
  34. package/dist/src/gen/google/protobuf/timestamp.d.ts +158 -0
  35. package/dist/src/gen/video/coordinator/broadcast_v1/broadcast.d.ts +66 -0
  36. package/dist/src/gen/video/coordinator/call_v1/call.d.ts +254 -0
  37. package/dist/src/gen/video/coordinator/client_v1_rpc/client_rpc.client.d.ts +351 -0
  38. package/dist/src/gen/video/coordinator/client_v1_rpc/client_rpc.d.ts +1488 -0
  39. package/dist/src/gen/video/coordinator/client_v1_rpc/envelopes.d.ts +143 -0
  40. package/dist/src/gen/video/coordinator/client_v1_rpc/websocket.d.ts +292 -0
  41. package/dist/src/gen/video/coordinator/edge_v1/edge.d.ts +183 -0
  42. package/dist/src/gen/video/coordinator/event_v1/event.d.ts +411 -0
  43. package/dist/src/gen/video/coordinator/geofence_v1/geofence.d.ts +63 -0
  44. package/dist/src/gen/video/coordinator/member_v1/member.d.ts +59 -0
  45. package/dist/src/gen/video/coordinator/participant_v1/participant.d.ts +103 -0
  46. package/dist/src/gen/video/coordinator/push_v1/push.d.ts +240 -0
  47. package/dist/src/gen/video/coordinator/stat_v1/stat.d.ts +308 -0
  48. package/dist/src/gen/video/coordinator/user_v1/user.d.ts +112 -0
  49. package/dist/src/gen/video/coordinator/utils_v1/utils.d.ts +47 -0
  50. package/dist/src/gen/video/sfu/event/events.d.ts +736 -0
  51. package/dist/src/gen/video/sfu/models/models.d.ts +460 -0
  52. package/dist/src/gen/video/sfu/signal_rpc/signal.client.d.ts +89 -0
  53. package/dist/src/gen/video/sfu/signal_rpc/signal.d.ts +320 -0
  54. package/dist/src/helpers/browsers.d.ts +8 -0
  55. package/dist/src/helpers/sound-detector.d.ts +34 -0
  56. package/dist/src/rpc/createClient.d.ts +10 -0
  57. package/dist/src/rpc/index.d.ts +2 -0
  58. package/dist/src/rpc/latency.d.ts +9 -0
  59. package/dist/src/rtc/Call.d.ts +180 -0
  60. package/dist/src/rtc/CallMetadata.d.ts +9 -0
  61. package/dist/src/rtc/Dispatcher.d.ts +9 -0
  62. package/dist/src/rtc/IceTrickleBuffer.d.ts +11 -0
  63. package/dist/src/rtc/callEventHandlers.d.ts +5 -0
  64. package/dist/src/rtc/codecs.d.ts +2 -0
  65. package/dist/src/rtc/helpers/iceCandidate.d.ts +2 -0
  66. package/dist/src/rtc/helpers/tracks.d.ts +3 -0
  67. package/dist/src/rtc/publisher.d.ts +53 -0
  68. package/dist/src/rtc/signal.d.ts +5 -0
  69. package/dist/src/rtc/subscriber.d.ts +7 -0
  70. package/dist/src/rtc/types.d.ts +84 -0
  71. package/dist/src/rtc/videoLayers.d.ts +17 -0
  72. package/dist/src/stats/coordinator-stats-reporter.d.ts +10 -0
  73. package/dist/src/stats/state-store-stats-reporter.d.ts +57 -0
  74. package/dist/src/stats/types.d.ts +42 -0
  75. package/dist/src/store/index.d.ts +2 -0
  76. package/dist/src/store/rxUtils.d.ts +18 -0
  77. package/dist/src/store/stateStore.d.ts +182 -0
  78. package/generate-openapi.sh +32 -0
  79. package/index.ts +30 -0
  80. package/openapitools.json +7 -0
  81. package/package.json +54 -0
  82. package/rollup.config.mjs +48 -0
  83. package/src/Batcher.ts +43 -0
  84. package/src/CallDropScheduler.ts +192 -0
  85. package/src/StreamSfuClient.ts +185 -0
  86. package/src/StreamVideoClient.ts +487 -0
  87. package/src/__tests__/StreamVideoClient.test.ts +83 -0
  88. package/src/config/defaultConfigs.ts +15 -0
  89. package/src/config/types.ts +30 -0
  90. package/src/coordinator/StreamCoordinatorClient.ts +111 -0
  91. package/src/coordinator/connection/base64.ts +80 -0
  92. package/src/coordinator/connection/client.ts +815 -0
  93. package/src/coordinator/connection/connection.ts +750 -0
  94. package/src/coordinator/connection/connection_fallback.ts +239 -0
  95. package/src/coordinator/connection/errors.ts +70 -0
  96. package/src/coordinator/connection/events.ts +10 -0
  97. package/src/coordinator/connection/insights.ts +88 -0
  98. package/src/coordinator/connection/signing.ts +104 -0
  99. package/src/coordinator/connection/token_manager.ts +160 -0
  100. package/src/coordinator/connection/types.ts +120 -0
  101. package/src/coordinator/connection/utils.ts +148 -0
  102. package/src/devices.ts +266 -0
  103. package/src/events/call.ts +166 -0
  104. package/src/events/internal.ts +47 -0
  105. package/src/events/participant.ts +97 -0
  106. package/src/events/speaker.ts +62 -0
  107. package/src/gen/coordinator/index.ts +1653 -0
  108. package/src/gen/google/protobuf/descriptor.ts +3466 -0
  109. package/src/gen/google/protobuf/duration.ts +232 -0
  110. package/src/gen/google/protobuf/struct.ts +481 -0
  111. package/src/gen/google/protobuf/timestamp.ts +291 -0
  112. package/src/gen/video/coordinator/broadcast_v1/broadcast.ts +154 -0
  113. package/src/gen/video/coordinator/call_v1/call.ts +651 -0
  114. package/src/gen/video/coordinator/client_v1_rpc/client_rpc.client.ts +463 -0
  115. package/src/gen/video/coordinator/client_v1_rpc/client_rpc.ts +3819 -0
  116. package/src/gen/video/coordinator/client_v1_rpc/envelopes.ts +424 -0
  117. package/src/gen/video/coordinator/client_v1_rpc/websocket.ts +719 -0
  118. package/src/gen/video/coordinator/edge_v1/edge.ts +532 -0
  119. package/src/gen/video/coordinator/event_v1/event.ts +1171 -0
  120. package/src/gen/video/coordinator/geofence_v1/geofence.ts +128 -0
  121. package/src/gen/video/coordinator/member_v1/member.ts +138 -0
  122. package/src/gen/video/coordinator/participant_v1/participant.ts +261 -0
  123. package/src/gen/video/coordinator/push_v1/push.ts +651 -0
  124. package/src/gen/video/coordinator/stat_v1/stat.ts +656 -0
  125. package/src/gen/video/coordinator/user_v1/user.ts +277 -0
  126. package/src/gen/video/coordinator/utils_v1/utils.ts +98 -0
  127. package/src/gen/video/sfu/event/events.ts +1962 -0
  128. package/src/gen/video/sfu/models/models.ts +1062 -0
  129. package/src/gen/video/sfu/signal_rpc/signal.client.ts +108 -0
  130. package/src/gen/video/sfu/signal_rpc/signal.ts +906 -0
  131. package/src/helpers/browsers.ts +13 -0
  132. package/src/helpers/sound-detector.ts +85 -0
  133. package/src/rpc/createClient.ts +50 -0
  134. package/src/rpc/index.ts +2 -0
  135. package/src/rpc/latency.ts +43 -0
  136. package/src/rtc/Call.ts +585 -0
  137. package/src/rtc/CallMetadata.ts +24 -0
  138. package/src/rtc/Dispatcher.ts +46 -0
  139. package/src/rtc/IceTrickleBuffer.ts +21 -0
  140. package/src/rtc/callEventHandlers.ts +37 -0
  141. package/src/rtc/codecs.ts +61 -0
  142. package/src/rtc/helpers/iceCandidate.ts +16 -0
  143. package/src/rtc/helpers/tracks.ts +18 -0
  144. package/src/rtc/publisher.ts +305 -0
  145. package/src/rtc/signal.ts +34 -0
  146. package/src/rtc/subscriber.ts +85 -0
  147. package/src/rtc/types.ts +105 -0
  148. package/src/rtc/videoLayers.ts +103 -0
  149. package/src/stats/coordinator-stats-reporter.ts +167 -0
  150. package/src/stats/state-store-stats-reporter.ts +364 -0
  151. package/src/stats/types.ts +46 -0
  152. package/src/store/index.ts +2 -0
  153. package/src/store/rxUtils.ts +42 -0
  154. package/src/store/stateStore.ts +341 -0
  155. package/tsconfig.json +25 -0
  156. package/typedoc.json +11 -0
  157. package/vite.config.ts +11 -0
@@ -0,0 +1,120 @@
1
+ import { AxiosRequestConfig, AxiosResponse } from 'axios';
2
+ import { EVENT_MAP } from './events';
3
+ import { StableWSConnection } from './connection';
4
+
5
+ export type UR = Record<string, unknown>;
6
+
7
+ export type User = {
8
+ id: string;
9
+ anon?: boolean;
10
+ name?: string;
11
+ role?: string;
12
+ teams?: string[];
13
+ username?: string;
14
+ };
15
+
16
+ export type UserResponse = User & {
17
+ banned?: boolean;
18
+ created_at?: string;
19
+ deactivated_at?: string;
20
+ deleted_at?: string;
21
+ last_active?: string;
22
+ online?: boolean;
23
+ revoke_tokens_issued_before?: string;
24
+ shadow_banned?: boolean;
25
+ updated_at?: string;
26
+ };
27
+
28
+ export type OwnUserBase = {
29
+ invisible?: boolean;
30
+ roles?: string[];
31
+ };
32
+
33
+ export type OwnUserResponse = UserResponse & OwnUserBase;
34
+
35
+ export type ConnectionOpen = {
36
+ connection_id: string;
37
+ cid?: string;
38
+ created_at?: string;
39
+ me?: OwnUserResponse;
40
+ type?: string;
41
+ };
42
+
43
+ export type ConnectAPIResponse = Promise<void | ConnectionOpen>;
44
+
45
+ export type LogLevel = 'info' | 'error' | 'warn';
46
+
47
+ type ErrorResponseDetails = {
48
+ code: number;
49
+ messages: string[];
50
+ };
51
+
52
+ export type APIErrorResponse = {
53
+ code: number;
54
+ duration: string;
55
+ message: string;
56
+ more_info: string;
57
+ StatusCode: number;
58
+ details?: ErrorResponseDetails;
59
+ };
60
+
61
+ export class ErrorFromResponse<T> extends Error {
62
+ code?: number;
63
+ response?: AxiosResponse<T>;
64
+ status?: number;
65
+ }
66
+ export type EventTypes = 'all' | keyof typeof EVENT_MAP;
67
+ export type Event = {
68
+ type: EventTypes;
69
+
70
+ received_at?: string | Date;
71
+ online?: boolean;
72
+ mode?: string;
73
+ // TODO OL: add more properties
74
+ };
75
+
76
+ export type EventHandler = (event: Event) => void;
77
+ export type Logger = (
78
+ logLevel: LogLevel,
79
+ message: string,
80
+ extraData?: Record<string, unknown>,
81
+ ) => void;
82
+
83
+ export type StreamClientOptions = Partial<AxiosRequestConfig> & {
84
+ /**
85
+ * Used to disable warnings that are triggered by using connectUser or connectAnonymousUser server-side.
86
+ */
87
+ allowServerSideConnect?: boolean;
88
+ axiosRequestConfig?: AxiosRequestConfig;
89
+ /**
90
+ * Base url to use for API
91
+ * such as https://chat-proxy-dublin.stream-io-api.com
92
+ */
93
+ baseURL?: string;
94
+ browser?: boolean;
95
+ // device?: BaseDeviceFields;
96
+ enableInsights?: boolean;
97
+ /** experimental feature, please contact support if you want this feature enabled for you */
98
+ enableWSFallback?: boolean;
99
+ logger?: Logger;
100
+ /**
101
+ * When true, user will be persisted on client. Otherwise if `connectUser` call fails, then you need to
102
+ * call `connectUser` again to retry.
103
+ * This is mainly useful for chat application working in offline mode, where you will need client.user to
104
+ * persist even if connectUser call fails.
105
+ */
106
+ persistUserOnConnectionFailure?: boolean;
107
+
108
+ /**
109
+ * The secret key for the API key. This is only needed for server side authentication.
110
+ */
111
+ secret?: string;
112
+
113
+ warmUp?: boolean;
114
+ // Set the instance of StableWSConnection on chat client. Its purely for testing purpose and should
115
+ // not be used in production apps.
116
+ wsConnection?: StableWSConnection;
117
+ };
118
+
119
+ export type TokenProvider = () => Promise<string>;
120
+ export type TokenOrProvider = null | string | TokenProvider | undefined;
@@ -0,0 +1,148 @@
1
+ export const sleep = (m: number): Promise<void> =>
2
+ new Promise((r) => setTimeout(r, m));
3
+
4
+ export function isFunction<T>(value: Function | T): value is Function {
5
+ return (
6
+ value &&
7
+ (Object.prototype.toString.call(value) === '[object Function]' ||
8
+ 'function' === typeof value ||
9
+ value instanceof Function)
10
+ );
11
+ }
12
+
13
+ export const chatCodes = {
14
+ TOKEN_EXPIRED: 40,
15
+ WS_CLOSED_SUCCESS: 1000,
16
+ };
17
+
18
+ /**
19
+ * retryInterval - A retry interval which increases acc to number of failures
20
+ *
21
+ * @return {number} Duration to wait in milliseconds
22
+ */
23
+ export function retryInterval(numberOfFailures: number) {
24
+ // try to reconnect in 0.25-25 seconds (random to spread out the load from failures)
25
+ const max = Math.min(500 + numberOfFailures * 2000, 25000);
26
+ const min = Math.min(Math.max(250, (numberOfFailures - 1) * 2000), 25000);
27
+ return Math.floor(Math.random() * (max - min) + min);
28
+ }
29
+
30
+ export function randomId() {
31
+ return generateUUIDv4();
32
+ }
33
+
34
+ function hex(bytes: Uint8Array): string {
35
+ let s = '';
36
+ for (let i = 0; i < bytes.length; i++) {
37
+ s += bytes[i].toString(16).padStart(2, '0');
38
+ }
39
+ return s;
40
+ }
41
+
42
+ // https://tools.ietf.org/html/rfc4122
43
+ export function generateUUIDv4() {
44
+ const bytes = getRandomBytes(16);
45
+ bytes[6] = (bytes[6] & 0x0f) | 0x40; // version
46
+ bytes[8] = (bytes[8] & 0xbf) | 0x80; // variant
47
+
48
+ return (
49
+ hex(bytes.subarray(0, 4)) +
50
+ '-' +
51
+ hex(bytes.subarray(4, 6)) +
52
+ '-' +
53
+ hex(bytes.subarray(6, 8)) +
54
+ '-' +
55
+ hex(bytes.subarray(8, 10)) +
56
+ '-' +
57
+ hex(bytes.subarray(10, 16))
58
+ );
59
+ }
60
+
61
+ function getRandomValuesWithMathRandom(bytes: Uint8Array): void {
62
+ const max = Math.pow(2, (8 * bytes.byteLength) / bytes.length);
63
+ for (let i = 0; i < bytes.length; i++) {
64
+ bytes[i] = Math.random() * max;
65
+ }
66
+ }
67
+ declare const msCrypto: Crypto;
68
+
69
+ const getRandomValues = (() => {
70
+ if (
71
+ typeof crypto !== 'undefined' &&
72
+ typeof crypto?.getRandomValues !== 'undefined'
73
+ ) {
74
+ return crypto.getRandomValues.bind(crypto);
75
+ } else if (typeof msCrypto !== 'undefined') {
76
+ return msCrypto.getRandomValues.bind(msCrypto);
77
+ } else {
78
+ return getRandomValuesWithMathRandom;
79
+ }
80
+ })();
81
+
82
+ function getRandomBytes(length: number): Uint8Array {
83
+ const bytes = new Uint8Array(length);
84
+ getRandomValues(bytes);
85
+ return bytes;
86
+ }
87
+
88
+ export function convertErrorToJson(err: Error) {
89
+ const jsonObj = {} as Record<string, unknown>;
90
+
91
+ if (!err) return jsonObj;
92
+
93
+ try {
94
+ Object.getOwnPropertyNames(err).forEach((key) => {
95
+ jsonObj[key] = Object.getOwnPropertyDescriptor(err, key);
96
+ });
97
+ } catch (_) {
98
+ return {
99
+ error: 'failed to serialize the error',
100
+ };
101
+ }
102
+
103
+ return jsonObj;
104
+ }
105
+
106
+ /**
107
+ * isOnline safely return the navigator.online value for browser env
108
+ * if navigator is not in global object, it always return true
109
+ */
110
+ export function isOnline() {
111
+ const nav =
112
+ typeof navigator !== 'undefined'
113
+ ? navigator
114
+ : typeof window !== 'undefined' && window.navigator
115
+ ? window.navigator
116
+ : undefined;
117
+
118
+ if (!nav) {
119
+ console.warn(
120
+ 'isOnline failed to access window.navigator and assume browser is online',
121
+ );
122
+ return true;
123
+ }
124
+
125
+ // RN navigator has undefined for onLine
126
+ if (typeof nav.onLine !== 'boolean') {
127
+ return true;
128
+ }
129
+
130
+ return nav.onLine;
131
+ }
132
+
133
+ /**
134
+ * listenForConnectionChanges - Adds an event listener fired on browser going online or offline
135
+ */
136
+ export function addConnectionEventListeners(cb: (e: Event) => void) {
137
+ if (typeof window !== 'undefined' && window.addEventListener) {
138
+ window.addEventListener('offline', cb);
139
+ window.addEventListener('online', cb);
140
+ }
141
+ }
142
+
143
+ export function removeConnectionEventListeners(cb: (e: Event) => void) {
144
+ if (typeof window !== 'undefined' && window.removeEventListener) {
145
+ window.removeEventListener('offline', cb);
146
+ window.removeEventListener('online', cb);
147
+ }
148
+ }
package/src/devices.ts ADDED
@@ -0,0 +1,266 @@
1
+ import {
2
+ combineLatest,
3
+ concatMap,
4
+ debounceTime,
5
+ filter,
6
+ firstValueFrom,
7
+ from,
8
+ map,
9
+ merge,
10
+ Observable,
11
+ shareReplay,
12
+ } from 'rxjs';
13
+
14
+ const getDevices = (constraints?: MediaStreamConstraints) => {
15
+ return new Observable<MediaDeviceInfo[]>((subscriber) => {
16
+ navigator.mediaDevices
17
+ .getUserMedia(constraints)
18
+ .then((media) => {
19
+ // in Firefox, devices can be enumerated after userMedia is requested
20
+ // and permissions granted. Otherwise, device labels are empty
21
+ navigator.mediaDevices.enumerateDevices().then((devices) => {
22
+ subscriber.next(devices);
23
+ // If we stop the tracks before enumerateDevices -> the labels won't show up in Firefox
24
+ media.getTracks().forEach((t) => t.stop());
25
+ subscriber.complete();
26
+ });
27
+ })
28
+ .catch((error) => {
29
+ console.error('Failed to get devices', error);
30
+ subscriber.error(error);
31
+ });
32
+ });
33
+ };
34
+
35
+ /**
36
+ * [Tells if the browser supports audio output change on 'audio' elements](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/setSinkId).
37
+ *
38
+ * @angular It's recommended to use the [`DeviceManagerService`](./DeviceManagerService.md) for a higher level API, use this low-level method only if the `DeviceManagerService` doesn't suit your requirements.
39
+ */
40
+ export const checkIfAudioOutputChangeSupported = () => {
41
+ if (typeof document === 'undefined') return false;
42
+ const element = document.createElement('audio');
43
+ const isFeatureSupported = (element as any).sinkId !== undefined;
44
+
45
+ return isFeatureSupported;
46
+ };
47
+
48
+ const audioDeviceConstraints: MediaStreamConstraints = {
49
+ audio: { noiseSuppression: true },
50
+ };
51
+ const videoDeviceConstraints: MediaStreamConstraints = {
52
+ video: { width: 960, height: 540 },
53
+ };
54
+
55
+ // Audio and video devices are requested in two separate requests: that way users will be presented with two separate prompts -> they can give access to just camera, or just microphone
56
+ const deviceChange$ = new Observable((subscriber) => {
57
+ const deviceChangeHandler = () => subscriber.next();
58
+
59
+ navigator.mediaDevices.addEventListener?.(
60
+ 'devicechange',
61
+ deviceChangeHandler,
62
+ );
63
+
64
+ return () =>
65
+ navigator.mediaDevices.removeEventListener?.(
66
+ 'devicechange',
67
+ deviceChangeHandler,
68
+ );
69
+ }).pipe(
70
+ debounceTime(500),
71
+ concatMap(() => from(navigator.mediaDevices.enumerateDevices())),
72
+ shareReplay(1),
73
+ );
74
+
75
+ const audioDevices$ = merge(
76
+ getDevices(audioDeviceConstraints),
77
+ deviceChange$,
78
+ ).pipe(shareReplay(1));
79
+
80
+ const videoDevices$ = merge(
81
+ getDevices(videoDeviceConstraints),
82
+ deviceChange$,
83
+ ).pipe(shareReplay(1));
84
+
85
+ /**
86
+ * Prompts the user for a permission to use audio devices (if not already granted) and lists the available 'audioinput' devices, if devices are added/removed the list is updated.
87
+ *
88
+ * @angular It's recommended to use the [`DeviceManagerService`](./DeviceManagerService.md) for a higher level API, use this low-level method only if the `DeviceManagerService` doesn't suit your requirements.
89
+ * @returns
90
+ */
91
+ export const getAudioDevices = () =>
92
+ audioDevices$.pipe(
93
+ map((values) => values.filter((d) => d.kind === 'audioinput')),
94
+ );
95
+
96
+ /**
97
+ * Prompts the user for a permission to use video devices (if not already granted) and lists the available 'videoinput' devices, if devices are added/removed the list is updated.
98
+ *
99
+ * @angular It's recommended to use the [`DeviceManagerService`](./DeviceManagerService.md) for a higher level API, use this low-level method only if the `DeviceManagerService` doesn't suit your requirements.
100
+ * @returns
101
+ */
102
+ export const getVideoDevices = () =>
103
+ videoDevices$.pipe(
104
+ map((values) =>
105
+ values.filter((d) => d.kind === 'videoinput' && d.deviceId.length),
106
+ ),
107
+ );
108
+
109
+ /**
110
+ * Prompts the user for a permission to use audio devices (if not already granted) and lists the available 'audiooutput' devices, if devices are added/removed the list is updated. Selecting 'audiooutput' device only makes sense if [the browser has support for changing audio output on 'audio' elements](#checkifaudiooutputchangesupported)
111
+ *
112
+ * @angular It's recommended to use the [`DeviceManagerService`](./DeviceManagerService.md) for a higher level API, use this low-level method only if the `DeviceManagerService` doesn't suit your requirements.
113
+ * @returns
114
+ */
115
+ export const getAudioOutputDevices = () => {
116
+ return audioDevices$.pipe(
117
+ map((values) => values.filter((d) => d.kind === 'audiooutput')),
118
+ );
119
+ };
120
+
121
+ const getStream = async (
122
+ kind: Exclude<MediaDeviceKind, 'audiooutput'>,
123
+ deviceId?: string,
124
+ ) => {
125
+ if (!deviceId) {
126
+ const allDevices = await firstValueFrom(
127
+ kind === 'audioinput' ? getAudioDevices() : getVideoDevices(),
128
+ );
129
+ if (allDevices.length === 0) {
130
+ throw new Error(`No available ${kind} device found`);
131
+ }
132
+ // TODO: store last used device in local storage and use that value
133
+ const selectedDevice = allDevices[0];
134
+ deviceId = selectedDevice.deviceId;
135
+ }
136
+ const type = kind === 'audioinput' ? 'audio' : 'video';
137
+ const defaultConstraints =
138
+ type === 'audio' ? audioDeviceConstraints : videoDeviceConstraints;
139
+
140
+ // merge the default constraints with the deviceId
141
+ const constraints: MediaStreamConstraints = {
142
+ [type]: {
143
+ ...(defaultConstraints[type] as {}),
144
+ deviceId,
145
+ },
146
+ };
147
+
148
+ try {
149
+ return await navigator.mediaDevices.getUserMedia(constraints);
150
+ } catch (e) {
151
+ console.error(`Failed to get ${type} stream for device ${deviceId}`, e);
152
+ throw e;
153
+ }
154
+ };
155
+
156
+ /**
157
+ * Returns an 'audioinput' media stream with the given `deviceId`, if no `deviceId` is provided, it uses the first available device.
158
+ *
159
+ * @angular It's recommended to use the [`DeviceManagerService`](./DeviceManagerService.md) for a higher level API, use this low-level method only if the `DeviceManagerService` doesn't suit your requirements.
160
+ * @param deviceId
161
+ * @returns
162
+ */
163
+ export const getAudioStream = async (deviceId?: string) => {
164
+ return getStream('audioinput', deviceId);
165
+ };
166
+
167
+ /**
168
+ * Returns a 'videoinput' media stream with the given `deviceId`, if no `deviceId` is provided, it uses the first available device.
169
+ *
170
+ * @angular It's recommended to use the [`DeviceManagerService`](./DeviceManagerService.md) for a higher level API, use this low-level method only if the `DeviceManagerService` doesn't suit your requirements.
171
+ * @param deviceId
172
+ * @returns
173
+ */
174
+ export const getVideoStream = async (deviceId?: string) => {
175
+ return getStream('videoinput', deviceId);
176
+ };
177
+
178
+ /**
179
+ * Prompts the user for a permission to share a screen.
180
+ * If the user grants the permission, a screen sharing stream is returned. Throws otherwise.
181
+ *
182
+ * The callers of this API are responsible to handle the possible errors.
183
+ *
184
+ * @angular It's recommended to use the [`DeviceManagerService`](./DeviceManagerService.md) for a higher level API, use this low-level method only if the `DeviceManagerService` doesn't suit your requirements.
185
+ *
186
+ * @param options any additional options to pass to the [`getDisplayMedia`](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia) API.
187
+ */
188
+ export const getScreenShareStream = async (
189
+ // TODO OL: switch to `DisplayMediaStreamConstraints` once Angular supports it
190
+ options?: Record<string, any>,
191
+ ) => {
192
+ try {
193
+ return await navigator.mediaDevices.getDisplayMedia({
194
+ video: true,
195
+ audio: false,
196
+ ...options,
197
+ });
198
+ } catch (e) {
199
+ console.error('Failed to get screen share stream', e);
200
+ throw e;
201
+ }
202
+ };
203
+
204
+ const watchForDisconnectedDevice = (
205
+ kind: MediaDeviceKind,
206
+ deviceId$: Observable<string | undefined>,
207
+ ) => {
208
+ let devices$;
209
+ switch (kind) {
210
+ case 'audioinput':
211
+ devices$ = getAudioDevices();
212
+ break;
213
+ case 'videoinput':
214
+ devices$ = getVideoDevices();
215
+ break;
216
+ case 'audiooutput':
217
+ devices$ = getAudioOutputDevices();
218
+ break;
219
+ }
220
+ return combineLatest([devices$, deviceId$]).pipe(
221
+ filter(
222
+ ([devices, deviceId]) =>
223
+ !!deviceId && !devices.find((d) => d.deviceId === deviceId),
224
+ ),
225
+ map(() => true),
226
+ );
227
+ };
228
+
229
+ /**
230
+ * Notifies the subscriber if a given 'audioinput' device is disconnected
231
+ *
232
+ * @angular It's recommended to use the [`DeviceManagerService`](./DeviceManagerService.md) for a higher level API, use this low-level method only if the `DeviceManagerService` doesn't suit your requirements.
233
+ * @param deviceId$ an Observable that specifies which device to watch for
234
+ * @returns
235
+ */
236
+ export const watchForDisconnectedAudioDevice = (
237
+ deviceId$: Observable<string | undefined>,
238
+ ) => {
239
+ return watchForDisconnectedDevice('audioinput', deviceId$);
240
+ };
241
+
242
+ /**
243
+ * Notifies the subscriber if a given 'videoinput' device is disconnected
244
+ *
245
+ * @angular It's recommended to use the [`DeviceManagerService`](./DeviceManagerService.md) for a higher level API, use this low-level method only if the `DeviceManagerService` doesn't suit your requirements.
246
+ * @param deviceId$ an Observable that specifies which device to watch for
247
+ * @returns
248
+ */
249
+ export const watchForDisconnectedVideoDevice = (
250
+ deviceId$: Observable<string | undefined>,
251
+ ) => {
252
+ return watchForDisconnectedDevice('videoinput', deviceId$);
253
+ };
254
+
255
+ /**
256
+ * Notifies the subscriber if a given 'audiooutput' device is disconnected
257
+ *
258
+ * @angular It's recommended to use the [`DeviceManagerService`](./DeviceManagerService.md) for a higher level API, use this low-level method only if the `DeviceManagerService` doesn't suit your requirements.
259
+ * @param deviceId$ an Observable that specifies which device to watch for
260
+ * @returns
261
+ */
262
+ export const watchForDisconnectedAudioOutputDevice = (
263
+ deviceId$: Observable<string | undefined>,
264
+ ) => {
265
+ return watchForDisconnectedDevice('audiooutput', deviceId$);
266
+ };
@@ -0,0 +1,166 @@
1
+ import { StreamVideoWriteableStateStore } from '../store';
2
+ import {
3
+ CallAccepted,
4
+ CallCancelled,
5
+ CallCreated,
6
+ CallRejected,
7
+ } from '../gen/coordinator';
8
+ import { CallMetadata } from '../rtc/CallMetadata';
9
+
10
+ /**
11
+ * Event handler that watches the delivery of CallCreated Websocket event
12
+ * Updates the state store and notifies its subscribers that
13
+ * a new pending call has been initiated.
14
+ */
15
+ export const watchCallCreated = (store: StreamVideoWriteableStateStore) => {
16
+ return function onCallCreated(event: CallCreated) {
17
+ const { call, members } = event;
18
+ if (!call) {
19
+ console.warn("Can't find call in CallCreated event");
20
+ return;
21
+ }
22
+
23
+ const currentUser = store.getCurrentValue(store.connectedUserSubject);
24
+ if (currentUser?.id === call.created_by.id) {
25
+ console.warn('Received CallCreated event sent by the current user');
26
+ return;
27
+ }
28
+
29
+ store.setCurrentValue(store.pendingCallsSubject, (pendingCalls) => [
30
+ ...pendingCalls,
31
+ new CallMetadata(call, members),
32
+ ]);
33
+ };
34
+ };
35
+
36
+ /**
37
+ * Event handler that watched the delivery of CallAccepted Websocket event
38
+ * Updates the state store and notifies its subscribers that
39
+ * the given user will be joining the call.
40
+ */
41
+ export const watchCallAccepted = (store: StreamVideoWriteableStateStore) => {
42
+ return function onCallAccepted(event: CallAccepted) {
43
+ const { call_cid } = event;
44
+ if (!call_cid) {
45
+ console.warn("Can't find call_cid in CallAccepted event");
46
+ return;
47
+ }
48
+
49
+ const acceptedIncomingCall = store
50
+ .getCurrentValue(store.incomingCalls$)
51
+ .find((incomingCall) => incomingCall.call.cid === call_cid);
52
+
53
+ if (acceptedIncomingCall) {
54
+ console.warn('Received CallAccepted event for an incoming call');
55
+ return;
56
+ }
57
+
58
+ const acceptedOutgoingCall = store
59
+ .getCurrentValue(store.outgoingCalls$)
60
+ .find((outgoingCall) => outgoingCall.call.cid === call_cid);
61
+ const activeCall = store.getCurrentValue(store.activeCallSubject);
62
+
63
+ // FIXME OL: we should revisit this logic, it is hard to follow
64
+ const acceptedActiveCall =
65
+ activeCall?.data.call.cid !== undefined &&
66
+ activeCall.data.call.cid === call_cid
67
+ ? activeCall
68
+ : undefined;
69
+
70
+ if (!acceptedOutgoingCall && !acceptedActiveCall) {
71
+ console.warn(
72
+ `CallAccepted event received for a non-existent outgoing call (CID: ${call_cid}`,
73
+ );
74
+ return;
75
+ }
76
+
77
+ // once in active call, it is unnecessary to keep track of accepted call events
78
+ if (call_cid === acceptedActiveCall?.data.call.cid) {
79
+ return;
80
+ }
81
+
82
+ store.setCurrentValue(store.acceptedCallSubject, event);
83
+ };
84
+ };
85
+
86
+ /**
87
+ * Event handler that watches delivery of CallRejected Websocket event.
88
+ * Updates the state store and notifies its subscribers that
89
+ * the given user will not be joining the call.
90
+ */
91
+ export const watchCallRejected = (store: StreamVideoWriteableStateStore) => {
92
+ return function onCallRejected(event: CallRejected) {
93
+ const { call_cid } = event;
94
+ if (!call_cid) {
95
+ console.warn("Can't find call_cid in CallRejected event");
96
+ return;
97
+ }
98
+
99
+ const rejectedIncomingCall = store
100
+ .getCurrentValue(store.incomingCalls$)
101
+ .find((incomingCall) => incomingCall.call.cid === call_cid);
102
+
103
+ if (rejectedIncomingCall) {
104
+ console.warn('Received CallRejected event for an incoming call');
105
+ return;
106
+ }
107
+
108
+ const rejectedOutgoingCall = store
109
+ .getCurrentValue(store.outgoingCalls$)
110
+ .find((outgoingCall) => outgoingCall.call.cid === call_cid);
111
+ const activeCall = store.getCurrentValue(store.activeCallSubject);
112
+ const rejectedActiveCall =
113
+ activeCall?.data.call.cid !== undefined &&
114
+ activeCall.data.call.cid === call_cid
115
+ ? activeCall
116
+ : undefined;
117
+
118
+ if (!rejectedOutgoingCall && !rejectedActiveCall) {
119
+ console.warn(
120
+ `CallRejected event received for a non-existent outgoing call (CID: ${call_cid}`,
121
+ );
122
+ return;
123
+ }
124
+
125
+ store.setCurrentValue(store.pendingCallsSubject, (pendingCalls) =>
126
+ pendingCalls.filter((pendingCall) => pendingCall.call.cid !== call_cid),
127
+ );
128
+ };
129
+ };
130
+
131
+ /**
132
+ * Event handler that watches the delivery of CallCancelled Websocket event
133
+ * Updates the state store and notifies its subscribers that
134
+ * the call is now considered terminated.
135
+ */
136
+ export const watchCallCancelled = (store: StreamVideoWriteableStateStore) => {
137
+ return function onCallCancelled(event: CallCancelled) {
138
+ const { call_cid } = event;
139
+ if (!call_cid) {
140
+ console.log("Can't find call in CallCancelled event");
141
+ return;
142
+ }
143
+
144
+ const cancelledIncomingCall = store
145
+ .getCurrentValue(store.incomingCalls$)
146
+ .find((incomingCall) => incomingCall.call.cid === call_cid);
147
+
148
+ const activeCall = store.getCurrentValue(store.activeCallSubject);
149
+ const cancelledActiveCall =
150
+ activeCall?.data.call.cid !== undefined &&
151
+ activeCall.data.call.cid === call_cid
152
+ ? activeCall
153
+ : undefined;
154
+
155
+ if (!cancelledIncomingCall && !cancelledActiveCall) {
156
+ console.warn(
157
+ `CallCancelled event received for a non-existent incoming call (CID: ${call_cid}`,
158
+ );
159
+ return;
160
+ }
161
+
162
+ store.setCurrentValue(store.pendingCallsSubject, (pendingCalls) =>
163
+ pendingCalls.filter((pendingCall) => pendingCall.call.cid !== call_cid),
164
+ );
165
+ };
166
+ };