@pmate/chat-sdk 0.5.1 → 0.6.1

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.
Files changed (41) hide show
  1. package/lib/index.d.ts +1 -0
  2. package/lib/index.js +1 -0
  3. package/lib/runtime/call.d.ts +72 -0
  4. package/lib/runtime/call.js +309 -0
  5. package/lib/runtime/index.d.ts +1 -0
  6. package/lib/runtime/index.js +1 -0
  7. package/package.json +2 -3
  8. package/dist/assets/ChatInput.stories-BQcIQDVE.js +0 -1852
  9. package/dist/assets/Color-AVL7NMMY-Q9rY0eHd.js +0 -1
  10. package/dist/assets/DocsRenderer-PQXLIZUC-B5DlnesB.js +0 -1243
  11. package/dist/assets/client-DTiYOHAb.js +0 -9
  12. package/dist/assets/iframe-1QrxCBqr.js +0 -1071
  13. package/dist/assets/iframe-4FbYaDjf.css +0 -1
  14. package/dist/assets/index-CkqBtKlT.js +0 -1
  15. package/dist/assets/preload-helper-PPVm8Dsz.js +0 -1
  16. package/dist/assets/react-18-6BKf31wF.js +0 -1
  17. package/dist/favicon-wrapper.svg +0 -46
  18. package/dist/favicon.svg +0 -1
  19. package/dist/iframe.html +0 -687
  20. package/dist/index.html +0 -148
  21. package/dist/index.json +0 -1
  22. package/dist/nunito-sans-bold-italic.woff2 +0 -0
  23. package/dist/nunito-sans-bold.woff2 +0 -0
  24. package/dist/nunito-sans-italic.woff2 +0 -0
  25. package/dist/nunito-sans-regular.woff2 +0 -0
  26. package/dist/project.json +0 -1
  27. package/dist/sb-addons/docs-1/manager-bundle.js +0 -151
  28. package/dist/sb-addons/docs-1/manager-bundle.js.LEGAL.txt +0 -0
  29. package/dist/sb-addons/storybook-core-server-presets-0/common-manager-bundle.js +0 -971
  30. package/dist/sb-addons/storybook-core-server-presets-0/common-manager-bundle.js.LEGAL.txt +0 -0
  31. package/dist/sb-common-assets/favicon-wrapper.svg +0 -46
  32. package/dist/sb-common-assets/favicon.svg +0 -1
  33. package/dist/sb-common-assets/nunito-sans-bold-italic.woff2 +0 -0
  34. package/dist/sb-common-assets/nunito-sans-bold.woff2 +0 -0
  35. package/dist/sb-common-assets/nunito-sans-italic.woff2 +0 -0
  36. package/dist/sb-common-assets/nunito-sans-regular.woff2 +0 -0
  37. package/dist/sb-manager/globals-module-info.js +0 -799
  38. package/dist/sb-manager/globals-runtime.js +0 -69791
  39. package/dist/sb-manager/globals.js +0 -34
  40. package/dist/sb-manager/runtime.js +0 -13198
  41. package/dist/vite-inject-mocker-entry.js +0 -2
package/lib/index.d.ts CHANGED
@@ -32,3 +32,4 @@ export * from "./mentions/model";
32
32
  export * from "./hooks/useMentions";
33
33
  export * from "./components/MentionMenu";
34
34
  export * from "./runtime/activity";
35
+ export * from "./runtime/call";
package/lib/index.js CHANGED
@@ -32,3 +32,4 @@ export * from "./mentions/model";
32
32
  export * from "./hooks/useMentions";
33
33
  export * from "./components/MentionMenu";
34
34
  export * from "./runtime/activity";
