crowdplaysdk 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  /**
2
- * crowdplaysdk CrowdPlay's lossless capture SDK for React Native (iOS).
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 so recording quality is identical
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 there is no code
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 no camera permission,
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 the call
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 call CrowdPlay.consentText() to show the
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. */
@@ -76,6 +84,18 @@ export interface ConsentGrant {
76
84
  * apply to the whole room. The app can never change the provider, the
77
85
  * model, or the API key from here.
78
86
  */
87
+ /** A tool the app offers the voice AI for one session (D-103). Requires
88
+ * the app's dashboard config to use the GPT-Live provider with an AI brain
89
+ * (delegation) enabled; ignored otherwise. */
90
+ export interface AgentTool {
91
+ /** Letters, digits, underscore, dash; max 64 chars. */
92
+ name: string;
93
+ /** What the tool does and when to use it, for the model. */
94
+ description: string;
95
+ /** JSON-schema object describing the arguments. Omit for none. */
96
+ parameters?: Record<string, unknown>;
97
+ }
98
+
79
99
  export interface AgentOptions {
80
100
  /** A voice of the app's configured provider. Gemini: Charon, Aoede,
81
101
  * Fenrir, Kore, Puck. OpenAI: marin, cedar, alloy, ash, ballad, coral,
@@ -89,6 +109,9 @@ export interface AgentOptions {
89
109
  * keep a few sentences per user in your own storage and pass them
90
110
  * here. Max 4000 chars. */
91
111
  context?: string;
112
+ /** Tools the AI may call this session (D-103). Handle calls with
113
+ * CrowdPlay.onAgentToolCall(). Max 16. */
114
+ tools?: AgentTool[];
92
115
  }
93
116
 
94
117
  export type Phase = 'idle' | 'connecting' | 'connected' | 'failed';
@@ -100,10 +123,10 @@ export interface RecordingSnapshot {
100
123
  droppedSamples: number;
101
124
  /** Live input level. Speech sits around −25…−45 dBFS. */
102
125
  inputLevelDbfs: number;
103
- /** True after 10 s of silence while unmuted a dead/disconnected mic.
126
+ /** True after 10 s of silence while unmuted: a dead/disconnected mic.
104
127
  * Surface this loudly; it is fixable mid-call. */
105
128
  inputSilent: boolean;
106
- /** Samples at digital full scale a too-hot mic, irreversibly clipped. */
129
+ /** Samples at digital full scale: a too-hot mic, irreversibly clipped. */
107
130
  fullScaleSamples: number;
108
131
  }
109
132
 
@@ -141,7 +164,7 @@ export interface CrowdPlayEvents {
141
164
  /** Connection lifecycle. `error` is set when phase === 'failed'. */
142
165
  phase: { phase: Phase; error?: string };
143
166
  /** Recording state. `error` non-null means the session is NOT being
144
- * captured show it prominently and offer retryRecording(). */
167
+ * captured; show it prominently and offer retryRecording(). */
145
168
  recording: { isRecording: boolean; error?: string };
146
169
  /** Upload progress for every session with outstanding or recent work. */
147
170
  uploads: { sessions: UploadProgress[]; onWifi: boolean };
@@ -156,6 +179,9 @@ export interface CrowdPlayEvents {
156
179
  * "iPhone speaker"), connected candidates, and whether headphones are
157
180
  * attached. Drive the call screen's output menu from this (D-079). */
158
181
  audioRoute: { currentOutputName: string; detectedOutputs: string[]; headphonesConnected: boolean };
182
+ /** The voice AI asked the app to run a tool (D-103). Prefer
183
+ * CrowdPlay.onAgentToolCall(), which answers for you. */
184
+ agentToolCall: { callId: string; name: string; argumentsJSON: string };
159
185
  }
160
186
 
161
187
  export type CrowdPlayEventName = keyof CrowdPlayEvents;
@@ -169,6 +195,8 @@ interface NativeCrowdPlay {
169
195
  consentText(): Promise<string>;
170
196
  join(displayName: string, roomCode: string, consentGrantedAtMs: number,
171
197
  agent: Record<string, string> | null): Promise<void>;
198
+ respondToAgentToolCall(callId: string, output: string): void;
199
+ updateAgentContext(text: string): void;
172
200
  leave(): Promise<void>;
173
201
  setMicMuted(muted: boolean): Promise<void>;
174
202
  setCameraEnabled(enabled: boolean): Promise<void>;
@@ -189,8 +217,8 @@ function native(): NativeCrowdPlay {
189
217
  "crowdplaysdk: native module not found. iOS setup: add the CrowdPlaySDK " +
190
218
  "Swift package (https://github.com/symbiateam/crowdplaysdk) to your Xcode " +
191
219
  "project and add the two bridge files from node_modules/crowdplaysdk/ios " +
192
- "to your app target see the package README. " +
193
- (Platform.OS !== 'ios' ? `(platform '${Platform.OS}' is not supported yet iOS only.)` : '')
220
+ "to your app target; see the package README. " +
221
+ (Platform.OS !== 'ios' ? `(platform '${Platform.OS}' is not supported yet; iOS only.)` : '')
194
222
  );
195
223
  }
196
224
  return module;
@@ -240,7 +268,7 @@ const CrowdPlay = {
240
268
  * Join a room. Recording starts automatically on join and stops on
241
269
  * leave(). Rejects if the connection fails.
242
270
  *
243
- * `consent` is REQUIRED produce it with <CrowdPlayConsentScreen> or from
271
+ * `consent` is REQUIRED; produce it with <CrowdPlayConsentScreen> or from
244
272
  * your own UI at the moment of agreement. There is no way to record
245
273
  * without it, here or natively.
246
274
  */
@@ -271,6 +299,11 @@ const CrowdPlay = {
271
299
  put('voice', agent.voice, 60);
272
300
  put('systemPrompt', agent.systemPrompt, 8192);
273
301
  put('context', agent.context, 4000);
302
+ // Tools cross the bridge as one JSON string; the native layer and the
303
+ // server both re-validate (D-103).
304
+ if (Array.isArray(agent.tools) && agent.tools.length) {
305
+ put('toolsJSON', JSON.stringify(agent.tools.slice(0, 16)), 8192);
306
+ }
274
307
  }
275
308
  return native().join(displayName.trim(), roomCode, consent.grantedAtMs, agentFields);
276
309
  },
@@ -322,6 +355,41 @@ const CrowdPlay = {
322
355
  return native().doctor();
323
356
  },
324
357
 
358
+ /** Handle the voice AI's tool calls (D-103). The handler gets the tool
359
+ * name and its arguments (parsed JSON object) and returns the result as
360
+ * an object or JSON string; return null to decline on this device
361
+ * (another participant's device may answer; first answer wins). Ordinary
362
+ * app code, no AI here. The AI waits about 12 seconds per call. Returns
363
+ * a subscription; call .remove(). */
364
+ onAgentToolCall(
365
+ handler: (name: string, args: Record<string, unknown>) =>
366
+ Promise<Record<string, unknown> | string | null> | Record<string, unknown> | string | null
367
+ ): { remove(): void } {
368
+ const subscription = events().addListener(
369
+ 'crowdplay:agentToolCall',
370
+ (payload: { callId: string; name: string; argumentsJSON: string }) => {
371
+ void (async () => {
372
+ let args: Record<string, unknown> = {};
373
+ try { args = JSON.parse(payload.argumentsJSON); } catch { /* {} */ }
374
+ const result = await handler(payload.name, args);
375
+ if (result === null || result === undefined) return;
376
+ const output = typeof result === 'string' ? result : JSON.stringify(result);
377
+ native().respondToAgentToolCall(payload.callId, output);
378
+ })();
379
+ }
380
+ );
381
+ return { remove: () => subscription.remove() };
382
+ },
383
+
384
+ /** Tell the AI what just happened in the app, mid-session ("Night ended.
385
+ * Sam was eliminated."). Keep it to a sentence or two; long pushes are
386
+ * truncated. GPT-Live provider only; other providers ignore it. */
387
+ updateAgentContext(text: string): void {
388
+ if (typeof text === 'string' && text.trim()) {
389
+ native().updateAgentContext(text.trim());
390
+ }
391
+ },
392
+
325
393
  /** Subscribe to state events. Returns a subscription; call .remove(). */
326
394
  addListener<E extends CrowdPlayEventName>(
327
395
  event: E,