@droponair/sdk-js 0.5.0 → 0.6.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,41 @@ This project follows [Semantic Versioning](https://semver.org/).
6
6
 
7
7
  ---
8
8
 
9
+ ## [0.6.0], 2026-05-18
10
+
11
+ ### Added
12
+ - **Screen sharing signaling.** New client APIs and event types so apps can
13
+ coordinate the start/stop of a screen-share track on top of the existing
14
+ WebRTC call. The SDK only signals - capture (`getDisplayMedia()` on web) and
15
+ track management remain in the app layer.
16
+ - **1:1 calls:**
17
+ - `client.startScreenShare(callId, payload?)` and `client.stopScreenShare(callId, payload?)`.
18
+ - New `CallEventType` values: `'CALL_SCREEN_SHARE_STARTED'`, `'CALL_SCREEN_SHARE_STOPPED'`.
19
+ - **Group calls:**
20
+ - `client.startGroupScreenShare(callId, groupId, payload?)` and `client.stopGroupScreenShare(callId, groupId, payload?)`.
21
+ - New `GroupCallEventType` values: `'GROUP_CALL_SCREEN_SHARE_STARTED'`, `'GROUP_CALL_SCREEN_SHARE_STOPPED'`.
22
+ - Server enforces a single concurrent sharer per group call. If another
23
+ participant already holds the slot when you call `startGroupScreenShare`,
24
+ you'll receive a `GROUP_CALL_SCREEN_SHARE_STOPPED` event with the current
25
+ holder's userId in `payload`, so the app can roll back its optimistic UI.
26
+ - When a sharer leaves the call, the server broadcasts a STOPPED event to
27
+ remaining participants before they see `PARTICIPANT_LEFT`.
28
+ - `GET /api/info` now advertises `"screen_sharing"` in the `features` array.
29
+
30
+ ### Notes
31
+ - Wire-level additive: legacy 0.5.x clients ignore the new signal type strings
32
+ (string discriminators on existing `CallFrame.type` / `GroupCallFrame.type`).
33
+ No proto schema change, no `PROTOCOL_VERSION` bump.
34
+ - Screen content is opaque WebRTC media; the server never sees it. Use
35
+ `getDisplayMedia()` (web) / `MediaProjection` (Android) / `ReplayKit` (iOS)
36
+ in the app, add the track to the existing peer connection, then call
37
+ `startScreenShare(...)` to notify the peer.
38
+ - Plan gate: `screen_sharing` is enabled on PRO/GROWTH/PAYG/ENTERPRISE and
39
+ disabled on FREE. Check `subscription.featuresEnabled.screen_sharing` (server
40
+ side, on the customer's backend).
41
+
42
+ ---
43
+
9
44
  ## [0.5.0], 2026-05-18
10
45
 
11
46
  ### Added
package/README.md CHANGED
@@ -209,6 +209,51 @@ client.onMessage(async (msg) => {
209
209
  | `sendGroupCallSignal(type, callId, groupId, targetUserId, payload)` | `void` | Send SDP/ICE to a peer |
210
210
  | `onGroupCallEvent(callback)` | `() => void` | Listen for group call events |
211
211
 
212
+ ### Screen Sharing
213
+
214
+ 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.
215
+
216
+ ```typescript
217
+ // 1:1 call
218
+ async function shareMyScreen(callId: string, peerConnection: RTCPeerConnection) {
219
+ const stream = await navigator.mediaDevices.getDisplayMedia({ video: true });
220
+ const track = stream.getVideoTracks()[0];
221
+ peerConnection.addTrack(track, stream);
222
+ client.startScreenShare(callId);
223
+ track.onended = () => client.stopScreenShare(callId);
224
+ }
225
+
226
+ // Group call
227
+ async function shareMyScreenInGroup(callId: string, groupId: string, peerConnections: RTCPeerConnection[]) {
228
+ const stream = await navigator.mediaDevices.getDisplayMedia({ video: true });
229
+ const track = stream.getVideoTracks()[0];
230
+ peerConnections.forEach(pc => pc.addTrack(track, stream));
231
+ client.startGroupScreenShare(callId, groupId);
232
+ track.onended = () => client.stopGroupScreenShare(callId, groupId);
233
+ }
234
+
235
+ client.onCallEvent((evt) => {
236
+ if (evt.type === 'CALL_SCREEN_SHARE_STARTED') showShareIndicator(evt.callId);
237
+ if (evt.type === 'CALL_SCREEN_SHARE_STOPPED') hideShareIndicator(evt.callId);
238
+ });
239
+
240
+ client.onGroupCallEvent((evt) => {
241
+ if (evt.type === 'GROUP_CALL_SCREEN_SHARE_STARTED') showShareIndicator(evt.callId, evt.payload); // payload = sharer userId
242
+ if (evt.type === 'GROUP_CALL_SCREEN_SHARE_STOPPED') hideShareIndicator(evt.callId);
243
+ });
244
+ ```
245
+
246
+ | Method | Returns | Description |
247
+ |--------|---------|-------------|
248
+ | `startScreenShare(callId, payload?)` | `void` | Signal start in a 1:1 call |
249
+ | `stopScreenShare(callId, payload?)` | `void` | Signal stop in a 1:1 call |
250
+ | `startGroupScreenShare(callId, groupId, payload?)` | `void` | Signal start in a group call |
251
+ | `stopGroupScreenShare(callId, groupId, payload?)` | `void` | Signal stop in a group call |
252
+
253
+ - The server enforces **one concurrent sharer per group call**. If another participant already holds the slot when you call `startGroupScreenShare`, the SDK delivers a `GROUP_CALL_SCREEN_SHARE_STOPPED` event with the current holder's userId in `payload`, so you can roll back the optimistic UI.
254
+ - When a sharer leaves the call, the server broadcasts a STOPPED event to remaining participants before they see `PARTICIPANT_LEFT`.
255
+ - Plan gate: `screen_sharing` is on `PRO`/`GROWTH`/`PAYG`/`ENTERPRISE`, off on `FREE`. Read `subscription.featuresEnabled.screen_sharing` on your backend to decide whether to expose the share button.
256
+
212
257
  ## Security
213
258
 
214
259
  - **X25519 ECDH** key agreement for shared secrets
@@ -136,6 +136,8 @@ export declare class MessagingClient implements DropOnAirClient {
136
136
  leaveGroupCall(callId: string): Promise<void>;
137
137
  endGroupCall(callId: string): Promise<void>;
138
138
  sendGroupCallSignal(type: 'GROUP_CALL_SDP_OFFER' | 'GROUP_CALL_SDP_ANSWER' | 'GROUP_CALL_ICE_CANDIDATE', callId: string, groupId: string, targetUserId: string, payload: string): void;
139
+ startGroupScreenShare(callId: string, groupId: string, payload?: string): void;
140
+ stopGroupScreenShare(callId: string, groupId: string, payload?: string): void;
139
141
  onGroupCallEvent(callback: GroupCallEventCallback): () => void;
140
142
  private sendGroupCallFrame;
141
143
  /**
@@ -149,6 +151,8 @@ export declare class MessagingClient implements DropOnAirClient {
149
151
  endCall(callId: string): Promise<void>;
150
152
  toggleVideo(callId: string, enabled: boolean): void;
151
153
  sendCallSignal(type: 'CALL_SDP_OFFER' | 'CALL_SDP_ANSWER' | 'CALL_ICE_CANDIDATE', callId: string, payload: string): void;
154
+ startScreenShare(callId: string, payload?: string): void;
155
+ stopScreenShare(callId: string, payload?: string): void;
152
156
  fetchTurnCredentials(): Promise<TurnCredentials>;
153
157
  private sendCallFrame;
154
158
  private emitCallEvent;
@@ -740,6 +740,24 @@ class MessagingClient {
740
740
  sendGroupCallSignal(type, callId, groupId, targetUserId, payload) {
741
741
  this.sendGroupCallFrame({ type, callId, groupId, targetUserId, payload });
742
742
  }
743
+ startGroupScreenShare(callId, groupId, payload) {
744
+ this.sendGroupCallFrame({
745
+ type: 'GROUP_CALL_SCREEN_SHARE_STARTED',
746
+ callId,
747
+ groupId,
748
+ targetUserId: '',
749
+ payload: payload ?? '',
750
+ });
751
+ }
752
+ stopGroupScreenShare(callId, groupId, payload) {
753
+ this.sendGroupCallFrame({
754
+ type: 'GROUP_CALL_SCREEN_SHARE_STOPPED',
755
+ callId,
756
+ groupId,
757
+ targetUserId: '',
758
+ payload: payload ?? '',
759
+ });
760
+ }
743
761
  onGroupCallEvent(callback) {
744
762
  this.groupCallListeners.add(callback);
745
763
  return () => this.groupCallListeners.delete(callback);
@@ -781,6 +799,12 @@ class MessagingClient {
781
799
  sendCallSignal(type, callId, payload) {
782
800
  this.sendCallFrame({ type, callId, payload });
783
801
  }
802
+ startScreenShare(callId, payload) {
803
+ this.sendCallFrame({ type: 'CALL_SCREEN_SHARE_STARTED', callId, payload: payload ?? '' });
804
+ }
805
+ stopScreenShare(callId, payload) {
806
+ this.sendCallFrame({ type: 'CALL_SCREEN_SHARE_STOPPED', callId, payload: payload ?? '' });
807
+ }
784
808
  async fetchTurnCredentials() {
785
809
  const jwt = await this.getValidDropOnAirJwt(false);
786
810
  const url = `${this.httpUrl}/api/v1/turn/credentials`;
@@ -57,7 +57,7 @@ export interface BroadcastMessage {
57
57
  sequenceNumber: number;
58
58
  }
59
59
  export type BroadcastCallback = (message: BroadcastMessage) => void;
60
- export type CallEventType = 'CALL_INVITE' | 'CALL_RINGING' | 'CALL_ACCEPTED' | 'CALL_REJECTED' | 'CALL_ENDED' | 'CALL_CANCELLED' | 'CALL_SDP_OFFER' | 'CALL_SDP_ANSWER' | 'CALL_ICE_CANDIDATE' | 'CALL_VIDEO_TOGGLE' | 'CALL_DENIED_LIMIT_REACHED';
60
+ export type CallEventType = 'CALL_INVITE' | 'CALL_RINGING' | 'CALL_ACCEPTED' | 'CALL_REJECTED' | 'CALL_ENDED' | 'CALL_CANCELLED' | 'CALL_SDP_OFFER' | 'CALL_SDP_ANSWER' | 'CALL_ICE_CANDIDATE' | 'CALL_VIDEO_TOGGLE' | 'CALL_SCREEN_SHARE_STARTED' | 'CALL_SCREEN_SHARE_STOPPED' | 'CALL_DENIED_LIMIT_REACHED';
61
61
  export interface CallEvent {
62
62
  type: CallEventType | string;
63
63
  callId?: string;
@@ -92,7 +92,7 @@ export interface DecryptedGroupMessage {
92
92
  plaintext: string;
93
93
  }
94
94
  export type GroupMessageCallback = (message: DecryptedGroupMessage) => void;
95
- 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_ALREADY_ACTIVE' | 'GROUP_CALL_SDP_OFFER' | 'GROUP_CALL_SDP_ANSWER' | 'GROUP_CALL_ICE_CANDIDATE';
95
+ 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_ALREADY_ACTIVE' | 'GROUP_CALL_SDP_OFFER' | 'GROUP_CALL_SDP_ANSWER' | 'GROUP_CALL_ICE_CANDIDATE' | 'GROUP_CALL_SCREEN_SHARE_STARTED' | 'GROUP_CALL_SCREEN_SHARE_STOPPED';
96
96
  export interface GroupCallEvent {
97
97
  type: GroupCallEventType | string;
98
98
  callId: string;
@@ -212,6 +212,16 @@ export interface DropOnAirClient {
212
212
  * The payload is forwarded opaquely, the server does NOT inspect or store it.
213
213
  */
214
214
  sendCallSignal(type: 'CALL_SDP_OFFER' | 'CALL_SDP_ANSWER' | 'CALL_ICE_CANDIDATE', callId: string, payload: string): void;
215
+ /**
216
+ * Signal that this user started sharing their screen in a 1:1 call
217
+ * (phase1/screen-sharing, PROTOCOL_VERSION 4+). The actual screen MediaStreamTrack
218
+ * is added to the existing peer connection by the application; the SDK only
219
+ * broadcasts the start/stop notification so the remote SDK can update its UI.
220
+ * Optional `payload` (JSON string) can carry track-id or app-specific metadata.
221
+ */
222
+ startScreenShare(callId: string, payload?: string): void;
223
+ /** Signal that this user stopped sharing their screen in a 1:1 call. */
224
+ stopScreenShare(callId: string, payload?: string): void;
215
225
  /** Register a listener for all incoming call events. Returns an unsubscribe function. */
216
226
  onCallEvent(callback: CallEventCallback): () => void;
217
227
  /** Fetch short-lived TURN credentials for ICE negotiation. */
@@ -244,6 +254,16 @@ export interface DropOnAirClient {
244
254
  endGroupCall(callId: string): Promise<void>;
245
255
  /** Send a signaling frame to a specific peer in a group call. */
246
256
  sendGroupCallSignal(type: 'GROUP_CALL_SDP_OFFER' | 'GROUP_CALL_SDP_ANSWER' | 'GROUP_CALL_ICE_CANDIDATE', callId: string, groupId: string, targetUserId: string, payload: string): void;
257
+ /**
258
+ * Signal that this user started sharing their screen in a group call.
259
+ * The server enforces one concurrent sharer per group call; if another
260
+ * participant already holds the slot, the SDK will receive a
261
+ * `GROUP_CALL_SCREEN_SHARE_STOPPED` event echoing the current holder's
262
+ * userId in `payload` so the app can roll back its optimistic UI.
263
+ */
264
+ startGroupScreenShare(callId: string, groupId: string, payload?: string): void;
265
+ /** Signal that this user stopped sharing their screen in a group call. */
266
+ stopGroupScreenShare(callId: string, groupId: string, payload?: string): void;
247
267
  /** Register a listener for group call events. Returns an unsubscribe function. */
248
268
  onGroupCallEvent(callback: GroupCallEventCallback): () => void;
249
269
  }
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.5.0";
10
+ export declare const SDK_VERSION = "0.6.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.5.0';
13
+ exports.SDK_VERSION = '0.6.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.5.0",
3
+ "version": "0.6.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",