crowdplaysdk 0.2.3
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/Examples/App.tsx +164 -0
- package/Examples/tsconfig.json +9 -0
- package/README.md +143 -0
- package/ios/CrowdPlayRNModule.swift +324 -0
- package/ios/CrowdPlayRNVideoView.swift +76 -0
- package/ios/CrowdPlayReactNative.m +42 -0
- package/ios/wire.rb +81 -0
- package/lib/ConsentScreen.d.ts +20 -0
- package/lib/ConsentScreen.js +110 -0
- package/lib/VideoView.d.ts +21 -0
- package/lib/VideoView.js +24 -0
- package/lib/index.d.ts +189 -0
- package/lib/index.js +145 -0
- package/llms.txt +224 -0
- package/package.json +44 -0
- package/src/ConsentScreen.tsx +93 -0
- package/src/VideoView.tsx +35 -0
- package/src/index.ts +294 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* crowdplaysdk — CrowdPlay's lossless capture SDK for React Native (iOS).
|
|
3
|
+
*
|
|
4
|
+
* The recording engine is 100% native (CrowdPlaySDK): studio-grade audio
|
|
5
|
+
* (48 kHz / 24-bit, unprocessed) + 1080p30 video captured locally during a
|
|
6
|
+
* live call, clock-synced across participants, uploaded to CrowdPlay with
|
|
7
|
+
* retries and crash recovery. **No media ever crosses the JS bridge** —
|
|
8
|
+
* only control calls and state events — so recording quality is identical
|
|
9
|
+
* to a fully native app.
|
|
10
|
+
*
|
|
11
|
+
* import CrowdPlay, { CrowdPlayConsentScreen, CrowdPlayVideoView } from 'crowdplaysdk';
|
|
12
|
+
*
|
|
13
|
+
* CrowdPlay.configure({ serverUrl: '…', appKey: 'liva_pk_…' });
|
|
14
|
+
* // Show CrowdPlayConsentScreen (or your own UI) → ConsentGrant
|
|
15
|
+
* await CrowdPlay.join({ displayName: 'Sam', roomCode: 'abc', consent });
|
|
16
|
+
* // …
|
|
17
|
+
* await CrowdPlay.leave(); // recording stops; uploads continue automatically
|
|
18
|
+
*
|
|
19
|
+
* Consent is enforced twice: join() throws here without a grant, and the
|
|
20
|
+
* native layer requires the consent record structurally — there is no code
|
|
21
|
+
* path that records without it.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { NativeEventEmitter, NativeModules, Platform } from 'react-native';
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Types
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
/** Live-call (Path A) quality. The RECORDING is always 1080p30 + 24-bit WAV
|
|
31
|
+
* regardless; raising these trades upload + thermal headroom (which the
|
|
32
|
+
* recording needs on long sessions) for a prettier live picture. Values are
|
|
33
|
+
* clamped natively to a safe envelope (≤1280×720, 10–30 fps, ≤2.5 Mbps
|
|
34
|
+
* video, 16–128 kbps audio). */
|
|
35
|
+
export interface CallQuality {
|
|
36
|
+
videoWidth?: number;
|
|
37
|
+
videoHeight?: number;
|
|
38
|
+
videoFps?: number;
|
|
39
|
+
videoBitrate?: number;
|
|
40
|
+
audioBitrate?: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface CrowdPlayConfig {
|
|
44
|
+
/** CrowdPlay backend base URL (https, no trailing slash). */
|
|
45
|
+
serverUrl: string;
|
|
46
|
+
/** From the CrowdPlay dashboard. Also your data-access credential. */
|
|
47
|
+
appKey: string;
|
|
48
|
+
/** Video (~2 GB/session) rides cellular too when true. Audio + metadata
|
|
49
|
+
* always upload on any network. Default false. */
|
|
50
|
+
videoUploadsOnCellular?: boolean;
|
|
51
|
+
/** Upload segments while the call records, so the post-call wait is
|
|
52
|
+
* roughly the final segment + manifests. Mid-call transfers run one at
|
|
53
|
+
* a time and pause automatically when call quality drops — the call
|
|
54
|
+
* always wins the uplink. Default true. */
|
|
55
|
+
uploadDuringCall?: boolean;
|
|
56
|
+
callQuality?: CallQuality;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Proof the participant agreed. Produce it with CrowdPlayConsentScreen, or —
|
|
60
|
+
* if you render your own consent UI — call CrowdPlay.consentText() to show the
|
|
61
|
+
* REQUIRED wording and construct the grant at the moment of agreement. */
|
|
62
|
+
export interface ConsentGrant {
|
|
63
|
+
/** Date.now() at the moment the participant agreed. */
|
|
64
|
+
grantedAtMs: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type Phase = 'idle' | 'connecting' | 'connected' | 'failed';
|
|
68
|
+
|
|
69
|
+
export interface RecordingSnapshot {
|
|
70
|
+
/** Seconds of audio safely written so far. */
|
|
71
|
+
seconds: number;
|
|
72
|
+
segmentsClosed: number;
|
|
73
|
+
droppedSamples: number;
|
|
74
|
+
/** Live input level. Speech sits around −25…−45 dBFS. */
|
|
75
|
+
inputLevelDbfs: number;
|
|
76
|
+
/** True after 10 s of silence while unmuted — a dead/disconnected mic.
|
|
77
|
+
* Surface this loudly; it is fixable mid-call. */
|
|
78
|
+
inputSilent: boolean;
|
|
79
|
+
/** Samples at digital full scale — a too-hot mic, irreversibly clipped. */
|
|
80
|
+
fullScaleSamples: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface UploadKindProgress {
|
|
84
|
+
total: number;
|
|
85
|
+
uploaded: number;
|
|
86
|
+
failed: number;
|
|
87
|
+
bytesTotal: number;
|
|
88
|
+
bytesUploaded: number;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface UploadProgress {
|
|
92
|
+
sessionId: string;
|
|
93
|
+
fraction: number;
|
|
94
|
+
isComplete: boolean;
|
|
95
|
+
failed: number;
|
|
96
|
+
/** Manifests + audio: upload on any network. */
|
|
97
|
+
audio: UploadKindProgress;
|
|
98
|
+
/** Video: waits for WiFi unless videoUploadsOnCellular. */
|
|
99
|
+
video: UploadKindProgress;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface Participant {
|
|
103
|
+
identity: string;
|
|
104
|
+
name: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface DoctorCheck {
|
|
108
|
+
id: string;
|
|
109
|
+
passed: boolean;
|
|
110
|
+
detail: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface CrowdPlayEvents {
|
|
114
|
+
/** Connection lifecycle. `error` is set when phase === 'failed'. */
|
|
115
|
+
phase: { phase: Phase; error?: string };
|
|
116
|
+
/** Recording state. `error` non-null means the session is NOT being
|
|
117
|
+
* captured — show it prominently and offer retryRecording(). */
|
|
118
|
+
recording: { isRecording: boolean; error?: string };
|
|
119
|
+
/** Upload progress for every session with outstanding or recent work. */
|
|
120
|
+
uploads: { sessions: UploadProgress[]; onWifi: boolean };
|
|
121
|
+
/** Remote participants (join/leave). Render a CrowdPlayVideoView per entry. */
|
|
122
|
+
participants: { participants: Participant[] };
|
|
123
|
+
/** Live capture warnings: audioStalled (engine down, auto-recovering),
|
|
124
|
+
* micSilent (dead mic), clipping (too-hot mic), crossTalk (loudspeaker
|
|
125
|
+
* leaking the far end into this mic), micPolicy (unexpected input). */
|
|
126
|
+
warning: { kind: 'audioStalled' | 'micSilent' | 'clipping' | 'crossTalk' | 'micPolicy'; active: boolean; message: string };
|
|
127
|
+
/** The audio output route: current device by name ("AirPods Pro",
|
|
128
|
+
* "iPhone speaker"), connected candidates, and whether headphones are
|
|
129
|
+
* attached. Drive the call screen's output menu from this (D-079). */
|
|
130
|
+
audioRoute: { currentOutputName: string; detectedOutputs: string[]; headphonesConnected: boolean };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export type CrowdPlayEventName = keyof CrowdPlayEvents;
|
|
134
|
+
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// Native module access
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
interface NativeCrowdPlay {
|
|
140
|
+
configure(config: Record<string, unknown>): void;
|
|
141
|
+
consentText(): Promise<string>;
|
|
142
|
+
join(displayName: string, roomCode: string, consentGrantedAtMs: number): Promise<void>;
|
|
143
|
+
leave(): Promise<void>;
|
|
144
|
+
setMicMuted(muted: boolean): Promise<void>;
|
|
145
|
+
setCameraEnabled(enabled: boolean): Promise<void>;
|
|
146
|
+
retryRecording(): void;
|
|
147
|
+
retryUploads(): void;
|
|
148
|
+
warmUp(): void;
|
|
149
|
+
setAudioOutput(output: string): void;
|
|
150
|
+
snapshot(): Promise<RecordingSnapshot | null>;
|
|
151
|
+
doctor(): Promise<DoctorCheck[]>;
|
|
152
|
+
addListener(eventName: string): void;
|
|
153
|
+
removeListeners(count: number): void;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function native(): NativeCrowdPlay {
|
|
157
|
+
const module = NativeModules.CrowdPlayReactNative as NativeCrowdPlay | undefined;
|
|
158
|
+
if (!module) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
"crowdplaysdk: native module not found. iOS setup: add the CrowdPlaySDK " +
|
|
161
|
+
"Swift package (https://github.com/symbiateam/crowdplaysdk) to your Xcode " +
|
|
162
|
+
"project and add the two bridge files from node_modules/crowdplaysdk/ios " +
|
|
163
|
+
"to your app target — see the package README. " +
|
|
164
|
+
(Platform.OS !== 'ios' ? `(platform '${Platform.OS}' is not supported yet — iOS only.)` : '')
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
return module;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
let emitter: NativeEventEmitter | undefined;
|
|
171
|
+
function events(): NativeEventEmitter {
|
|
172
|
+
if (!emitter) {
|
|
173
|
+
emitter = new NativeEventEmitter(NativeModules.CrowdPlayReactNative);
|
|
174
|
+
}
|
|
175
|
+
return emitter;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
let configured = false;
|
|
179
|
+
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
// Public API
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
const CrowdPlay = {
|
|
185
|
+
/** Call once at app start, before anything else. Also resumes uploads
|
|
186
|
+
* interrupted by a crash, reboot or force-quit. */
|
|
187
|
+
configure(config: CrowdPlayConfig): void {
|
|
188
|
+
if (!config?.serverUrl || !/^https:\/\//.test(config.serverUrl)) {
|
|
189
|
+
throw new Error('crowdplaysdk: configure() needs a https serverUrl');
|
|
190
|
+
}
|
|
191
|
+
if (!config.appKey) {
|
|
192
|
+
throw new Error('crowdplaysdk: configure() needs the appKey from your CrowdPlay dashboard');
|
|
193
|
+
}
|
|
194
|
+
native().configure(config as unknown as Record<string, unknown>);
|
|
195
|
+
configured = true;
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
/** The REQUIRED consent wording (owned by CrowdPlay, versioned by hash). Show
|
|
199
|
+
* it verbatim if you build your own consent UI. */
|
|
200
|
+
consentText(): Promise<string> {
|
|
201
|
+
return native().consentText();
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
/** Pre-warms CrowdPlay's backend so joining is fast. Call from your join/lobby
|
|
205
|
+
* screen. Fire-and-forget. */
|
|
206
|
+
warmUp(): void {
|
|
207
|
+
native().warmUp();
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Join a room. Recording starts automatically on join and stops on
|
|
212
|
+
* leave(). Rejects if the connection fails.
|
|
213
|
+
*
|
|
214
|
+
* `consent` is REQUIRED — produce it with <CrowdPlayConsentScreen> or from
|
|
215
|
+
* your own UI at the moment of agreement. There is no way to record
|
|
216
|
+
* without it, here or natively.
|
|
217
|
+
*/
|
|
218
|
+
async join(options: { displayName: string; roomCode: string; consent: ConsentGrant }): Promise<void> {
|
|
219
|
+
if (!configured) {
|
|
220
|
+
throw new Error('crowdplaysdk: call CrowdPlay.configure() before join()');
|
|
221
|
+
}
|
|
222
|
+
const { displayName, roomCode, consent } = options ?? ({} as never);
|
|
223
|
+
if (!displayName?.trim()) throw new Error('crowdplaysdk: displayName is required');
|
|
224
|
+
if (!roomCode?.trim()) throw new Error('crowdplaysdk: roomCode is required');
|
|
225
|
+
if (!consent || typeof consent.grantedAtMs !== 'number' || consent.grantedAtMs <= 0) {
|
|
226
|
+
throw new Error(
|
|
227
|
+
'crowdplaysdk: join() requires a ConsentGrant. Show CrowdPlayConsentScreen ' +
|
|
228
|
+
'(or your own UI displaying CrowdPlay.consentText()) and pass its result. ' +
|
|
229
|
+
'Recording without consent is not supported.'
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
return native().join(displayName.trim(), roomCode, consent.grantedAtMs);
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
/** Leave the room. Recording stops; uploads proceed automatically (they
|
|
236
|
+
* survive backgrounding; force-quit pauses them until next launch). */
|
|
237
|
+
leave(): Promise<void> {
|
|
238
|
+
return native().leave();
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
/** Mutes the call AND writes silence into the recording (never records
|
|
242
|
+
* someone who believes they are muted). */
|
|
243
|
+
setMicMuted(muted: boolean): Promise<void> {
|
|
244
|
+
return native().setMicMuted(muted);
|
|
245
|
+
},
|
|
246
|
+
|
|
247
|
+
/** Camera off sends + records black frames; the timeline stays continuous. */
|
|
248
|
+
setCameraEnabled(enabled: boolean): Promise<void> {
|
|
249
|
+
return native().setCameraEnabled(enabled);
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
/** Retry after a `recording` event with an error (the meeting is NOT
|
|
253
|
+
* being captured until this succeeds). */
|
|
254
|
+
retryRecording(): void {
|
|
255
|
+
native().retryRecording();
|
|
256
|
+
},
|
|
257
|
+
|
|
258
|
+
/** Re-attempts any uploads that settled as failed. */
|
|
259
|
+
/** Route call audio: 'automatic' = the connected device (AirPods,
|
|
260
|
+
* wired, ...), 'speaker' = force the loudspeaker. Pair with the
|
|
261
|
+
* 'audioRoute' event to build an output menu. */
|
|
262
|
+
setAudioOutput(output: 'automatic' | 'speaker'): void {
|
|
263
|
+
native().setAudioOutput(output);
|
|
264
|
+
},
|
|
265
|
+
|
|
266
|
+
retryUploads(): void {
|
|
267
|
+
native().retryUploads();
|
|
268
|
+
},
|
|
269
|
+
|
|
270
|
+
/** Live recording stats (REC timer, input level, silence/clipping). Null
|
|
271
|
+
* when not recording. Poll ~1 Hz for a status line. */
|
|
272
|
+
snapshot(): Promise<RecordingSnapshot | null> {
|
|
273
|
+
return native().snapshot();
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
/** Integration self-check: configuration, permissions, disk, backend
|
|
277
|
+
* reachability, app-key auth. Every failing check names its fix. */
|
|
278
|
+
doctor(): Promise<DoctorCheck[]> {
|
|
279
|
+
return native().doctor();
|
|
280
|
+
},
|
|
281
|
+
|
|
282
|
+
/** Subscribe to state events. Returns a subscription; call .remove(). */
|
|
283
|
+
addListener<E extends CrowdPlayEventName>(
|
|
284
|
+
event: E,
|
|
285
|
+
listener: (payload: CrowdPlayEvents[E]) => void
|
|
286
|
+
): { remove(): void } {
|
|
287
|
+
const subscription = events().addListener(`crowdplay:${event}`, listener as (payload: unknown) => void);
|
|
288
|
+
return { remove: () => subscription.remove() };
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
export default CrowdPlay;
|
|
293
|
+
export { CrowdPlayConsentScreen } from './ConsentScreen';
|
|
294
|
+
export { CrowdPlayVideoView } from './VideoView';
|