@droponair/sdk-js 0.16.0 → 0.18.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,37 @@ This project follows [Semantic Versioning](https://semver.org/).
6
6
 
7
7
  ---
8
8
 
9
+ ## [0.18.0], 2026-05-23
10
+
11
+ ### Added
12
+
13
+ - **SFU recording (Phase 3.7.5).** Server-side recording for SFU-mode rooms with `mediaEncryption: 'SFU'`. The platform's media server records the composite of every track in the room and uploads the finalized file directly to the customer's storage bucket - the platform never holds the recorded bytes. Storage destinations (S3 / GCS / Azure) are registered once in the panel; the SDK references each by an opaque `destinationId`.
14
+ - Three new client methods:
15
+ - `startSfuRecording(roomId, destinationId)` -> `SfuRecording`
16
+ - `stopSfuRecording(roomId, recordingId)` -> `SfuRecording`
17
+ - `listSfuRecordings(roomId)` -> `SfuRecording[]`
18
+ - New `SfuRecording` type exported from the package root.
19
+ - Completion is also observable via the new runtime webhooks: `sfu-recording.started`, `sfu-recording.completed`, `sfu-recording.failed`.
20
+
21
+ ### Notes
22
+
23
+ - Only rooms with both `policy.mediaMode: 'SFU'` and `policy.mediaEncryption: 'SFU'` are server-recordable; E2EE-encrypted SFU rooms cannot be recorded by design (the media server forwards traffic it cannot decrypt). For E2EE rooms, continue using the client-side `startRecording` (0.16.0).
24
+
25
+ ---
26
+
27
+ ## [0.17.0], 2026-05-22
28
+
29
+ ### Added
30
+
31
+ - **SFU media transport (opt-in per room).** Two new `RoomPolicy` fields choose how a room's live call moves media: `mediaMode: 'MESH' | 'SFU'` (default `'MESH'`) and `mediaEncryption: 'E2EE' | 'SFU'` (default `'E2EE'`). Mesh stays peer-to-peer; SFU routes through the platform's media server, suited to larger calls. With `E2EE` the media server forwards traffic it cannot decrypt, so the platform stays a blind relay even in routed mode.
32
+ - **`getSfuToken(roomId)`** - fetches a short-lived access token to join a room's SFU media. Returns `{ url, token, room, mediaEncryption, expiresAt }`. Hand the URL + token to your SFU client (e.g. LiveKit) to connect; mesh-mode rooms don't need this call. New `SfuToken` type exported from the package root.
33
+
34
+ ### Notes
35
+
36
+ - The SFU is one transport option you compose per room - mesh remains the default and is fine for small calls. SFU mode is required only when the participant count outgrows mesh or when you need server-side recording (`mediaEncryption: 'SFU'`).
37
+
38
+ ---
39
+
9
40
  ## [0.16.0], 2026-05-21
10
41
 
11
42
  ### Added
package/README.md CHANGED
@@ -312,8 +312,12 @@ Available since SDK `0.14.0`. A **room** is an addressable container a live mult
312
312
  | `deleteRoom(roomId)` | `Promise<void>` | Delete a room (creator only) |
313
313
  | `joinRoom(roomId)` | `Promise<string>` | Join the room's live call, returns callId |
314
314
  | `leaveRoom(callId)` | `Promise<void>` | Leave the room's live call |
315
+ | `getSfuToken(roomId)` | `Promise<SfuToken>` | Token to join the room's SFU media (SFU-mode rooms only) |
316
+ | `startSfuRecording(roomId, destinationId)` | `Promise<SfuRecording>` | Start server-side recording (SFU + `mediaEncryption: 'SFU'` only) |
317
+ | `stopSfuRecording(roomId, recordingId)` | `Promise<SfuRecording>` | Stop a running SFU recording |
318
+ | `listSfuRecordings(roomId)` | `Promise<SfuRecording[]>` | List SFU recordings for a room, newest first |
315
319
 
316
- Room policy (`RoomPolicy`, all optional): `waitingRoom` (non-hosts wait for host admit), `requireHost` (non-hosts cannot open the call), `maxParticipants` (per-room cap), `autoCloseWhenEmpty` (room flips to `CLOSED` when the last participant leaves).
320
+ Room policy (`RoomPolicy`, all optional): `waitingRoom` (non-hosts wait for host admit), `requireHost` (non-hosts cannot open the call), `maxParticipants` (per-room cap), `autoCloseWhenEmpty` (room flips to `CLOSED` when the last participant leaves), `mediaMode` (`'MESH'` default | `'SFU'`), `mediaEncryption` (`'E2EE'` default | `'SFU'`).
317
321
 
318
322
  ```typescript
319
323
  const room = await client.createRoom({
@@ -362,6 +366,37 @@ client.onGroupCallEvent((e) => {
362
366
 
363
367
  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
368
 
369
+ ### Routed media (SFU)
370
+
371
+ Available since SDK `0.17.0`. A room can opt out of peer-to-peer mesh and route its live-call media through the platform's media server. Set `policy.mediaMode` to `'SFU'` at create time, then ask the SDK for a join token; hand the token to your SFU client (e.g. LiveKit) to connect. `policy.mediaEncryption` controls whether the media server can decrypt the media: `'E2EE'` (default) keeps the platform blind, `'SFU'` lets the server terminate media so it can be recorded server-side.
372
+
373
+ ```typescript
374
+ const room = await client.createRoom({
375
+ name: 'All hands',
376
+ policy: { mediaMode: 'SFU', mediaEncryption: 'E2EE' },
377
+ });
378
+
379
+ const sfu = await client.getSfuToken(room.roomId);
380
+ // sfu = { url, token, room, mediaEncryption, expiresAt }
381
+ // connect with your SFU client using sfu.url + sfu.token
382
+ ```
383
+
384
+ Mesh remains the default; only switch to SFU once the participant count outgrows mesh or you specifically need server-side recording. The `getSfuToken` call returns `409` for mesh-mode rooms and `503` if the media server is not available.
385
+
386
+ ### SFU recording
387
+
388
+ Available since SDK `0.18.0`. Server-side recording for SFU-mode rooms with `mediaEncryption: 'SFU'`. The platform's media server captures the room composite and uploads the finalized file directly to your storage bucket (S3, GCS, or Azure); the platform never holds the recorded bytes. Storage destinations are registered in the panel; the SDK references each by an opaque `destinationId`.
389
+
390
+ ```typescript
391
+ const rec = await client.startSfuRecording(room.roomId, 'dst_aBc123');
392
+ // { recordingId, status: 'STARTING', startedAt, ... }
393
+
394
+ // Later, when the host ends the recording:
395
+ await client.stopSfuRecording(room.roomId, rec.recordingId);
396
+ ```
397
+
398
+ Completion lands as the `sfu-recording.completed` webhook (or by polling `listSfuRecordings`) and carries `locationUri` pointing at the finalized file in your bucket. E2EE-encrypted SFU rooms cannot be server-recorded by design (the media server forwards traffic it cannot decrypt) - use the client-side `startRecording` for those.
399
+
365
400
  ### Call Recording
366
401
 
367
402
  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.
@@ -1,6 +1,6 @@
1
1
  import { CryptoService } from '../crypto/crypto-service';
2
2
  import { SessionManager } from './session-manager';
3
- import { BroadcastCallback, CallEventCallback, DropOnAirClient, EventCallback, CreateRoomOptions, GroupCallEventCallback, GroupInfo, GroupMessageCallback, Room, UpdateRoomOptions, InitializeOptions, MessageCallback, MessageDeleteCallback, MessageEditCallback, TurnCredentials, DeviceInfo, ReadReceiptCallback, NotificationClearCallback, DraftSyncCallback } from './types';
3
+ import { BroadcastCallback, CallEventCallback, DropOnAirClient, EventCallback, CreateRoomOptions, GroupCallEventCallback, GroupInfo, GroupMessageCallback, Room, UpdateRoomOptions, SfuToken, SfuRecording, InitializeOptions, MessageCallback, MessageDeleteCallback, MessageEditCallback, TurnCredentials, DeviceInfo, ReadReceiptCallback, NotificationClearCallback, DraftSyncCallback } from './types';
4
4
  import { AttachmentRef, CreateUploadSessionOptions, DownloadedAttachment, PrepareAttachmentOptions, UploadSession } from '../attachment/attachment-types';
5
5
  export declare class MessagingClient implements DropOnAirClient {
6
6
  private readonly options;
@@ -233,6 +233,10 @@ export declare class MessagingClient implements DropOnAirClient {
233
233
  getRoom(roomId: string): Promise<Room>;
234
234
  updateRoom(roomId: string, update: UpdateRoomOptions): Promise<Room>;
235
235
  deleteRoom(roomId: string): Promise<void>;
236
+ getSfuToken(roomId: string): Promise<SfuToken>;
237
+ startSfuRecording(roomId: string, destinationId: string): Promise<SfuRecording>;
238
+ stopSfuRecording(roomId: string, recordingId: string): Promise<SfuRecording>;
239
+ listSfuRecordings(roomId: string): Promise<SfuRecording[]>;
236
240
  /**
237
241
  * Join the live call in a room. Resolves with the callId once joined; that
238
242
  * callId works with the existing group-call signaling methods (pass an empty
@@ -1018,6 +1018,38 @@ class MessagingClient {
1018
1018
  if (!res.ok)
1019
1019
  throw new Error(`deleteRoom failed (HTTP ${res.status})`);
1020
1020
  }
1021
+ async getSfuToken(roomId) {
1022
+ const jwt = await this.getValidDropOnAirJwt(false);
1023
+ const res = await this.fetchFn(`${this.httpUrl}/api/rooms/${encodeURIComponent(roomId)}/sfu-token`, { method: 'POST', headers: { Authorization: `Bearer ${jwt}` } });
1024
+ if (!res.ok)
1025
+ throw new Error(`getSfuToken failed (HTTP ${res.status})`);
1026
+ return res.json();
1027
+ }
1028
+ async startSfuRecording(roomId, destinationId) {
1029
+ const jwt = await this.getValidDropOnAirJwt(false);
1030
+ const res = await this.fetchFn(`${this.httpUrl}/api/rooms/${encodeURIComponent(roomId)}/sfu-recordings`, {
1031
+ method: 'POST',
1032
+ headers: { Authorization: `Bearer ${jwt}`, 'Content-Type': 'application/json' },
1033
+ body: JSON.stringify({ destinationId }),
1034
+ });
1035
+ if (!res.ok)
1036
+ throw new Error(`startSfuRecording failed (HTTP ${res.status})`);
1037
+ return res.json();
1038
+ }
1039
+ async stopSfuRecording(roomId, recordingId) {
1040
+ const jwt = await this.getValidDropOnAirJwt(false);
1041
+ const res = await this.fetchFn(`${this.httpUrl}/api/rooms/${encodeURIComponent(roomId)}/sfu-recordings/${encodeURIComponent(recordingId)}`, { method: 'DELETE', headers: { Authorization: `Bearer ${jwt}` } });
1042
+ if (!res.ok)
1043
+ throw new Error(`stopSfuRecording failed (HTTP ${res.status})`);
1044
+ return res.json();
1045
+ }
1046
+ async listSfuRecordings(roomId) {
1047
+ const jwt = await this.getValidDropOnAirJwt(false);
1048
+ const res = await this.fetchFn(`${this.httpUrl}/api/rooms/${encodeURIComponent(roomId)}/sfu-recordings`, { method: 'GET', headers: { Authorization: `Bearer ${jwt}` } });
1049
+ if (!res.ok)
1050
+ throw new Error(`listSfuRecordings failed (HTTP ${res.status})`);
1051
+ return res.json();
1052
+ }
1021
1053
  /**
1022
1054
  * Join the live call in a room. Resolves with the callId once joined; that
1023
1055
  * callId works with the existing group-call signaling methods (pass an empty
@@ -165,6 +165,54 @@ export interface RoomPolicy {
165
165
  * hand to be promoted. Mesh-scale - suited to panels and small stages.
166
166
  */
167
167
  stageMode?: boolean;
168
+ /**
169
+ * Media transport for the room's live call. 'MESH' (default) is peer-to-peer;
170
+ * 'SFU' routes media through the platform's media server, suited to larger calls.
171
+ */
172
+ mediaMode?: 'MESH' | 'SFU';
173
+ /**
174
+ * Media encryption for an SFU-mode call. 'E2EE' (default) keeps media encrypted
175
+ * past the media server; 'SFU' lets the server terminate media (enables
176
+ * server-side recording). Ignored when mediaMode is 'MESH'.
177
+ */
178
+ mediaEncryption?: 'E2EE' | 'SFU';
179
+ }
180
+ /** Access credentials to join a room's SFU (routed) media. Returned by getSfuToken. */
181
+ export interface SfuToken {
182
+ /** WebSocket URL of the media server to connect your SFU client to. */
183
+ url: string;
184
+ /** Signed, short-lived access token for the media room. */
185
+ token: string;
186
+ /** Media room name to join. */
187
+ room: string;
188
+ /** Media encryption in effect for the room: 'E2EE' or 'SFU'. */
189
+ mediaEncryption: 'E2EE' | 'SFU';
190
+ /** Token expiry, Unix epoch millis. */
191
+ expiresAt: number;
192
+ }
193
+ /**
194
+ * A server-side recording of an SFU-mode room (Phase 3.7.5). The platform
195
+ * never holds the recorded bytes - LiveKit Egress uploads the finalized file
196
+ * directly to the destination configured in the panel under `destinationId`.
197
+ */
198
+ export interface SfuRecording {
199
+ /** Opaque ID; pass to stopSfuRecording. */
200
+ recordingId: string;
201
+ roomId: string;
202
+ /** ID of the panel-registered RecordingDestination this recording targets. */
203
+ destinationId: string;
204
+ /** Lifecycle state. */
205
+ status: 'STARTING' | 'ACTIVE' | 'STOPPING' | 'COMPLETED' | 'FAILED';
206
+ /** Unix epoch millis when the recording was kicked off. */
207
+ startedAt: number | null;
208
+ /** Populated once Egress finalizes the file (COMPLETED or FAILED). */
209
+ finishedAt: number | null;
210
+ durationSeconds: number | null;
211
+ fileSizeBytes: number | null;
212
+ /** Customer-side URI of the finalized file (e.g. s3://bucket/path.mp4). */
213
+ locationUri: string | null;
214
+ /** Populated when status is FAILED. */
215
+ errorMessage: string | null;
168
216
  }
169
217
  export interface Room {
170
218
  roomId: string;
@@ -492,6 +540,25 @@ export interface DropOnAirClient {
492
540
  joinRoom(roomId: string): Promise<string>;
493
541
  /** Leave the live call in a room. */
494
542
  leaveRoom(callId: string): Promise<void>;
543
+ /**
544
+ * Fetch an access token to join a room's SFU (routed) media. Only valid for
545
+ * rooms whose policy sets mediaMode 'SFU'; mesh-mode rooms run peer-to-peer
546
+ * and need no token. Hand the returned url + token to your SFU client.
547
+ */
548
+ getSfuToken(roomId: string): Promise<SfuToken>;
549
+ /**
550
+ * Start a server-side recording of an SFU-mode room. The destination is a
551
+ * RecordingDestination pre-registered in the panel; the platform never
552
+ * holds the recorded bytes, LiveKit Egress uploads directly. Only valid
553
+ * when the room's policy sets mediaEncryption 'SFU' (E2EE rooms cannot
554
+ * be server-recorded). Completion is observable via the
555
+ * sfu-recording.completed webhook or by polling listSfuRecordings.
556
+ */
557
+ startSfuRecording(roomId: string, destinationId: string): Promise<SfuRecording>;
558
+ /** Stop a running SFU recording. Idempotent on terminal status. */
559
+ stopSfuRecording(roomId: string, recordingId: string): Promise<SfuRecording>;
560
+ /** List SFU recordings for a room, most-recent first. */
561
+ listSfuRecordings(roomId: string): Promise<SfuRecording[]>;
495
562
  /** Raise your hand to request promotion to speaker (any participant). */
496
563
  raiseHand(callId: string): void;
497
564
  /** Lower your previously raised hand. */
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { InitializeOptions, DropOnAirClient } from './core/types';
2
2
  export { SDK_VERSION, PROTOCOL_VERSION, PAYLOAD_FORMAT_VERSION } from './version';
3
3
  export declare function initialize(options: InitializeOptions): Promise<DropOnAirClient>;
4
- export type { InitializeOptions, DropOnAirClient, DropOnAirEvent, MessageCallback, EventCallback, DecryptedMessage, KeyStorageAdapter, CallEvent, CallEventType, CallEventCallback, TurnCredentials, BroadcastMessage, BroadcastCallback, GroupInfo, GroupMemberInfo, DecryptedGroupMessage, GroupMessageCallback, GroupCallEvent, GroupCallEventType, GroupCallEventCallback, Room, RoomPolicy, CreateRoomOptions, UpdateRoomOptions, MessageEditEvent, MessageEditCallback, MessageDeleteEvent, MessageDeleteCallback, DeviceInfo, ReadReceiptEvent, ReadReceiptCallback, NotificationClearEvent, NotificationClearCallback, DraftSyncEvent, DraftSyncCallback, } from './core/types';
4
+ export type { InitializeOptions, DropOnAirClient, DropOnAirEvent, MessageCallback, EventCallback, DecryptedMessage, KeyStorageAdapter, CallEvent, CallEventType, CallEventCallback, TurnCredentials, BroadcastMessage, BroadcastCallback, GroupInfo, GroupMemberInfo, DecryptedGroupMessage, GroupMessageCallback, GroupCallEvent, GroupCallEventType, GroupCallEventCallback, Room, RoomPolicy, CreateRoomOptions, UpdateRoomOptions, SfuToken, SfuRecording, MessageEditEvent, MessageEditCallback, MessageDeleteEvent, MessageDeleteCallback, DeviceInfo, ReadReceiptEvent, ReadReceiptCallback, NotificationClearEvent, NotificationClearCallback, DraftSyncEvent, DraftSyncCallback, } from './core/types';
5
5
  export type { AttachmentEncryptionType, AttachmentConversationType, AttachmentRef, DeviceWrappedKey, CreateUploadSessionOptions, UploadSession, PrepareAttachmentOptions, DownloadedAttachment, } from './attachment/attachment-types';
6
6
  declare const _default: {
7
7
  initialize: typeof initialize;
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.16.0";
10
+ export declare const SDK_VERSION = "0.18.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.16.0';
13
+ exports.SDK_VERSION = '0.18.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.16.0",
3
+ "version": "0.18.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",