@hubtalk-ai/web-sdk 0.2.0 → 0.3.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.
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { ConnectionError as A, ConnectionErrorReason as u, MediaDeviceFailure as m, Room as I, RoomEvent as a, ConnectionState as p, Track as C, DisconnectReason as S } from "livekit-client";
2
- class y {
1
+ import { ConnectionError as b, ConnectionErrorReason as p, MediaDeviceFailure as l, Room as I, RoomEvent as a, ConnectionState as m, ParticipantKind as _, Track as E, DisconnectReason as S } from "livekit-client";
2
+ class k {
3
3
  constructor(t) {
4
4
  if (t) {
5
5
  this.element = t, this.owned = !1;
@@ -20,35 +20,36 @@ class y {
20
20
  this.clear(), this.owned && this.element.remove();
21
21
  }
22
22
  }
23
- class w {
23
+ class N {
24
24
  constructor() {
25
25
  this.listeners = /* @__PURE__ */ new Map();
26
26
  }
27
27
  on(t, e) {
28
- let n = this.listeners.get(t);
29
- n || (n = /* @__PURE__ */ new Set(), this.listeners.set(t, n)), n.add(e);
28
+ let i = this.listeners.get(t);
29
+ i || (i = /* @__PURE__ */ new Set(), this.listeners.set(t, i)), i.add(e);
30
30
  }
31
31
  off(t, e) {
32
- var n;
33
- (n = this.listeners.get(t)) == null || n.delete(e);
32
+ var i;
33
+ (i = this.listeners.get(t)) == null || i.delete(e);
34
34
  }
35
35
  once(t, e) {
36
- const n = (i) => {
37
- this.off(t, n), e(i);
36
+ const i = (n) => {
37
+ this.off(t, i), e(n);
38
38
  };
39
- this.on(t, n);
39
+ this.on(t, i);
40
40
  }
41
41
  emit(t, e) {
42
- const n = this.listeners.get(t);
43
- if (n)
44
- for (const i of [...n])
45
- i(e);
42
+ const i = this.listeners.get(t);
43
+ if (i)
44
+ for (const n of [...i])
45
+ n(e);
46
46
  }
47
47
  clear() {
48
48
  this.listeners.clear();
49
49
  }
50
50
  }
51
- const N = {
51
+ const w = {
52
+ AUDIO_PLAYBACK_BLOCKED: "Tap Enable sound to hear the agent",
52
53
  INVALID_PARAMS: "accessToken и url обязательны",
53
54
  WEBRTC_UNSUPPORTED: "браузер/контекст не поддерживает WebRTC (нужен HTTPS)",
54
55
  MICROPHONE_PERMISSION_DENIED: "доступ к микрофону запрещён пользователем",
@@ -61,83 +62,93 @@ const N = {
61
62
  NOT_IN_CALL: "нет активного звонка",
62
63
  SERVER_ERROR: "внутренняя ошибка сервиса сигналинга"
63
64
  };
64
- class _ extends Error {
65
- constructor(t, e, n) {
66
- super(N[t]), this.name = "VoiceCallError", this.code = t, this.state = e, this.cause = n;
65
+ class O extends Error {
66
+ constructor(t, e, i) {
67
+ super(w[t]), this.name = "VoiceCallError", this.code = t, this.state = e, this.cause = i;
67
68
  }
68
69
  }
69
- function b(o) {
70
- if (o instanceof A)
71
- switch (o.reason) {
72
- case u.NotAllowed:
70
+ function P(s) {
71
+ if (s instanceof b)
72
+ switch (s.reason) {
73
+ case p.NotAllowed:
73
74
  return "TOKEN_INVALID";
74
- case u.ServerUnreachable:
75
- case u.Timeout:
76
- case u.WebSocket:
75
+ case p.ServerUnreachable:
76
+ case p.Timeout:
77
+ case p.WebSocket:
77
78
  return "CONNECTION_FAILED";
78
79
  default:
79
80
  return "SERVER_ERROR";
80
81
  }
81
82
  return "SERVER_ERROR";
82
83
  }
83
- function O(o) {
84
- return m.getFailure(o) === m.PermissionDenied ? "MICROPHONE_PERMISSION_DENIED" : "MICROPHONE_UNAVAILABLE";
84
+ function L(s) {
85
+ return l.getFailure(s) === l.PermissionDenied ? "MICROPHONE_PERMISSION_DENIED" : "MICROPHONE_UNAVAILABLE";
86
+ }
87
+ function D(s) {
88
+ const t = l.getFailure(s);
89
+ return t === l.PermissionDenied ? "MICROPHONE_PERMISSION_DENIED" : t === l.NotFound || t === l.DeviceInUse || s instanceof DOMException && [
90
+ "AbortError",
91
+ "OverconstrainedError",
92
+ "SecurityError",
93
+ "InvalidStateError",
94
+ "NotSupportedError"
95
+ ].includes(s.name) ? "MICROPHONE_UNAVAILABLE" : "SERVER_ERROR";
85
96
  }
86
- const T = "[redacted]", k = /^[\w-]+\.[\w-]+\.[\w-]+$/, D = /token/i;
87
- function f(o) {
88
- if (typeof o == "string")
89
- return k.test(o) ? T : o;
90
- if (Array.isArray(o))
91
- return o.map(f);
92
- if (o && typeof o == "object") {
97
+ const T = "[redacted]", V = /^[\w-]+\.[\w-]+\.[\w-]+$/, M = /token/i;
98
+ function g(s) {
99
+ if (typeof s == "string")
100
+ return V.test(s) ? T : s;
101
+ if (Array.isArray(s))
102
+ return s.map(g);
103
+ if (s && typeof s == "object") {
93
104
  const t = {};
94
- for (const [e, n] of Object.entries(o))
95
- t[e] = D.test(e) ? T : f(n);
105
+ for (const [e, i] of Object.entries(s))
106
+ t[e] = M.test(e) ? T : g(i);
96
107
  return t;
97
108
  }
98
- return o;
109
+ return s;
99
110
  }
100
- function L(o, t) {
101
- if (o === "silent")
111
+ function v(s, t) {
112
+ if (s === "silent")
102
113
  return { error: () => {
103
114
  }, debug: () => {
104
115
  } };
105
116
  const e = t ?? {
106
- error: (n, ...i) => console.error(n, ...i),
107
- debug: (n, ...i) => console.debug(n, ...i)
117
+ error: (i, ...n) => console.error(i, ...n),
118
+ debug: (i, ...n) => console.debug(i, ...n)
108
119
  };
109
120
  return {
110
- error: (n, ...i) => e.error(n, ...i.map(f)),
111
- debug: (n, ...i) => {
112
- o === "debug" && e.debug(n, ...i.map(f));
121
+ error: (i, ...n) => e.error(i, ...n.map(g)),
122
+ debug: (i, ...n) => {
123
+ s === "debug" && e.debug(i, ...n.map(g));
113
124
  }
114
125
  };
115
126
  }
116
- function P(o = !0) {
127
+ function B(s = !0) {
117
128
  var t;
118
- return typeof window > "u" || !window.isSecureContext || typeof RTCPeerConnection > "u" ? !1 : o ? typeof navigator < "u" && !!((t = navigator.mediaDevices) != null && t.getUserMedia) : !0;
129
+ return typeof window > "u" || !window.isSecureContext || typeof RTCPeerConnection > "u" ? !1 : s ? typeof navigator < "u" && !!((t = navigator.mediaDevices) != null && t.getUserMedia) : !0;
119
130
  }
120
- function M(o) {
121
- const t = o.split(".")[1];
131
+ function U(s) {
132
+ const t = s.split(".")[1];
122
133
  if (!t) return null;
123
134
  try {
124
- const e = t.replace(/-/g, "+").replace(/_/g, "/"), n = decodeURIComponent(
125
- Array.from(atob(e)).map((r) => "%" + r.charCodeAt(0).toString(16).padStart(2, "0")).join("")
126
- ), i = JSON.parse(n);
127
- return typeof i.exp == "number" ? i.exp : null;
135
+ const e = t.replace(/-/g, "+").replace(/_/g, "/"), i = decodeURIComponent(
136
+ Array.from(atob(e)).map((c) => "%" + c.charCodeAt(0).toString(16).padStart(2, "0")).join("")
137
+ ), n = JSON.parse(i);
138
+ return typeof n.exp == "number" ? n.exp : null;
128
139
  } catch {
129
140
  return null;
130
141
  }
131
142
  }
132
- const v = "lk.agent.state", V = /* @__PURE__ */ new Set(["initializing", "listening", "thinking", "speaking"]), U = /* @__PURE__ */ new Set([
143
+ const H = "lk.agent.state", K = /* @__PURE__ */ new Set(["initializing", "listening", "thinking", "speaking"]), y = "lk.publish_on_behalf", F = /* @__PURE__ */ new Set([
133
144
  S.ROOM_DELETED,
134
145
  S.PARTICIPANT_REMOVED,
135
146
  S.SERVER_SHUTDOWN,
136
147
  S.ROOM_CLOSED
137
- ]), E = /* @__PURE__ */ new Set(["connecting", "connected", "reconnecting"]), R = "lk.transcription", H = "lk.chat", F = "lk.segment_id", x = "lk.transcription_final", K = 1e3;
138
- class B {
148
+ ]), A = /* @__PURE__ */ new Set(["connecting", "connected", "reconnecting"]), R = "lk.transcription", W = "lk.chat", G = "lk.segment_id", Y = "lk.transcription_final", j = 1e3;
149
+ class X {
139
150
  constructor(t = {}) {
140
- this.emitter = new w(), this.connectionState = "idle", this.agentState = "unknown", this.room = null, this.callId = null, this.playAudio = !0, this.stoppingLocally = !1, this.reconnectSeen = !1, this.destroyed = !1, this.roomCleanup = [], this.audio = new y(t.audioContainer), this.log = L(t.logLevel ?? "error", t.logger), this.connectTimeoutMs = t.connectTimeoutMs;
151
+ this.emitter = new N(), this.connectionState = "idle", this.agentState = "unknown", this.room = null, this.callId = null, this.playAudio = !0, this.agentVideoTrack = null, this.agentVideoElements = /* @__PURE__ */ new Set(), this.microphoneEnabled = !1, this.microphoneOperation = Promise.resolve(), this.stoppingLocally = !1, this.reconnectSeen = !1, this.destroyed = !1, this.roomCleanup = [], this.audio = new k(t.audioContainer), this.log = v(t.logLevel ?? "error", t.logger), this.connectTimeoutMs = t.connectTimeoutMs;
141
152
  }
142
153
  getState() {
143
154
  return this.connectionState;
@@ -178,59 +189,121 @@ class B {
178
189
  * флагу звонка, тогда текст пойдёт сразу и синтеза не будет вовсе.
179
190
  */
180
191
  async startCall(t, e = {}) {
181
- if (E.has(this.connectionState))
192
+ if (this.destroyed) throw new Error("VoiceClient has been destroyed");
193
+ if (A.has(this.connectionState))
182
194
  throw this.raiseError("ALREADY_IN_CALL", this.connectionState);
183
- const n = e.audio !== !1;
184
- this.playAudio = n;
185
- const { accessToken: i, url: r } = this.validateStartParams(t, n);
195
+ const i = e.audio !== !1;
196
+ this.playAudio = i;
197
+ const { accessToken: n, url: c } = this.validateStartParams(t, i);
186
198
  this.stoppingLocally = !1, this.reconnectSeen = !1, this.setConnectionState("connecting");
187
- const c = new I();
188
- if (this.room = c, this.wireRoomEvents(c), await this.connectRoom(c, r, i), this.stoppingLocally) {
189
- await this.abandonConnectingCall(c);
199
+ const o = new I();
200
+ if (this.room = o, this.wireRoomEvents(o), await this.connectRoom(o, c, n), this.stoppingLocally || this.destroyed || this.room !== o) {
201
+ await this.abandonConnectingCall(o);
202
+ return;
203
+ }
204
+ if (i && await this.publishMicrophone(o), this.stoppingLocally || this.destroyed || this.room !== o) {
205
+ await this.abandonConnectingCall(o);
206
+ return;
207
+ }
208
+ if (i)
209
+ try {
210
+ await o.startAudio();
211
+ } catch {
212
+ this.room === o && !this.destroyed && this.emitter.emit("audioPlaybackBlocked", {});
213
+ }
214
+ if (this.stoppingLocally || this.destroyed || this.room !== o) {
215
+ await this.abandonConnectingCall(o);
190
216
  return;
191
217
  }
192
- n && await this.publishMicrophone(c), this.callId = c.name, this.setConnectionState("connected"), this.syncAgentState(c), this.emitter.emit("callStarted", { callId: this.callId });
218
+ this.callId = o.name, this.setConnectionState("connected"), this.syncAgentState(o), this.emitter.emit("callStarted", { callId: this.callId });
193
219
  }
194
220
  validateStartParams(t, e) {
195
- const n = t.accessToken ?? t.access_token, i = t.url;
196
- if (!n || !i)
221
+ const i = t.accessToken ?? t.access_token, n = t.url;
222
+ if (!i || !n)
197
223
  throw this.raiseError("INVALID_PARAMS", "error");
198
- if (!P(e))
224
+ if (!B(e))
199
225
  throw this.raiseError("WEBRTC_UNSUPPORTED", "error");
200
- const r = M(n);
201
- if (r !== null && r * 1e3 <= Date.now())
226
+ const c = U(i);
227
+ if (c !== null && c * 1e3 <= Date.now())
202
228
  throw this.raiseError("TOKEN_EXPIRED", "error");
203
- return { accessToken: n, url: i };
229
+ return { accessToken: i, url: n };
204
230
  }
205
- async connectRoom(t, e, n) {
231
+ async connectRoom(t, e, i) {
206
232
  try {
207
233
  await t.connect(
208
234
  e,
209
- n,
235
+ i,
210
236
  this.connectTimeoutMs === void 0 ? void 0 : {
211
237
  peerConnectionTimeout: this.connectTimeoutMs,
212
238
  websocketTimeout: this.connectTimeoutMs
213
239
  }
214
240
  );
215
- } catch (i) {
216
- throw this.teardownRoom(), this.raiseError(b(i), "error", i);
241
+ } catch (n) {
242
+ if (this.destroyed || this.room !== t) return;
243
+ throw this.teardownRoom(), this.raiseError(P(n), "error", n);
217
244
  }
218
245
  }
219
246
  // stopCall() пришёл во время connecting (AC4/§1.2) — сворачиваем попытку
220
247
  // без callStarted/error вместо продолжения к публикации микрофона
221
248
  async abandonConnectingCall(t) {
222
249
  await t.disconnect().catch(() => {
223
- }), this.teardownRoom(), this.setConnectionState("disconnected");
250
+ }), this.room === t && this.teardownRoom(), !this.destroyed && !this.room && this.setConnectionState("disconnected");
224
251
  }
225
252
  async publishMicrophone(t) {
226
253
  try {
227
- await t.localParticipant.setMicrophoneEnabled(!0), await t.startAudio();
254
+ await t.localParticipant.setMicrophoneEnabled(!0), this.room === t && !this.destroyed && this.confirmMicrophone(t.localParticipant.isMicrophoneEnabled);
228
255
  } catch (e) {
229
- const n = O(e);
256
+ if (this.destroyed || this.room !== t) return;
257
+ const i = L(e);
230
258
  throw await t.disconnect().catch(() => {
231
- }), this.teardownRoom(), this.raiseError(n, "error", e);
259
+ }), this.teardownRoom(), this.raiseError(i, "error", e);
232
260
  }
233
261
  }
262
+ isMicrophoneEnabled() {
263
+ return this.microphoneEnabled;
264
+ }
265
+ async setMicrophoneEnabled(t) {
266
+ const e = this.room;
267
+ if (!e || !this.playAudio || this.connectionState !== "connected")
268
+ throw this.raiseError("NOT_IN_CALL", this.connectionState);
269
+ const i = this.microphoneOperation.then(async () => {
270
+ if (!e || this.room !== e || !this.playAudio || this.connectionState !== "connected")
271
+ throw this.raiseError("NOT_IN_CALL", this.connectionState);
272
+ try {
273
+ await e.localParticipant.setMicrophoneEnabled(t);
274
+ } catch (n) {
275
+ throw this.raiseError(D(n), this.connectionState, n);
276
+ }
277
+ this.room === e && !this.destroyed && this.confirmMicrophone(e.localParticipant.isMicrophoneEnabled);
278
+ });
279
+ return this.microphoneOperation = i.catch(() => {
280
+ }), i;
281
+ }
282
+ async resumeAudio() {
283
+ const t = this.room;
284
+ if (!t || !this.playAudio || this.connectionState !== "connected")
285
+ throw this.raiseError("NOT_IN_CALL", this.connectionState);
286
+ try {
287
+ await t.startAudio();
288
+ } catch (e) {
289
+ throw this.room === t && !this.destroyed && this.emitter.emit("audioPlaybackBlocked", {}), this.raiseError("AUDIO_PLAYBACK_BLOCKED", this.connectionState, e);
290
+ }
291
+ }
292
+ /**
293
+ * VPT-857 §1.3: attach the avatar worker's current and any later video
294
+ * track to `element`; the returned function detaches. Outside a call it
295
+ * attaches nothing and returns a no-op detach.
296
+ */
297
+ attachAgentVideo(t) {
298
+ return !this.room || this.destroyed ? () => {
299
+ } : (this.agentVideoElements.add(t), this.agentVideoTrack && this.agentVideoTrack.attach(t), () => {
300
+ var e;
301
+ this.agentVideoElements.delete(t), (e = this.agentVideoTrack) == null || e.detach(t);
302
+ });
303
+ }
304
+ confirmMicrophone(t) {
305
+ this.microphoneEnabled !== t && (this.microphoneEnabled = t, this.emitter.emit("microphoneStateChanged", { enabled: t }));
306
+ }
234
307
  /**
235
308
  * Отправляет текстовую реплику посетителя в разговор (VPT-108 §2.1).
236
309
  * Своё сообщение в чат SDK не кладёт — его вернёт агент событием
@@ -238,81 +311,86 @@ class B {
238
311
  */
239
312
  async sendText(t) {
240
313
  const e = this.room;
241
- if (!e || !E.has(this.connectionState))
314
+ if (!e || !A.has(this.connectionState))
242
315
  throw this.raiseError("NOT_IN_CALL", this.connectionState);
243
- const n = t.trim().slice(0, K);
244
- if (n)
316
+ const i = t.trim().slice(0, j);
317
+ if (i)
245
318
  try {
246
- await e.localParticipant.sendText(n, { topic: H });
247
- } catch (i) {
248
- throw this.raiseError("SERVER_ERROR", this.connectionState, i);
319
+ await e.localParticipant.sendText(i, { topic: W });
320
+ } catch (n) {
321
+ throw this.raiseError("SERVER_ERROR", this.connectionState, n);
249
322
  }
250
323
  }
251
324
  async stopCall() {
252
325
  this.stoppingLocally = !0;
253
326
  const t = this.room;
254
- !t || !E.has(this.connectionState) || this.connectionState !== "connecting" && await t.disconnect().catch((e) => this.log.debug("[voice-client] disconnect", e));
327
+ !t || !A.has(this.connectionState) || this.connectionState !== "connecting" && await t.disconnect().catch((e) => this.log.debug("[voice-client] disconnect", e));
255
328
  }
256
329
  destroy() {
257
- this.stopCall(), this.playAudio = !0, this.destroyed = !0, this.teardownRoom(), this.emitter.clear(), this.audio.destroy(), this.connectionState = "idle", this.agentState = "unknown", this.callId = null;
330
+ this.stopCall(), this.playAudio = !0, this.destroyed = !0, this.teardownRoom(), this.emitter.clear(), this.audio.destroy(), this.agentVideoElements.clear(), this.connectionState = "idle", this.agentState = "unknown", this.callId = null;
258
331
  }
259
332
  wireRoomEvents(t) {
260
- const e = (s, d, h) => {
333
+ const e = (r, h, d) => {
261
334
  this.log.debug("[voice-client] remote track subscribed", {
262
- participant: h.identity,
263
- participantID: h.sid,
264
- trackID: s.sid,
265
- streamTrackID: s.mediaStreamTrack.id,
266
- kind: s.kind,
267
- source: d.source,
268
- muted: d.isMuted
269
- }), s.kind === C.Kind.Audio && this.playAudio && this.audio.attach(s);
270
- }, n = (s, d, h) => {
335
+ participant: d.identity,
336
+ participantID: d.sid,
337
+ trackID: r.sid,
338
+ streamTrackID: r.mediaStreamTrack.id,
339
+ kind: r.kind,
340
+ source: h.source,
341
+ muted: h.isMuted
342
+ }), r.kind === E.Kind.Audio && this.playAudio && this.audio.attach(r), r.kind === E.Kind.Video && this.syncAgentVideo(t);
343
+ }, i = (r, h, d) => {
271
344
  this.log.debug("[voice-client] remote track unsubscribed", {
272
- participant: h.identity,
273
- participantID: h.sid,
274
- trackID: s.sid,
275
- kind: s.kind,
276
- source: d.source
277
- }), s.kind === C.Kind.Audio && this.audio.detach(s);
278
- }, i = (s, d, h) => {
279
- this.log.debug("[voice-client] remote track subscription failed", {
280
345
  participant: d.identity,
281
346
  participantID: d.sid,
282
- trackID: s,
283
- reason: h
347
+ trackID: r.sid,
348
+ kind: r.kind,
349
+ source: h.source
350
+ }), r.kind === E.Kind.Audio && this.audio.detach(r), r.kind === E.Kind.Video && this.syncAgentVideo(t);
351
+ }, n = (r, h, d) => {
352
+ this.log.debug("[voice-client] remote track subscription failed", {
353
+ participant: h.identity,
354
+ participantID: h.sid,
355
+ trackID: r,
356
+ reason: d
284
357
  });
285
- }, r = (s) => {
358
+ }, c = (r) => {
286
359
  this.log.debug("[voice-client] remote participant connected", {
287
360
  room: t.name,
288
- participant: s.identity,
289
- participantID: s.sid
361
+ participant: r.identity,
362
+ participantID: r.sid
290
363
  }), !this.destroyed && this.syncAgentState(t);
291
- }, c = () => {
292
- this.destroyed || this.syncAgentState(t);
293
- }, l = (s) => {
294
- this.destroyed || this.applyLkConnectionState(s);
295
- }, g = (s) => {
364
+ }, o = () => {
365
+ this.destroyed || (this.syncAgentState(t), this.syncAgentVideo(t));
366
+ }, u = (r) => {
367
+ this.destroyed || this.applyLkConnectionState(r);
368
+ }, f = (r) => {
296
369
  this.log.debug("[voice-client] room disconnected", {
297
370
  room: t.name,
298
- reason: s,
371
+ reason: r,
299
372
  stoppingLocally: this.stoppingLocally
300
- }), !this.destroyed && this.handleDisconnected(s);
373
+ }), !this.destroyed && this.handleDisconnected(r);
301
374
  };
302
- t.registerTextStreamHandler(R, (s, d) => {
375
+ t.registerTextStreamHandler(R, (r, h) => {
303
376
  if (this.destroyed) return;
304
- const h = d.identity === t.localParticipant.identity ? "user" : "agent";
305
- this.readTranscriptSegment(s, h);
306
- }), t.on(a.TrackSubscribed, e), t.on(a.TrackUnsubscribed, n), t.on(a.TrackSubscriptionFailed, i), t.on(a.ParticipantAttributesChanged, c), t.on(a.ParticipantConnected, r), t.on(a.ParticipantDisconnected, c), t.on(a.ConnectionStateChanged, l), t.on(a.Disconnected, g), this.roomCleanup = [
377
+ const d = h.identity === t.localParticipant.identity ? "user" : "agent";
378
+ this.readTranscriptSegment(r, d, t);
379
+ });
380
+ const C = () => {
381
+ this.playAudio && !t.canPlaybackAudio && !this.destroyed && this.emitter.emit("audioPlaybackBlocked", {});
382
+ };
383
+ t.on(a.AudioPlaybackStatusChanged, C), t.on(a.TrackSubscribed, e), t.on(a.TrackUnsubscribed, i), t.on(a.TrackSubscriptionFailed, n), t.on(a.ParticipantAttributesChanged, o), t.on(a.ParticipantConnected, c), t.on(a.ParticipantDisconnected, o), t.on(a.ConnectionStateChanged, u), t.on(a.Disconnected, f), this.roomCleanup = [
384
+ () => t.off(a.AudioPlaybackStatusChanged, C),
307
385
  () => t.unregisterTextStreamHandler(R),
308
386
  () => t.off(a.TrackSubscribed, e),
309
- () => t.off(a.TrackUnsubscribed, n),
310
- () => t.off(a.TrackSubscriptionFailed, i),
311
- () => t.off(a.ParticipantAttributesChanged, c),
312
- () => t.off(a.ParticipantConnected, r),
313
- () => t.off(a.ParticipantDisconnected, c),
314
- () => t.off(a.ConnectionStateChanged, l),
315
- () => t.off(a.Disconnected, g)
387
+ () => t.off(a.TrackUnsubscribed, i),
388
+ () => t.off(a.TrackSubscriptionFailed, n),
389
+ () => t.off(a.ParticipantAttributesChanged, o),
390
+ () => t.off(a.ParticipantConnected, c),
391
+ () => t.off(a.ParticipantDisconnected, o),
392
+ () => t.off(a.ConnectionStateChanged, u),
393
+ () => t.off(a.Disconnected, f)
316
394
  ];
317
395
  }
318
396
  /**
@@ -332,52 +410,78 @@ class B {
332
410
  * трейлера в `reader.info` перед закрытием — поэтому читаем атрибут ПОСЛЕ
333
411
  * цикла чтения.
334
412
  */
335
- readTranscriptSegment(t, e) {
336
- var i;
337
- const n = ((i = t.info.attributes) == null ? void 0 : i[F]) || t.info.id;
413
+ readTranscriptSegment(t, e, i) {
414
+ var c;
415
+ const n = ((c = t.info.attributes) == null ? void 0 : c[G]) || t.info.id;
338
416
  (async () => {
339
- var c;
340
- let r = "";
417
+ var u;
418
+ let o = "";
341
419
  try {
342
- for await (const l of t)
343
- r += l, this.emitter.emit("transcript", { role: e, text: r, final: !1, id: n });
420
+ for await (const f of t) {
421
+ if (this.destroyed || this.room !== i) return;
422
+ o += f, this.emitter.emit("transcript", { role: e, text: o, final: !1, id: n });
423
+ }
344
424
  } catch {
345
425
  return;
346
426
  }
347
- ((c = t.info.attributes) == null ? void 0 : c[x]) === "true" && this.emitter.emit("transcript", { role: e, text: r, final: !0, id: n });
427
+ !this.destroyed && this.room === i && ((u = t.info.attributes) == null ? void 0 : u[Y]) === "true" && this.emitter.emit("transcript", { role: e, text: o, final: !0, id: n });
348
428
  })();
349
429
  }
350
430
  applyLkConnectionState(t) {
351
- t === p.Reconnecting || t === p.SignalReconnecting ? (this.reconnectSeen = !0, this.setConnectionState("reconnecting")) : t === p.Connected && this.connectionState === "reconnecting" && this.setConnectionState("connected");
431
+ var e;
432
+ t === m.Reconnecting || t === m.SignalReconnecting ? (this.reconnectSeen = !0, this.setConnectionState("reconnecting")) : t === m.Connected && this.connectionState === "reconnecting" && (this.confirmMicrophone(
433
+ this.playAudio && !!((e = this.room) != null && e.localParticipant.isMicrophoneEnabled)
434
+ ), this.setConnectionState("connected"));
352
435
  }
353
436
  handleDisconnected(t) {
354
- const e = this.callId, n = this.reconnectSeen, i = this.stoppingLocally;
355
- if (this.teardownRoom(), i) {
437
+ const e = this.callId, i = this.reconnectSeen, n = this.stoppingLocally;
438
+ if (this.teardownRoom(), n) {
356
439
  this.setConnectionState("disconnected"), this.emitter.emit("callEnded", { callId: e, reason: "local" });
357
440
  return;
358
441
  }
359
- if (n) {
442
+ if (i) {
360
443
  this.setConnectionState("disconnected"), this.raiseError("CONNECTION_LOST", "disconnected"), this.emitter.emit("callEnded", { callId: e, reason: "connection_lost" });
361
444
  return;
362
445
  }
363
- const c = t !== void 0 && U.has(t) ? "remote" : "error";
364
- this.setConnectionState("disconnected"), this.emitter.emit("callEnded", { callId: e, reason: c });
446
+ const o = t !== void 0 && F.has(t) ? "remote" : "error";
447
+ this.setConnectionState("disconnected"), this.emitter.emit("callEnded", { callId: e, reason: o });
365
448
  }
366
449
  teardownRoom() {
367
- this.roomCleanup.forEach((t) => t()), this.roomCleanup = [], this.audio.clear(), this.room = null, this.agentState = "unknown";
450
+ this.confirmMicrophone(!1), this.setAgentVideoTrack(null), this.roomCleanup.forEach((t) => t()), this.roomCleanup = [], this.audio.clear(), this.room = null, this.agentState = "unknown";
368
451
  }
369
452
  syncAgentState(t) {
370
- var n;
453
+ var i, n;
371
454
  let e = "unknown";
372
- for (const i of t.remoteParticipants.values()) {
373
- const r = (n = i.attributes) == null ? void 0 : n[v];
374
- if (r && V.has(r)) {
375
- e = r;
455
+ for (const c of t.remoteParticipants.values()) {
456
+ if ((i = c.attributes) != null && i[y]) continue;
457
+ const o = (n = c.attributes) == null ? void 0 : n[H];
458
+ if (o && K.has(o)) {
459
+ e = o;
376
460
  break;
377
461
  }
378
462
  }
379
463
  this.setAgentState(e);
380
464
  }
465
+ /** The subscribed video track of the avatar worker, if any (§1.3). */
466
+ findAgentVideoTrack(t) {
467
+ var e;
468
+ for (const i of t.remoteParticipants.values()) {
469
+ const n = (e = i.attributes) == null ? void 0 : e[y];
470
+ if (!(!n || i.kind !== _.AGENT) && t.remoteParticipants.has(n))
471
+ for (const c of i.videoTrackPublications.values()) {
472
+ const o = c.track;
473
+ if (o && c.isSubscribed) return o;
474
+ }
475
+ }
476
+ return null;
477
+ }
478
+ syncAgentVideo(t) {
479
+ this.destroyed || this.room !== t || this.setAgentVideoTrack(this.findAgentVideoTrack(t));
480
+ }
481
+ setAgentVideoTrack(t) {
482
+ const e = this.agentVideoTrack;
483
+ e !== t && (e && this.agentVideoElements.forEach((i) => e.detach(i)), this.agentVideoTrack = t, t && this.agentVideoElements.forEach((i) => t.attach(i)), !!e != !!t && this.emitter.emit("agentVideoChanged", { available: !!t }));
484
+ }
381
485
  setConnectionState(t) {
382
486
  const e = this.connectionState;
383
487
  e !== t && (this.connectionState = t, this.emitter.emit("connectionStateChanged", { state: t, previous: e }));
@@ -388,13 +492,13 @@ class B {
388
492
  }
389
493
  /** Переводит ось соединения (если нужно), логирует и эмитит error (§1.4);
390
494
  * возвращает VoiceCallError — вызывающий сам решает throw или нет. */
391
- raiseError(t, e, n) {
495
+ raiseError(t, e, i) {
392
496
  e !== this.connectionState && this.setConnectionState(e);
393
- const i = new _(t, this.connectionState, n);
394
- return this.log.error(`[voice-client] ${t}`, n), this.emitter.emit("error", i), i;
497
+ const n = new O(t, this.connectionState, i);
498
+ return this.log.error(`[voice-client] ${t}`, i), this.emitter.emit("error", n), n;
395
499
  }
396
500
  }
397
501
  export {
398
- _ as VoiceCallError,
399
- B as VoiceClient
502
+ O as VoiceCallError,
503
+ X as VoiceClient
400
504
  };
package/dist/types.d.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  export type ConnectionState = 'idle' | 'connecting' | 'connected' | 'reconnecting' | 'disconnected' | 'error';
3
3
  /** Ось активности агента (D4 §1.1) — из атрибута участника lk.agent.state. */
4
4
  export type AgentState = 'unknown' | 'initializing' | 'listening' | 'thinking' | 'speaking';
5
- export type VoiceErrorCode = 'INVALID_PARAMS' | 'WEBRTC_UNSUPPORTED' | 'MICROPHONE_PERMISSION_DENIED' | 'MICROPHONE_UNAVAILABLE' | 'TOKEN_EXPIRED' | 'TOKEN_INVALID' | 'CONNECTION_FAILED' | 'CONNECTION_LOST' | 'ALREADY_IN_CALL' | 'NOT_IN_CALL' | 'SERVER_ERROR';
5
+ export type VoiceErrorCode = 'AUDIO_PLAYBACK_BLOCKED' | 'INVALID_PARAMS' | 'WEBRTC_UNSUPPORTED' | 'MICROPHONE_PERMISSION_DENIED' | 'MICROPHONE_UNAVAILABLE' | 'TOKEN_EXPIRED' | 'TOKEN_INVALID' | 'CONNECTION_FAILED' | 'CONNECTION_LOST' | 'ALREADY_IN_CALL' | 'NOT_IN_CALL' | 'SERVER_ERROR';
6
6
  export interface VoiceError {
7
7
  code: VoiceErrorCode;
8
8
  message: string;
@@ -72,7 +72,19 @@ export interface TranscriptEvent {
72
72
  /** Идентификатор реплики: сшивает дельты одного сегмента в один пузырь. */
73
73
  id: string;
74
74
  }
75
+ /**
76
+ * VPT-857 §1.3: the avatar worker's video track was subscribed (true) or went
77
+ * away (false). Reset to false on call end, failed startup and destroy().
78
+ */
79
+ export interface AgentVideoChangedEvent {
80
+ available: boolean;
81
+ }
75
82
  export interface VoiceClientEventMap {
83
+ microphoneStateChanged: {
84
+ enabled: boolean;
85
+ };
86
+ agentVideoChanged: AgentVideoChangedEvent;
87
+ audioPlaybackBlocked: Record<string, never>;
76
88
  callStarted: CallStartedEvent;
77
89
  callEnded: CallEndedEvent;
78
90
  connectionStateChanged: ConnectionStateChangedEvent;