@droponair/sdk-js 0.14.0 → 0.16.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/CHANGELOG.md CHANGED
@@ -6,6 +6,31 @@ This project follows [Semantic Versioning](https://semver.org/).
6
6
 
7
7
  ---
8
8
 
9
+ ## [0.16.0], 2026-05-21
10
+
11
+ ### Added
12
+
13
+ - **Call recording signal.** `startRecording(callId)` / `stopRecording(callId)` / `markRecordingAvailable(callId, location)` for group and room calls. The SDK signals recording state; your app owns the media capture and uploads the file to your own storage (the same split as screen sharing - the platform never holds media). The platform broadcasts the signal to every participant, including late joiners, and fires `recording.started` / `recording.stopped` / `recording.available` webhooks. New `GROUP_CALL_RECORDING_*` event types.
14
+
15
+ ### Notes
16
+
17
+ - A recording is always signaled to every participant - that transparency is enforced server-side and is not optional. Control-plane signaling only; no proto change.
18
+
19
+ ---
20
+
21
+ ## [0.15.0], 2026-05-21
22
+
23
+ ### Added
24
+
25
+ - **Live stage roles & audience controls.** A room created with `policy.stageMode` runs its live call as a stage: hosts join as speakers, everyone else joins as receive-only audience. New `raiseHand` / `lowerHand` (any participant), `promoteToSpeaker` / `demoteToAudience` (host / co-host), and `submitStageQuestion` - all act on the callId from `joinRoom`. Role changes surface via `GROUP_CALL_ROLE_CHANGED` events (role `SPEAKER` / `AUDIENCE`); the room-call participant list now carries each participant's role.
26
+ - New `GROUP_CALL_STAGE_*` event types.
27
+
28
+ ### Notes
29
+
30
+ - Stage mode is a roles + signaling layer, the media topology is still the WebRTC mesh, suited to panels and small/medium stages. Large-audience streaming needs an SFU and is out of scope. Pure control-plane signaling; no proto change.
31
+
32
+ ---
33
+
9
34
  ## [0.14.0], 2026-05-21
10
35
 
11
36
  ### Added
package/README.md CHANGED
@@ -333,6 +333,58 @@ client.sendGroupCallSignal('GROUP_CALL_SDP_OFFER', callId, '', peerUserId, sdp);
333
333
 
334
334
  `joinRoom` rejects with `HOST_REQUIRED`, `ROOM_CLOSED`, or `WAITING_ROOM_PENDING` (listen for `GROUP_CALL_WAITING_ROOM_ADMITTED`, then call `joinRoom` again).
335
335
 
336
+ ### Live Stage
337
+
338
+ Available since SDK `0.15.0`. Create a room with `policy.stageMode` and its live call runs as a stage: hosts join as **speakers**, everyone else joins as receive-only **audience**. Audience can raise a hand; a host promotes them to speaker. The media topology is still the WebRTC mesh, so a stage is suited to panels and small/medium audiences - large-audience streaming needs an SFU and is out of scope.
339
+
340
+ | Method | Description |
341
+ |--------|-------------|
342
+ | `raiseHand(callId)` | Request promotion to speaker (any participant) |
343
+ | `lowerHand(callId)` | Lower your raised hand |
344
+ | `promoteToSpeaker(callId, userId)` | Promote an audience member (host / co-host) |
345
+ | `demoteToAudience(callId, userId)` | Demote a speaker (host / co-host) |
346
+ | `submitStageQuestion(callId, text)` | Submit a text question to the stage's speakers |
347
+
348
+ ```typescript
349
+ const room = await client.createRoom({ name: 'AMA', policy: { stageMode: true, requireHost: true } });
350
+ const callId = await client.joinRoom(room.roomId);
351
+
352
+ // Audience side
353
+ client.raiseHand(callId);
354
+ client.submitStageQuestion(callId, 'How does E2EE key rotation work?');
355
+
356
+ // Host side - role changes arrive as GROUP_CALL_ROLE_CHANGED events
357
+ client.onGroupCallEvent((e) => {
358
+ if (e.type === 'GROUP_CALL_STAGE_HAND_RAISED') client.promoteToSpeaker(callId, e.payload!);
359
+ if (e.type === 'GROUP_CALL_ROLE_CHANGED') updateStageUi(e.payload); // {"userId","role":"SPEAKER"|"AUDIENCE"}
360
+ });
361
+ ```
362
+
363
+ Audience members are receive-only by convention: on `GROUP_CALL_ROLE_CHANGED` to `AUDIENCE`, your app simply does not publish a local media track. The platform signals the role; your app owns the WebRTC tracks.
364
+
365
+ ### Call Recording
366
+
367
+ Available since SDK `0.16.0`. The SDK **signals** recording state on a group or room call; your app does the actual media capture (`MediaRecorder`) and uploads the file to your own storage — the platform never holds the media. The recording signal is broadcast to every participant (including anyone who joins later); that transparency is enforced server-side.
368
+
369
+ | Method | Description |
370
+ |--------|-------------|
371
+ | `startRecording(callId)` | Signal that you started recording |
372
+ | `stopRecording(callId)` | Signal that you stopped recording |
373
+ | `markRecordingAvailable(callId, location)` | Announce an uploaded recording (`location` references it in your storage) |
374
+
375
+ ```typescript
376
+ client.startRecording(callId);
377
+ // ... your app records the streams and uploads the file ...
378
+ client.markRecordingAvailable(callId, 's3://my-bucket/recordings/call-123.webm');
379
+ client.stopRecording(callId);
380
+
381
+ client.onGroupCallEvent((e) => {
382
+ if (e.type === 'GROUP_CALL_RECORDING_STARTED') showRecordingBanner(e.payload); // recorder userId
383
+ });
384
+ ```
385
+
386
+ The platform also fires `recording.started` / `recording.stopped` / `recording.available` webhooks. For E2EE calls, recording stays on the client (the platform has no media plane); a server-side SFU recording mode is a future option.
387
+
336
388
  ### Screen Sharing
337
389
 
338
390
  Available since SDK `0.6.0`. The SDK only signals start/stop - capture (via the browser's `getDisplayMedia()`) and adding the resulting `MediaStreamTrack` to the existing peer connection are the app's responsibility.
@@ -289,6 +289,15 @@ export declare class MessagingClient implements DropOnAirClient {
289
289
  requestWaitingRoomEntry(callId: string, groupId: string): void;
290
290
  admitFromWaitingRoom(callId: string, groupId: string, userId: string): void;
291
291
  rejectFromWaitingRoom(callId: string, groupId: string, userId: string): void;
292
+ raiseHand(callId: string): void;
293
+ lowerHand(callId: string): void;
294
+ promoteToSpeaker(callId: string, userId: string): void;
295
+ demoteToAudience(callId: string, userId: string): void;
296
+ submitStageQuestion(callId: string, text: string): void;
297
+ startRecording(callId: string): void;
298
+ stopRecording(callId: string): void;
299
+ /** Announce an uploaded recording; `location` is a reference into your storage. */
300
+ markRecordingAvailable(callId: string, location: string): void;
292
301
  private sendGroupCallFrame;
293
302
  /**
294
303
  * Initiate an outgoing call.
@@ -1307,6 +1307,48 @@ class MessagingClient {
1307
1307
  payload: JSON.stringify({ userId }),
1308
1308
  });
1309
1309
  }
1310
+ // Live stage controls (Feature 3.2). These act on a room call running in
1311
+ // stage mode; pass the callId from joinRoom(). raiseHand / lowerHand and
1312
+ // submitStageQuestion are open to any participant; promote / demote are
1313
+ // host / co-host authority. Roles surface via GROUP_CALL_ROLE_CHANGED.
1314
+ raiseHand(callId) {
1315
+ this.sendGroupCallFrame({ type: 'GROUP_CALL_STAGE_HAND_RAISED', callId, groupId: '' });
1316
+ }
1317
+ lowerHand(callId) {
1318
+ this.sendGroupCallFrame({ type: 'GROUP_CALL_STAGE_HAND_LOWERED', callId, groupId: '' });
1319
+ }
1320
+ promoteToSpeaker(callId, userId) {
1321
+ this.sendGroupCallFrame({
1322
+ type: 'GROUP_CALL_STAGE_PROMOTE',
1323
+ callId,
1324
+ groupId: '',
1325
+ payload: JSON.stringify({ userId }),
1326
+ });
1327
+ }
1328
+ demoteToAudience(callId, userId) {
1329
+ this.sendGroupCallFrame({
1330
+ type: 'GROUP_CALL_STAGE_DEMOTE',
1331
+ callId,
1332
+ groupId: '',
1333
+ payload: JSON.stringify({ userId }),
1334
+ });
1335
+ }
1336
+ submitStageQuestion(callId, text) {
1337
+ this.sendGroupCallFrame({ type: 'GROUP_CALL_STAGE_QUESTION', callId, groupId: '', payload: text });
1338
+ }
1339
+ // Call recording (Feature 3.5a). These SIGNAL recording state; the actual
1340
+ // media capture + upload to your storage is your app's job (same split as
1341
+ // screen sharing). The platform broadcasts the signal to every participant.
1342
+ startRecording(callId) {
1343
+ this.sendGroupCallFrame({ type: 'GROUP_CALL_RECORDING_STARTED', callId, groupId: '' });
1344
+ }
1345
+ stopRecording(callId) {
1346
+ this.sendGroupCallFrame({ type: 'GROUP_CALL_RECORDING_STOPPED', callId, groupId: '' });
1347
+ }
1348
+ /** Announce an uploaded recording; `location` is a reference into your storage. */
1349
+ markRecordingAvailable(callId, location) {
1350
+ this.sendGroupCallFrame({ type: 'GROUP_CALL_RECORDING_AVAILABLE', callId, groupId: '', payload: location });
1351
+ }
1310
1352
  sendGroupCallFrame(frame) {
1311
1353
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
1312
1354
  throw new Error('DropOnAir websocket is not connected');
@@ -135,7 +135,7 @@ export interface DecryptedGroupMessage {
135
135
  plaintext: string;
136
136
  }
137
137
  export type GroupMessageCallback = (message: DecryptedGroupMessage) => void;
138
- export type GroupCallEventType = 'GROUP_CALL_INVITE' | 'GROUP_CALL_RINGING' | 'GROUP_CALL_JOIN' | 'GROUP_CALL_LEAVE' | 'GROUP_CALL_END' | 'GROUP_CALL_ENDED' | 'GROUP_CALL_PARTICIPANT_JOINED' | 'GROUP_CALL_PARTICIPANT_LEFT' | 'GROUP_CALL_PARTICIPANT_REMOVED' | 'GROUP_CALL_ALREADY_ACTIVE' | 'GROUP_CALL_SDP_OFFER' | 'GROUP_CALL_SDP_ANSWER' | 'GROUP_CALL_ICE_CANDIDATE' | 'GROUP_CALL_SCREEN_SHARE_STARTED' | 'GROUP_CALL_SCREEN_SHARE_STOPPED' | 'GROUP_CALL_HOST_TRANSFER' | 'GROUP_CALL_COHOST_APPOINT' | 'GROUP_CALL_COHOST_REVOKE' | 'GROUP_CALL_ROLE_CHANGED' | 'GROUP_CALL_MUTE_PARTICIPANT' | 'GROUP_CALL_REMOVE_PARTICIPANT' | 'GROUP_CALL_WAITING_ROOM_REQUEST' | 'GROUP_CALL_WAITING_ROOM_JOINED' | 'GROUP_CALL_WAITING_ROOM_ADMIT' | 'GROUP_CALL_WAITING_ROOM_ADMITTED' | 'GROUP_CALL_WAITING_ROOM_REJECT' | 'GROUP_CALL_WAITING_ROOM_REJECTED' | 'GROUP_CALL_JOINED' | 'GROUP_CALL_WAITING_ROOM_PENDING' | 'GROUP_CALL_HOST_REQUIRED' | 'GROUP_CALL_ROOM_CLOSED';
138
+ export type GroupCallEventType = 'GROUP_CALL_INVITE' | 'GROUP_CALL_RINGING' | 'GROUP_CALL_JOIN' | 'GROUP_CALL_LEAVE' | 'GROUP_CALL_END' | 'GROUP_CALL_ENDED' | 'GROUP_CALL_PARTICIPANT_JOINED' | 'GROUP_CALL_PARTICIPANT_LEFT' | 'GROUP_CALL_PARTICIPANT_REMOVED' | 'GROUP_CALL_ALREADY_ACTIVE' | 'GROUP_CALL_SDP_OFFER' | 'GROUP_CALL_SDP_ANSWER' | 'GROUP_CALL_ICE_CANDIDATE' | 'GROUP_CALL_SCREEN_SHARE_STARTED' | 'GROUP_CALL_SCREEN_SHARE_STOPPED' | 'GROUP_CALL_HOST_TRANSFER' | 'GROUP_CALL_COHOST_APPOINT' | 'GROUP_CALL_COHOST_REVOKE' | 'GROUP_CALL_ROLE_CHANGED' | 'GROUP_CALL_MUTE_PARTICIPANT' | 'GROUP_CALL_REMOVE_PARTICIPANT' | 'GROUP_CALL_WAITING_ROOM_REQUEST' | 'GROUP_CALL_WAITING_ROOM_JOINED' | 'GROUP_CALL_WAITING_ROOM_ADMIT' | 'GROUP_CALL_WAITING_ROOM_ADMITTED' | 'GROUP_CALL_WAITING_ROOM_REJECT' | 'GROUP_CALL_WAITING_ROOM_REJECTED' | 'GROUP_CALL_JOINED' | 'GROUP_CALL_WAITING_ROOM_PENDING' | 'GROUP_CALL_HOST_REQUIRED' | 'GROUP_CALL_ROOM_CLOSED' | 'GROUP_CALL_STAGE_HAND_RAISED' | 'GROUP_CALL_STAGE_HAND_LOWERED' | 'GROUP_CALL_STAGE_PROMOTE' | 'GROUP_CALL_STAGE_DEMOTE' | 'GROUP_CALL_STAGE_QUESTION' | 'GROUP_CALL_RECORDING_STARTED' | 'GROUP_CALL_RECORDING_STOPPED' | 'GROUP_CALL_RECORDING_AVAILABLE';
139
139
  export interface GroupCallEvent {
140
140
  type: GroupCallEventType | string;
141
141
  callId: string;
@@ -159,6 +159,12 @@ export interface RoomPolicy {
159
159
  maxParticipants?: number;
160
160
  /** When true, the room flips to CLOSED once the last participant leaves the call. */
161
161
  autoCloseWhenEmpty?: boolean;
162
+ /**
163
+ * When true, the room's live call runs in stage mode (Feature 3.2): hosts
164
+ * join as speakers, everyone else as receive-only audience who can raise a
165
+ * hand to be promoted. Mesh-scale - suited to panels and small stages.
166
+ */
167
+ stageMode?: boolean;
162
168
  }
163
169
  export interface Room {
164
170
  roomId: string;
@@ -486,4 +492,20 @@ export interface DropOnAirClient {
486
492
  joinRoom(roomId: string): Promise<string>;
487
493
  /** Leave the live call in a room. */
488
494
  leaveRoom(callId: string): Promise<void>;
495
+ /** Raise your hand to request promotion to speaker (any participant). */
496
+ raiseHand(callId: string): void;
497
+ /** Lower your previously raised hand. */
498
+ lowerHand(callId: string): void;
499
+ /** Promote an audience member to speaker (host / co-host). */
500
+ promoteToSpeaker(callId: string, userId: string): void;
501
+ /** Demote a speaker back to audience (host / co-host). */
502
+ demoteToAudience(callId: string, userId: string): void;
503
+ /** Submit a text question; relayed to the stage's speakers. */
504
+ submitStageQuestion(callId: string, text: string): void;
505
+ /** Signal that you have started recording the call. */
506
+ startRecording(callId: string): void;
507
+ /** Signal that you have stopped recording. */
508
+ stopRecording(callId: string): void;
509
+ /** Announce an uploaded recording; `location` references it in your storage. */
510
+ markRecordingAvailable(callId: string, location: string): void;
489
511
  }
package/dist/version.d.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  * MINOR, additive feature (e.g. multi-device payloads, new call event type)
8
8
  * PATCH, bug-fix / perf improvement with no wire or API change
9
9
  */
10
- export declare const SDK_VERSION = "0.14.0";
10
+ export declare const SDK_VERSION = "0.16.0";
11
11
  /**
12
12
  * Binary encrypted-payload format version.
13
13
  * Included as the first byte of every encrypted payload so receivers can
package/dist/version.js CHANGED
@@ -10,7 +10,7 @@ exports.PROTOCOL_VERSION = exports.PAYLOAD_FORMAT_VERSION = exports.SDK_VERSION
10
10
  * MINOR, additive feature (e.g. multi-device payloads, new call event type)
11
11
  * PATCH, bug-fix / perf improvement with no wire or API change
12
12
  */
13
- exports.SDK_VERSION = '0.14.0';
13
+ exports.SDK_VERSION = '0.16.0';
14
14
  /**
15
15
  * Binary encrypted-payload format version.
16
16
  * Included as the first byte of every encrypted payload so receivers can
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@droponair/sdk-js",
3
- "version": "0.14.0",
3
+ "version": "0.16.0",
4
4
  "description": "DropOnAir SDK for end-to-end encrypted messaging",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",