@voltkit-rtc/react-native 0.0.0-stage → 0.1.1
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 +121 -0
- package/LICENSE +21 -0
- package/README.md +1608 -2
- package/android/build.gradle +70 -0
- package/android/consumer-rules.pro +21 -0
- package/android/src/main/AndroidManifest.xml +25 -0
- package/android/src/main/java/com/voltkit/rtc/reactnative/AppVisibility.kt +67 -0
- package/android/src/main/java/com/voltkit/rtc/reactnative/AudioDevices.kt +67 -0
- package/android/src/main/java/com/voltkit/rtc/reactnative/AudioRouter.kt +487 -0
- package/android/src/main/java/com/voltkit/rtc/reactnative/CallServiceTypes.kt +120 -0
- package/android/src/main/java/com/voltkit/rtc/reactnative/NativeTimers.kt +106 -0
- package/android/src/main/java/com/voltkit/rtc/reactnative/NetworkWatch.kt +120 -0
- package/android/src/main/java/com/voltkit/rtc/reactnative/OutputChoice.kt +65 -0
- package/android/src/main/java/com/voltkit/rtc/reactnative/RtcConfig.kt +83 -0
- package/android/src/main/java/com/voltkit/rtc/reactnative/ScoLink.kt +61 -0
- package/android/src/main/java/com/voltkit/rtc/reactnative/VoltkitCallService.kt +287 -0
- package/android/src/main/java/com/voltkit/rtc/reactnative/VoltkitRtcInitProvider.kt +215 -0
- package/android/src/main/java/com/voltkit/rtc/reactnative/VoltkitRtcModule.kt +313 -0
- package/android/src/main/java/com/voltkit/rtc/reactnative/VoltkitRtcPackage.kt +30 -0
- package/android/src/main/res/drawable/voltkit_rtc_default_call_icon.xml +15 -0
- package/android/src/main/res/raw/voltkit_rtc_keep.xml +7 -0
- package/app.plugin.js +17 -0
- package/lib/commonjs/audio.js +215 -0
- package/lib/commonjs/audio.js.map +1 -0
- package/lib/commonjs/components.js +207 -0
- package/lib/commonjs/components.js.map +1 -0
- package/lib/commonjs/create-client.js +47 -0
- package/lib/commonjs/create-client.js.map +1 -0
- package/lib/commonjs/errors.js +99 -0
- package/lib/commonjs/errors.js.map +1 -0
- package/lib/commonjs/globals.js +61 -0
- package/lib/commonjs/globals.js.map +1 -0
- package/lib/commonjs/index.js +234 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/local-media.js +392 -0
- package/lib/commonjs/local-media.js.map +1 -0
- package/lib/commonjs/native-session.js +365 -0
- package/lib/commonjs/native-session.js.map +1 -0
- package/lib/commonjs/native.js +99 -0
- package/lib/commonjs/native.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/permissions.js +166 -0
- package/lib/commonjs/permissions.js.map +1 -0
- package/lib/commonjs/platform/android.js +126 -0
- package/lib/commonjs/platform/android.js.map +1 -0
- package/lib/commonjs/platform/common.js +138 -0
- package/lib/commonjs/platform/common.js.map +1 -0
- package/lib/commonjs/platform/devices.js +151 -0
- package/lib/commonjs/platform/devices.js.map +1 -0
- package/lib/commonjs/platform/index.js +35 -0
- package/lib/commonjs/platform/index.js.map +1 -0
- package/lib/commonjs/platform/ios.js +54 -0
- package/lib/commonjs/platform/ios.js.map +1 -0
- package/lib/commonjs/platform/streams.js +273 -0
- package/lib/commonjs/platform/streams.js.map +1 -0
- package/lib/commonjs/platform/timers.js +71 -0
- package/lib/commonjs/platform/timers.js.map +1 -0
- package/lib/commonjs/platform/visibility.js +60 -0
- package/lib/commonjs/platform/visibility.js.map +1 -0
- package/lib/commonjs/preflight.js +77 -0
- package/lib/commonjs/preflight.js.map +1 -0
- package/lib/commonjs/provider.js +75 -0
- package/lib/commonjs/provider.js.map +1 -0
- package/lib/commonjs/specs/NativeVoltkitRtc.js +29 -0
- package/lib/commonjs/specs/NativeVoltkitRtc.js.map +1 -0
- package/lib/module/audio.js +207 -0
- package/lib/module/audio.js.map +1 -0
- package/lib/module/components.js +200 -0
- package/lib/module/components.js.map +1 -0
- package/lib/module/create-client.js +42 -0
- package/lib/module/create-client.js.map +1 -0
- package/lib/module/errors.js +87 -0
- package/lib/module/errors.js.map +1 -0
- package/lib/module/globals.js +53 -0
- package/lib/module/globals.js.map +1 -0
- package/lib/module/index.js +33 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/local-media.js +387 -0
- package/lib/module/local-media.js.map +1 -0
- package/lib/module/native-session.js +360 -0
- package/lib/module/native-session.js.map +1 -0
- package/lib/module/native.js +87 -0
- package/lib/module/native.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/permissions.js +160 -0
- package/lib/module/permissions.js.map +1 -0
- package/lib/module/platform/android.js +122 -0
- package/lib/module/platform/android.js.map +1 -0
- package/lib/module/platform/common.js +128 -0
- package/lib/module/platform/common.js.map +1 -0
- package/lib/module/platform/devices.js +142 -0
- package/lib/module/platform/devices.js.map +1 -0
- package/lib/module/platform/index.js +24 -0
- package/lib/module/platform/index.js.map +1 -0
- package/lib/module/platform/ios.js +49 -0
- package/lib/module/platform/ios.js.map +1 -0
- package/lib/module/platform/streams.js +268 -0
- package/lib/module/platform/streams.js.map +1 -0
- package/lib/module/platform/timers.js +67 -0
- package/lib/module/platform/timers.js.map +1 -0
- package/lib/module/platform/visibility.js +54 -0
- package/lib/module/platform/visibility.js.map +1 -0
- package/lib/module/preflight.js +72 -0
- package/lib/module/preflight.js.map +1 -0
- package/lib/module/provider.js +70 -0
- package/lib/module/provider.js.map +1 -0
- package/lib/module/specs/NativeVoltkitRtc.js +37 -0
- package/lib/module/specs/NativeVoltkitRtc.js.map +1 -0
- package/lib/typescript/commonjs/audio.d.ts +58 -0
- package/lib/typescript/commonjs/audio.d.ts.map +1 -0
- package/lib/typescript/commonjs/components.d.ts +58 -0
- package/lib/typescript/commonjs/components.d.ts.map +1 -0
- package/lib/typescript/commonjs/create-client.d.ts +35 -0
- package/lib/typescript/commonjs/create-client.d.ts.map +1 -0
- package/lib/typescript/commonjs/errors.d.ts +34 -0
- package/lib/typescript/commonjs/errors.d.ts.map +1 -0
- package/lib/typescript/commonjs/globals.d.ts +16 -0
- package/lib/typescript/commonjs/globals.d.ts.map +1 -0
- package/lib/typescript/commonjs/index.d.ts +24 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/local-media.d.ts +86 -0
- package/lib/typescript/commonjs/local-media.d.ts.map +1 -0
- package/lib/typescript/commonjs/native-session.d.ts +38 -0
- package/lib/typescript/commonjs/native-session.d.ts.map +1 -0
- package/lib/typescript/commonjs/native.d.ts +18 -0
- package/lib/typescript/commonjs/native.d.ts.map +1 -0
- package/lib/typescript/commonjs/package.json +1 -0
- package/lib/typescript/commonjs/permissions.d.ts +30 -0
- package/lib/typescript/commonjs/permissions.d.ts.map +1 -0
- package/lib/typescript/commonjs/platform/android.d.ts +4 -0
- package/lib/typescript/commonjs/platform/android.d.ts.map +1 -0
- package/lib/typescript/commonjs/platform/common.d.ts +101 -0
- package/lib/typescript/commonjs/platform/common.d.ts.map +1 -0
- package/lib/typescript/commonjs/platform/devices.d.ts +42 -0
- package/lib/typescript/commonjs/platform/devices.d.ts.map +1 -0
- package/lib/typescript/commonjs/platform/index.d.ts +6 -0
- package/lib/typescript/commonjs/platform/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/platform/ios.d.ts +4 -0
- package/lib/typescript/commonjs/platform/ios.d.ts.map +1 -0
- package/lib/typescript/commonjs/platform/streams.d.ts +82 -0
- package/lib/typescript/commonjs/platform/streams.d.ts.map +1 -0
- package/lib/typescript/commonjs/platform/timers.d.ts +19 -0
- package/lib/typescript/commonjs/platform/timers.d.ts.map +1 -0
- package/lib/typescript/commonjs/platform/visibility.d.ts +11 -0
- package/lib/typescript/commonjs/platform/visibility.d.ts.map +1 -0
- package/lib/typescript/commonjs/preflight.d.ts +19 -0
- package/lib/typescript/commonjs/preflight.d.ts.map +1 -0
- package/lib/typescript/commonjs/provider.d.ts +35 -0
- package/lib/typescript/commonjs/provider.d.ts.map +1 -0
- package/lib/typescript/commonjs/specs/NativeVoltkitRtc.d.ts +157 -0
- package/lib/typescript/commonjs/specs/NativeVoltkitRtc.d.ts.map +1 -0
- package/lib/typescript/module/audio.d.ts +58 -0
- package/lib/typescript/module/audio.d.ts.map +1 -0
- package/lib/typescript/module/components.d.ts +58 -0
- package/lib/typescript/module/components.d.ts.map +1 -0
- package/lib/typescript/module/create-client.d.ts +35 -0
- package/lib/typescript/module/create-client.d.ts.map +1 -0
- package/lib/typescript/module/errors.d.ts +34 -0
- package/lib/typescript/module/errors.d.ts.map +1 -0
- package/lib/typescript/module/globals.d.ts +16 -0
- package/lib/typescript/module/globals.d.ts.map +1 -0
- package/lib/typescript/module/index.d.ts +24 -0
- package/lib/typescript/module/index.d.ts.map +1 -0
- package/lib/typescript/module/local-media.d.ts +86 -0
- package/lib/typescript/module/local-media.d.ts.map +1 -0
- package/lib/typescript/module/native-session.d.ts +38 -0
- package/lib/typescript/module/native-session.d.ts.map +1 -0
- package/lib/typescript/module/native.d.ts +18 -0
- package/lib/typescript/module/native.d.ts.map +1 -0
- package/lib/typescript/module/package.json +1 -0
- package/lib/typescript/module/permissions.d.ts +30 -0
- package/lib/typescript/module/permissions.d.ts.map +1 -0
- package/lib/typescript/module/platform/android.d.ts +4 -0
- package/lib/typescript/module/platform/android.d.ts.map +1 -0
- package/lib/typescript/module/platform/common.d.ts +101 -0
- package/lib/typescript/module/platform/common.d.ts.map +1 -0
- package/lib/typescript/module/platform/devices.d.ts +42 -0
- package/lib/typescript/module/platform/devices.d.ts.map +1 -0
- package/lib/typescript/module/platform/index.d.ts +6 -0
- package/lib/typescript/module/platform/index.d.ts.map +1 -0
- package/lib/typescript/module/platform/ios.d.ts +4 -0
- package/lib/typescript/module/platform/ios.d.ts.map +1 -0
- package/lib/typescript/module/platform/streams.d.ts +82 -0
- package/lib/typescript/module/platform/streams.d.ts.map +1 -0
- package/lib/typescript/module/platform/timers.d.ts +19 -0
- package/lib/typescript/module/platform/timers.d.ts.map +1 -0
- package/lib/typescript/module/platform/visibility.d.ts +11 -0
- package/lib/typescript/module/platform/visibility.d.ts.map +1 -0
- package/lib/typescript/module/preflight.d.ts +19 -0
- package/lib/typescript/module/preflight.d.ts.map +1 -0
- package/lib/typescript/module/provider.d.ts +35 -0
- package/lib/typescript/module/provider.d.ts.map +1 -0
- package/lib/typescript/module/specs/NativeVoltkitRtc.d.ts +157 -0
- package/lib/typescript/module/specs/NativeVoltkitRtc.d.ts.map +1 -0
- package/package.json +140 -4
- package/plugin/build/android.d.ts +36 -0
- package/plugin/build/android.js +265 -0
- package/plugin/build/index.d.ts +9 -0
- package/plugin/build/index.js +54 -0
- package/plugin/build/ios.d.ts +4 -0
- package/plugin/build/ios.js +43 -0
- package/plugin/build/options.d.ts +93 -0
- package/plugin/build/options.js +140 -0
- package/react-native.config.js +11 -0
- package/src/audio.ts +221 -0
- package/src/components.tsx +233 -0
- package/src/create-client.ts +53 -0
- package/src/errors.ts +81 -0
- package/src/globals.ts +54 -0
- package/src/index.ts +95 -0
- package/src/local-media.ts +420 -0
- package/src/native-session.tsx +384 -0
- package/src/native.ts +84 -0
- package/src/permissions.ts +173 -0
- package/src/platform/android.ts +128 -0
- package/src/platform/common.ts +157 -0
- package/src/platform/devices.ts +148 -0
- package/src/platform/index.ts +22 -0
- package/src/platform/ios.ts +48 -0
- package/src/platform/streams.ts +285 -0
- package/src/platform/timers.ts +72 -0
- package/src/platform/visibility.ts +59 -0
- package/src/preflight.ts +60 -0
- package/src/provider.tsx +88 -0
- package/src/specs/NativeVoltkitRtc.ts +180 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every native MediaStream this package makes, and the order in which streams and tracks are let go.
|
|
3
|
+
*
|
|
4
|
+
* In react-native-webrtc a MediaStream is a native object that holds its tracks. Releasing a stream after one of its
|
|
5
|
+
* tracks was disposed -- a local track released, or a remote one whose peer connection closed -- kills the app:
|
|
6
|
+
* `mediaStreamRemoveTrack` cannot find the track, so it stays in the native stream, and disposing the stream throws
|
|
7
|
+
* `IllegalStateException: MediaStreamTrack has been disposed` on the module's thread (Phase 0 finding 1, seen on a
|
|
8
|
+
* reconnect, a room closing and a plain release in the wrong order). So:
|
|
9
|
+
*
|
|
10
|
+
* - Views (VideoTrack) show a track through a wrapper stream made only here: one per track, shared by every view of it,
|
|
11
|
+
* released (`release(false)`, never the track) when the last view goes.
|
|
12
|
+
* - Before the client closes what receives remote tracks (a consumer, the receive transport, the session) it calls the
|
|
13
|
+
* platform's `detachTracks`: their wrappers are released then, while the peer connection still exists, and no new
|
|
14
|
+
* one is made for them.
|
|
15
|
+
* - `stop(track)` (the platform's stopTrack) releases the track's wrappers and takes it out of every other stream made
|
|
16
|
+
* here before it disposes the track; a capture stream left empty is released too. A stream the app got from
|
|
17
|
+
* getUserMedia through this package can therefore never be left holding a disposed track.
|
|
18
|
+
* - A stream the app gave the client (`client.publish(stream)` with a stream the app captured itself) is the app's: the
|
|
19
|
+
* client stops and releases its tracks but never the stream. Once told of it (`adoptAppStream`), `stop(track)` takes
|
|
20
|
+
* the track out of it too while the track is alive, so the app can still release it afterwards; this package never
|
|
21
|
+
* releases it. @voltkit-rtc/client 0.9.12's publish() and publishScreen({ stream }) call that hook.
|
|
22
|
+
* - A stream that would still be released while holding a disposed track (a remote track already ended, a track
|
|
23
|
+
* released elsewhere) is left alone instead: a small native leak, never a crash. A view wrapper whose track ended
|
|
24
|
+
* before the view let go is left the same way: only the end shows that an app released the track itself.
|
|
25
|
+
*/
|
|
26
|
+
import type { MediaStream as RNMediaStream } from 'react-native-webrtc';
|
|
27
|
+
import { loadWebRTC } from '../globals';
|
|
28
|
+
import { logError, webrtcMissing } from '../errors';
|
|
29
|
+
|
|
30
|
+
/** What this module needs of a track: react-native-webrtc's MediaStreamTrack, or a test's stand-in. */
|
|
31
|
+
export interface TrackLike {
|
|
32
|
+
readonly id: string;
|
|
33
|
+
readonly kind: string;
|
|
34
|
+
readonly remote?: boolean;
|
|
35
|
+
readonly readyState: string;
|
|
36
|
+
enabled: boolean;
|
|
37
|
+
stop(): void;
|
|
38
|
+
release?(): void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** What this module needs of a stream: react-native-webrtc's MediaStream. */
|
|
42
|
+
export interface StreamLike {
|
|
43
|
+
getTracks(): TrackLike[];
|
|
44
|
+
removeTrack(track: TrackLike): void;
|
|
45
|
+
release(releaseTracks?: boolean): void;
|
|
46
|
+
toURL(): string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Why a stream is kept: a capture (getUserMedia, getDisplayMedia), the client's own (createStream), a view, or the
|
|
51
|
+
* app's own that the client publishes (adoptAppStream: tracks taken out of it, never released here).
|
|
52
|
+
*/
|
|
53
|
+
export type StreamRole = 'capture' | 'client' | 'view' | 'app';
|
|
54
|
+
|
|
55
|
+
interface Entry {
|
|
56
|
+
readonly stream: StreamLike;
|
|
57
|
+
readonly role: StreamRole;
|
|
58
|
+
released: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface View {
|
|
62
|
+
readonly entry: Entry;
|
|
63
|
+
refs: number;
|
|
64
|
+
readonly ended: Set<() => void>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** One view's hold on a track's wrapper stream. */
|
|
68
|
+
export interface ViewHandle {
|
|
69
|
+
/** The wrapper's URL, for RTCView's streamURL. */
|
|
70
|
+
readonly url: string;
|
|
71
|
+
/** Called when the wrapper goes before this view lets go of it (the track was stopped or detached). */
|
|
72
|
+
onEnd(listener: () => void): () => void;
|
|
73
|
+
/** This view is done; the wrapper is released when the last view of the track lets go. */
|
|
74
|
+
release(): void;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export class StreamBook {
|
|
78
|
+
private readonly live = new Set<Entry>();
|
|
79
|
+
private readonly entries = new WeakMap<object, Entry>();
|
|
80
|
+
private readonly views = new Map<TrackLike, View>();
|
|
81
|
+
/** Local tracks this package disposed (released). */
|
|
82
|
+
private readonly disposed = new WeakSet<object>();
|
|
83
|
+
/** Remote tracks whose connection is closing: no new view, their wrappers already released. */
|
|
84
|
+
private readonly detached = new WeakSet<object>();
|
|
85
|
+
private readonly stopped = new WeakSet<object>();
|
|
86
|
+
private readonly screens = new WeakSet<object>();
|
|
87
|
+
/** Streams left unreleased because releasing them would have crashed the app. */
|
|
88
|
+
leaked = 0;
|
|
89
|
+
/** Called after a screen track is stopped and released (react-native-webrtc's MediaProjectionService then stops). */
|
|
90
|
+
onScreenStopped: (() => void) | null = null;
|
|
91
|
+
|
|
92
|
+
constructor(private readonly makeStream: (tracks: TrackLike[]) => StreamLike) {}
|
|
93
|
+
|
|
94
|
+
/** Keep `stream`, one the platform made (or the app's, `app`), to let go of in the right order. */
|
|
95
|
+
adopt(stream: StreamLike, role: StreamRole): void {
|
|
96
|
+
const known = this.entries.get(stream);
|
|
97
|
+
if (known) {
|
|
98
|
+
// The app's stream given again (published once more): watched again.
|
|
99
|
+
if (known.role === 'app' && !known.released) this.live.add(known);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const entry: Entry = { stream, role, released: false };
|
|
103
|
+
this.entries.set(stream, entry);
|
|
104
|
+
this.live.add(entry);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** A stream of these tracks for the client's own use (ClientPlatform.createStream). */
|
|
108
|
+
create(tracks: TrackLike[]): StreamLike {
|
|
109
|
+
const stream = this.makeStream(tracks);
|
|
110
|
+
this.adopt(stream, 'client');
|
|
111
|
+
return stream;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The tracks of `stream` are a screen capture's. */
|
|
115
|
+
markScreen(stream: StreamLike): void {
|
|
116
|
+
for (const track of stream.getTracks()) this.screens.add(track);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Release `stream` without its tracks (ClientPlatform.releaseStream). Again does nothing. */
|
|
120
|
+
release(stream: StreamLike): void {
|
|
121
|
+
let entry = this.entries.get(stream);
|
|
122
|
+
if (!entry) {
|
|
123
|
+
// Not one made here: released the same careful way.
|
|
124
|
+
entry = { stream, role: 'client', released: false };
|
|
125
|
+
this.entries.set(stream, entry);
|
|
126
|
+
}
|
|
127
|
+
this.releaseEntry(entry);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Stop and release `track` (ClientPlatform.stopTrack): its view wrappers are released and it leaves every stream
|
|
132
|
+
* kept here first (made here, or the app's adopted), while the native track still exists. Again does nothing. A
|
|
133
|
+
* remote track is only let go of: its connection owns it.
|
|
134
|
+
*/
|
|
135
|
+
stop(track: TrackLike): void {
|
|
136
|
+
if (this.stopped.has(track)) return;
|
|
137
|
+
this.stopped.add(track);
|
|
138
|
+
this.dropViews(track);
|
|
139
|
+
const safe = !this.unsafe(track);
|
|
140
|
+
for (const entry of [...this.live]) {
|
|
141
|
+
if (entry.role === 'view' || !entry.stream.getTracks().includes(track)) continue;
|
|
142
|
+
// A track the native side can no longer find stays where it is: the stream is then left, never released.
|
|
143
|
+
if (safe) {
|
|
144
|
+
try {
|
|
145
|
+
entry.stream.removeTrack(track);
|
|
146
|
+
} catch (err) {
|
|
147
|
+
logError('could not take a track out of a stream', err);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (entry.stream.getTracks().length > 0) continue;
|
|
151
|
+
if (entry.role === 'capture') this.releaseEntry(entry);
|
|
152
|
+
// The app's, now empty: nothing in it can crash its release, which is the app's to do.
|
|
153
|
+
else if (entry.role === 'app') this.live.delete(entry);
|
|
154
|
+
}
|
|
155
|
+
if (track.remote) return;
|
|
156
|
+
try {
|
|
157
|
+
track.stop();
|
|
158
|
+
} catch (err) {
|
|
159
|
+
logError('could not stop a track', err);
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
track.release?.();
|
|
163
|
+
} catch (err) {
|
|
164
|
+
logError('could not release a track', err);
|
|
165
|
+
}
|
|
166
|
+
this.disposed.add(track);
|
|
167
|
+
if (this.screens.has(track)) {
|
|
168
|
+
try {
|
|
169
|
+
this.onScreenStopped?.();
|
|
170
|
+
} catch (err) {
|
|
171
|
+
logError('could not stop the screen-share service', err);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* The client is about to close what receives these remote tracks (ClientPlatform.detachTracks): release their
|
|
178
|
+
* wrappers now, while their peer connection still exists, and make no new ones.
|
|
179
|
+
*/
|
|
180
|
+
detach(tracks: readonly TrackLike[]): void {
|
|
181
|
+
for (const track of tracks) {
|
|
182
|
+
this.dropViews(track);
|
|
183
|
+
this.detached.add(track);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** A view of `track` (VideoTrack), or null when it cannot be shown: ended, stopped or detached. */
|
|
188
|
+
view(track: TrackLike): ViewHandle | null {
|
|
189
|
+
if (this.stopped.has(track) || this.detached.has(track) || this.unsafe(track) || track.readyState === 'ended') return null;
|
|
190
|
+
let view = this.views.get(track);
|
|
191
|
+
if (!view) {
|
|
192
|
+
let stream: StreamLike;
|
|
193
|
+
try {
|
|
194
|
+
stream = this.makeStream([track]);
|
|
195
|
+
} catch (err) {
|
|
196
|
+
logError('could not make a stream to show a track', err);
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
const entry: Entry = { stream, role: 'view', released: false };
|
|
200
|
+
this.entries.set(stream, entry);
|
|
201
|
+
this.live.add(entry);
|
|
202
|
+
view = { entry, refs: 0, ended: new Set() };
|
|
203
|
+
this.views.set(track, view);
|
|
204
|
+
}
|
|
205
|
+
view.refs += 1;
|
|
206
|
+
const held = view;
|
|
207
|
+
let done = false;
|
|
208
|
+
return {
|
|
209
|
+
url: held.entry.stream.toURL(),
|
|
210
|
+
onEnd: (listener) => {
|
|
211
|
+
held.ended.add(listener);
|
|
212
|
+
return () => {
|
|
213
|
+
held.ended.delete(listener);
|
|
214
|
+
};
|
|
215
|
+
},
|
|
216
|
+
release: () => {
|
|
217
|
+
if (done) return;
|
|
218
|
+
done = true;
|
|
219
|
+
held.refs -= 1;
|
|
220
|
+
if (held.refs > 0 || this.views.get(track) !== held) return;
|
|
221
|
+
this.views.delete(track);
|
|
222
|
+
this.releaseEntry(held.entry);
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** How many streams made here are not released yet, how many tracks have views, and how many were left unreleased. */
|
|
228
|
+
stats(): { live: number; views: number; leaked: number } {
|
|
229
|
+
return { live: this.live.size, views: this.views.size, leaked: this.leaked };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** A track the native side may no longer have: disposed here, or remote and ended (its connection closed). */
|
|
233
|
+
private unsafe(track: TrackLike): boolean {
|
|
234
|
+
return this.disposed.has(track) || (track.remote === true && track.readyState === 'ended');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private dropViews(track: TrackLike): void {
|
|
238
|
+
const view = this.views.get(track);
|
|
239
|
+
if (!view) return;
|
|
240
|
+
this.views.delete(track);
|
|
241
|
+
this.releaseEntry(view.entry);
|
|
242
|
+
for (const listener of [...view.ended]) {
|
|
243
|
+
try {
|
|
244
|
+
listener();
|
|
245
|
+
} catch (err) {
|
|
246
|
+
logError('listener for viewEnded threw', err);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private releaseEntry(entry: Entry): void {
|
|
252
|
+
if (entry.released) return;
|
|
253
|
+
entry.released = true;
|
|
254
|
+
this.live.delete(entry);
|
|
255
|
+
// A view's track that ended may have been released outside this package (an app's own stop() and release()),
|
|
256
|
+
// which only its end shows: its wrapper is left too. Views of live tracks are let go while their tracks live.
|
|
257
|
+
const hazard = entry.role === 'view'
|
|
258
|
+
? (track: TrackLike) => this.unsafe(track) || track.readyState === 'ended'
|
|
259
|
+
: (track: TrackLike) => this.unsafe(track);
|
|
260
|
+
if (entry.stream.getTracks().some(hazard)) {
|
|
261
|
+
// Released, it would dispose a native stream that still holds a disposed track, and the app would die.
|
|
262
|
+
this.leaked += 1;
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
try {
|
|
266
|
+
entry.stream.release(false);
|
|
267
|
+
} catch (err) {
|
|
268
|
+
logError('could not release a media stream', err);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
let shared: StreamBook | null = null;
|
|
274
|
+
|
|
275
|
+
/** The process's book of streams (one for every client and view). */
|
|
276
|
+
export function streamBook(): StreamBook {
|
|
277
|
+
if (!shared) {
|
|
278
|
+
shared = new StreamBook((tracks) => {
|
|
279
|
+
const webrtc = loadWebRTC();
|
|
280
|
+
if (!webrtc) throw webrtcMissing();
|
|
281
|
+
return new webrtc.MediaStream(tracks as unknown as ConstructorParameters<typeof RNMediaStream>[0] & object) as unknown as StreamLike;
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
return shared;
|
|
285
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The timers the client's own work runs on (reconnect backoff, socket liveness, request deadlines, sampling).
|
|
3
|
+
*
|
|
4
|
+
* React Native's JavaScript timers run on Choreographer frames, which stop when the app leaves the foreground, when
|
|
5
|
+
* the screen goes off even with the app in front, and while a permission dialog is open (Phase 0 S5): a call whose
|
|
6
|
+
* timers stop cannot notice a dead socket or reconnect. On Android they wait natively instead (the module's
|
|
7
|
+
* timerStart/timerStop on a thread of their own) and fire back through its onTimer event, which reaches JavaScript
|
|
8
|
+
* whether frames run or not. Without the native module (iOS, Expo Go) they are JavaScript's own.
|
|
9
|
+
*/
|
|
10
|
+
import type { PlatformTimers } from '@voltkit-rtc/client';
|
|
11
|
+
import type { Spec } from '../specs/NativeVoltkitRtc';
|
|
12
|
+
import { listen } from '../native';
|
|
13
|
+
import { logError } from '../errors';
|
|
14
|
+
|
|
15
|
+
/** JavaScript's own timers, looked up at each call (a test's fake clock replaces them, and is used). */
|
|
16
|
+
export const jsTimers: PlatformTimers = Object.freeze({
|
|
17
|
+
setTimeout: (fn: () => void, ms: number): unknown => setTimeout(fn, ms),
|
|
18
|
+
clearTimeout: (handle: unknown): void => clearTimeout(handle as ReturnType<typeof setTimeout>),
|
|
19
|
+
setInterval: (fn: () => void, ms: number): unknown => setInterval(fn, ms),
|
|
20
|
+
clearInterval: (handle: unknown): void => clearInterval(handle as ReturnType<typeof setInterval>),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
interface Pending {
|
|
24
|
+
fn: () => void;
|
|
25
|
+
repeat: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Timers that wait natively. Ids are this side's and never reused, so a fire already on its way for a timer stopped
|
|
30
|
+
* since is recognised and ignored. One onTimer subscription for the whole process.
|
|
31
|
+
*/
|
|
32
|
+
export function nativeTimers(module: Spec): PlatformTimers {
|
|
33
|
+
const pending = new Map<number, Pending>();
|
|
34
|
+
let next = 0;
|
|
35
|
+
let subscribed = false;
|
|
36
|
+
|
|
37
|
+
const fire = (id: number) => {
|
|
38
|
+
const timer = pending.get(id);
|
|
39
|
+
if (!timer) return;
|
|
40
|
+
if (!timer.repeat) pending.delete(id);
|
|
41
|
+
try {
|
|
42
|
+
timer.fn();
|
|
43
|
+
} catch (err) {
|
|
44
|
+
logError('a timer callback threw', err);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const start = (fn: () => void, ms: number, repeat: boolean): number => {
|
|
49
|
+
if (!subscribed) {
|
|
50
|
+
subscribed = true;
|
|
51
|
+
listen(module.onTimer, 'onTimer', fire);
|
|
52
|
+
}
|
|
53
|
+
next += 1;
|
|
54
|
+
const id = next;
|
|
55
|
+
pending.set(id, { fn, repeat });
|
|
56
|
+
const delay = typeof ms === 'number' && Number.isFinite(ms) && ms > 0 ? ms : 0;
|
|
57
|
+
module.timerStart(id, delay, repeat);
|
|
58
|
+
return id;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const stop = (handle: unknown) => {
|
|
62
|
+
if (typeof handle !== 'number' || !pending.delete(handle)) return;
|
|
63
|
+
module.timerStop(handle);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return Object.freeze({
|
|
67
|
+
setTimeout: (fn: () => void, ms: number): unknown => start(fn, ms, false),
|
|
68
|
+
clearTimeout: stop,
|
|
69
|
+
setInterval: (fn: () => void, ms: number): unknown => start(fn, ms, true),
|
|
70
|
+
clearInterval: stop,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether the app can be seen, for the background pause, adaptive stream and the call service.
|
|
3
|
+
*
|
|
4
|
+
* Android: from the process lifecycle (the native module's isAppVisible / onAppVisibilityChanged, ProcessLifecycleOwner):
|
|
5
|
+
* the app is visible while any of its activities is started. React Native's AppState is not used there: it says
|
|
6
|
+
* `background` while a permission dialog or the screen-share consent is open over the app (Phase 0 S8), which would
|
|
7
|
+
* pause the camera for a dialog.
|
|
8
|
+
*
|
|
9
|
+
* iOS (preview) and Android without the native module: AppState, with `inactive` (a call banner, the app switcher
|
|
10
|
+
* opening) counted as visible.
|
|
11
|
+
*/
|
|
12
|
+
import { AppState, type AppStateStatus } from 'react-native';
|
|
13
|
+
import type { Spec } from '../specs/NativeVoltkitRtc';
|
|
14
|
+
import { listen } from '../native';
|
|
15
|
+
import { logError } from '../errors';
|
|
16
|
+
|
|
17
|
+
export interface Visibility {
|
|
18
|
+
visible(): boolean;
|
|
19
|
+
/** Hear every change; returns the function that stops. */
|
|
20
|
+
subscribe(listener: (visible: boolean) => void): () => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const shown = (state: AppStateStatus | string | null | undefined): boolean => state !== 'background';
|
|
24
|
+
|
|
25
|
+
/** From the process lifecycle (Android). */
|
|
26
|
+
export function nativeVisibility(module: Spec): Visibility {
|
|
27
|
+
return {
|
|
28
|
+
visible: () => {
|
|
29
|
+
try {
|
|
30
|
+
return module.isAppVisible();
|
|
31
|
+
} catch (err) {
|
|
32
|
+
logError('could not read the app\'s visibility', err);
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
subscribe: (listener) => listen(module.onAppVisibilityChanged, 'onAppVisibilityChanged', (e) => listener(e.visible)),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** From AppState: `active` and `inactive` are visible, `background` is not. */
|
|
41
|
+
export function appStateVisibility(): Visibility {
|
|
42
|
+
return {
|
|
43
|
+
visible: () => shown(AppState.currentState),
|
|
44
|
+
subscribe: (listener) => {
|
|
45
|
+
let last = shown(AppState.currentState);
|
|
46
|
+
const subscription = AppState.addEventListener('change', (state) => {
|
|
47
|
+
const now = shown(state);
|
|
48
|
+
if (now === last) return;
|
|
49
|
+
last = now;
|
|
50
|
+
try {
|
|
51
|
+
listener(now);
|
|
52
|
+
} catch (err) {
|
|
53
|
+
logError('listener for appVisibility threw', err);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
return () => subscription?.remove();
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
package/src/preflight.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pre-call check on React Native: @voltkit-rtc/client's preflight with this package's platform. It joins the room
|
|
3
|
+
* the token names, publishes for a few seconds, measures and leaves, and never rejects: offline, a refused permission,
|
|
4
|
+
* a bad token or a missing native module all give a report whose steps say what failed.
|
|
5
|
+
*
|
|
6
|
+
* React Native cannot make a test tone or picture. Without `options.media` the check publishes the microphone, which
|
|
7
|
+
* it captures itself once joined (so nothing opens the microphone for a join that fails) and releases at the end. It
|
|
8
|
+
* never asks for the permission: where it is not granted, the join is still tested and the media step says why.
|
|
9
|
+
* Android shows its microphone indicator for those seconds.
|
|
10
|
+
*/
|
|
11
|
+
import { StreamClient, type PreflightOptions, type PreflightReport } from '@voltkit-rtc/client';
|
|
12
|
+
import { registerVoltkitGlobals } from './globals';
|
|
13
|
+
import { describeError, webrtcMissing } from './errors';
|
|
14
|
+
import { reactNativeClientOptions, type BackgroundOptions } from './create-client';
|
|
15
|
+
|
|
16
|
+
export interface ReactNativePreflightOptions extends PreflightOptions {
|
|
17
|
+
/** The test client's background pause (as createClient's). */
|
|
18
|
+
background?: BackgroundOptions;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** A report for a check that could not even start. */
|
|
22
|
+
function failed(error: unknown): PreflightReport {
|
|
23
|
+
return {
|
|
24
|
+
ok: false,
|
|
25
|
+
steps: [{ name: 'join', ok: false, ms: null, error: describeError(error) }],
|
|
26
|
+
path: null,
|
|
27
|
+
rttMs: null,
|
|
28
|
+
lossPct: null,
|
|
29
|
+
uploadKbps: null,
|
|
30
|
+
level: null,
|
|
31
|
+
warnings: [],
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* What the client's check reads from `options.media` on a platform that cannot copy tracks: which devices to capture
|
|
37
|
+
* again once joined (it never publishes the app's own tracks, which its close would stop). One microphone, the default
|
|
38
|
+
* one, and no camera; nothing native behind it.
|
|
39
|
+
*/
|
|
40
|
+
const MICROPHONE_TRACK = Object.freeze({ kind: 'audio', getSettings: () => ({}) });
|
|
41
|
+
const MICROPHONE_FOR_TEST = Object.freeze({
|
|
42
|
+
getTracks: () => [MICROPHONE_TRACK],
|
|
43
|
+
getAudioTracks: () => [MICROPHONE_TRACK],
|
|
44
|
+
getVideoTracks: () => [],
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/** Test the connection and the media path before a call. Never rejects. */
|
|
48
|
+
export async function preflight(wsUrl: string, token: string, options: ReactNativePreflightOptions = {}): Promise<PreflightReport> {
|
|
49
|
+
if (!registerVoltkitGlobals()) return failed(webrtcMissing());
|
|
50
|
+
const { background, clientOptions, ...rest } = options;
|
|
51
|
+
try {
|
|
52
|
+
return await StreamClient.preflight(wsUrl, token, {
|
|
53
|
+
...rest,
|
|
54
|
+
media: rest.media ?? (MICROPHONE_FOR_TEST as unknown as PreflightOptions['media']),
|
|
55
|
+
clientOptions: reactNativeClientOptions({ background, clientOptions }),
|
|
56
|
+
});
|
|
57
|
+
} catch (err) {
|
|
58
|
+
return failed(err);
|
|
59
|
+
}
|
|
60
|
+
}
|
package/src/provider.tsx
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VoltkitProvider for React Native: @voltkit-rtc/react's provider (the same props, token hand-over and StrictMode
|
|
3
|
+
* behaviour, so every re-exported hook works below it) making its client with this package's platform, plus what a call
|
|
4
|
+
* needs from the device: the call audio, the call service and the background pause.
|
|
5
|
+
*
|
|
6
|
+
* In Expo Go, or an app not rebuilt after installing react-native-webrtc, no client is made: onError gets
|
|
7
|
+
* `media-unavailable` with `{ reason: 'webrtc-missing' }` (it needs a development build) and the hooks keep their
|
|
8
|
+
* defaults. Nothing throws.
|
|
9
|
+
*/
|
|
10
|
+
import { useEffect, useMemo, useRef, type ReactElement } from 'react';
|
|
11
|
+
import { Platform } from 'react-native';
|
|
12
|
+
import { VoltkitProvider as ReactVoltkitProvider, type VoltkitProviderProps as ReactVoltkitProviderProps } from '@voltkit-rtc/react';
|
|
13
|
+
import { registerVoltkitGlobals } from './globals';
|
|
14
|
+
import { nativeModule } from './native';
|
|
15
|
+
import { nativeMissing, webrtcMissing } from './errors';
|
|
16
|
+
import { reactNativeClientOptions, type BackgroundOptions } from './create-client';
|
|
17
|
+
import { NativeSession, type AudioSessionOptions, type CallServiceText } from './native-session';
|
|
18
|
+
|
|
19
|
+
export interface VoltkitProviderProps extends ReactVoltkitProviderProps {
|
|
20
|
+
/**
|
|
21
|
+
* The call audio (Android): held from the join to the close -- the audio mode, focus and routing -- with the
|
|
22
|
+
* loudspeaker unless `speakerphone: false` (the earpiece) or setSpeakerphoneOn chose. `false`, or `manage: false`,
|
|
23
|
+
* leaves the device's audio to the app. Default `{ speakerphone: true, manage: true }`.
|
|
24
|
+
*/
|
|
25
|
+
audio?: AudioSessionOptions | false;
|
|
26
|
+
/**
|
|
27
|
+
* The background pause, read when the client is made: the camera goes off in the background and comes back with the
|
|
28
|
+
* app (default `pauseCamera: true`); the microphone keeps going (`pauseMic: false`). Your `clientOptions` win.
|
|
29
|
+
*/
|
|
30
|
+
background?: BackgroundOptions;
|
|
31
|
+
/**
|
|
32
|
+
* The call service (Android): a foreground service that keeps the call's network and microphone alive in the
|
|
33
|
+
* background and with the screen off. Started on the join when the app's manifest declares it (the Expo plugin's
|
|
34
|
+
* callService, default on), with this notification; `false` never starts it. Default true.
|
|
35
|
+
*/
|
|
36
|
+
callService?: boolean | CallServiceText;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** One StreamClient for the components below, with the device's audio, the call service and the background pause. */
|
|
40
|
+
export function VoltkitProvider({
|
|
41
|
+
audio,
|
|
42
|
+
background,
|
|
43
|
+
callService,
|
|
44
|
+
clientOptions,
|
|
45
|
+
client,
|
|
46
|
+
wsUrl,
|
|
47
|
+
token,
|
|
48
|
+
onError,
|
|
49
|
+
children,
|
|
50
|
+
...rest
|
|
51
|
+
}: VoltkitProviderProps): ReactElement {
|
|
52
|
+
const webrtc = useMemo(() => registerVoltkitGlobals(), []);
|
|
53
|
+
const errors = useRef(onError);
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
errors.current = onError;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const wanted = Boolean(client || (wsUrl && token));
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (!wanted) return;
|
|
61
|
+
if (!webrtc) errors.current?.(webrtcMissing());
|
|
62
|
+
else if (Platform.OS === 'android' && !nativeModule()) errors.current?.(nativeMissing('Background calls, audio routing and native timers'));
|
|
63
|
+
}, [webrtc, wanted]);
|
|
64
|
+
|
|
65
|
+
// Read when the provider makes a client, as @voltkit-rtc/react reads clientOptions: a later change does not remake it.
|
|
66
|
+
const options = useMemo(
|
|
67
|
+
() => (webrtc ? reactNativeClientOptions({ background, clientOptions }) : undefined),
|
|
68
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
69
|
+
[webrtc, wsUrl, token],
|
|
70
|
+
);
|
|
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;
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<ReactVoltkitProvider
|
|
77
|
+
{...rest}
|
|
78
|
+
wsUrl={webrtc ? wsUrl : undefined}
|
|
79
|
+
token={webrtc ? token : undefined}
|
|
80
|
+
client={webrtc ? client : null}
|
|
81
|
+
clientOptions={options}
|
|
82
|
+
onError={onError}
|
|
83
|
+
>
|
|
84
|
+
{webrtc ? <NativeSession audio={audio} callService={callService} pauseCamera={pauseCamera} onError={errors} /> : null}
|
|
85
|
+
{children}
|
|
86
|
+
</ReactVoltkitProvider>
|
|
87
|
+
);
|
|
88
|
+
}
|