crowdplaysdk 0.4.0 → 0.4.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/Examples/App.tsx +2 -2
- package/LICENSE.md +14 -0
- package/README.md +201 -119
- package/ios/CrowdPlayRNModule.swift +12 -6
- package/ios/CrowdPlayRNVideoView.swift +1 -1
- package/ios/CrowdPlayReactNative.m +1 -1
- package/ios/wire.rb +4 -4
- package/lib/ConsentScreen.js +1 -1
- package/lib/VideoView.d.ts +1 -1
- package/lib/VideoView.js +1 -1
- package/lib/VoiceView.d.ts +4 -4
- package/lib/VoiceView.js +5 -5
- package/lib/index.d.ts +20 -12
- package/lib/index.js +7 -7
- package/llms.txt +38 -32
- package/package.json +3 -2
- package/src/ConsentScreen.tsx +1 -1
- package/src/VideoView.tsx +1 -1
- package/src/VoiceView.tsx +5 -5
- package/src/index.ts +22 -14
package/Examples/App.tsx
CHANGED
|
@@ -91,7 +91,7 @@ export default function App(): React.JSX.Element {
|
|
|
91
91
|
|
|
92
92
|
return (
|
|
93
93
|
<SafeAreaView style={styles.screen}>
|
|
94
|
-
{/* The session is NOT being recorded when this is non-null
|
|
94
|
+
{/* The session is NOT being recorded when this is non-null; always show it. */}
|
|
95
95
|
{recordingError ? (
|
|
96
96
|
<View style={styles.banner}>
|
|
97
97
|
<Text style={styles.bannerText}>NOT RECORDING: {recordingError}</Text>
|
|
@@ -121,7 +121,7 @@ export default function App(): React.JSX.Element {
|
|
|
121
121
|
/>
|
|
122
122
|
</View>
|
|
123
123
|
|
|
124
|
-
{/* Audio output menu (D-079): standard in every build
|
|
124
|
+
{/* Audio output menu (D-079): standard in every build; shows the
|
|
125
125
|
device by name and lets the user flip to Speaker and back. */}
|
|
126
126
|
<View style={styles.controls}>
|
|
127
127
|
<Text>{route?.currentOutputName ?? 'audio output'}</Text>
|
package/LICENSE.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# CrowdPlay SDK License
|
|
2
|
+
|
|
3
|
+
Copyright © 2026 Symbia Co. All rights reserved.
|
|
4
|
+
|
|
5
|
+
The CrowdPlay SDK is distributed as a compiled binary for integration into
|
|
6
|
+
applications that deliver recorded sessions to CrowdPlay. You may download,
|
|
7
|
+
link, and ship the binary as part of such an application. You may not
|
|
8
|
+
reverse engineer, decompile, redistribute the binary standalone, or use it
|
|
9
|
+
to deliver data anywhere other than CrowdPlay. No source license is granted.
|
|
10
|
+
|
|
11
|
+
This package depends on open-source software fetched from its own
|
|
12
|
+
repositories under its own licenses: LiveKit Swift SDK and WebRTC
|
|
13
|
+
(Apache License 2.0), swift-atomics (Apache License 2.0 with Runtime
|
|
14
|
+
Library Exception). Their license terms apply to those components.
|
package/README.md
CHANGED
|
@@ -1,150 +1,232 @@
|
|
|
1
1
|
# crowdplaysdk
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
CrowdPlaySDK adds live calls and high-quality conversation capture to React
|
|
4
|
+
Native iOS apps. It is the same compiled engine as the native CrowdPlaySDK
|
|
5
|
+
Swift package, driven from JavaScript with control calls and state events.
|
|
6
|
+
No media ever crosses the JS bridge, so recording quality is identical to a
|
|
7
|
+
fully native app.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
native core (CrowdPlaySDK) our reference app runs; React Native drives it with
|
|
11
|
-
control calls and receives state events. Recording quality is identical to
|
|
12
|
-
a fully native app.
|
|
9
|
+
## What the SDK handles
|
|
13
10
|
|
|
14
|
-
|
|
11
|
+
* Joining and leaving calls
|
|
12
|
+
* Microphone and camera access
|
|
13
|
+
* Participant audio and video
|
|
14
|
+
* Mute, camera, and audio-output controls
|
|
15
|
+
* User consent before capture begins
|
|
16
|
+
* Per-participant data capture and synchronization
|
|
17
|
+
* Uploads, retries, and recovery
|
|
18
|
+
* Session lifecycle and common interruptions
|
|
15
19
|
|
|
20
|
+
Once a user consents and joins a call, capture and delivery happen
|
|
21
|
+
automatically.
|
|
22
|
+
|
|
23
|
+
## What you build
|
|
24
|
+
|
|
25
|
+
You build the actual product and user experience:
|
|
26
|
+
|
|
27
|
+
* Your app's screens, navigation, and design
|
|
28
|
+
* What users do in your app
|
|
29
|
+
* When and why a call happens
|
|
30
|
+
* The UI around the call
|
|
31
|
+
* Your own accounts, profiles, content, or other product features
|
|
32
|
+
|
|
33
|
+
For example, if you're building a tutoring app, **you build the tutoring
|
|
34
|
+
experience**. CrowdPlaySDK provides the call and capture infrastructure
|
|
35
|
+
underneath it.
|
|
36
|
+
|
|
37
|
+
Your integration is essentially:
|
|
38
|
+
|
|
39
|
+
**Your app → consent → join call → CrowdPlay handles the rest**
|
|
40
|
+
|
|
41
|
+
## Three kinds of calls
|
|
42
|
+
|
|
43
|
+
You pick one when you create your app on the dashboard, and you can change
|
|
44
|
+
it later:
|
|
45
|
+
|
|
46
|
+
* **Video + audio.** Face-to-face calls between people. Both are recorded.
|
|
47
|
+
* **Audio-only.** Voice calls between people, no camera anywhere in the app.
|
|
48
|
+
* **Voice AI.** Your users talk with a live AI voice instead of another
|
|
49
|
+
person. Audio only, no camera.
|
|
50
|
+
|
|
51
|
+
## Getting started
|
|
52
|
+
|
|
53
|
+
Sign in at **https://dashboard.crowdplay.ai**, create your app, and press
|
|
54
|
+
**Copy agent prompt**. The prompt is pre-filled with your key and tailored
|
|
55
|
+
to the tool you build with, so paste it into your coding agent and it will
|
|
56
|
+
build the app. Your dashboard then tracks setup and shows your recordings.
|
|
57
|
+
|
|
58
|
+
**Building in Swift?** Use the CrowdPlaySDK Swift package instead:
|
|
59
|
+
https://github.com/symbiateam/crowdplaysdk
|
|
60
|
+
|
|
61
|
+
## Setup (iOS)
|
|
62
|
+
|
|
63
|
+
Requirements: iOS 17+, React Native 0.71 or newer, a real device to test
|
|
64
|
+
(the Simulator has no camera or mic), and an **app key** from CrowdPlay.
|
|
65
|
+
iOS only for now; calls to the module on other platforms throw with a
|
|
66
|
+
clear message.
|
|
67
|
+
|
|
68
|
+
**1. Install the package and wire the engine.**
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
npm install crowdplaysdk
|
|
72
|
+
cd ios && pod install
|
|
73
|
+
gem install xcodeproj --user-install # one time
|
|
74
|
+
ruby node_modules/crowdplaysdk/ios/wire.rb <YourProjectName>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`wire.rb` is idempotent. It adds the compiled CrowdPlaySDK Swift package,
|
|
78
|
+
copies the three bridge files into your app target, and points the Swift
|
|
79
|
+
bridging header at the React headers. There is no CocoaPod of our own, so
|
|
80
|
+
your Podfile stays untouched.
|
|
81
|
+
|
|
82
|
+
**2. Declare permissions in Info.plist.**
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
NSMicrophoneUsageDescription = "Records your microphone during sessions."
|
|
86
|
+
NSCameraUsageDescription = "Records your camera during sessions."
|
|
87
|
+
UIBackgroundModes = [audio]
|
|
16
88
|
```
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
a small REC indicator during the call, join/leave, and a small
|
|
29
|
-
audio-output menu.
|
|
30
|
-
- Keep the SDK integration minimal: consent → join/leave → call screen.
|
|
31
|
-
The call screen MUST include a small audio-output menu showing the
|
|
32
|
-
current output device by name (from the 'audioRoute' event) with a
|
|
33
|
-
switch between the device and Speaker (CrowdPlay.setAudioOutput). Do
|
|
34
|
-
NOT build upload screens, diagnostics screens, or SDK settings.
|
|
35
|
-
Uploads happen automatically in the background.
|
|
36
|
-
|
|
37
|
-
- npm install crowdplaysdk
|
|
38
|
-
- In ios/: pod install, then run
|
|
39
|
-
gem install xcodeproj --user-install # one-time
|
|
40
|
-
ruby node_modules/crowdplaysdk/ios/wire.rb <MyProjectName>
|
|
41
|
-
(adds the compiled CrowdPlay engine + bridge to the Xcode project)
|
|
42
|
-
- Info.plist: NSMicrophoneUsageDescription, NSCameraUsageDescription,
|
|
43
|
-
UIBackgroundModes = [audio]
|
|
44
|
-
- My CrowdPlay app key: <APP KEY from the CrowdPlay dashboard>
|
|
45
|
-
- Follow node_modules/crowdplaysdk/llms.txt exactly: configure at
|
|
46
|
-
startup, the CrowdPlayConsentScreen before joining (join() throws
|
|
47
|
-
without a ConsentGrant), CrowdPlayVideoView tiles from the participants
|
|
48
|
-
event, and surface the warning events on the call screen.
|
|
49
|
-
- Never add, pin or import LiveKit/WebRTC yourself, and never write your
|
|
50
|
-
own AVAudioSession/AVCaptureSession code — the engine owns the mic and
|
|
51
|
-
camera.
|
|
52
|
-
- When done, run CrowdPlay.doctor() as a development check (not an app
|
|
53
|
-
screen) and fix anything actionable.
|
|
54
|
-
- Your FINAL message must walk me through testing on my real iPhone,
|
|
55
|
-
step by step (the Simulator has no camera or mic, so nothing is proven
|
|
56
|
-
until then): plugging it in, signing/Developer Mode, running from
|
|
57
|
-
Xcode, allowing permissions, doing a ~1-minute test call, and checking
|
|
58
|
-
my session at https://dashboard.crowdplay.ai. Do not just say the
|
|
59
|
-
implementation is complete.
|
|
89
|
+
|
|
90
|
+
**3. Let uploads finish while the app is suspended** (recommended). In your
|
|
91
|
+
AppDelegate:
|
|
92
|
+
|
|
93
|
+
```objc
|
|
94
|
+
// AppDelegate.mm, with #import "<YourProjectName>-Swift.h" at the top
|
|
95
|
+
- (void)application:(UIApplication *)application
|
|
96
|
+
handleEventsForBackgroundURLSession:(NSString *)identifier
|
|
97
|
+
completionHandler:(void (^)(void))completionHandler {
|
|
98
|
+
[CrowdPlayRNBackground handleWithCompletionHandler:completionHandler];
|
|
99
|
+
}
|
|
60
100
|
```
|
|
61
101
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
1. `npm install crowdplaysdk`
|
|
65
|
-
2. `cd ios && pod install`
|
|
66
|
-
(one-time: `gem install xcodeproj --user-install` — the wiring script needs it)
|
|
67
|
-
3. `ruby node_modules/crowdplaysdk/ios/wire.rb <YourProjectName>` —
|
|
68
|
-
idempotent; adds the CrowdPlaySDK Swift package (the compiled engine), copies
|
|
69
|
-
the three bridge files into your app target, and points the Swift
|
|
70
|
-
bridging header at the React headers. (No CocoaPod of our own: the
|
|
71
|
-
engine ships as a binary Swift package, which keeps your Podfile
|
|
72
|
-
untouched.)
|
|
73
|
-
4. Info.plist: `NSMicrophoneUsageDescription`, `NSCameraUsageDescription`,
|
|
74
|
-
`UIBackgroundModes = [audio]`.
|
|
75
|
-
5. Recommended, for uploads that finish while your app is suspended — in
|
|
76
|
-
your AppDelegate:
|
|
77
|
-
|
|
78
|
-
```objc
|
|
79
|
-
// AppDelegate.mm — add #import "<YourProjectName>-Swift.h" at the top
|
|
80
|
-
- (void)application:(UIApplication *)application
|
|
81
|
-
handleEventsForBackgroundURLSession:(NSString *)identifier
|
|
82
|
-
completionHandler:(void (^)(void))completionHandler {
|
|
83
|
-
[CrowdPlayRNBackground handleWithCompletionHandler:completionHandler];
|
|
84
|
-
}
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
(Swift AppDelegates call `CrowdPlaySDK.handleBackgroundURLSessionEvents(completionHandler:)`.)
|
|
102
|
+
Swift AppDelegates call `CrowdPlaySDK.handleBackgroundURLSessionEvents(completionHandler:)`.
|
|
88
103
|
|
|
89
104
|
## Usage
|
|
90
105
|
|
|
91
106
|
```tsx
|
|
92
107
|
import CrowdPlay, { CrowdPlayConsentScreen, CrowdPlayVideoView } from 'crowdplaysdk';
|
|
93
108
|
|
|
94
|
-
CrowdPlay.configure({
|
|
95
|
-
|
|
96
|
-
|
|
109
|
+
CrowdPlay.configure({
|
|
110
|
+
serverUrl: 'https://dashboard.crowdplay.ai',
|
|
111
|
+
appKey: 'liva_pk_…',
|
|
112
|
+
// audioOnly: true, // audio-only or Voice AI app: no camera at all
|
|
113
|
+
// otherAudio: 'mix', // keep Apple Music / Spotify playing (0.4.0+)
|
|
114
|
+
});
|
|
97
115
|
|
|
98
|
-
// 1. Consent (
|
|
116
|
+
// 1. Consent (required; join() throws without it):
|
|
99
117
|
<CrowdPlayConsentScreen onConsent={(grant) => setConsent(grant)} />
|
|
100
118
|
|
|
101
|
-
// 2. Join
|
|
119
|
+
// 2. Join. Recording starts automatically:
|
|
102
120
|
await CrowdPlay.join({ displayName, roomCode, consent });
|
|
103
121
|
|
|
104
122
|
// 3. Render the call:
|
|
105
|
-
<CrowdPlayVideoView participant="local" style={…} />
|
|
123
|
+
<CrowdPlayVideoView participant="local" style={…} /> // self view
|
|
106
124
|
{participants.map(p => <CrowdPlayVideoView participant={p.identity} … />)}
|
|
107
125
|
|
|
108
126
|
// 4. Controls:
|
|
109
|
-
await CrowdPlay.setMicMuted(true);
|
|
127
|
+
await CrowdPlay.setMicMuted(true); // mutes the call and records silence
|
|
110
128
|
await CrowdPlay.setCameraEnabled(false); // black frames, timeline continuous
|
|
111
129
|
await CrowdPlay.leave(); // stops recording; uploads continue
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
State arrives as events:
|
|
112
133
|
|
|
113
|
-
|
|
114
|
-
CrowdPlay.addListener('phase', …) // idle/connecting/connected/failed
|
|
134
|
+
```tsx
|
|
135
|
+
CrowdPlay.addListener('phase', …) // idle / connecting / connected / failed
|
|
115
136
|
CrowdPlay.addListener('participants', …) // who to render tiles for
|
|
116
137
|
CrowdPlay.addListener('uploads', …) // per-session progress
|
|
117
|
-
CrowdPlay.addListener('warning', …) // micSilent / clipping / crossTalk
|
|
118
|
-
CrowdPlay.addListener('recording', …) // error
|
|
119
|
-
CrowdPlay.addListener('audioRoute', …) // current output device
|
|
138
|
+
CrowdPlay.addListener('warning', …) // micSilent / clipping / crossTalk: show these
|
|
139
|
+
CrowdPlay.addListener('recording', …) // error means NOT capturing; offer retryRecording()
|
|
140
|
+
CrowdPlay.addListener('audioRoute', …) // current output device, for the output menu
|
|
120
141
|
CrowdPlay.setAudioOutput('speaker') // or 'automatic' (the connected device)
|
|
121
|
-
CrowdPlay.snapshot() // poll 1 Hz for REC timer
|
|
122
|
-
|
|
142
|
+
CrowdPlay.snapshot() // poll at 1 Hz for the REC timer and input level
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
That's the whole integration. Capture, sync, upload, retry and crash
|
|
146
|
+
recovery are invisible. The complete example lives in this package's
|
|
147
|
+
`Examples` folder: one file, consent → join → tiles → warnings → uploads.
|
|
148
|
+
|
|
149
|
+
## Voice AI
|
|
150
|
+
|
|
151
|
+
If you picked **Voice AI**, a live AI voice joins the call as an ordinary
|
|
152
|
+
participant, greets your user, and holds a real spoken conversation. Users
|
|
153
|
+
can interrupt it and it responds with natural timing. Both sides are
|
|
154
|
+
recorded and transcribed like any other call.
|
|
155
|
+
|
|
156
|
+
**Your app writes no AI code.** No model API calls, no API keys, no speech
|
|
157
|
+
recognition, no text-to-speech. You choose the AI's voice and personality
|
|
158
|
+
on the dashboard and can change them any time; everything else runs on
|
|
159
|
+
CrowdPlay's servers.
|
|
160
|
+
|
|
161
|
+
The only thing your app draws is what the AI looks like on screen. It has
|
|
162
|
+
no camera, so never give it a video tile. The SDK ships an animated orb
|
|
163
|
+
that breathes when idle, ripples with your user's voice, and pulses when
|
|
164
|
+
the AI speaks:
|
|
165
|
+
|
|
166
|
+
```tsx
|
|
167
|
+
<CrowdPlayVoiceView />
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Restyle it with props, wrap your own artwork in `<VoiceReactive>`, or drive
|
|
171
|
+
any visual from the `useVoiceActivity()` hook. Show live subtitles with
|
|
172
|
+
`useLatestCaption()`, and identify the AI participant by name `agent`.
|
|
173
|
+
|
|
174
|
+
You can also override the voice, personality and context for a single call,
|
|
175
|
+
which is how you give the AI memory of a particular user:
|
|
176
|
+
|
|
177
|
+
```tsx
|
|
178
|
+
await CrowdPlay.join({
|
|
179
|
+
displayName, roomCode, consent,
|
|
180
|
+
agent: { voice: 'Aoede', context: 'Their name is Sam. Last time you discussed Kyoto.' },
|
|
181
|
+
});
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
See `llms.txt` in this package for the full menu of visuals and options.
|
|
185
|
+
|
|
186
|
+
## Verifying your integration
|
|
187
|
+
|
|
188
|
+
```tsx
|
|
189
|
+
const checks = await CrowdPlay.doctor();
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Doctor verifies configuration, Info.plist strings, permissions, free disk,
|
|
193
|
+
backend reachability and your app key. Every failing check names its own
|
|
194
|
+
fix. Then record a one-minute session on a real device and confirm the
|
|
195
|
+
upload finishes.
|
|
196
|
+
|
|
197
|
+
## Accessing your recordings
|
|
198
|
+
|
|
199
|
+
Your app key is also your data-access credential: plain HTTPS, no AWS
|
|
200
|
+
account, no SDK required.
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
BASE=https://dashboard.crowdplay.ai
|
|
204
|
+
|
|
205
|
+
curl -H "x-liva-key: $APP_KEY" $BASE/sessions
|
|
206
|
+
curl -H "x-liva-key: $APP_KEY" $BASE/sessions/<sessionId>/files
|
|
123
207
|
```
|
|
124
208
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
quality with the app launched from the home screen — Metro/debugger
|
|
150
|
-
overhead can reproduce the same symptoms.
|
|
209
|
+
`deliverable/` holds what you actually use: one clean audio file per
|
|
210
|
+
person, aligned so they play together with no offset, plus a mixed version
|
|
211
|
+
and a quality report. You can only reach sessions recorded with your key.
|
|
212
|
+
|
|
213
|
+
## Things to know
|
|
214
|
+
|
|
215
|
+
- **Don't force-quit the app** after a session: iOS pauses background
|
|
216
|
+
transfers until next launch. They resume automatically and nothing is
|
|
217
|
+
lost.
|
|
218
|
+
- Sessions are roughly 2 GB per participant-hour, dominated by video. The
|
|
219
|
+
SDK refuses to join below 8 GB free disk.
|
|
220
|
+
- Participants should wear **wired or closed-back headphones**. A
|
|
221
|
+
loudspeaker pipes the far end into the local mic and contaminates the
|
|
222
|
+
track; the SDK surfaces this live through the `warning` event.
|
|
223
|
+
- **Slow join or glitchy first seconds is bad radio, not a bug.** Congested
|
|
224
|
+
2.4 GHz WiFi and Bluetooth headphones share an antenna. Capture
|
|
225
|
+
self-heals and the rest of the session is fine. Metro and the debugger
|
|
226
|
+
cause the same symptoms, so launch from the home screen when judging
|
|
227
|
+
quality.
|
|
228
|
+
- The npm version can run ahead of the engine version `wire.rb` pins;
|
|
229
|
+
bridge and doc fixes ship without a new engine binary.
|
|
230
|
+
|
|
231
|
+
`llms.txt` carries the full reference, including every event and warning
|
|
232
|
+
and the voice-AI options. It is written for coding agents.
|
|
@@ -113,19 +113,19 @@ final class CrowdPlayRNCore {
|
|
|
113
113
|
let snapshot = engine.recordingSnapshot()
|
|
114
114
|
updateWarning("audioStalled",
|
|
115
115
|
active: snapshot?.audioStalled == true,
|
|
116
|
-
message: "Audio capture stalled
|
|
116
|
+
message: "Audio capture stalled; the engine is being restarted. "
|
|
117
117
|
+ "If this persists, leave and rejoin.")
|
|
118
118
|
updateWarning("micSilent",
|
|
119
119
|
active: snapshot?.inputSilent == true && !engine.isMicMuted,
|
|
120
|
-
message: "No sound is reaching the microphone
|
|
120
|
+
message: "No sound is reaching the microphone; check the headset "
|
|
121
121
|
+ "plug or switch to the phone microphone.")
|
|
122
122
|
updateWarning("clipping",
|
|
123
123
|
active: (snapshot?.fullScaleSamples ?? 0) > 100,
|
|
124
|
-
message: "The microphone is clipping
|
|
124
|
+
message: "The microphone is clipping; audio is distorting. Move "
|
|
125
125
|
+ "the mic away or lower the input.")
|
|
126
126
|
updateWarning("crossTalk",
|
|
127
127
|
active: engine.crossTalkRisk,
|
|
128
|
-
message: "Playing through the loudspeaker
|
|
128
|
+
message: "Playing through the loudspeaker; the other person's "
|
|
129
129
|
+ "voice is being recorded into this microphone. Use headphones.")
|
|
130
130
|
updateWarning("micPolicy",
|
|
131
131
|
active: engine.micPolicyViolation != nil,
|
|
@@ -136,7 +136,7 @@ final class CrowdPlayRNCore {
|
|
|
136
136
|
|
|
137
137
|
private var voiceTimer: Timer?
|
|
138
138
|
|
|
139
|
-
/// Fast (0.15 s) voice-activity stream for the animated voice UI
|
|
139
|
+
/// Fast (0.15 s) voice-activity stream for the animated voice UI ,
|
|
140
140
|
/// runs only while some JS component is subscribed, so the bridge
|
|
141
141
|
/// stays quiet for apps that never render it.
|
|
142
142
|
func startVoiceActivity() {
|
|
@@ -205,7 +205,7 @@ final class CrowdPlayRNCore {
|
|
|
205
205
|
/// [CrowdPlayRNBackground handleWithCompletionHandler:completionHandler];
|
|
206
206
|
///
|
|
207
207
|
/// iOS eventually stops delivering background upload events to apps that
|
|
208
|
-
/// skip this
|
|
208
|
+
/// skip this; uploads still finish, just less promptly.
|
|
209
209
|
@objc(CrowdPlayRNBackground)
|
|
210
210
|
public final class CrowdPlayRNBackground: NSObject {
|
|
211
211
|
@objc public static func handle(completionHandler: @escaping () -> Void) {
|
|
@@ -258,6 +258,12 @@ public final class CrowdPlayRNModule: RCTEventEmitter {
|
|
|
258
258
|
if let duringCall = config["uploadDuringCall"] as? Bool {
|
|
259
259
|
configuration.uploadDuringCall = duringCall
|
|
260
260
|
}
|
|
261
|
+
// D-093: other apps' audio during the call. Unknown strings keep
|
|
262
|
+
// the default (.interrupt) rather than failing configure.
|
|
263
|
+
if let policy = config["otherAudio"] as? String,
|
|
264
|
+
let parsed = CrowdPlayConfiguration.OtherAudioPolicy(rawValue: policy) {
|
|
265
|
+
configuration.otherAudio = parsed
|
|
266
|
+
}
|
|
261
267
|
if let quality = config["callQuality"] as? [String: Any] {
|
|
262
268
|
var callQuality = CallQuality()
|
|
263
269
|
if let v = quality["videoWidth"] as? Int { callQuality.videoWidth = v }
|
|
@@ -5,7 +5,7 @@ import React
|
|
|
5
5
|
import UIKit
|
|
6
6
|
|
|
7
7
|
/// Native video tile hosted in the RN layout. Resolves the requested
|
|
8
|
-
/// participant's track from the shared engine on a short poll
|
|
8
|
+
/// participant's track from the shared engine on a short poll; idempotent
|
|
9
9
|
/// and reconnect-proof, and the video itself never touches JavaScript.
|
|
10
10
|
final class CrowdPlayRNVideoView: UIView {
|
|
11
11
|
private let videoView = VideoView()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Objective-C registration for the CrowdPlay RN bridge. Add this file AND the
|
|
2
2
|
// two Swift files to your app target (they compile alongside your app, with
|
|
3
|
-
// CrowdPlaySDK coming from the Swift package
|
|
3
|
+
// CrowdPlaySDK coming from the Swift package; see the package README).
|
|
4
4
|
#import <React/RCTBridgeModule.h>
|
|
5
5
|
#import <React/RCTEventEmitter.h>
|
|
6
6
|
#import <React/RCTViewManager.h>
|
package/ios/wire.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
#
|
|
4
4
|
# ruby node_modules/crowdplaysdk/ios/wire.rb <YourProjectName>
|
|
5
5
|
#
|
|
6
|
-
# What it does (idempotent
|
|
6
|
+
# What it does (idempotent; safe to re-run):
|
|
7
7
|
# 1. adds the CrowdPlaySDK Swift package (compiled engine) to the project
|
|
8
8
|
# 2. copies the three bridge files into your app group + target
|
|
9
9
|
# 3. creates/points the Swift bridging header at the React headers
|
|
@@ -30,7 +30,7 @@ BRIDGE_FILES = ['CrowdPlayRNModule.swift', 'CrowdPlayRNVideoView.swift', 'CrowdP
|
|
|
30
30
|
|
|
31
31
|
project_name = ARGV[0] or abort 'usage: ruby wire.rb <YourProjectName>'
|
|
32
32
|
project_path = "#{project_name}.xcodeproj"
|
|
33
|
-
abort "#{project_path} not found
|
|
33
|
+
abort "#{project_path} not found; run from your app's ios/ directory" unless File.exist?(project_path)
|
|
34
34
|
|
|
35
35
|
package_dir = File.expand_path(File.dirname(__FILE__))
|
|
36
36
|
project = Xcodeproj::Project.open(project_path)
|
|
@@ -68,7 +68,7 @@ end
|
|
|
68
68
|
target.build_configurations.each do |config|
|
|
69
69
|
config.build_settings['SWIFT_VERSION'] ||= '5.0'
|
|
70
70
|
# CrowdPlaySDK requires iOS 17; RN templates default far lower. Only the APP
|
|
71
|
-
# target is raised
|
|
71
|
+
# target is raised; pods keep their own targets, which is fine.
|
|
72
72
|
current = config.build_settings['IPHONEOS_DEPLOYMENT_TARGET']
|
|
73
73
|
if current.nil? || current.to_f < 17.0
|
|
74
74
|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '17.0'
|
|
@@ -86,7 +86,7 @@ target.build_configurations.each do |config|
|
|
|
86
86
|
end
|
|
87
87
|
# The setting must point at wherever the file ACTUALLY went. When the
|
|
88
88
|
# project has no group named after itself (newer RN templates), the
|
|
89
|
-
# fallback above places files at the project root
|
|
89
|
+
# fallback above places files at the project root; hardcoding
|
|
90
90
|
# "<Project>/<header>" there produced a missing-input build failure
|
|
91
91
|
# (field-reported on the RN 0.87 template). Derive it instead:
|
|
92
92
|
# SRCROOT is the .xcodeproj's directory.
|
package/lib/ConsentScreen.js
CHANGED
|
@@ -65,7 +65,7 @@ function CrowdPlayConsentScreen({ onConsent, style }) {
|
|
|
65
65
|
})
|
|
66
66
|
.catch(() => {
|
|
67
67
|
if (!cancelled)
|
|
68
|
-
setText('Consent text unavailable
|
|
68
|
+
setText('Consent text unavailable. Check that CrowdPlay.configure() ran and the native module is installed.');
|
|
69
69
|
});
|
|
70
70
|
return () => {
|
|
71
71
|
cancelled = true;
|
package/lib/VideoView.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Native video tile. Renders the LOCAL camera preview or a REMOTE
|
|
3
3
|
* participant's live video (identified by the `identity` from the
|
|
4
|
-
* `participants` event). The video never touches JavaScript
|
|
4
|
+
* `participants` event). The video never touches JavaScript; this is a
|
|
5
5
|
* native LiveKit view hosted in your RN layout.
|
|
6
6
|
*
|
|
7
7
|
* <CrowdPlayVideoView participant="local" style={{ width: 120, height: 160 }} />
|
package/lib/VideoView.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Native video tile. Renders the LOCAL camera preview or a REMOTE
|
|
4
4
|
* participant's live video (identified by the `identity` from the
|
|
5
|
-
* `participants` event). The video never touches JavaScript
|
|
5
|
+
* `participants` event). The video never touches JavaScript; this is a
|
|
6
6
|
* native LiveKit view hosted in your RN layout.
|
|
7
7
|
*
|
|
8
8
|
* <CrowdPlayVideoView participant="local" style={{ width: 120, height: 160 }} />
|
package/lib/VoiceView.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The standard voice-AI visual (founders, 2026-08-22): a breathing orb in
|
|
3
|
-
* the style of realtime assistants
|
|
3
|
+
* the style of realtime assistants: calm while idle, a cool ring while the
|
|
4
4
|
* user talks, an energetic pulse while the AI speaks. The React Native
|
|
5
5
|
* counterpart of the native SDK's CrowdPlayVoiceView, driven by the same
|
|
6
6
|
* engine signal over a fast bridge event that runs only while mounted.
|
|
7
7
|
*
|
|
8
8
|
* Two levels of customization:
|
|
9
|
-
* 1. Props on <CrowdPlayVoiceView
|
|
10
|
-
* 2. useVoiceActivity()
|
|
9
|
+
* 1. Props on <CrowdPlayVoiceView>: colors and size of the built-in orb.
|
|
10
|
+
* 2. useVoiceActivity(): the phase + smoothed energy underneath, for apps
|
|
11
11
|
* that want to draw something entirely their own.
|
|
12
12
|
*/
|
|
13
13
|
import React from 'react';
|
|
@@ -27,7 +27,7 @@ export interface VoiceCaption {
|
|
|
27
27
|
/** The latest live transcript line (speech bubbles, subtitles). Captions
|
|
28
28
|
* are broadcast by the voice agent as it speaks; null until the first one. */
|
|
29
29
|
export declare function useLatestCaption(): VoiceCaption | null;
|
|
30
|
-
/** Make ANY child view breathe, pulse, and glow with the conversation
|
|
30
|
+
/** Make ANY child view breathe, pulse, and glow with the conversation;
|
|
31
31
|
* the one-line path from the app's own art (mascot, logo, character) to a
|
|
32
32
|
* living AI presence. */
|
|
33
33
|
export declare function VoiceReactive({ children, glowColor, }: {
|
package/lib/VoiceView.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
3
|
* The standard voice-AI visual (founders, 2026-08-22): a breathing orb in
|
|
4
|
-
* the style of realtime assistants
|
|
4
|
+
* the style of realtime assistants: calm while idle, a cool ring while the
|
|
5
5
|
* user talks, an energetic pulse while the AI speaks. The React Native
|
|
6
6
|
* counterpart of the native SDK's CrowdPlayVoiceView, driven by the same
|
|
7
7
|
* engine signal over a fast bridge event that runs only while mounted.
|
|
8
8
|
*
|
|
9
9
|
* Two levels of customization:
|
|
10
|
-
* 1. Props on <CrowdPlayVoiceView
|
|
11
|
-
* 2. useVoiceActivity()
|
|
10
|
+
* 1. Props on <CrowdPlayVoiceView>: colors and size of the built-in orb.
|
|
11
|
+
* 2. useVoiceActivity(): the phase + smoothed energy underneath, for apps
|
|
12
12
|
* that want to draw something entirely their own.
|
|
13
13
|
*/
|
|
14
14
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
@@ -81,7 +81,7 @@ function useVoiceActivity() {
|
|
|
81
81
|
phase = 'idle';
|
|
82
82
|
target = 0;
|
|
83
83
|
}
|
|
84
|
-
// Fast attack, slow release
|
|
84
|
+
// Fast attack, slow release: speech onsets snap, tails breathe out.
|
|
85
85
|
const rate = target > energyRef.current ? 0.55 : 0.12;
|
|
86
86
|
energyRef.current += (target - energyRef.current) * rate;
|
|
87
87
|
setActivity({ phase, energy: energyRef.current });
|
|
@@ -108,7 +108,7 @@ function useLatestCaption() {
|
|
|
108
108
|
}, []);
|
|
109
109
|
return caption;
|
|
110
110
|
}
|
|
111
|
-
/** Make ANY child view breathe, pulse, and glow with the conversation
|
|
111
|
+
/** Make ANY child view breathe, pulse, and glow with the conversation;
|
|
112
112
|
* the one-line path from the app's own art (mascot, logo, character) to a
|
|
113
113
|
* living AI presence. */
|
|
114
114
|
function VoiceReactive({ children, glowColor = '#598CFF', }) {
|
package/lib/index.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* crowdplaysdk
|
|
2
|
+
* crowdplaysdk: CrowdPlay's lossless capture SDK for React Native (iOS).
|
|
3
3
|
*
|
|
4
4
|
* The recording engine is 100% native (CrowdPlaySDK): studio-grade audio
|
|
5
5
|
* (48 kHz / 24-bit, unprocessed) + 1080p30 video captured locally during a
|
|
6
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
|
|
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
9
|
* to a fully native app.
|
|
10
10
|
*
|
|
11
11
|
* import CrowdPlay, { CrowdPlayConsentScreen, CrowdPlayVideoView } from 'crowdplaysdk';
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* await CrowdPlay.leave(); // recording stops; uploads continue automatically
|
|
18
18
|
*
|
|
19
19
|
* Consent is enforced twice: join() throws here without a grant, and the
|
|
20
|
-
* native layer requires the consent record structurally
|
|
20
|
+
* native layer requires the consent record structurally; there is no code
|
|
21
21
|
* path that records without it.
|
|
22
22
|
*/
|
|
23
23
|
/** Live-call (Path A) quality. The RECORDING is always 1080p30 + 24-bit WAV
|
|
@@ -37,7 +37,7 @@ export interface CrowdPlayConfig {
|
|
|
37
37
|
serverUrl: string;
|
|
38
38
|
/** From the CrowdPlay dashboard. Also your data-access credential. */
|
|
39
39
|
appKey: string;
|
|
40
|
-
/** AUDIO-ONLY app: the camera is never touched
|
|
40
|
+
/** AUDIO-ONLY app: the camera is never touched: no camera permission,
|
|
41
41
|
* no video captured or uploaded, voice-only calls. The audio path is
|
|
42
42
|
* identical to an A/V app. Requires engine 0.3.0+. Default false. */
|
|
43
43
|
audioOnly?: boolean;
|
|
@@ -46,13 +46,21 @@ export interface CrowdPlayConfig {
|
|
|
46
46
|
videoUploadsOnCellular?: boolean;
|
|
47
47
|
/** Upload segments while the call records, so the post-call wait is
|
|
48
48
|
* roughly the final segment + manifests. Mid-call transfers run one at
|
|
49
|
-
* a time and pause automatically when call quality drops
|
|
49
|
+
* a time and pause automatically when call quality drops; the call
|
|
50
50
|
* always wins the uplink. Default true. */
|
|
51
51
|
uploadDuringCall?: boolean;
|
|
52
|
+
/** What happens to OTHER apps' audio (Apple Music, Spotify, podcasts)
|
|
53
|
+
* while a call is live. 'interrupt' (default) pauses them at join, the
|
|
54
|
+
* classic call behaviour. 'mix' keeps them playing alongside the call
|
|
55
|
+
* and the recording, for shared-listening and DJ apps; 'mixDucked' also
|
|
56
|
+
* lowers them for the whole call. Music stays on the device and never
|
|
57
|
+
* rides the call; listeners must wear headphones or it lands in the
|
|
58
|
+
* recording. Requires engine 0.4.0+. */
|
|
59
|
+
otherAudio?: 'interrupt' | 'mix' | 'mixDucked';
|
|
52
60
|
callQuality?: CallQuality;
|
|
53
61
|
}
|
|
54
|
-
/** Proof the participant agreed. Produce it with CrowdPlayConsentScreen, or
|
|
55
|
-
* if you render your own consent UI
|
|
62
|
+
/** Proof the participant agreed. Produce it with CrowdPlayConsentScreen, or,
|
|
63
|
+
* if you render your own consent UI, call CrowdPlay.consentText() to show the
|
|
56
64
|
* REQUIRED wording and construct the grant at the moment of agreement. */
|
|
57
65
|
export interface ConsentGrant {
|
|
58
66
|
/** Date.now() at the moment the participant agreed. */
|
|
@@ -88,10 +96,10 @@ export interface RecordingSnapshot {
|
|
|
88
96
|
droppedSamples: number;
|
|
89
97
|
/** Live input level. Speech sits around −25…−45 dBFS. */
|
|
90
98
|
inputLevelDbfs: number;
|
|
91
|
-
/** True after 10 s of silence while unmuted
|
|
99
|
+
/** True after 10 s of silence while unmuted: a dead/disconnected mic.
|
|
92
100
|
* Surface this loudly; it is fixable mid-call. */
|
|
93
101
|
inputSilent: boolean;
|
|
94
|
-
/** Samples at digital full scale
|
|
102
|
+
/** Samples at digital full scale: a too-hot mic, irreversibly clipped. */
|
|
95
103
|
fullScaleSamples: number;
|
|
96
104
|
}
|
|
97
105
|
export interface UploadKindProgress {
|
|
@@ -127,7 +135,7 @@ export interface CrowdPlayEvents {
|
|
|
127
135
|
error?: string;
|
|
128
136
|
};
|
|
129
137
|
/** Recording state. `error` non-null means the session is NOT being
|
|
130
|
-
* captured
|
|
138
|
+
* captured; show it prominently and offer retryRecording(). */
|
|
131
139
|
recording: {
|
|
132
140
|
isRecording: boolean;
|
|
133
141
|
error?: string;
|
|
@@ -175,7 +183,7 @@ declare const CrowdPlay: {
|
|
|
175
183
|
* Join a room. Recording starts automatically on join and stops on
|
|
176
184
|
* leave(). Rejects if the connection fails.
|
|
177
185
|
*
|
|
178
|
-
* `consent` is REQUIRED
|
|
186
|
+
* `consent` is REQUIRED; produce it with <CrowdPlayConsentScreen> or from
|
|
179
187
|
* your own UI at the moment of agreement. There is no way to record
|
|
180
188
|
* without it, here or natively.
|
|
181
189
|
*/
|
package/lib/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* crowdplaysdk
|
|
3
|
+
* crowdplaysdk: CrowdPlay's lossless capture SDK for React Native (iOS).
|
|
4
4
|
*
|
|
5
5
|
* The recording engine is 100% native (CrowdPlaySDK): studio-grade audio
|
|
6
6
|
* (48 kHz / 24-bit, unprocessed) + 1080p30 video captured locally during a
|
|
7
7
|
* live call, clock-synced across participants, uploaded to CrowdPlay with
|
|
8
|
-
* retries and crash recovery. **No media ever crosses the JS bridge
|
|
9
|
-
* only control calls and state events
|
|
8
|
+
* retries and crash recovery. **No media ever crosses the JS bridge**,
|
|
9
|
+
* only control calls and state events, so recording quality is identical
|
|
10
10
|
* to a fully native app.
|
|
11
11
|
*
|
|
12
12
|
* import CrowdPlay, { CrowdPlayConsentScreen, CrowdPlayVideoView } from 'crowdplaysdk';
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* await CrowdPlay.leave(); // recording stops; uploads continue automatically
|
|
19
19
|
*
|
|
20
20
|
* Consent is enforced twice: join() throws here without a grant, and the
|
|
21
|
-
* native layer requires the consent record structurally
|
|
21
|
+
* native layer requires the consent record structurally; there is no code
|
|
22
22
|
* path that records without it.
|
|
23
23
|
*/
|
|
24
24
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -30,8 +30,8 @@ function native() {
|
|
|
30
30
|
throw new Error("crowdplaysdk: native module not found. iOS setup: add the CrowdPlaySDK " +
|
|
31
31
|
"Swift package (https://github.com/symbiateam/crowdplaysdk) to your Xcode " +
|
|
32
32
|
"project and add the two bridge files from node_modules/crowdplaysdk/ios " +
|
|
33
|
-
"to your app target
|
|
34
|
-
(react_native_1.Platform.OS !== 'ios' ? `(platform '${react_native_1.Platform.OS}' is not supported yet
|
|
33
|
+
"to your app target; see the package README. " +
|
|
34
|
+
(react_native_1.Platform.OS !== 'ios' ? `(platform '${react_native_1.Platform.OS}' is not supported yet; iOS only.)` : ''));
|
|
35
35
|
}
|
|
36
36
|
return module;
|
|
37
37
|
}
|
|
@@ -73,7 +73,7 @@ const CrowdPlay = {
|
|
|
73
73
|
* Join a room. Recording starts automatically on join and stops on
|
|
74
74
|
* leave(). Rejects if the connection fails.
|
|
75
75
|
*
|
|
76
|
-
* `consent` is REQUIRED
|
|
76
|
+
* `consent` is REQUIRED; produce it with <CrowdPlayConsentScreen> or from
|
|
77
77
|
* your own UI at the moment of agreement. There is no way to record
|
|
78
78
|
* without it, here or natively.
|
|
79
79
|
*/
|
package/llms.txt
CHANGED
|
@@ -29,7 +29,7 @@ Consequences:
|
|
|
29
29
|
- Recording starts automatically on join() and stops on leave(). Do not
|
|
30
30
|
build a separate record button.
|
|
31
31
|
|
|
32
|
-
## The HOST APP is the product
|
|
32
|
+
## The HOST APP is the product, not recording
|
|
33
33
|
|
|
34
34
|
The builder came to you with an app idea (a tutoring app, a language-
|
|
35
35
|
exchange app, a remote interview tool…). Build THAT app. The SDK records
|
|
@@ -37,11 +37,11 @@ its video calls invisibly; recording is infrastructure, not identity:
|
|
|
37
37
|
- The app's NAME, description, and screens are about the builder's idea.
|
|
38
38
|
Never name or describe the app around recording.
|
|
39
39
|
- Recording surfaces in the UI in exactly four places: the consent
|
|
40
|
-
screen before joining (required
|
|
40
|
+
screen before joining (required, never minimized away), a small REC
|
|
41
41
|
indicator during the call, join/leave, and a small audio-output menu
|
|
42
42
|
(REQUIRED standard control: show the current output device by name from
|
|
43
43
|
the 'audioRoute' event, with a switch between the device and Speaker
|
|
44
|
-
via CrowdPlay.setAudioOutput
|
|
44
|
+
via CrowdPlay.setAudioOutput; Bluetooth users expect it).
|
|
45
45
|
- Do NOT build upload-progress screens, diagnostics screens, SDK settings
|
|
46
46
|
pages, or recording libraries/playback. Uploads run automatically in
|
|
47
47
|
the background; doctor() is a development-time check, not a screen.
|
|
@@ -56,7 +56,7 @@ its video calls invisibly; recording is infrastructure, not identity:
|
|
|
56
56
|
add audio-session-touching RN packages like react-native-incall-manager)
|
|
57
57
|
in an app using this SDK. The engine owns the microphone and camera.
|
|
58
58
|
3. NEVER skip or fake consent. CrowdPlay.join() THROWS without a ConsentGrant,
|
|
59
|
-
and the user must actually SEE the consent wording first
|
|
59
|
+
and the user must actually SEE the consent wording first; use
|
|
60
60
|
<CrowdPlayConsentScreen/>, or show CrowdPlay.consentText() verbatim in your own
|
|
61
61
|
UI and construct { grantedAtMs: Date.now() } at the moment of
|
|
62
62
|
agreement. Fabricating a grant creates false legal evidence.
|
|
@@ -79,7 +79,7 @@ its video calls invisibly; recording is infrastructure, not identity:
|
|
|
79
79
|
4. Info.plist: NSMicrophoneUsageDescription, NSCameraUsageDescription,
|
|
80
80
|
UIBackgroundModes = [audio]. A missing usage string makes iOS kill
|
|
81
81
|
the app at first mic/camera access (uncatchable).
|
|
82
|
-
5. AppDelegate (recommended
|
|
82
|
+
5. AppDelegate (recommended; lets uploads finish while suspended):
|
|
83
83
|
// AppDelegate.mm, with #import "<YourProjectName>-Swift.h"
|
|
84
84
|
- (void)application:(UIApplication *)application
|
|
85
85
|
handleEventsForBackgroundURLSession:(NSString *)identifier
|
|
@@ -112,18 +112,18 @@ its video calls invisibly; recording is infrastructure, not identity:
|
|
|
112
112
|
already went out during the call). Show at most one quiet
|
|
113
113
|
"finishing up…" line while an 'uploads' session is incomplete; do
|
|
114
114
|
NOT build a dedicated upload screen.
|
|
115
|
-
10. Verify: const checks = await CrowdPlay.doctor();
|
|
115
|
+
10. Verify: const checks = await CrowdPlay.doctor(); every failing check's
|
|
116
116
|
detail names its own fix. NOTE: on a fresh install the "permissions"
|
|
117
|
-
check reports "not requested yet
|
|
118
|
-
That is not an integration bug
|
|
117
|
+
check reports "not requested yet (NORMAL)"; iOS prompts at first join.
|
|
118
|
+
That is not an integration bug; only a DENIED state needs action.
|
|
119
119
|
11. Hand off (REQUIRED final message): the Simulator has no camera or
|
|
120
120
|
mic, so nothing is proven until the app runs on a real iPhone. Do
|
|
121
|
-
not end with "the implementation is complete"
|
|
121
|
+
not end with "the implementation is complete"; your FINAL message
|
|
122
122
|
must walk the builder through: plugging in the iPhone (Trust +
|
|
123
123
|
Developer Mode), picking a signing Team in Xcode and pressing Run on
|
|
124
124
|
the device (via the .xcworkspace), allowing mic/camera permissions,
|
|
125
125
|
a ~1-minute test call, and checking the session at
|
|
126
|
-
https://dashboard.crowdplay.ai
|
|
126
|
+
https://dashboard.crowdplay.ai; that is the end-to-end proof.
|
|
127
127
|
|
|
128
128
|
## API reference (exact TypeScript surface)
|
|
129
129
|
|
|
@@ -155,12 +155,12 @@ useVoiceActivity() -> { phase: 'connecting'|'idle'|'listening'|'speaking', energ
|
|
|
155
155
|
|
|
156
156
|
The voice AI (apps with the Voice AI switch enabled on the dashboard):
|
|
157
157
|
|
|
158
|
-
How it works
|
|
158
|
+
How it works (all server-side, nothing to wire up). When any user joins a
|
|
159
159
|
call, CrowdPlay's servers put a live AI participant into the room within
|
|
160
160
|
~3 seconds. It joins with name "agent" (identity "agent-<8hex>"), greets
|
|
161
161
|
the user, and holds a real spoken conversation (speech-to-speech: it
|
|
162
162
|
hears the user directly, answers in natural voice, can be interrupted).
|
|
163
|
-
The app writes ZERO AI code
|
|
163
|
+
The app writes ZERO AI code: no model API calls, no AI keys, no chat
|
|
164
164
|
backend, no STT/TTS. Provider, model and API key are set by the BUILDER on
|
|
165
165
|
the CrowdPlay dashboard, and so are the DEFAULT persona and voice.
|
|
166
166
|
Per-join overrides (0.4.0+): the app MAY pass `agent` to join() to change
|
|
@@ -177,34 +177,34 @@ and key cannot be changed from the app. MEMORY is built with `context`:
|
|
|
177
177
|
keep a few sentences per user in the app's own storage and pass them at
|
|
178
178
|
every join.
|
|
179
179
|
Conversations are recorded and transcribed by the platform automatically.
|
|
180
|
-
RECOMMENDED: voice-AI apps should be AUDIO-ONLY (audioOnly: true
|
|
180
|
+
RECOMMENDED: voice-AI apps should be AUDIO-ONLY (audioOnly: true; no
|
|
181
181
|
camera permission, no video anywhere); a camera adds nothing to talking
|
|
182
182
|
with an AI. Keep video only when the app ALSO has calls between people.
|
|
183
183
|
|
|
184
|
-
The AI conversation screen
|
|
184
|
+
The AI conversation screen, the approved standard layout: the animated
|
|
185
185
|
visual front and center, large (it IS the screen); NO self camera
|
|
186
186
|
preview and NO video tiles on this screen, ever; around it only the AI's
|
|
187
187
|
name/persona, a small REC indicator, a leave button, and the small
|
|
188
188
|
audio-output menu. One calm screen, one living visual, nothing competing
|
|
189
189
|
with it.
|
|
190
190
|
|
|
191
|
-
What the app builds
|
|
191
|
+
What the app builds: the AI's on-screen presence. The AI publishes
|
|
192
192
|
AUDIO ONLY: never a camera tile, an empty box, or a static image; never
|
|
193
193
|
count it as a generic user. Detect it in the participants event by name
|
|
194
194
|
"agent" (or identity prefix "agent-").
|
|
195
195
|
- The visual is a MENU, zero-effort to fully custom (0.3.4+ for 2-5):
|
|
196
|
-
1. Orb (default): <CrowdPlayVoiceView
|
|
196
|
+
1. Orb (default): <CrowdPlayVoiceView />, breathes idle, rings with
|
|
197
197
|
the user's voice, pulses with the AI's. Props: agentColor,
|
|
198
198
|
listeningColor, idleColor, size.
|
|
199
|
-
2. Bars: <CrowdPlayVoiceView variant='bars'
|
|
199
|
+
2. Bars: <CrowdPlayVoiceView variant='bars' />, equalizer look, good
|
|
200
200
|
under other content (scoreboards, question cards, quizzes).
|
|
201
201
|
3. Halo around the app's own art: pass a mascot portrait / logo /
|
|
202
|
-
character as children of <CrowdPlayVoiceView
|
|
202
|
+
character as children of <CrowdPlayVoiceView>, animated ring +
|
|
203
203
|
glow around whatever you provide.
|
|
204
|
-
4. Make ANY view alive: wrap it in <VoiceReactive
|
|
204
|
+
4. Make ANY view alive: wrap it in <VoiceReactive>, breathing, pulse
|
|
205
205
|
and speech glow on the app's own art in one line.
|
|
206
206
|
5. Fully custom (best for characters): drive ANY visual from the
|
|
207
|
-
useVoiceActivity() hook
|
|
207
|
+
useVoiceActivity() hook, returning { phase: 'connecting'|'idle'|'listening'|
|
|
208
208
|
'speaking', energy: 0…1 smoothed loudness }. Example, an illustrated
|
|
209
209
|
tutor that reacts to the conversation:
|
|
210
210
|
|
|
@@ -215,10 +215,10 @@ count it as a generic user. Detect it in the participants event by name
|
|
|
215
215
|
pose={phase === 'listening' ? 'leanIn' : 'idleBlink'} />;
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
-
Any art pipeline works (Animated, Lottie, Rive, sprite frames)
|
|
218
|
+
Any art pipeline works (Animated, Lottie, Rive, sprite frames): bind
|
|
219
219
|
mouth/pose/glow to phase + energy.
|
|
220
220
|
- Live captions (0.3.4+): each line the AI and the user say is streamed
|
|
221
|
-
as it is spoken
|
|
221
|
+
as it is spoken; the useLatestCaption() hook returns { role, text }.
|
|
222
222
|
For speech bubbles, subtitles, language-learning corrections. Optional
|
|
223
223
|
UI; the full timestamped transcript is always delivered with the
|
|
224
224
|
session regardless.
|
|
@@ -228,8 +228,14 @@ count it as a generic user. Detect it in the participants event by name
|
|
|
228
228
|
Types:
|
|
229
229
|
CrowdPlayConfig { serverUrl; appKey; audioOnly?=false;
|
|
230
230
|
videoUploadsOnCellular?=false;
|
|
231
|
-
uploadDuringCall?=true;
|
|
232
|
-
|
|
231
|
+
uploadDuringCall?=true;
|
|
232
|
+
otherAudio?='interrupt'|'mix'|'mixDucked'; callQuality? }
|
|
233
|
+
// otherAudio (0.4.0+): 'interrupt' pauses other apps'
|
|
234
|
+
// audio at join (classic call); 'mix' keeps Apple Music /
|
|
235
|
+
// Spotify playing through the call and the recording, for
|
|
236
|
+
// shared-listening apps; 'mixDucked' also lowers it. Music
|
|
237
|
+
// never rides the call; listeners must wear headphones.
|
|
238
|
+
// audioOnly (0.3.0+): the camera is never touched; no
|
|
233
239
|
// camera permission, no video captured or uploaded,
|
|
234
240
|
// voice-only calls. Skip NSCameraUsageDescription, skip
|
|
235
241
|
// CrowdPlayVideoView and every camera control.
|
|
@@ -242,8 +248,8 @@ Types:
|
|
|
242
248
|
ConsentGrant { grantedAtMs: number }
|
|
243
249
|
RecordingSnapshot { seconds; segmentsClosed; droppedSamples;
|
|
244
250
|
inputLevelDbfs; // speech ≈ −25…−45 dBFS
|
|
245
|
-
inputSilent; // dead mic
|
|
246
|
-
fullScaleSamples } // clipping
|
|
251
|
+
inputSilent; // dead mic: surface loudly
|
|
252
|
+
fullScaleSamples } // clipping: mic too hot
|
|
247
253
|
UploadProgress { sessionId; fraction; isComplete; failed;
|
|
248
254
|
audio: UploadKindProgress; video: UploadKindProgress }
|
|
249
255
|
DoctorCheck { id; passed; detail }
|
|
@@ -269,7 +275,7 @@ Events (CrowdPlay.addListener):
|
|
|
269
275
|
// this mic (wear headphones); micPolicy = unexpected
|
|
270
276
|
// input device.
|
|
271
277
|
|
|
272
|
-
## Accessing recordings (no AWS, no SDK
|
|
278
|
+
## Accessing recordings (no AWS, no SDK, plain HTTPS)
|
|
273
279
|
|
|
274
280
|
The app key doubles as the data credential:
|
|
275
281
|
GET https://dashboard.crowdplay.ai/sessions (x-liva-key header)
|
|
@@ -289,21 +295,21 @@ Humans use https://dashboard.crowdplay.ai directly (sign in, download).
|
|
|
289
295
|
pauses uploads until next launch (nothing lost).
|
|
290
296
|
- Participants should wear wired or closed-back headphones (crossTalk
|
|
291
297
|
warns live when they don't).
|
|
292
|
-
- SIMULATOR detection (engine 0.3.0+): on any Simulator
|
|
293
|
-
previews
|
|
298
|
+
- SIMULATOR detection (engine 0.3.0+): on any Simulator, including CLOUD
|
|
299
|
+
previews, doctor()'s "platform" check fails and join() rejects with a
|
|
294
300
|
clear message instead of a cryptic audio-engine error. Expected, not a
|
|
295
301
|
bug: no microphone exists there. A doctor() run also flips the "health
|
|
296
302
|
check" step on the builder's dashboard checklist automatically.
|
|
297
303
|
- Slow join or glitchy first seconds of a recording = poor radio
|
|
298
304
|
environment (congested 2.4 GHz WiFi + Bluetooth share the antenna) or
|
|
299
|
-
Metro/debugger overhead
|
|
305
|
+
Metro/debugger overhead; NOT an integration bug. Capture self-heals.
|
|
300
306
|
Advise: better network, wired headphones, evaluate from a home-screen
|
|
301
307
|
launch (release build).
|
|
302
308
|
- Build error mentioning LiveKit/WebRTC duplicate symbols: the app added
|
|
303
|
-
its own LiveKit dependency
|
|
309
|
+
its own LiveKit dependency; remove it (rule 1).
|
|
304
310
|
- "Swift bridging" or "<Project>-Swift.h not found" errors: rerun
|
|
305
311
|
ios/wire.rb, then a clean build (the header is generated at build time).
|
|
306
|
-
- 401/"unauthorized": wrong app key
|
|
312
|
+
- 401/"unauthorized": wrong app key; sign in at
|
|
307
313
|
https://dashboard.crowdplay.ai, reveal the key, and copy it again.
|
|
308
314
|
- Uploads crawling on an otherwise-fine network: check for a VPN, iCloud
|
|
309
315
|
Private Relay, or Low Data Mode on the phone (a VPN can cut throughput
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "crowdplaysdk",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "CrowdPlay lossless conversation capture for React Native (iOS). Studio-grade per-participant recording during live video calls, delivered to CrowdPlay automatically.",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"ios",
|
|
11
11
|
"README.md",
|
|
12
12
|
"llms.txt",
|
|
13
|
-
"Examples"
|
|
13
|
+
"Examples",
|
|
14
|
+
"LICENSE.md"
|
|
14
15
|
],
|
|
15
16
|
"scripts": {
|
|
16
17
|
"build": "tsc",
|
package/src/ConsentScreen.tsx
CHANGED
|
@@ -33,7 +33,7 @@ export function CrowdPlayConsentScreen({ onConsent, style }: CrowdPlayConsentScr
|
|
|
33
33
|
if (!cancelled) setText(wording);
|
|
34
34
|
})
|
|
35
35
|
.catch(() => {
|
|
36
|
-
if (!cancelled) setText('Consent text unavailable
|
|
36
|
+
if (!cancelled) setText('Consent text unavailable. Check that CrowdPlay.configure() ran and the native module is installed.');
|
|
37
37
|
});
|
|
38
38
|
return () => {
|
|
39
39
|
cancelled = true;
|
package/src/VideoView.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Native video tile. Renders the LOCAL camera preview or a REMOTE
|
|
3
3
|
* participant's live video (identified by the `identity` from the
|
|
4
|
-
* `participants` event). The video never touches JavaScript
|
|
4
|
+
* `participants` event). The video never touches JavaScript; this is a
|
|
5
5
|
* native LiveKit view hosted in your RN layout.
|
|
6
6
|
*
|
|
7
7
|
* <CrowdPlayVideoView participant="local" style={{ width: 120, height: 160 }} />
|
package/src/VoiceView.tsx
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The standard voice-AI visual (founders, 2026-08-22): a breathing orb in
|
|
3
|
-
* the style of realtime assistants
|
|
3
|
+
* the style of realtime assistants: calm while idle, a cool ring while the
|
|
4
4
|
* user talks, an energetic pulse while the AI speaks. The React Native
|
|
5
5
|
* counterpart of the native SDK's CrowdPlayVoiceView, driven by the same
|
|
6
6
|
* engine signal over a fast bridge event that runs only while mounted.
|
|
7
7
|
*
|
|
8
8
|
* Two levels of customization:
|
|
9
|
-
* 1. Props on <CrowdPlayVoiceView
|
|
10
|
-
* 2. useVoiceActivity()
|
|
9
|
+
* 1. Props on <CrowdPlayVoiceView>: colors and size of the built-in orb.
|
|
10
|
+
* 2. useVoiceActivity(): the phase + smoothed energy underneath, for apps
|
|
11
11
|
* that want to draw something entirely their own.
|
|
12
12
|
*/
|
|
13
13
|
|
|
@@ -58,7 +58,7 @@ export function useVoiceActivity(): VoiceActivity {
|
|
|
58
58
|
phase = 'idle';
|
|
59
59
|
target = 0;
|
|
60
60
|
}
|
|
61
|
-
// Fast attack, slow release
|
|
61
|
+
// Fast attack, slow release: speech onsets snap, tails breathe out.
|
|
62
62
|
const rate = target > energyRef.current ? 0.55 : 0.12;
|
|
63
63
|
energyRef.current += (target - energyRef.current) * rate;
|
|
64
64
|
setActivity({ phase, energy: energyRef.current });
|
|
@@ -92,7 +92,7 @@ export function useLatestCaption(): VoiceCaption | null {
|
|
|
92
92
|
return caption;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
/** Make ANY child view breathe, pulse, and glow with the conversation
|
|
95
|
+
/** Make ANY child view breathe, pulse, and glow with the conversation;
|
|
96
96
|
* the one-line path from the app's own art (mascot, logo, character) to a
|
|
97
97
|
* living AI presence. */
|
|
98
98
|
export function VoiceReactive({
|
package/src/index.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* crowdplaysdk
|
|
2
|
+
* crowdplaysdk: CrowdPlay's lossless capture SDK for React Native (iOS).
|
|
3
3
|
*
|
|
4
4
|
* The recording engine is 100% native (CrowdPlaySDK): studio-grade audio
|
|
5
5
|
* (48 kHz / 24-bit, unprocessed) + 1080p30 video captured locally during a
|
|
6
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
|
|
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
9
|
* to a fully native app.
|
|
10
10
|
*
|
|
11
11
|
* import CrowdPlay, { CrowdPlayConsentScreen, CrowdPlayVideoView } from 'crowdplaysdk';
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* await CrowdPlay.leave(); // recording stops; uploads continue automatically
|
|
18
18
|
*
|
|
19
19
|
* Consent is enforced twice: join() throws here without a grant, and the
|
|
20
|
-
* native layer requires the consent record structurally
|
|
20
|
+
* native layer requires the consent record structurally; there is no code
|
|
21
21
|
* path that records without it.
|
|
22
22
|
*/
|
|
23
23
|
|
|
@@ -45,7 +45,7 @@ export interface CrowdPlayConfig {
|
|
|
45
45
|
serverUrl: string;
|
|
46
46
|
/** From the CrowdPlay dashboard. Also your data-access credential. */
|
|
47
47
|
appKey: string;
|
|
48
|
-
/** AUDIO-ONLY app: the camera is never touched
|
|
48
|
+
/** AUDIO-ONLY app: the camera is never touched: no camera permission,
|
|
49
49
|
* no video captured or uploaded, voice-only calls. The audio path is
|
|
50
50
|
* identical to an A/V app. Requires engine 0.3.0+. Default false. */
|
|
51
51
|
audioOnly?: boolean;
|
|
@@ -54,14 +54,22 @@ export interface CrowdPlayConfig {
|
|
|
54
54
|
videoUploadsOnCellular?: boolean;
|
|
55
55
|
/** Upload segments while the call records, so the post-call wait is
|
|
56
56
|
* roughly the final segment + manifests. Mid-call transfers run one at
|
|
57
|
-
* a time and pause automatically when call quality drops
|
|
57
|
+
* a time and pause automatically when call quality drops; the call
|
|
58
58
|
* always wins the uplink. Default true. */
|
|
59
59
|
uploadDuringCall?: boolean;
|
|
60
|
+
/** What happens to OTHER apps' audio (Apple Music, Spotify, podcasts)
|
|
61
|
+
* while a call is live. 'interrupt' (default) pauses them at join, the
|
|
62
|
+
* classic call behaviour. 'mix' keeps them playing alongside the call
|
|
63
|
+
* and the recording, for shared-listening and DJ apps; 'mixDucked' also
|
|
64
|
+
* lowers them for the whole call. Music stays on the device and never
|
|
65
|
+
* rides the call; listeners must wear headphones or it lands in the
|
|
66
|
+
* recording. Requires engine 0.4.0+. */
|
|
67
|
+
otherAudio?: 'interrupt' | 'mix' | 'mixDucked';
|
|
60
68
|
callQuality?: CallQuality;
|
|
61
69
|
}
|
|
62
70
|
|
|
63
|
-
/** Proof the participant agreed. Produce it with CrowdPlayConsentScreen, or
|
|
64
|
-
* if you render your own consent UI
|
|
71
|
+
/** Proof the participant agreed. Produce it with CrowdPlayConsentScreen, or,
|
|
72
|
+
* if you render your own consent UI, call CrowdPlay.consentText() to show the
|
|
65
73
|
* REQUIRED wording and construct the grant at the moment of agreement. */
|
|
66
74
|
export interface ConsentGrant {
|
|
67
75
|
/** Date.now() at the moment the participant agreed. */
|
|
@@ -100,10 +108,10 @@ export interface RecordingSnapshot {
|
|
|
100
108
|
droppedSamples: number;
|
|
101
109
|
/** Live input level. Speech sits around −25…−45 dBFS. */
|
|
102
110
|
inputLevelDbfs: number;
|
|
103
|
-
/** True after 10 s of silence while unmuted
|
|
111
|
+
/** True after 10 s of silence while unmuted: a dead/disconnected mic.
|
|
104
112
|
* Surface this loudly; it is fixable mid-call. */
|
|
105
113
|
inputSilent: boolean;
|
|
106
|
-
/** Samples at digital full scale
|
|
114
|
+
/** Samples at digital full scale: a too-hot mic, irreversibly clipped. */
|
|
107
115
|
fullScaleSamples: number;
|
|
108
116
|
}
|
|
109
117
|
|
|
@@ -141,7 +149,7 @@ export interface CrowdPlayEvents {
|
|
|
141
149
|
/** Connection lifecycle. `error` is set when phase === 'failed'. */
|
|
142
150
|
phase: { phase: Phase; error?: string };
|
|
143
151
|
/** Recording state. `error` non-null means the session is NOT being
|
|
144
|
-
* captured
|
|
152
|
+
* captured; show it prominently and offer retryRecording(). */
|
|
145
153
|
recording: { isRecording: boolean; error?: string };
|
|
146
154
|
/** Upload progress for every session with outstanding or recent work. */
|
|
147
155
|
uploads: { sessions: UploadProgress[]; onWifi: boolean };
|
|
@@ -189,8 +197,8 @@ function native(): NativeCrowdPlay {
|
|
|
189
197
|
"crowdplaysdk: native module not found. iOS setup: add the CrowdPlaySDK " +
|
|
190
198
|
"Swift package (https://github.com/symbiateam/crowdplaysdk) to your Xcode " +
|
|
191
199
|
"project and add the two bridge files from node_modules/crowdplaysdk/ios " +
|
|
192
|
-
"to your app target
|
|
193
|
-
(Platform.OS !== 'ios' ? `(platform '${Platform.OS}' is not supported yet
|
|
200
|
+
"to your app target; see the package README. " +
|
|
201
|
+
(Platform.OS !== 'ios' ? `(platform '${Platform.OS}' is not supported yet; iOS only.)` : '')
|
|
194
202
|
);
|
|
195
203
|
}
|
|
196
204
|
return module;
|
|
@@ -240,7 +248,7 @@ const CrowdPlay = {
|
|
|
240
248
|
* Join a room. Recording starts automatically on join and stops on
|
|
241
249
|
* leave(). Rejects if the connection fails.
|
|
242
250
|
*
|
|
243
|
-
* `consent` is REQUIRED
|
|
251
|
+
* `consent` is REQUIRED; produce it with <CrowdPlayConsentScreen> or from
|
|
244
252
|
* your own UI at the moment of agreement. There is no way to record
|
|
245
253
|
* without it, here or natively.
|
|
246
254
|
*/
|