@voltkit-rtc/react-native 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -0
- package/README.md +9 -7
- package/lib/commonjs/local-media.js +52 -10
- package/lib/commonjs/local-media.js.map +1 -1
- package/lib/commonjs/native-session.js +61 -10
- package/lib/commonjs/native-session.js.map +1 -1
- package/lib/commonjs/native.js +1 -1
- package/lib/commonjs/platform/common.js +1 -1
- package/lib/commonjs/provider.js +3 -1
- package/lib/commonjs/provider.js.map +1 -1
- package/lib/module/local-media.js +52 -10
- package/lib/module/local-media.js.map +1 -1
- package/lib/module/native-session.js +61 -10
- package/lib/module/native-session.js.map +1 -1
- package/lib/module/native.js +1 -1
- package/lib/module/platform/common.js +1 -1
- package/lib/module/provider.js +3 -1
- package/lib/module/provider.js.map +1 -1
- package/lib/typescript/commonjs/native-session.d.ts +1 -1
- package/lib/typescript/commonjs/native-session.d.ts.map +1 -1
- package/lib/typescript/commonjs/native.d.ts +1 -1
- package/lib/typescript/commonjs/platform/common.d.ts +1 -1
- package/lib/typescript/commonjs/provider.d.ts.map +1 -1
- package/lib/typescript/module/native-session.d.ts +1 -1
- package/lib/typescript/module/native-session.d.ts.map +1 -1
- package/lib/typescript/module/native.d.ts +1 -1
- package/lib/typescript/module/platform/common.d.ts +1 -1
- package/lib/typescript/module/provider.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/local-media.ts +49 -11
- package/src/native-session.tsx +59 -13
- package/src/native.ts +1 -1
- package/src/platform/common.ts +1 -1
- package/src/provider.tsx +3 -1
package/src/local-media.ts
CHANGED
|
@@ -307,13 +307,28 @@ async function publishDevices(client: StreamClient, options: LocalPublishOptions
|
|
|
307
307
|
if (publishing.has(client) || localStream(client)?.getTracks().some((track) => track.readyState === 'live')) {
|
|
308
308
|
throw new RpcError('source-already-published', 'already publishing: unpublish() first');
|
|
309
309
|
}
|
|
310
|
-
const permissions = client.mediaPermissions;
|
|
311
|
-
if (audio !== false && !permissions.audio) throw new RpcError('media-disabled', 'audio is switched off for this project');
|
|
312
|
-
if (video !== false && !permissions.video) throw new RpcError('media-disabled', 'video is switched off for this project');
|
|
313
|
-
if (!registerVoltkitGlobals()) throw webrtcMissing();
|
|
314
|
-
|
|
315
310
|
publishing.add(client);
|
|
311
|
+
// Stop wins over a start still under way (0.1.2): unpublish() -- the user's Stop -- while the permission dialog is up
|
|
312
|
+
// or the camera is opening says so with localStreamChanged(null). No device opens after it, what opened is released,
|
|
313
|
+
// nothing is published, and this start rejects with `unpublished`.
|
|
314
|
+
let stopped = false;
|
|
315
|
+
const unwatch = client.on('localStreamChanged', (local) => {
|
|
316
|
+
if (local === null) stopped = true;
|
|
317
|
+
});
|
|
318
|
+
const stoppedError = () => new RpcError('unpublished', 'unpublish() ran while the camera and microphone were opening');
|
|
316
319
|
try {
|
|
320
|
+
let permissions = client.mediaPermissions;
|
|
321
|
+
if ((audio !== false && !permissions.audio) || (video !== false && !permissions.video)) {
|
|
322
|
+
// A hint from the join (client 0.9.14): the controller is asked before refusing. Null: it cannot say, and the
|
|
323
|
+
// client's publish lets it decide.
|
|
324
|
+
const fresh = await refreshPermissions(client, permissions);
|
|
325
|
+
permissions = fresh ?? { ...permissions, audio: true, video: true };
|
|
326
|
+
if (stopped) throw stoppedError();
|
|
327
|
+
}
|
|
328
|
+
if (audio !== false && !permissions.audio) throw new RpcError('media-disabled', 'audio is switched off for this project');
|
|
329
|
+
if (video !== false && !permissions.video) throw new RpcError('media-disabled', 'video is switched off for this project');
|
|
330
|
+
if (!registerVoltkitGlobals()) throw webrtcMissing();
|
|
331
|
+
|
|
317
332
|
const kinds: MediaPermissionKind[] = [];
|
|
318
333
|
if (video !== false) kinds.push('camera');
|
|
319
334
|
if (audio !== false) kinds.push('microphone');
|
|
@@ -323,27 +338,50 @@ async function publishDevices(client: StreamClient, options: LocalPublishOptions
|
|
|
323
338
|
if (status !== 'granted') throw permissionRefused(kind, status);
|
|
324
339
|
}
|
|
325
340
|
// The dialog stays up as long as the user thinks: the call may be over by now (the screen unmounted, a kick, the
|
|
326
|
-
// room closed). Nothing opens a device
|
|
341
|
+
// room closed), or the user tapped Stop. Nothing opens a device then.
|
|
342
|
+
if (stopped) throw stoppedError();
|
|
327
343
|
if (client.connectionState !== 'connected') throw new RpcError('not-connected', 'the session ended while the permissions were being asked for');
|
|
328
344
|
const constraints = captureConstraints(options, permissions);
|
|
329
345
|
const platform = reactNativePlatform();
|
|
330
346
|
const stream = (await platform.getUserMedia!(constraints)) as unknown as ClientStream;
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
await client.publish(stream, constraints, publishOptions);
|
|
334
|
-
} catch (error) {
|
|
335
|
-
// Released here, the stream before its tracks (platform/streams.ts): the client kept none of it.
|
|
347
|
+
// Released here, the stream before its tracks (platform/streams.ts): the client kept none of it.
|
|
348
|
+
const release = () => {
|
|
336
349
|
const book = streamBook();
|
|
337
350
|
const tracks = (stream as unknown as StreamLike).getTracks();
|
|
338
351
|
book.release(stream as unknown as StreamLike);
|
|
339
352
|
for (const track of tracks) book.stop(track as TrackLike);
|
|
353
|
+
};
|
|
354
|
+
if (stopped) {
|
|
355
|
+
release();
|
|
356
|
+
throw stoppedError();
|
|
357
|
+
}
|
|
358
|
+
try {
|
|
359
|
+
// With the constraints, a reconnect that finds the devices gone opens them again.
|
|
360
|
+
await client.publish(stream, constraints, publishOptions);
|
|
361
|
+
} catch (error) {
|
|
362
|
+
release();
|
|
340
363
|
throw error;
|
|
341
364
|
}
|
|
342
365
|
} finally {
|
|
366
|
+
if (typeof unwatch === 'function') unwatch();
|
|
343
367
|
publishing.delete(client);
|
|
344
368
|
}
|
|
345
369
|
}
|
|
346
370
|
|
|
371
|
+
/**
|
|
372
|
+
* The project's permissions as the controller says now (client 0.9.14), or null when it cannot say. A client older
|
|
373
|
+
* than 0.9.14 cannot ask: its hint stands. A failed question leaves the hint standing too.
|
|
374
|
+
*/
|
|
375
|
+
async function refreshPermissions(client: StreamClient, hint: MediaPermissions): Promise<MediaPermissions | null> {
|
|
376
|
+
const refresh = (client as { refreshMediaPermissions?: () => Promise<MediaPermissions | null> }).refreshMediaPermissions;
|
|
377
|
+
if (typeof refresh !== 'function') return hint;
|
|
378
|
+
try {
|
|
379
|
+
return await refresh.call(client);
|
|
380
|
+
} catch {
|
|
381
|
+
return hint;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
347
385
|
/** Each quality's 16:9 frame, as the SDK sends it. */
|
|
348
386
|
const FRAMES: Record<VideoQuality, { width: number; height: number }> = {
|
|
349
387
|
'1080p': { width: 1920, height: 1080 },
|
package/src/native-session.tsx
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* Failures reach the provider's onError as `media-unavailable` with `{ feature: 'callService' }`.
|
|
14
14
|
*/
|
|
15
15
|
import { useEffect, useMemo, useRef, useSyncExternalStore } from 'react';
|
|
16
|
-
import type { StreamClient } from '@voltkit-rtc/client';
|
|
16
|
+
import type { ClientOptions, StreamClient } from '@voltkit-rtc/client';
|
|
17
17
|
import { useVoltkitClient } from '@voltkit-rtc/react';
|
|
18
18
|
import type { CallServiceOptions as NativeCallServiceOptions, CallServiceStatus, Spec } from './specs/NativeVoltkitRtc';
|
|
19
19
|
import { listen, nativeConstants, nativeModule } from './native';
|
|
@@ -48,6 +48,7 @@ interface Want {
|
|
|
48
48
|
micPublished: boolean;
|
|
49
49
|
cameraPublished: boolean;
|
|
50
50
|
sharing: boolean;
|
|
51
|
+
/** The client switches its camera off in the background, so the service needs no camera type. */
|
|
51
52
|
pauseCamera: boolean;
|
|
52
53
|
text: CallServiceText;
|
|
53
54
|
leave(): void;
|
|
@@ -66,10 +67,14 @@ export interface CallServiceState {
|
|
|
66
67
|
const NOT_DECLARED: CallServiceState = Object.freeze({ declared: false, running: false, types: Object.freeze([]) as unknown as string[] });
|
|
67
68
|
|
|
68
69
|
class CallServices {
|
|
69
|
-
/**
|
|
70
|
-
|
|
70
|
+
/**
|
|
71
|
+
* What each call wants, read afresh at every decision: a call's state is its client's, which emits no event for some
|
|
72
|
+
* changes. With how to tell the call when its state cannot be read at all.
|
|
73
|
+
*/
|
|
74
|
+
private readonly wants = new Map<object, { want: () => Want; report: (error: Error) => void }>();
|
|
71
75
|
private running = false;
|
|
72
|
-
|
|
76
|
+
/** The call to the native module under way: a `failed` event during a start or update is its rejection's twin. */
|
|
77
|
+
private inFlight: 'start' | 'update' | 'stop' | null = null;
|
|
73
78
|
private again = false;
|
|
74
79
|
/** The options the service runs with, as sent. */
|
|
75
80
|
private applied: string | null = null;
|
|
@@ -79,8 +84,8 @@ class CallServices {
|
|
|
79
84
|
private state: CallServiceState | null = null;
|
|
80
85
|
private readonly listeners = new Set<() => void>();
|
|
81
86
|
|
|
82
|
-
set(key: object, want: (() => Want) | null): void {
|
|
83
|
-
if (want) this.wants.set(key, want);
|
|
87
|
+
set(key: object, want: (() => Want) | null, report: (error: Error) => void = () => undefined): void {
|
|
88
|
+
if (want) this.wants.set(key, { want, report });
|
|
84
89
|
else this.wants.delete(key);
|
|
85
90
|
this.reconcile();
|
|
86
91
|
}
|
|
@@ -120,7 +125,26 @@ class CallServices {
|
|
|
120
125
|
}
|
|
121
126
|
|
|
122
127
|
private live(): Want[] {
|
|
123
|
-
|
|
128
|
+
const live: Want[] = [];
|
|
129
|
+
for (const [key, { want, report }] of [...this.wants]) {
|
|
130
|
+
let w: Want;
|
|
131
|
+
try {
|
|
132
|
+
w = want();
|
|
133
|
+
} catch (err) {
|
|
134
|
+
// A client whose state cannot be read (a wrapper that throws on a read): this call is left out of the service
|
|
135
|
+
// for good, said once, and never keeps the other calls from theirs (the verifier's F3 on 0.1.1).
|
|
136
|
+
this.wants.delete(key);
|
|
137
|
+
logError('a call is left out of the call service: its client\'s state could not be read', err);
|
|
138
|
+
try {
|
|
139
|
+
report(callServiceFailed(`this call is left out, its client's state could not be read: ${describeError(err)}`));
|
|
140
|
+
} catch (reportErr) {
|
|
141
|
+
logError('onError threw', reportErr);
|
|
142
|
+
}
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (w.live) live.push(w);
|
|
146
|
+
}
|
|
147
|
+
return live;
|
|
124
148
|
}
|
|
125
149
|
|
|
126
150
|
private reconcile(): void {
|
|
@@ -128,7 +152,7 @@ class CallServices {
|
|
|
128
152
|
if (!module || !nativeConstants()?.callServiceDeclared) return;
|
|
129
153
|
const live = this.live();
|
|
130
154
|
this.watch(module, live.length > 0 || this.running);
|
|
131
|
-
if (this.
|
|
155
|
+
if (this.inFlight) {
|
|
132
156
|
this.again = true;
|
|
133
157
|
return;
|
|
134
158
|
}
|
|
@@ -146,7 +170,7 @@ class CallServices {
|
|
|
146
170
|
}
|
|
147
171
|
|
|
148
172
|
private run(module: Spec, what: 'start' | 'update' | 'stop', options: NativeCallServiceOptions | null): void {
|
|
149
|
-
this.
|
|
173
|
+
this.inFlight = what;
|
|
150
174
|
const key = options ? JSON.stringify(options) : null;
|
|
151
175
|
const call = what === 'stop' ? module.stopCallService() : what === 'start' ? module.startCallService(options!) : module.updateCallService(options!);
|
|
152
176
|
call.then(
|
|
@@ -166,7 +190,7 @@ class CallServices {
|
|
|
166
190
|
for (const want of this.live()) want.report(error);
|
|
167
191
|
},
|
|
168
192
|
).finally(() => {
|
|
169
|
-
this.
|
|
193
|
+
this.inFlight = null;
|
|
170
194
|
if (this.again) {
|
|
171
195
|
this.again = false;
|
|
172
196
|
this.reconcile();
|
|
@@ -220,6 +244,8 @@ class CallServices {
|
|
|
220
244
|
for (const want of this.live()) want.leave();
|
|
221
245
|
return;
|
|
222
246
|
case 'failed': {
|
|
247
|
+
// VoltkitCallService.runWith tells `failed` and then rejects the start or update: the rejection reports it (RN010-2).
|
|
248
|
+
if (this.inFlight === 'start' || this.inFlight === 'update') return;
|
|
223
249
|
const error = callServiceFailed(message || 'Android refused it');
|
|
224
250
|
for (const want of this.live()) want.report(error);
|
|
225
251
|
return;
|
|
@@ -265,7 +291,7 @@ export function useCallService(): CallServiceState {
|
|
|
265
291
|
export interface NativeSessionProps {
|
|
266
292
|
audio: AudioSessionOptions | false | undefined;
|
|
267
293
|
callService: boolean | CallServiceText | undefined;
|
|
268
|
-
/** The
|
|
294
|
+
/** The provider's background.pauseCamera: for a client whose own options cannot be read (see pausesCameraInBackground). */
|
|
269
295
|
pauseCamera: boolean;
|
|
270
296
|
onError: { readonly current: ((error: Error) => void) | undefined };
|
|
271
297
|
}
|
|
@@ -305,7 +331,8 @@ export function NativeSession({ audio, callService, pauseCamera, onError }: Nati
|
|
|
305
331
|
useEffect(() => {
|
|
306
332
|
if (!client || !wantsService || !nativeModule()) return;
|
|
307
333
|
const key = {};
|
|
308
|
-
|
|
334
|
+
const report = (error: Error) => onError.current?.(error);
|
|
335
|
+
callServices.set(key, () => wantOf(client, latest.current, report), report);
|
|
309
336
|
const update = () => callServices.changed();
|
|
310
337
|
// publish() and a camera switch emit no event of their own: useLocalMedia's calls say so through the local session.
|
|
311
338
|
const offs = [
|
|
@@ -330,9 +357,28 @@ function wantOf(client: StreamClient, latest: { text: CallServiceText; pauseCame
|
|
|
330
357
|
micPublished: (local?.getAudioTracks().length ?? 0) > 0,
|
|
331
358
|
cameraPublished: (local?.getVideoTracks().length ?? 0) > 0,
|
|
332
359
|
sharing: localSessionOf(client).screen !== null,
|
|
333
|
-
pauseCamera: latest.pauseCamera,
|
|
360
|
+
pauseCamera: pausesCameraInBackground(client) ?? latest.pauseCamera,
|
|
334
361
|
text: latest.text,
|
|
335
362
|
leave: () => client.close(),
|
|
336
363
|
report,
|
|
337
364
|
};
|
|
338
365
|
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Whether the client switches its camera off in the background, from its own options: the provider's `background` only
|
|
369
|
+
* describes a client the provider made, and an app-made one (`createClient`, the `client` prop) keeps its camera on with
|
|
370
|
+
* `pauseCamera: false` while the service was asked for no camera type, so Android cut the camera (RN010-1). The client
|
|
371
|
+
* pauses only with `pauseCameraInBackground === true` (left out, the camera runs on). Null when the options cannot be
|
|
372
|
+
* read (missing, not an object, or a read that throws: a wrapped or stubbed client): the provider's setting stands in.
|
|
373
|
+
*/
|
|
374
|
+
function pausesCameraInBackground(client: StreamClient): boolean | null {
|
|
375
|
+
let options: unknown;
|
|
376
|
+
try {
|
|
377
|
+
options = (client as unknown as { options?: unknown }).options;
|
|
378
|
+
} catch {
|
|
379
|
+
// A wrapper that forbids the read (a logging or sealing Proxy): not an error, the provider's setting stands in.
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
if (!options || typeof options !== 'object') return null;
|
|
383
|
+
return (options as ClientOptions).pauseCameraInBackground === true;
|
|
384
|
+
}
|
package/src/native.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { iosUnsupported, logError, nativeMissing } from './errors';
|
|
|
10
10
|
import type { AudioDeviceType, DeviceKind, MediaDevice } from '@voltkit-rtc/client';
|
|
11
11
|
|
|
12
12
|
/** The version of this package: what getDiagnostics() reports, and what the native side was built from. */
|
|
13
|
-
export const SDK_VERSION = '0.1.
|
|
13
|
+
export const SDK_VERSION = '0.1.2';
|
|
14
14
|
|
|
15
15
|
/** The native module, or null where there is none (iOS, Expo Go, an app not rebuilt). */
|
|
16
16
|
export function nativeModule(): Spec | null {
|
package/src/platform/common.ts
CHANGED
|
@@ -89,7 +89,7 @@ export function localStream(client: { readonly local: unknown }): MediaStreamLik
|
|
|
89
89
|
return (client.local as MediaStreamLike | null | undefined) ?? null;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
/** getDiagnostics().userAgent: `ReactNative android 37; react-native-webrtc 155.0.1; @voltkit-rtc/react-native 0.1.
|
|
92
|
+
/** getDiagnostics().userAgent: `ReactNative android 37; react-native-webrtc 155.0.1; @voltkit-rtc/react-native 0.1.2`. */
|
|
93
93
|
export function describePlatform(): string {
|
|
94
94
|
return `ReactNative ${Platform.OS} ${String(Platform.Version)}; react-native-webrtc ${webrtcVersion() ?? 'unknown'}; @voltkit-rtc/react-native ${SDK_VERSION}`;
|
|
95
95
|
}
|
package/src/provider.tsx
CHANGED
|
@@ -68,7 +68,9 @@ export function VoltkitProvider({
|
|
|
68
68
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
69
69
|
[webrtc, wsUrl, token],
|
|
70
70
|
);
|
|
71
|
-
|
|
71
|
+
// The call service reads the setting from the client it runs for (an app-made client has its own); this stands in
|
|
72
|
+
// when the client's options cannot be read.
|
|
73
|
+
const pauseCamera = options?.pauseCameraInBackground === true;
|
|
72
74
|
|
|
73
75
|
return (
|
|
74
76
|
<ReactVoltkitProvider
|