@unityevolv/ofiskit-realtime-client 0.1.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/LICENSE +661 -0
- package/README.md +36 -0
- package/dist/client.d.ts +190 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +326 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +47 -0
- package/dist/index.js.map +1 -0
- package/dist/office-state.d.ts +143 -0
- package/dist/office-state.d.ts.map +1 -0
- package/dist/office-state.js +228 -0
- package/dist/office-state.js.map +1 -0
- package/dist/rtc/adapter.d.ts +238 -0
- package/dist/rtc/adapter.d.ts.map +1 -0
- package/dist/rtc/adapter.js +80 -0
- package/dist/rtc/adapter.js.map +1 -0
- package/dist/rtc/devices.d.ts +80 -0
- package/dist/rtc/devices.d.ts.map +1 -0
- package/dist/rtc/devices.js +107 -0
- package/dist/rtc/devices.js.map +1 -0
- package/dist/rtc/mesh.d.ts +13 -0
- package/dist/rtc/mesh.d.ts.map +1 -0
- package/dist/rtc/mesh.js +756 -0
- package/dist/rtc/mesh.js.map +1 -0
- package/dist/rtc/screen.d.ts +104 -0
- package/dist/rtc/screen.d.ts.map +1 -0
- package/dist/rtc/screen.js +103 -0
- package/dist/rtc/screen.js.map +1 -0
- package/package.json +21 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import type { IceServer, SignalMessage } from '@unityevolv/ofiskit-realtime-core/protocol';
|
|
2
|
+
import type { ShareOptions } from './screen.js';
|
|
3
|
+
/**
|
|
4
|
+
* The RTC provider interface, client half.
|
|
5
|
+
*
|
|
6
|
+
* The other half is the server plugin in the core, and **both must exist** for a
|
|
7
|
+
* provider to be usable. This is the one the controls bar, the tiles and the
|
|
8
|
+
* indicators actually talk to: none of them imports a provider SDK, they consume
|
|
9
|
+
* the events below in one shape whichever provider is behind them.
|
|
10
|
+
*
|
|
11
|
+
* The built-in mesh adapter is the reference implementation and arrives with the
|
|
12
|
+
* signalling story. An external provider's adapter wraps that provider's SDK and
|
|
13
|
+
* talks to their servers; our socket carries only call state, never their media
|
|
14
|
+
* or signalling.
|
|
15
|
+
*
|
|
16
|
+
* No DOM anywhere except the media types, which React Native shims — which is
|
|
17
|
+
* why this lives in the platform-agnostic package and the tiles do not.
|
|
18
|
+
*/
|
|
19
|
+
/** One thing that happened in the call, in the shape every provider reports. */
|
|
20
|
+
export type RtcEvent = {
|
|
21
|
+
type: 'participant.joined';
|
|
22
|
+
deviceId: string;
|
|
23
|
+
userId: string;
|
|
24
|
+
displayName: string;
|
|
25
|
+
} | {
|
|
26
|
+
type: 'participant.left';
|
|
27
|
+
deviceId: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* A stream arrived from a peer.
|
|
31
|
+
*
|
|
32
|
+
* `source` separates a camera from a screen share, because they are laid out
|
|
33
|
+
* completely differently and a share that arrives as a camera tile is useless.
|
|
34
|
+
*/
|
|
35
|
+
| {
|
|
36
|
+
type: 'track';
|
|
37
|
+
deviceId: string;
|
|
38
|
+
stream: MediaStream;
|
|
39
|
+
source: 'camera' | 'screen' | 'audio';
|
|
40
|
+
} | {
|
|
41
|
+
type: 'track.ended';
|
|
42
|
+
deviceId: string;
|
|
43
|
+
source: 'camera' | 'screen' | 'audio';
|
|
44
|
+
}
|
|
45
|
+
/** Your own microphone level, a few times a second and only while it changes. */
|
|
46
|
+
| {
|
|
47
|
+
type: 'speaking';
|
|
48
|
+
speaking: boolean;
|
|
49
|
+
level: number;
|
|
50
|
+
} | {
|
|
51
|
+
type: 'quality';
|
|
52
|
+
deviceId: string;
|
|
53
|
+
relayed: boolean;
|
|
54
|
+
packetLoss: number;
|
|
55
|
+
roundTripMs: number;
|
|
56
|
+
}
|
|
57
|
+
/** What you are publishing, reported after the adapter actually did it. */
|
|
58
|
+
| {
|
|
59
|
+
type: 'state';
|
|
60
|
+
muted: boolean;
|
|
61
|
+
cameraOn: boolean;
|
|
62
|
+
sharing: boolean;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* A connection failed.
|
|
66
|
+
*
|
|
67
|
+
* `deviceId` present means one peer is unreachable and the rest of the call is
|
|
68
|
+
* fine, which is worth saying precisely: a three-way call with one unreachable
|
|
69
|
+
* person is not a total failure and must not be reported as one.
|
|
70
|
+
*/
|
|
71
|
+
| {
|
|
72
|
+
type: 'failed';
|
|
73
|
+
deviceId?: string;
|
|
74
|
+
reason: string;
|
|
75
|
+
}
|
|
76
|
+
/** Your own video was reduced, so nobody has to wonder why they look blurry. */
|
|
77
|
+
| {
|
|
78
|
+
type: 'degraded';
|
|
79
|
+
videoDropped: boolean;
|
|
80
|
+
reason: string;
|
|
81
|
+
} | {
|
|
82
|
+
type: 'local';
|
|
83
|
+
stream: MediaStream | null;
|
|
84
|
+
source: 'camera' | 'screen';
|
|
85
|
+
};
|
|
86
|
+
export type RtcHandler = (event: RtcEvent) => void;
|
|
87
|
+
export interface JoinOptions {
|
|
88
|
+
callId: string;
|
|
89
|
+
/** Who this client is, as a call leg. A leg is a device, not a person. */
|
|
90
|
+
deviceId: string;
|
|
91
|
+
/** Whatever the server plugin issued. The adapter is the only thing that reads it. */
|
|
92
|
+
credentials: unknown;
|
|
93
|
+
iceServers: IceServer[];
|
|
94
|
+
/** Who is already here, so a new peer knows who to connect to. */
|
|
95
|
+
participants: Array<{
|
|
96
|
+
userId: string;
|
|
97
|
+
deviceId: string;
|
|
98
|
+
displayName: string;
|
|
99
|
+
}>;
|
|
100
|
+
audio: boolean;
|
|
101
|
+
video: boolean;
|
|
102
|
+
/** Chosen devices, from the device picker. */
|
|
103
|
+
audioDeviceId?: string;
|
|
104
|
+
videoDeviceId?: string;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* How the adapter reaches the other side.
|
|
108
|
+
*
|
|
109
|
+
* Injected rather than imported, so the adapter does not know whether it is
|
|
110
|
+
* talking over our socket or a provider's own channel. The built-in adapter uses
|
|
111
|
+
* our socket; an external one would not need this at all.
|
|
112
|
+
*/
|
|
113
|
+
export interface Signaller {
|
|
114
|
+
send(message: SignalMessage): void;
|
|
115
|
+
receive(handler: (message: SignalMessage & {
|
|
116
|
+
from: string;
|
|
117
|
+
}) => void): () => void;
|
|
118
|
+
}
|
|
119
|
+
export interface RtcClientAdapter {
|
|
120
|
+
join(options: JoinOptions): Promise<void>;
|
|
121
|
+
leave(): Promise<void>;
|
|
122
|
+
setMicrophone(on: boolean): Promise<void>;
|
|
123
|
+
setCamera(on: boolean): Promise<void>;
|
|
124
|
+
/**
|
|
125
|
+
* Start sharing, and say whether it started.
|
|
126
|
+
*
|
|
127
|
+
* False covers both the person closing the picker and a capture that failed, and
|
|
128
|
+
* the difference is not the caller's business: a failure has already been
|
|
129
|
+
* reported as a `failed` event, and a cancellation is not a failure at all.
|
|
130
|
+
*
|
|
131
|
+
* With no options the browser's own picker chooses what to share. A `sourceId`
|
|
132
|
+
* means the host drew the picker — a desktop app, where the browser has none —
|
|
133
|
+
* and the person has already chosen.
|
|
134
|
+
*/
|
|
135
|
+
startScreenShare(options?: ShareOptions): Promise<boolean>;
|
|
136
|
+
stopScreenShare(): Promise<void>;
|
|
137
|
+
/**
|
|
138
|
+
* Which peers should send video.
|
|
139
|
+
*
|
|
140
|
+
* Only the visible tiles receive video; everybody else is audio-only. Paging
|
|
141
|
+
* the tile strip changes this set, and that is what bounds what each person
|
|
142
|
+
* downloads however big the call gets.
|
|
143
|
+
*/
|
|
144
|
+
setVideoSubscriptions(deviceIds: string[]): void;
|
|
145
|
+
/** Swap microphone or camera mid-call, from the device picker. */
|
|
146
|
+
useDevices(devices: {
|
|
147
|
+
audioDeviceId?: string;
|
|
148
|
+
videoDeviceId?: string;
|
|
149
|
+
}): Promise<void>;
|
|
150
|
+
on(handler: RtcHandler): () => void;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Ceilings per stream, so four cameras plus a share stay inside what a home
|
|
154
|
+
* connection can upload.
|
|
155
|
+
*
|
|
156
|
+
* A mesh cannot use simulcast: the sender uploads a separate copy to every peer,
|
|
157
|
+
* so the **sender** has to do the adapting. These are the steps it moves between.
|
|
158
|
+
*/
|
|
159
|
+
export declare const VIDEO_STEPS: readonly [{
|
|
160
|
+
readonly height: 720;
|
|
161
|
+
readonly maxBitrate: 1200000;
|
|
162
|
+
readonly maxFramerate: 30;
|
|
163
|
+
}, {
|
|
164
|
+
readonly height: 480;
|
|
165
|
+
readonly maxBitrate: 600000;
|
|
166
|
+
readonly maxFramerate: 30;
|
|
167
|
+
}, {
|
|
168
|
+
readonly height: 360;
|
|
169
|
+
readonly maxBitrate: 300000;
|
|
170
|
+
readonly maxFramerate: 24;
|
|
171
|
+
}, {
|
|
172
|
+
readonly height: 180;
|
|
173
|
+
readonly maxBitrate: 120000;
|
|
174
|
+
readonly maxFramerate: 15;
|
|
175
|
+
}];
|
|
176
|
+
/**
|
|
177
|
+
* The screen share's ceiling.
|
|
178
|
+
*
|
|
179
|
+
* Resolution is favoured over frame rate, because text has to stay readable and a
|
|
180
|
+
* slightly jerky slide is far better than a sharp one nobody can read.
|
|
181
|
+
*/
|
|
182
|
+
export declare const SCREEN_CEILING: {
|
|
183
|
+
readonly maxBitrate: 1500000;
|
|
184
|
+
readonly maxFramerate: 8;
|
|
185
|
+
};
|
|
186
|
+
/** Audio is protected and never steps down. Video degrades first, always. */
|
|
187
|
+
export declare const AUDIO_BITRATE = 32000;
|
|
188
|
+
/**
|
|
189
|
+
* What counts as talking, and how long it goes on counting.
|
|
190
|
+
*
|
|
191
|
+
* The level is the root mean square of the waveform, which is volume — not the
|
|
192
|
+
* average across the frequency bins, which is volume divided by however much of
|
|
193
|
+
* the spectrum happens to be empty. A voice puts almost all of its energy below
|
|
194
|
+
* two kilohertz, so averaging it across twenty-four kilohertz of mostly silence
|
|
195
|
+
* gives a number an order of magnitude smaller than the sound actually is, and a
|
|
196
|
+
* threshold picked against that number is a threshold nobody normal crosses.
|
|
197
|
+
*
|
|
198
|
+
* `SPEAKING_HOLD_MS` is what stops the ring strobing. Speech is not continuous:
|
|
199
|
+
* there is a gap between every word and a longer one between sentences, and an
|
|
200
|
+
* indicator that follows the waveform exactly flickers all the way through a
|
|
201
|
+
* sentence. Holding it briefly after the level drops reads as "this is the person
|
|
202
|
+
* talking" rather than as a light fault, and it cuts what goes over the socket,
|
|
203
|
+
* because only a change is sent.
|
|
204
|
+
*/
|
|
205
|
+
export declare const SPEAKING_LEVEL = 0.05;
|
|
206
|
+
export declare const SPEAKING_HOLD_MS = 1000;
|
|
207
|
+
/** How often the level is measured. Five times a second is under the eye's notice. */
|
|
208
|
+
export declare const LEVEL_INTERVAL_MS = 200;
|
|
209
|
+
/**
|
|
210
|
+
* How loud the waveform is, between 0 and 1.
|
|
211
|
+
*
|
|
212
|
+
* Takes the bytes an `AnalyserNode` gives for the time domain, where 128 is
|
|
213
|
+
* silence and the distance either side of it is the amplitude. Pure and exported
|
|
214
|
+
* so the one number the speaking indicator depends on can be tested without a
|
|
215
|
+
* browser, an audio context or a microphone.
|
|
216
|
+
*/
|
|
217
|
+
export declare function rmsLevel(samples: Uint8Array): number;
|
|
218
|
+
/**
|
|
219
|
+
* Whether this device counts as talking, now.
|
|
220
|
+
*
|
|
221
|
+
* `loudAt` is when the level was last above the threshold. Muting wins over the
|
|
222
|
+
* hold: a muted microphone is silent, and an indicator that says otherwise for a
|
|
223
|
+
* second afterwards is the one mistake this indicator must never make.
|
|
224
|
+
*/
|
|
225
|
+
export declare function speakingNow(state: {
|
|
226
|
+
muted: boolean;
|
|
227
|
+
loudAt: number;
|
|
228
|
+
now: number;
|
|
229
|
+
}): boolean;
|
|
230
|
+
/**
|
|
231
|
+
* How long to try before giving up.
|
|
232
|
+
*
|
|
233
|
+
* Bounded on purpose. Some networks block UDP and the relay ports outright, and
|
|
234
|
+
* on those the connection is never going to happen; an indefinite spinner just
|
|
235
|
+
* makes somebody wait longer to find that out.
|
|
236
|
+
*/
|
|
237
|
+
export declare const CONNECT_TIMEOUT_MS = 15000;
|
|
238
|
+
//# sourceMappingURL=adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../src/rtc/adapter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ceilings per stream, so four cameras plus a share stay inside what a home
|
|
3
|
+
* connection can upload.
|
|
4
|
+
*
|
|
5
|
+
* A mesh cannot use simulcast: the sender uploads a separate copy to every peer,
|
|
6
|
+
* so the **sender** has to do the adapting. These are the steps it moves between.
|
|
7
|
+
*/
|
|
8
|
+
export const VIDEO_STEPS = [
|
|
9
|
+
{ height: 720, maxBitrate: 1_200_000, maxFramerate: 30 },
|
|
10
|
+
{ height: 480, maxBitrate: 600_000, maxFramerate: 30 },
|
|
11
|
+
{ height: 360, maxBitrate: 300_000, maxFramerate: 24 },
|
|
12
|
+
{ height: 180, maxBitrate: 120_000, maxFramerate: 15 },
|
|
13
|
+
];
|
|
14
|
+
/**
|
|
15
|
+
* The screen share's ceiling.
|
|
16
|
+
*
|
|
17
|
+
* Resolution is favoured over frame rate, because text has to stay readable and a
|
|
18
|
+
* slightly jerky slide is far better than a sharp one nobody can read.
|
|
19
|
+
*/
|
|
20
|
+
export const SCREEN_CEILING = { maxBitrate: 1_500_000, maxFramerate: 8 };
|
|
21
|
+
/** Audio is protected and never steps down. Video degrades first, always. */
|
|
22
|
+
export const AUDIO_BITRATE = 32_000;
|
|
23
|
+
/**
|
|
24
|
+
* What counts as talking, and how long it goes on counting.
|
|
25
|
+
*
|
|
26
|
+
* The level is the root mean square of the waveform, which is volume — not the
|
|
27
|
+
* average across the frequency bins, which is volume divided by however much of
|
|
28
|
+
* the spectrum happens to be empty. A voice puts almost all of its energy below
|
|
29
|
+
* two kilohertz, so averaging it across twenty-four kilohertz of mostly silence
|
|
30
|
+
* gives a number an order of magnitude smaller than the sound actually is, and a
|
|
31
|
+
* threshold picked against that number is a threshold nobody normal crosses.
|
|
32
|
+
*
|
|
33
|
+
* `SPEAKING_HOLD_MS` is what stops the ring strobing. Speech is not continuous:
|
|
34
|
+
* there is a gap between every word and a longer one between sentences, and an
|
|
35
|
+
* indicator that follows the waveform exactly flickers all the way through a
|
|
36
|
+
* sentence. Holding it briefly after the level drops reads as "this is the person
|
|
37
|
+
* talking" rather than as a light fault, and it cuts what goes over the socket,
|
|
38
|
+
* because only a change is sent.
|
|
39
|
+
*/
|
|
40
|
+
export const SPEAKING_LEVEL = 0.05;
|
|
41
|
+
export const SPEAKING_HOLD_MS = 1_000;
|
|
42
|
+
/** How often the level is measured. Five times a second is under the eye's notice. */
|
|
43
|
+
export const LEVEL_INTERVAL_MS = 200;
|
|
44
|
+
/**
|
|
45
|
+
* How loud the waveform is, between 0 and 1.
|
|
46
|
+
*
|
|
47
|
+
* Takes the bytes an `AnalyserNode` gives for the time domain, where 128 is
|
|
48
|
+
* silence and the distance either side of it is the amplitude. Pure and exported
|
|
49
|
+
* so the one number the speaking indicator depends on can be tested without a
|
|
50
|
+
* browser, an audio context or a microphone.
|
|
51
|
+
*/
|
|
52
|
+
export function rmsLevel(samples) {
|
|
53
|
+
if (samples.length === 0)
|
|
54
|
+
return 0;
|
|
55
|
+
let total = 0;
|
|
56
|
+
for (const sample of samples) {
|
|
57
|
+
const amplitude = (sample - 128) / 128;
|
|
58
|
+
total += amplitude * amplitude;
|
|
59
|
+
}
|
|
60
|
+
return Math.sqrt(total / samples.length);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Whether this device counts as talking, now.
|
|
64
|
+
*
|
|
65
|
+
* `loudAt` is when the level was last above the threshold. Muting wins over the
|
|
66
|
+
* hold: a muted microphone is silent, and an indicator that says otherwise for a
|
|
67
|
+
* second afterwards is the one mistake this indicator must never make.
|
|
68
|
+
*/
|
|
69
|
+
export function speakingNow(state) {
|
|
70
|
+
return !state.muted && state.now - state.loudAt < SPEAKING_HOLD_MS;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* How long to try before giving up.
|
|
74
|
+
*
|
|
75
|
+
* Bounded on purpose. Some networks block UDP and the relay ports outright, and
|
|
76
|
+
* on those the connection is never going to happen; an indefinite spinner just
|
|
77
|
+
* makes somebody wait longer to find that out.
|
|
78
|
+
*/
|
|
79
|
+
export const CONNECT_TIMEOUT_MS = 15_000;
|
|
80
|
+
//# sourceMappingURL=adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter.js","sourceRoot":"","sources":["../../src/rtc/adapter.ts"],"names":[],"mappings":"AAmHA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,EAAE,EAAE;IACxD,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,EAAE,OAAO,EAAE,YAAY,EAAE,EAAE,EAAE;IACtD,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,EAAE,OAAO,EAAE,YAAY,EAAE,EAAE,EAAE;IACtD,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,EAAE,OAAO,EAAE,YAAY,EAAE,EAAE,EAAE;CAC9C,CAAA;AAEV;;;;;GAKG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,CAAC,EAAW,CAAA;AAEjF,6EAA6E;AAC7E,MAAM,CAAC,MAAM,aAAa,GAAG,MAAM,CAAA;AAEnC;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,IAAI,CAAA;AAClC,MAAM,CAAC,MAAM,gBAAgB,GAAG,KAAK,CAAA;AACrC,sFAAsF;AACtF,MAAM,CAAC,MAAM,iBAAiB,GAAG,GAAG,CAAA;AAEpC;;;;;;;GAOG;AACH,MAAM,UAAU,QAAQ,CAAC,OAAmB;IAC1C,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,CAAC,CAAA;IAClC,IAAI,KAAK,GAAG,CAAC,CAAA;IACb,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,SAAS,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC,GAAG,GAAG,CAAA;QACtC,KAAK,IAAI,SAAS,GAAG,SAAS,CAAA;IAChC,CAAC;IACD,OAAO,IAAI,CAAC,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;AAC1C,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CAAC,KAAsD;IAChF,OAAO,CAAC,KAAK,CAAC,KAAK,IAAI,KAAK,CAAC,GAAG,GAAG,KAAK,CAAC,MAAM,GAAG,gBAAgB,CAAA;AACpE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,MAAM,CAAA"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Choosing a microphone, a camera and a speaker, and coping when the browser
|
|
3
|
+
* says no.
|
|
4
|
+
*
|
|
5
|
+
* Almost all of the value here is in the failure cases. Picking a device is
|
|
6
|
+
* easy; telling somebody the difference between "you denied permission" and
|
|
7
|
+
* "Zoom still has your camera" is what stops a support ticket, and the browser
|
|
8
|
+
* will not do it for you.
|
|
9
|
+
*/
|
|
10
|
+
export interface Devices {
|
|
11
|
+
microphones: MediaDeviceInfo[];
|
|
12
|
+
cameras: MediaDeviceInfo[];
|
|
13
|
+
speakers: MediaDeviceInfo[];
|
|
14
|
+
}
|
|
15
|
+
export interface DeviceChoice {
|
|
16
|
+
audioDeviceId?: string;
|
|
17
|
+
videoDeviceId?: string;
|
|
18
|
+
/** Output routing, so a call can go to headphones while alerts stay on speakers. */
|
|
19
|
+
speakerDeviceId?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Where a choice is remembered.
|
|
23
|
+
*
|
|
24
|
+
* Injected rather than reached for, because this package has to run on a phone
|
|
25
|
+
* and `localStorage` does not exist there. The web app passes an adapter over
|
|
26
|
+
* localStorage; React Native would pass one over its own storage.
|
|
27
|
+
*/
|
|
28
|
+
export interface DeviceStorage {
|
|
29
|
+
read(): DeviceChoice;
|
|
30
|
+
write(choice: DeviceChoice): void;
|
|
31
|
+
}
|
|
32
|
+
export declare function memoryDeviceStorage(initial?: DeviceChoice): DeviceStorage;
|
|
33
|
+
/**
|
|
34
|
+
* What is plugged in.
|
|
35
|
+
*
|
|
36
|
+
* Labels are empty until permission has been granted at least once, which is
|
|
37
|
+
* why the pre-join panel asks for permission before showing the list: a picker
|
|
38
|
+
* offering "Microphone 1" and "Microphone 2" is no use to anybody.
|
|
39
|
+
*/
|
|
40
|
+
export declare function listDevices(): Promise<Devices>;
|
|
41
|
+
export interface PermissionOutcome {
|
|
42
|
+
granted: boolean;
|
|
43
|
+
/** Present when it failed: what happened, and what to do about it. */
|
|
44
|
+
problem?: string;
|
|
45
|
+
/** True when permission was granted but the device was unusable. */
|
|
46
|
+
inUseElsewhere?: boolean;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Ask for permission, and find out what actually happened.
|
|
50
|
+
*
|
|
51
|
+
* The awkward case this exists for: on some platforms a camera held by another
|
|
52
|
+
* application grants permission and then hands back a stream with no data in it
|
|
53
|
+
* at all. That looks identical to a working camera until somebody says they
|
|
54
|
+
* cannot see you, so a black stream is checked for explicitly and reported as
|
|
55
|
+
* the different problem it is.
|
|
56
|
+
*/
|
|
57
|
+
export declare function requestPermission(want: {
|
|
58
|
+
audio: boolean;
|
|
59
|
+
video: boolean;
|
|
60
|
+
}, deviceChoice?: DeviceChoice): Promise<PermissionOutcome & {
|
|
61
|
+
stream?: MediaStream;
|
|
62
|
+
}>;
|
|
63
|
+
/**
|
|
64
|
+
* Watch for devices arriving and leaving.
|
|
65
|
+
*
|
|
66
|
+
* Unplugging a headset mid-call should switch to the built-in microphone and
|
|
67
|
+
* say so. A silent switch is worse than no switch: somebody carries on talking
|
|
68
|
+
* into a headset that is no longer connected.
|
|
69
|
+
*/
|
|
70
|
+
export declare function watchDevices(onChange: (devices: Devices) => void): () => void;
|
|
71
|
+
/**
|
|
72
|
+
* Whether a remembered choice is still available.
|
|
73
|
+
*
|
|
74
|
+
* A USB headset that is unplugged should fall back to the operating system's
|
|
75
|
+
* default rather than failing with a constraint error nobody can interpret.
|
|
76
|
+
*/
|
|
77
|
+
export declare function stillAvailable(choice: DeviceChoice, devices: Devices): DeviceChoice;
|
|
78
|
+
/** The name to show for a device that has lost its label. */
|
|
79
|
+
export declare function labelFor(device: MediaDeviceInfo, index: number): string;
|
|
80
|
+
//# sourceMappingURL=devices.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"devices.d.ts","sourceRoot":"","sources":["../../src/rtc/devices.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AAEH,MAAM,WAAW,OAAO;IACtB,WAAW,EAAE,eAAe,EAAE,CAAA;IAC9B,OAAO,EAAE,eAAe,EAAE,CAAA;IAC1B,QAAQ,EAAE,eAAe,EAAE,CAAA;CAC5B;AAED,MAAM,WAAW,YAAY;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,oFAAoF;IACpF,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC5B,IAAI,IAAI,YAAY,CAAA;IACpB,KAAK,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI,CAAA;CAClC;AAED,wBAAgB,mBAAmB,CAAC,OAAO,GAAE,YAAiB,GAAG,aAAa,CAQ7E;AAED;;;;;;GAMG;AACH,wBAAsB,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC,CAOpD;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,OAAO,CAAA;IAChB,sEAAsE;IACtE,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,oEAAoE;IACpE,cAAc,CAAC,EAAE,OAAO,CAAA;CACzB;AAED;;;;;;;;GAQG;AACH,wBAAsB,iBAAiB,CACrC,IAAI,EAAE;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,EACxC,YAAY,GAAE,YAAiB,GAC9B,OAAO,CAAC,iBAAiB,GAAG;IAAE,MAAM,CAAC,EAAE,WAAW,CAAA;CAAE,CAAC,CAmCvD;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,GAAG,MAAM,IAAI,CAM7E;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO,GAAG,YAAY,CAWnF;AAED,6DAA6D;AAC7D,wBAAgB,QAAQ,CAAC,MAAM,EAAE,eAAe,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAKvE"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { describeMediaError } from './mesh.js';
|
|
2
|
+
export function memoryDeviceStorage(initial = {}) {
|
|
3
|
+
let held = initial;
|
|
4
|
+
return {
|
|
5
|
+
read: () => held,
|
|
6
|
+
write: (choice) => {
|
|
7
|
+
held = choice;
|
|
8
|
+
},
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* What is plugged in.
|
|
13
|
+
*
|
|
14
|
+
* Labels are empty until permission has been granted at least once, which is
|
|
15
|
+
* why the pre-join panel asks for permission before showing the list: a picker
|
|
16
|
+
* offering "Microphone 1" and "Microphone 2" is no use to anybody.
|
|
17
|
+
*/
|
|
18
|
+
export async function listDevices() {
|
|
19
|
+
const all = await navigator.mediaDevices.enumerateDevices();
|
|
20
|
+
return {
|
|
21
|
+
microphones: all.filter((device) => device.kind === 'audioinput'),
|
|
22
|
+
cameras: all.filter((device) => device.kind === 'videoinput'),
|
|
23
|
+
speakers: all.filter((device) => device.kind === 'audiooutput'),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Ask for permission, and find out what actually happened.
|
|
28
|
+
*
|
|
29
|
+
* The awkward case this exists for: on some platforms a camera held by another
|
|
30
|
+
* application grants permission and then hands back a stream with no data in it
|
|
31
|
+
* at all. That looks identical to a working camera until somebody says they
|
|
32
|
+
* cannot see you, so a black stream is checked for explicitly and reported as
|
|
33
|
+
* the different problem it is.
|
|
34
|
+
*/
|
|
35
|
+
export async function requestPermission(want, deviceChoice = {}) {
|
|
36
|
+
try {
|
|
37
|
+
const stream = await navigator.mediaDevices.getUserMedia({
|
|
38
|
+
audio: want.audio
|
|
39
|
+
? {
|
|
40
|
+
...(deviceChoice.audioDeviceId ? { deviceId: { exact: deviceChoice.audioDeviceId } } : {}),
|
|
41
|
+
echoCancellation: true,
|
|
42
|
+
noiseSuppression: true,
|
|
43
|
+
}
|
|
44
|
+
: false,
|
|
45
|
+
video: want.video
|
|
46
|
+
? deviceChoice.videoDeviceId
|
|
47
|
+
? { deviceId: { exact: deviceChoice.videoDeviceId } }
|
|
48
|
+
: true
|
|
49
|
+
: false,
|
|
50
|
+
});
|
|
51
|
+
const dead = stream.getTracks().filter((track) => track.readyState === 'ended' || track.muted);
|
|
52
|
+
if (dead.length > 0 && dead.length === stream.getTracks().length) {
|
|
53
|
+
for (const track of stream.getTracks())
|
|
54
|
+
track.stop();
|
|
55
|
+
return {
|
|
56
|
+
granted: true,
|
|
57
|
+
inUseElsewhere: true,
|
|
58
|
+
problem: 'Your camera or microphone is open in another app. Close it and try again — the permission is fine, the device is just busy.',
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return { granted: true, stream };
|
|
62
|
+
}
|
|
63
|
+
catch (cause) {
|
|
64
|
+
return {
|
|
65
|
+
granted: false,
|
|
66
|
+
problem: describeMediaError(cause, want.video ? 'camera' : 'microphone'),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Watch for devices arriving and leaving.
|
|
72
|
+
*
|
|
73
|
+
* Unplugging a headset mid-call should switch to the built-in microphone and
|
|
74
|
+
* say so. A silent switch is worse than no switch: somebody carries on talking
|
|
75
|
+
* into a headset that is no longer connected.
|
|
76
|
+
*/
|
|
77
|
+
export function watchDevices(onChange) {
|
|
78
|
+
const handler = () => {
|
|
79
|
+
void listDevices().then(onChange);
|
|
80
|
+
};
|
|
81
|
+
navigator.mediaDevices.addEventListener('devicechange', handler);
|
|
82
|
+
return () => navigator.mediaDevices.removeEventListener('devicechange', handler);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Whether a remembered choice is still available.
|
|
86
|
+
*
|
|
87
|
+
* A USB headset that is unplugged should fall back to the operating system's
|
|
88
|
+
* default rather than failing with a constraint error nobody can interpret.
|
|
89
|
+
*/
|
|
90
|
+
export function stillAvailable(choice, devices) {
|
|
91
|
+
const has = (list, id) => id !== undefined && list.some((device) => device.deviceId === id);
|
|
92
|
+
return {
|
|
93
|
+
...(has(devices.microphones, choice.audioDeviceId) ? { audioDeviceId: choice.audioDeviceId } : {}),
|
|
94
|
+
...(has(devices.cameras, choice.videoDeviceId) ? { videoDeviceId: choice.videoDeviceId } : {}),
|
|
95
|
+
...(has(devices.speakers, choice.speakerDeviceId)
|
|
96
|
+
? { speakerDeviceId: choice.speakerDeviceId }
|
|
97
|
+
: {}),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/** The name to show for a device that has lost its label. */
|
|
101
|
+
export function labelFor(device, index) {
|
|
102
|
+
if (device.label)
|
|
103
|
+
return device.label;
|
|
104
|
+
const kind = device.kind === 'audioinput' ? 'Microphone' : device.kind === 'videoinput' ? 'Camera' : 'Speaker';
|
|
105
|
+
return `${kind} ${index + 1}`;
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=devices.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"devices.js","sourceRoot":"","sources":["../../src/rtc/devices.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type RtcClientAdapter, type Signaller } from './adapter.js';
|
|
2
|
+
export declare function meshAdapter(signaller: Signaller): RtcClientAdapter;
|
|
3
|
+
/**
|
|
4
|
+
* Turn a getUserMedia failure into something worth reading.
|
|
5
|
+
*
|
|
6
|
+
* Denied permission is the most common support question in any call product,
|
|
7
|
+
* and the browser's own message is no help at all. A device held by another app
|
|
8
|
+
* is a different problem with a different fix, and on some platforms it arrives
|
|
9
|
+
* as a silent black stream rather than an error, so the two must not be
|
|
10
|
+
* collapsed into "something went wrong".
|
|
11
|
+
*/
|
|
12
|
+
export declare function describeMediaError(cause: unknown, device: 'microphone' | 'camera'): string;
|
|
13
|
+
//# sourceMappingURL=mesh.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mesh.d.ts","sourceRoot":"","sources":["../../src/rtc/mesh.ts"],"names":[],"mappings":"AAEA,OAAO,EAUL,KAAK,gBAAgB,EAGrB,KAAK,SAAS,EACf,MAAM,cAAc,CAAA;AA8DrB,wBAAgB,WAAW,CAAC,SAAS,EAAE,SAAS,GAAG,gBAAgB,CA2vBlE;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,YAAY,GAAG,QAAQ,GAAG,MAAM,CAkB1F"}
|