@pmate/chat-sdk 0.7.3 → 0.8.0-meeting-review.2

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.
@@ -1,9 +1,20 @@
1
+ export type MeetingRoom = {
2
+ groupId: string;
3
+ meetingId: string;
4
+ startedAt: number;
5
+ initiatorId: string;
6
+ profileIds: string[];
7
+ };
1
8
  export type CallPhase = "idle" | "requesting" | "ringing" | "incoming" | "connecting" | "connected" | "ended";
2
9
  export type CallSnapshot = {
3
10
  phase: CallPhase;
4
11
  ready: boolean;
5
12
  watchedPeerId?: string;
6
13
  peerOnline?: boolean;
14
+ peerInCall?: boolean;
15
+ rooms?: MeetingRoom[];
16
+ roomsError?: string;
17
+ screenPending?: boolean;
7
18
  callId?: string;
8
19
  peerId?: string;
9
20
  direction?: "incoming" | "outgoing";
@@ -48,6 +59,7 @@ export declare class CallClient {
48
59
  private closed;
49
60
  private capturePending;
50
61
  private requestId?;
62
+ private groupIds;
51
63
  private heartbeat?;
52
64
  private timeout?;
53
65
  private reconnect?;
@@ -57,6 +69,7 @@ export declare class CallClient {
57
69
  subscribe(listener: (state: CallSnapshot) => void): () => void;
58
70
  connect(): void;
59
71
  watchPeer(peerId?: string): void;
72
+ watchGroups(groupIds: string[]): void;
60
73
  start(peerId: string): Promise<void>;
61
74
  accept(): Promise<void>;
62
75
  hangup(): void;
@@ -13,6 +13,7 @@ export class CallClient {
13
13
  closed = false;
14
14
  capturePending = false;
15
15
  requestId;
16
+ groupIds = [];
16
17
  heartbeat;
17
18
  timeout;
18
19
  reconnect;
@@ -53,16 +54,25 @@ export class CallClient {
53
54
  clearInterval(this.heartbeat);
54
55
  const active = !["idle", "ended"].includes(this.state.phase);
55
56
  this.release();
56
- this.update({ ready: false, peerOnline: undefined, phase: active ? "ended" : this.state.phase, error: event.code === 1008 ? "UNAUTHORIZED" : "DISCONNECTED" });
57
+ this.update({ ready: false, peerOnline: undefined, peerInCall: undefined, rooms: undefined, phase: active ? "ended" : this.state.phase, error: event.code === 1008 ? "UNAUTHORIZED" : "DISCONNECTED" });
57
58
  if (!this.closed && event.code !== 1008)
58
59
  this.reconnect = setTimeout(() => this.connect(), 3000);
59
60
  };
60
61
  }
61
62
  watchPeer(peerId) {
62
- this.update({ watchedPeerId: peerId, peerOnline: undefined });
63
+ this.update({ watchedPeerId: peerId, peerOnline: undefined, peerInCall: undefined });
63
64
  if (this.state.ready)
64
65
  this.send({ type: "presence.watch", peerId: peerId ?? "" });
65
66
  }
67
+ watchGroups(groupIds) {
68
+ const next = [...new Set(groupIds)].sort().slice(0, 64);
69
+ if (JSON.stringify(next) === JSON.stringify(this.groupIds))
70
+ return;
71
+ this.groupIds = next;
72
+ this.update({ rooms: this.state.rooms?.filter(room => next.includes(room.groupId)), roomsError: undefined });
73
+ if (this.state.ready)
74
+ this.send({ type: "rooms.watch", groupIds: next });
75
+ }
66
76
  async start(peerId) {
67
77
  if (!this.state.ready || !["idle", "ended"].includes(this.state.phase))
68
78
  return;
@@ -122,6 +132,7 @@ export class CallClient {
122
132
  return this.stopSharing();
123
133
  const gen = this.generation;
124
134
  this.capturePending = true;
135
+ this.update({ screenPending: true, error: undefined });
125
136
  try {
126
137
  const media = this.options.mediaDevices ?? navigator.mediaDevices;
127
138
  if (!media.getDisplayMedia)
@@ -147,13 +158,14 @@ export class CallClient {
147
158
  catch (error) {
148
159
  if (gen === this.generation) {
149
160
  await this.stopSharing();
150
- if (!(error instanceof DOMException && error.name === "NotAllowedError"))
151
- this.update({ error: "SCREEN_UNAVAILABLE" });
161
+ this.update({ error: error instanceof DOMException && error.name === "NotAllowedError" ? "SCREEN_NOT_ALLOWED" : "SCREEN_UNAVAILABLE" });
152
162
  }
153
163
  }
154
164
  finally {
155
- if (gen === this.generation)
165
+ if (gen === this.generation) {
156
166
  this.capturePending = false;
167
+ this.update({ screenPending: false });
168
+ }
157
169
  }
158
170
  }
159
171
  async stopSharing() {
@@ -208,25 +220,40 @@ export class CallClient {
208
220
  stream?.getTracks().forEach(t => { t.onended = null; t.stop(); });
209
221
  this.microphone = undefined;
210
222
  this.screen = undefined;
211
- this.update({ muted: false, sharing: false, remoteSharing: false, remoteStream: undefined, localScreen: undefined });
223
+ this.update({ muted: false, sharing: false, remoteSharing: false, remoteStream: undefined, localScreen: undefined, screenPending: false });
212
224
  }
213
225
  async receive(message) {
214
226
  if (message.type === "ready") {
215
227
  this.update({ ready: true, error: undefined });
216
228
  if (this.state.watchedPeerId)
217
229
  this.send({ type: "presence.watch", peerId: this.state.watchedPeerId });
230
+ if (this.groupIds.length)
231
+ this.send({ type: "rooms.watch", groupIds: this.groupIds });
218
232
  clearInterval(this.heartbeat);
219
233
  this.heartbeat = setInterval(() => this.send({ type: "ping" }), 20000);
220
234
  return;
221
235
  }
222
236
  if (message.type === "presence") {
223
237
  if (message.peerId === this.state.watchedPeerId)
224
- this.update({ peerOnline: typeof message.online === "boolean" ? message.online : undefined });
238
+ this.update({ peerOnline: typeof message.online === "boolean" ? message.online : undefined, peerInCall: typeof message.inCall === "boolean" ? message.inCall : undefined });
239
+ return;
240
+ }
241
+ if (message.type === "rooms.snapshot") {
242
+ this.update({ rooms: message.rooms.filter((room) => this.groupIds.includes(room.groupId)), roomsError: undefined });
243
+ return;
244
+ }
245
+ if (message.type === "rooms.changed") {
246
+ if (this.groupIds.includes(message.groupId))
247
+ this.update({ rooms: [...(this.state.rooms ?? []).filter(room => room.groupId !== message.groupId), ...(message.room ? [message.room] : [])], roomsError: undefined });
225
248
  return;
226
249
  }
227
250
  if (message.type === "pong")
228
251
  return;
229
252
  if (message.type === "error") {
253
+ if (message.scope === "rooms") {
254
+ this.update({ rooms: undefined, roomsError: message.code });
255
+ return;
256
+ }
230
257
  if (message.requestId && message.requestId !== this.requestId)
231
258
  return;
232
259
  if (!message.callId || message.callId === this.state.callId)
@@ -304,6 +331,13 @@ export class CallClient {
304
331
  for (const candidate of this.candidates.splice(0))
305
332
  await pc.addIceCandidate(candidate);
306
333
  if (message.description.type === "offer") {
334
+ const negotiated = pc.getTransceivers().find(t => t.mid !== null && t.receiver.track.kind === "video");
335
+ if (negotiated) {
336
+ if (this.video !== negotiated && this.video?.mid === null)
337
+ this.video.stop();
338
+ this.video = negotiated;
339
+ negotiated.direction = "sendrecv";
340
+ }
307
341
  await pc.setLocalDescription(await pc.createAnswer());
308
342
  if (gen === this.generation)
309
343
  this.send({ type: "sdp", callId: this.state.callId, description: pc.localDescription });
@@ -9,6 +9,8 @@ export type MeetingParticipant = {
9
9
  export type MeetingSnapshot = {
10
10
  phase: "idle" | "joining" | "joined" | "ended";
11
11
  groupId?: string;
12
+ startedAt?: number;
13
+ screenPending?: boolean;
12
14
  meetingId?: string;
13
15
  selfId?: string;
14
16
  participants: MeetingParticipant[];
@@ -69,6 +69,7 @@ export class GroupMeetingClient {
69
69
  }
70
70
  const gen = this.generation;
71
71
  this.pendingCapture = true;
72
+ this.update({ screenPending: true, error: undefined });
72
73
  try {
73
74
  const stream = await (this.options.mediaDevices ?? navigator.mediaDevices).getDisplayMedia({ video: { width: { ideal: 1280, max: 1920 }, height: { ideal: 720, max: 1080 }, frameRate: { ideal: 10, max: 15 } }, audio: false });
74
75
  if (gen !== this.generation) {
@@ -88,13 +89,14 @@ export class GroupMeetingClient {
88
89
  catch (e) {
89
90
  if (gen === this.generation) {
90
91
  await this.stopSharing();
91
- if (!(e instanceof DOMException && e.name === "NotAllowedError"))
92
- this.update({ error: "SCREEN_UNAVAILABLE" });
92
+ this.update({ error: e instanceof DOMException && e.name === "NotAllowedError" ? "SCREEN_NOT_ALLOWED" : "SCREEN_UNAVAILABLE" });
93
93
  }
94
94
  }
95
95
  finally {
96
- if (gen === this.generation)
96
+ if (gen === this.generation) {
97
97
  this.pendingCapture = false;
98
+ this.update({ screenPending: false });
99
+ }
98
100
  }
99
101
  }
100
102
  async stopSharing() {
@@ -130,7 +132,7 @@ export class GroupMeetingClient {
130
132
  stream?.getTracks().forEach(t => { t.onended = null; t.stop(); });
131
133
  this.microphone = undefined;
132
134
  this.screen = undefined;
133
- this.update({ participants: [], meetingId: undefined, selfId: undefined, sharingId: undefined, localScreen: undefined, muted: false });
135
+ this.update({ participants: [], meetingId: undefined, selfId: undefined, sharingId: undefined, localScreen: undefined, muted: false, screenPending: false, startedAt: undefined });
134
136
  }
135
137
  participant(id, patch) { this.update({ participants: this.state.participants.map(p => p.id === id ? { ...p, ...patch } : p) }); }
136
138
  link(id, gen) {
@@ -194,7 +196,7 @@ export class GroupMeetingClient {
194
196
  return;
195
197
  clearTimeout(this.timer);
196
198
  this.iceServers = m.iceServers;
197
- this.update({ phase: "joined", meetingId: m.meetingId, selfId: m.selfId });
199
+ this.update({ phase: "joined", meetingId: m.meetingId, selfId: m.selfId, startedAt: m.startedAt });
198
200
  return;
199
201
  }
200
202
  if (m.meetingId !== this.state.meetingId || this.state.phase !== "joined")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmate/chat-sdk",
3
- "version": "0.7.3",
3
+ "version": "0.8.0-meeting-review.2",
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",