@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.
- package/LICENSE +219 -0
- package/README.md +14 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +14663 -0
- package/dist/index.js.map +1 -0
- package/dist/src/Batcher.d.ts +12 -0
- package/dist/src/CallDropScheduler.d.ts +44 -0
- package/dist/src/StreamSfuClient.d.ts +25 -0
- package/dist/src/StreamVideoClient.d.ts +145 -0
- package/dist/src/__tests__/StreamVideoClient.test.d.ts +1 -0
- package/dist/src/config/defaultConfigs.d.ts +2 -0
- package/dist/src/config/types.d.ts +29 -0
- package/dist/src/coordinator/StreamCoordinatorClient.d.ts +19 -0
- package/dist/src/coordinator/connection/base64.d.ts +2 -0
- package/dist/src/coordinator/connection/client.d.ts +174 -0
- package/dist/src/coordinator/connection/connection.d.ts +139 -0
- package/dist/src/coordinator/connection/connection_fallback.d.ts +38 -0
- package/dist/src/coordinator/connection/errors.d.ts +16 -0
- package/dist/src/coordinator/connection/events.d.ts +7 -0
- package/dist/src/coordinator/connection/insights.d.ts +58 -0
- package/dist/src/coordinator/connection/signing.d.ts +30 -0
- package/dist/src/coordinator/connection/token_manager.d.ts +39 -0
- package/dist/src/coordinator/connection/types.d.ts +96 -0
- package/dist/src/coordinator/connection/utils.d.ts +25 -0
- package/dist/src/devices.d.ts +79 -0
- package/dist/src/events/call.d.ts +26 -0
- package/dist/src/events/internal.d.ts +8 -0
- package/dist/src/events/participant.d.ts +21 -0
- package/dist/src/events/speaker.d.ts +10 -0
- package/dist/src/gen/coordinator/index.d.ts +1664 -0
- package/dist/src/gen/google/protobuf/descriptor.d.ts +1650 -0
- package/dist/src/gen/google/protobuf/duration.d.ts +113 -0
- package/dist/src/gen/google/protobuf/struct.d.ts +184 -0
- package/dist/src/gen/google/protobuf/timestamp.d.ts +158 -0
- package/dist/src/gen/video/coordinator/broadcast_v1/broadcast.d.ts +66 -0
- package/dist/src/gen/video/coordinator/call_v1/call.d.ts +254 -0
- package/dist/src/gen/video/coordinator/client_v1_rpc/client_rpc.client.d.ts +351 -0
- package/dist/src/gen/video/coordinator/client_v1_rpc/client_rpc.d.ts +1488 -0
- package/dist/src/gen/video/coordinator/client_v1_rpc/envelopes.d.ts +143 -0
- package/dist/src/gen/video/coordinator/client_v1_rpc/websocket.d.ts +292 -0
- package/dist/src/gen/video/coordinator/edge_v1/edge.d.ts +183 -0
- package/dist/src/gen/video/coordinator/event_v1/event.d.ts +411 -0
- package/dist/src/gen/video/coordinator/geofence_v1/geofence.d.ts +63 -0
- package/dist/src/gen/video/coordinator/member_v1/member.d.ts +59 -0
- package/dist/src/gen/video/coordinator/participant_v1/participant.d.ts +103 -0
- package/dist/src/gen/video/coordinator/push_v1/push.d.ts +240 -0
- package/dist/src/gen/video/coordinator/stat_v1/stat.d.ts +308 -0
- package/dist/src/gen/video/coordinator/user_v1/user.d.ts +112 -0
- package/dist/src/gen/video/coordinator/utils_v1/utils.d.ts +47 -0
- package/dist/src/gen/video/sfu/event/events.d.ts +736 -0
- package/dist/src/gen/video/sfu/models/models.d.ts +460 -0
- package/dist/src/gen/video/sfu/signal_rpc/signal.client.d.ts +89 -0
- package/dist/src/gen/video/sfu/signal_rpc/signal.d.ts +320 -0
- package/dist/src/helpers/browsers.d.ts +8 -0
- package/dist/src/helpers/sound-detector.d.ts +34 -0
- package/dist/src/rpc/createClient.d.ts +10 -0
- package/dist/src/rpc/index.d.ts +2 -0
- package/dist/src/rpc/latency.d.ts +9 -0
- package/dist/src/rtc/Call.d.ts +180 -0
- package/dist/src/rtc/CallMetadata.d.ts +9 -0
- package/dist/src/rtc/Dispatcher.d.ts +9 -0
- package/dist/src/rtc/IceTrickleBuffer.d.ts +11 -0
- package/dist/src/rtc/callEventHandlers.d.ts +5 -0
- package/dist/src/rtc/codecs.d.ts +2 -0
- package/dist/src/rtc/helpers/iceCandidate.d.ts +2 -0
- package/dist/src/rtc/helpers/tracks.d.ts +3 -0
- package/dist/src/rtc/publisher.d.ts +53 -0
- package/dist/src/rtc/signal.d.ts +5 -0
- package/dist/src/rtc/subscriber.d.ts +7 -0
- package/dist/src/rtc/types.d.ts +84 -0
- package/dist/src/rtc/videoLayers.d.ts +17 -0
- package/dist/src/stats/coordinator-stats-reporter.d.ts +10 -0
- package/dist/src/stats/state-store-stats-reporter.d.ts +57 -0
- package/dist/src/stats/types.d.ts +42 -0
- package/dist/src/store/index.d.ts +2 -0
- package/dist/src/store/rxUtils.d.ts +18 -0
- package/dist/src/store/stateStore.d.ts +182 -0
- package/generate-openapi.sh +32 -0
- package/index.ts +30 -0
- package/openapitools.json +7 -0
- package/package.json +54 -0
- package/rollup.config.mjs +48 -0
- package/src/Batcher.ts +43 -0
- package/src/CallDropScheduler.ts +192 -0
- package/src/StreamSfuClient.ts +185 -0
- package/src/StreamVideoClient.ts +487 -0
- package/src/__tests__/StreamVideoClient.test.ts +83 -0
- package/src/config/defaultConfigs.ts +15 -0
- package/src/config/types.ts +30 -0
- package/src/coordinator/StreamCoordinatorClient.ts +111 -0
- package/src/coordinator/connection/base64.ts +80 -0
- package/src/coordinator/connection/client.ts +815 -0
- package/src/coordinator/connection/connection.ts +750 -0
- package/src/coordinator/connection/connection_fallback.ts +239 -0
- package/src/coordinator/connection/errors.ts +70 -0
- package/src/coordinator/connection/events.ts +10 -0
- package/src/coordinator/connection/insights.ts +88 -0
- package/src/coordinator/connection/signing.ts +104 -0
- package/src/coordinator/connection/token_manager.ts +160 -0
- package/src/coordinator/connection/types.ts +120 -0
- package/src/coordinator/connection/utils.ts +148 -0
- package/src/devices.ts +266 -0
- package/src/events/call.ts +166 -0
- package/src/events/internal.ts +47 -0
- package/src/events/participant.ts +97 -0
- package/src/events/speaker.ts +62 -0
- package/src/gen/coordinator/index.ts +1653 -0
- package/src/gen/google/protobuf/descriptor.ts +3466 -0
- package/src/gen/google/protobuf/duration.ts +232 -0
- package/src/gen/google/protobuf/struct.ts +481 -0
- package/src/gen/google/protobuf/timestamp.ts +291 -0
- package/src/gen/video/coordinator/broadcast_v1/broadcast.ts +154 -0
- package/src/gen/video/coordinator/call_v1/call.ts +651 -0
- package/src/gen/video/coordinator/client_v1_rpc/client_rpc.client.ts +463 -0
- package/src/gen/video/coordinator/client_v1_rpc/client_rpc.ts +3819 -0
- package/src/gen/video/coordinator/client_v1_rpc/envelopes.ts +424 -0
- package/src/gen/video/coordinator/client_v1_rpc/websocket.ts +719 -0
- package/src/gen/video/coordinator/edge_v1/edge.ts +532 -0
- package/src/gen/video/coordinator/event_v1/event.ts +1171 -0
- package/src/gen/video/coordinator/geofence_v1/geofence.ts +128 -0
- package/src/gen/video/coordinator/member_v1/member.ts +138 -0
- package/src/gen/video/coordinator/participant_v1/participant.ts +261 -0
- package/src/gen/video/coordinator/push_v1/push.ts +651 -0
- package/src/gen/video/coordinator/stat_v1/stat.ts +656 -0
- package/src/gen/video/coordinator/user_v1/user.ts +277 -0
- package/src/gen/video/coordinator/utils_v1/utils.ts +98 -0
- package/src/gen/video/sfu/event/events.ts +1962 -0
- package/src/gen/video/sfu/models/models.ts +1062 -0
- package/src/gen/video/sfu/signal_rpc/signal.client.ts +108 -0
- package/src/gen/video/sfu/signal_rpc/signal.ts +906 -0
- package/src/helpers/browsers.ts +13 -0
- package/src/helpers/sound-detector.ts +85 -0
- package/src/rpc/createClient.ts +50 -0
- package/src/rpc/index.ts +2 -0
- package/src/rpc/latency.ts +43 -0
- package/src/rtc/Call.ts +585 -0
- package/src/rtc/CallMetadata.ts +24 -0
- package/src/rtc/Dispatcher.ts +46 -0
- package/src/rtc/IceTrickleBuffer.ts +21 -0
- package/src/rtc/callEventHandlers.ts +37 -0
- package/src/rtc/codecs.ts +61 -0
- package/src/rtc/helpers/iceCandidate.ts +16 -0
- package/src/rtc/helpers/tracks.ts +18 -0
- package/src/rtc/publisher.ts +305 -0
- package/src/rtc/signal.ts +34 -0
- package/src/rtc/subscriber.ts +85 -0
- package/src/rtc/types.ts +105 -0
- package/src/rtc/videoLayers.ts +103 -0
- package/src/stats/coordinator-stats-reporter.ts +167 -0
- package/src/stats/state-store-stats-reporter.ts +364 -0
- package/src/stats/types.ts +46 -0
- package/src/store/index.ts +2 -0
- package/src/store/rxUtils.ts +42 -0
- package/src/store/stateStore.ts +341 -0
- package/tsconfig.json +25 -0
- package/typedoc.json +11 -0
- 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
|
+
};
|