35
+ export * from "./runtime/call";
@@ -0,0 +1,72 @@
1
+ export type CallPhase = "idle" | "requesting" | "ringing" | "incoming" | "connecting" | "connected" | "ended";
2
+ export type CallSnapshot = {
3
+ phase: CallPhase;
4
+ ready: boolean;
5
+ callId?: string;
6
+ peerId?: string;
7
+ direction?: "incoming" | "outgoing";
8
+ startedAt?: number;
9
+ muted: boolean;
10
+ sharing: boolean;
11
+ remoteSharing: boolean;
12
+ remoteStream?: MediaStream;
13
+ localScreen?: MediaStream;
14
+ error?: string;
15
+ reason?: string;
16
+ };
17
+ export type CallEnded = {
18
+ peerId: string;
19
+ callId: string;
20
+ direction: "incoming" | "outgoing";
21
+ reason: string;
22
+ durationMs: number;
23
+ };
24
+ export type CallClientOptions = {
25
+ profileId: string;
26
+ token: string;
27
+ url?: string;
28
+ iceTransportPolicy?: RTCIceTransportPolicy;
29
+ onEnded?: (event: CallEnded) => void;
30
+ mediaDevices?: Pick<MediaDevices, "getUserMedia" | "getDisplayMedia">;
31
+ createPeerConnection?: (config: RTCConfiguration) => RTCPeerConnection;
32
+ createWebSocket?: (url: string) => WebSocket;
33
+ };
34
+ /** Explicit user actions own microphone/display capture. No media is acquired on incoming calls. */
35
+ export declare class CallClient {
36
+ private options;
37
+ private state;
38
+ private listeners;
39
+ private ws?;
40
+ private pc?;
41
+ private microphone?;
42
+ private screen?;
43
+ private video?;
44
+ private candidates;
45
+ private generation;
46
+ private closed;
47
+ private capturePending;
48
+ private requestId?;
49
+ private heartbeat?;
50
+ private timeout?;
51
+ private reconnect?;
52
+ private queue;
53
+ constructor(options: CallClientOptions);
54
+ snapshot(): CallSnapshot;
55
+ subscribe(listener: (state: CallSnapshot) => void): () => void;
56
+ connect(): void;
57
+ start(peerId: string): Promise<void>;
58
+ accept(): Promise<void>;
59
+ hangup(): void;
60
+ dismiss(): void;
61
+ mute(): void;
62
+ shareScreen(): Promise<void>;
63
+ stopSharing(): Promise<void>;
64
+ close(): void;
65
+ private update;
66
+ private send;
67
+ private acquireMicrophone;
68
+ private armTimeout;
69
+ private fail;
70
+ private release;
71
+ private receive;
72
+ }
@@ -0,0 +1,309 @@
1
+ /** Explicit user actions own microphone/display capture. No media is acquired on incoming calls. */
2
+ export class CallClient {
3
+ options;
4
+ state = { phase: "idle", ready: false, muted: false, sharing: false, remoteSharing: false };
5
+ listeners = new Set();
6
+ ws;
7
+ pc;
8
+ microphone;
9
+ screen;
10
+ video;
11
+ candidates = [];
12
+ generation = 0;
13
+ closed = false;
14
+ capturePending = false;
15
+ requestId;
16
+ heartbeat;
17
+ timeout;
18
+ reconnect;
19
+ queue = Promise.resolve();
20
+ constructor(options) {
21
+ this.options = options;
22
+ }
23
+ snapshot() { return this.state; }
24
+ subscribe(listener) { this.listeners.add(listener); listener(this.state); return () => { this.listeners.delete(listener); }; }
25
+ connect() {
26
+ if (this.closed || this.ws)
27
+ return;
28
+ const ws = (this.options.createWebSocket ?? (url => new WebSocket(url)))(this.options.url ?? "wss://calls.pmate.chat/call");
29
+ this.ws = ws;
30
+ ws.onopen = () => { if (this.ws === ws)
31
+ this.send({ type: "auth", token: this.options.token, profileId: this.options.profileId }); };
32
+ ws.onmessage = event => {
33
+ if (this.ws !== ws)
34
+ return;
35
+ this.queue = this.queue.then(async () => {
36
+ if (this.ws !== ws)
37
+ return;
38
+ const gen = this.generation;
39
+ try {
40
+ await this.receive(JSON.parse(String(event.data)));
41
+ }
42
+ catch {
43
+ if (gen === this.generation)
44
+ this.fail("CONNECTION_FAILED");
45
+ }
46
+ });
47
+ };
48
+ ws.onerror = () => { };
49
+ ws.onclose = event => {
50
+ if (this.ws !== ws)
51
+ return;
52
+ this.ws = undefined;
53
+ clearInterval(this.heartbeat);
54
+ const active = !["idle", "ended"].includes(this.state.phase);
55
+ this.release();
56
+ this.update({ ready: false, phase: active ? "ended" : this.state.phase, error: event.code === 1008 ? "UNAUTHORIZED" : "DISCONNECTED" });
57
+ if (!this.closed && event.code !== 1008)
58
+ this.reconnect = setTimeout(() => this.connect(), 3000);
59
+ };
60
+ }
61
+ async start(peerId) {
62
+ if (!this.state.ready || !["idle", "ended"].includes(this.state.phase))
63
+ return;
64
+ this.release();
65
+ this.update({ phase: "requesting", peerId, direction: "outgoing", error: undefined, reason: undefined, callId: undefined, startedAt: undefined });
66
+ const gen = this.generation;
67
+ this.requestId = String(gen);
68
+ try {
69
+ if (!await this.acquireMicrophone(gen))
70
+ return;
71
+ this.send({ type: "invite", peerId, requestId: this.requestId });
72
+ this.armTimeout(40000);
73
+ }
74
+ catch {
75
+ if (gen === this.generation)
76
+ this.fail("MICROPHONE_DENIED");
77
+ }
78
+ }
79
+ async accept() {
80
+ if (this.state.phase !== "incoming" || this.capturePending)
81
+ return;
82
+ const gen = this.generation;
83
+ this.capturePending = true;
84
+ this.update({ phase: "connecting", error: undefined });
85
+ try {
86
+ if (!await this.acquireMicrophone(gen))
87
+ return;
88
+ this.send({ type: "accept", callId: this.state.callId });
89
+ this.armTimeout(30000);
90
+ }
91
+ catch {
92
+ if (gen === this.generation)
93
+ this.fail("MICROPHONE_DENIED");
94
+ }
95
+ finally {
96
+ if (gen === this.generation)
97
+ this.capturePending = false;
98
+ }
99
+ }
100
+ hangup() {
101
+ if (this.state.callId && !["idle", "ended"].includes(this.state.phase))
102
+ this.send({ type: "hangup", callId: this.state.callId });
103
+ this.release();
104
+ this.update({ phase: "ended", reason: "cancelled" });
105
+ }
106
+ dismiss() { if (this.state.phase === "ended")
107
+ this.update({ phase: "idle", error: undefined, reason: undefined, callId: undefined, peerId: undefined }); }
108
+ mute() {
109
+ const muted = !this.state.muted;
110
+ this.microphone?.getAudioTracks().forEach(track => { track.enabled = !muted; });
111
+ this.update({ muted });
112
+ }
113
+ async shareScreen() {
114
+ if (this.state.phase !== "connected" || !this.video || this.capturePending)
115
+ return;
116
+ if (this.screen)
117
+ return this.stopSharing();
118
+ const gen = this.generation;
119
+ this.capturePending = true;
120
+ try {
121
+ const media = this.options.mediaDevices ?? navigator.mediaDevices;
122
+ if (!media.getDisplayMedia)
123
+ throw new Error("unsupported");
124
+ const stream = await media.getDisplayMedia({ video: { frameRate: { ideal: 15, max: 30 }, width: { ideal: 1920 }, height: { ideal: 1080 } }, audio: false });
125
+ if (gen !== this.generation) {
126
+ stream.getTracks().forEach(t => t.stop());
127
+ return;
128
+ }
129
+ this.screen = stream;
130
+ const track = stream.getVideoTracks()[0];
131
+ if (!track)
132
+ throw new Error("No display track");
133
+ track.contentHint = "detail";
134
+ await this.video.sender.replaceTrack(track);
135
+ if (gen !== this.generation)
136
+ return;
137
+ track.onended = () => { if (gen === this.generation)
138
+ void this.stopSharing(); };
139
+ this.send({ type: "sharing", callId: this.state.callId, active: true });
140
+ this.update({ sharing: true, localScreen: stream, error: undefined });
141
+ }
142
+ catch (error) {
143
+ if (gen === this.generation) {
144
+ await this.stopSharing();
145
+ if (!(error instanceof DOMException && error.name === "NotAllowedError"))
146
+ this.update({ error: "SCREEN_UNAVAILABLE" });
147
+ }
148
+ }
149
+ finally {
150
+ if (gen === this.generation)
151
+ this.capturePending = false;
152
+ }
153
+ }
154
+ async stopSharing() {
155
+ const gen = this.generation;
156
+ const screen = this.screen;
157
+ this.screen = undefined;
158
+ screen?.getTracks().forEach(t => { t.onended = null; t.stop(); });
159
+ try {
160
+ await this.video?.sender.replaceTrack(null);
161
+ }
162
+ catch { /* peer may have closed */ }
163
+ if (gen !== this.generation)
164
+ return;
165
+ this.send({ type: "sharing", callId: this.state.callId, active: false });
166
+ this.update({ sharing: false, localScreen: undefined });
167
+ }
168
+ close() {
169
+ this.closed = true;
170
+ clearTimeout(this.reconnect);
171
+ clearInterval(this.heartbeat);
172
+ this.hangup();
173
+ this.ws?.close();
174
+ this.ws = undefined;
175
+ this.update({ ready: false });
176
+ this.listeners.clear();
177
+ }
178
+ update(patch) { this.state = { ...this.state, ...patch }; this.listeners.forEach(fn => fn(this.state)); }
179
+ send(message) { if (this.ws?.readyState === 1)
180
+ this.ws.send(JSON.stringify(message)); }
181
+ async acquireMicrophone(gen) {
182
+ const stream = await (this.options.mediaDevices ?? navigator.mediaDevices).getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true }, video: false });
183
+ if (gen !== this.generation) {
184
+ stream.getTracks().forEach(t => t.stop());
185
+ return false;
186
+ }
187
+ this.microphone = stream;
188
+ stream.getAudioTracks().forEach(track => { track.onended = () => { if (gen === this.generation)
189
+ this.fail("MICROPHONE_DENIED"); }; });
190
+ return true;
191
+ }
192
+ armTimeout(ms) { clearTimeout(this.timeout); this.timeout = setTimeout(() => this.fail("CONNECTION_TIMEOUT"), ms); }
193
+ fail(error) { this.hangup(); this.update({ error }); }
194
+ release() {
195
+ this.generation++;
196
+ clearTimeout(this.timeout);
197
+ this.capturePending = false;
198
+ this.pc?.close();
199
+ this.pc = undefined;
200
+ this.video = undefined;
201
+ this.candidates = [];
202
+ for (const stream of [this.microphone, this.screen])
203
+ stream?.getTracks().forEach(t => { t.onended = null; t.stop(); });
204
+ this.microphone = undefined;
205
+ this.screen = undefined;
206
+ this.update({ muted: false, sharing: false, remoteSharing: false, remoteStream: undefined, localScreen: undefined });
207
+ }
208
+ async receive(message) {
209
+ if (message.type === "ready") {
210
+ this.update({ ready: true, error: undefined });
211
+ clearInterval(this.heartbeat);
212
+ this.heartbeat = setInterval(() => this.send({ type: "ping" }), 20000);
213
+ return;
214
+ }
215
+ if (message.type === "pong")
216
+ return;
217
+ if (message.type === "error") {
218
+ if (message.requestId && message.requestId !== this.requestId)
219
+ return;
220
+ if (!message.callId || message.callId === this.state.callId)
221
+ this.fail(message.code);
222
+ return;
223
+ }
224
+ if (message.type === "ringing") {
225
+ if (message.direction === "outgoing" && (this.state.phase !== "requesting" || message.requestId !== this.requestId)) {
226
+ this.send({ type: "hangup", callId: message.callId });
227
+ return;
228
+ }
229
+ if (message.direction === "incoming" && !["idle", "ended"].includes(this.state.phase)) {
230
+ this.send({ type: "hangup", callId: message.callId });
231
+ return;
232
+ }
233
+ this.update({ phase: message.direction === "incoming" ? "incoming" : "ringing", callId: message.callId, peerId: message.peerId, direction: message.direction, error: undefined, reason: undefined, startedAt: undefined });
234
+ this.armTimeout(35000);
235
+ return;
236
+ }
237
+ if (message.callId !== this.state.callId)
238
+ return;
239
+ if (message.type === "ended") {
240
+ const event = { callId: message.callId, peerId: this.state.peerId, direction: this.state.direction, reason: message.reason, durationMs: message.durationMs };
241
+ this.release();
242
+ this.update({ phase: "ended", reason: message.reason });
243
+ this.options.onEnded?.(event);
244
+ return;
245
+ }
246
+ if (["idle", "ended"].includes(this.state.phase))
247
+ return;
248
+ const gen = this.generation;
249
+ if (message.type === "accepted") {
250
+ this.update({ phase: "connecting" });
251
+ this.armTimeout(30000);
252
+ const pc = (this.options.createPeerConnection ?? (config => new RTCPeerConnection(config)))({ iceServers: message.iceServers, iceTransportPolicy: this.options.iceTransportPolicy ?? "all" });
253
+ this.pc = pc;
254
+ const microphone = this.microphone;
255
+ for (const track of microphone.getAudioTracks())
256
+ pc.addTrack(track, microphone);
257
+ this.video = pc.addTransceiver("video", { direction: "sendrecv" });
258
+ const remote = new MediaStream();
259
+ pc.ontrack = event => {
260
+ if (gen !== this.generation)
261
+ return;
262
+ remote.addTrack(event.track);
263
+ this.update({ remoteStream: remote });
264
+ };
265
+ pc.onicecandidate = event => { if (gen === this.generation && event.candidate)
266
+ this.send({ type: "ice", callId: this.state.callId, candidate: event.candidate.toJSON() }); };
267
+ pc.onconnectionstatechange = () => {
268
+ if (gen !== this.generation)
269
+ return;
270
+ if (pc.connectionState === "connected") {
271
+ clearTimeout(this.timeout);
272
+ this.update({ phase: "connected", startedAt: this.state.startedAt ?? Date.now(), error: undefined });
273
+ }
274
+ else if (pc.connectionState === "failed")
275
+ this.fail("CONNECTION_FAILED");
276
+ else if (pc.connectionState === "disconnected") {
277
+ this.update({ phase: "connecting" });
278
+ this.armTimeout(15000);
279
+ }
280
+ };
281
+ if (this.state.direction === "outgoing") {
282
+ await pc.setLocalDescription(await pc.createOffer());
283
+ if (gen === this.generation)
284
+ this.send({ type: "sdp", callId: this.state.callId, description: pc.localDescription });
285
+ }
286
+ }
287
+ else if (message.type === "sdp" && this.pc) {
288
+ const pc = this.pc;
289
+ await pc.setRemoteDescription(message.description);
290
+ if (gen !== this.generation)
291
+ return;
292
+ for (const candidate of this.candidates.splice(0))
293
+ await pc.addIceCandidate(candidate);
294
+ if (message.description.type === "offer") {
295
+ await pc.setLocalDescription(await pc.createAnswer());
296
+ if (gen === this.generation)
297
+ this.send({ type: "sdp", callId: this.state.callId, description: pc.localDescription });
298
+ }
299
+ }
300
+ else if (message.type === "ice") {
301
+ if (this.pc?.remoteDescription)
302
+ await this.pc.addIceCandidate(message.candidate);
303
+ else if (this.candidates.length < 100)
304
+ this.candidates.push(message.candidate);
305
+ }
306
+ else if (message.type === "sharing")
307
+ this.update({ remoteSharing: message.active });
308
+ }
309
+ }
@@ -5,3 +5,4 @@ export * from "./protocol.js";
5
5
  export * from "./readerClient.js";
6
6
  export * from "./thread.js";
7
7
  export * from "./activity.js";
8
+ export * from "./call.js";
@@ -5,3 +5,4 @@ export * from "./protocol.js";
5
5
  export * from "./readerClient.js";
6
6
  export * from "./thread.js";
7
7
  export * from "./activity.js";
8
+ export * from "./call.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmate/chat-sdk",
3
- "version": "0.5.1",
3
+ "version": "0.6.1",
4
4
  "description": "Cross-platform PMate chat input components, voice input primitives, and ASR helpers",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -11,8 +11,7 @@
11
11
  },
12
12
  "homepage": "https://github.com/parrot-mate/pmate-mono/tree/main/packages/chat-sdk",
13
13
  "files": [
14
- "lib",
15
- "dist"
14
+ "lib"
16
15
  ],
17
16
  "exports": {
18
17
  ".": {