@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/README.md +274 -411
- package/README.ru.md +338 -0
- package/dist/errors.d.ts +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +256 -152
- package/dist/types.d.ts +13 -1
- package/dist/voice-client.bundle.js +6 -6
- package/dist/voice-client.d.ts +18 -0
- package/dist/widget-avatar.d.ts +7 -0
- package/dist/widget.bundle.js +267 -0
- package/dist/widget.css +597 -0
- package/dist/widget.d.ts +40 -0
- package/dist/widget.js +787 -0
- package/package.json +20 -7
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ConnectionError as
|
|
2
|
-
class
|
|
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
|
|
23
|
+
class N {
|
|
24
24
|
constructor() {
|
|
25
25
|
this.listeners = /* @__PURE__ */ new Map();
|
|
26
26
|
}
|
|
27
27
|
on(t, e) {
|
|
28
|
-
let
|
|
29
|
-
|
|
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
|
|
33
|
-
(
|
|
32
|
+
var i;
|
|
33
|
+
(i = this.listeners.get(t)) == null || i.delete(e);
|
|
34
34
|
}
|
|
35
35
|
once(t, e) {
|
|
36
|
-
const
|
|
37
|
-
this.off(t,
|
|
36
|
+
const i = (n) => {
|
|
37
|
+
this.off(t, i), e(n);
|
|
38
38
|
};
|
|
39
|
-
this.on(t,
|
|
39
|
+
this.on(t, i);
|
|
40
40
|
}
|
|
41
41
|
emit(t, e) {
|
|
42
|
-
const
|
|
43
|
-
if (
|
|
44
|
-
for (const
|
|
45
|
-
|
|
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
|
|
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
|
|
65
|
-
constructor(t, e,
|
|
66
|
-
super(
|
|
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
|
|
70
|
-
if (
|
|
71
|
-
switch (
|
|
72
|
-
case
|
|
70
|
+
function P(s) {
|
|
71
|
+
if (s instanceof b)
|
|
72
|
+
switch (s.reason) {
|
|
73
|
+
case p.NotAllowed:
|
|
73
74
|
return "TOKEN_INVALID";
|
|
74
|
-
case
|
|
75
|
-
case
|
|
76
|
-
case
|
|
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
|
|
84
|
-
return
|
|
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]",
|
|
87
|
-
function
|
|
88
|
-
if (typeof
|
|
89
|
-
return
|
|
90
|
-
if (Array.isArray(
|
|
91
|
-
return
|
|
92
|
-
if (
|
|
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,
|
|
95
|
-
t[e] =
|
|
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
|
|
109
|
+
return s;
|
|
99
110
|
}
|
|
100
|
-
function
|
|
101
|
-
if (
|
|
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: (
|
|
107
|
-
debug: (
|
|
117
|
+
error: (i, ...n) => console.error(i, ...n),
|
|
118
|
+
debug: (i, ...n) => console.debug(i, ...n)
|
|
108
119
|
};
|
|
109
120
|
return {
|
|
110
|
-
error: (
|
|
111
|
-
debug: (
|
|
112
|
-
|
|
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
|
|
127
|
+
function B(s = !0) {
|
|
117
128
|
var t;
|
|
118
|
-
return typeof window > "u" || !window.isSecureContext || typeof RTCPeerConnection > "u" ? !1 :
|
|
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
|
|
121
|
-
const t =
|
|
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, "/"),
|
|
125
|
-
Array.from(atob(e)).map((
|
|
126
|
-
),
|
|
127
|
-
return typeof
|
|
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
|
|
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
|
-
]),
|
|
138
|
-
class
|
|
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
|
|
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 (
|
|
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
|
|
184
|
-
this.playAudio =
|
|
185
|
-
const { accessToken:
|
|
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
|
|
188
|
-
if (this.room =
|
|
189
|
-
await this.abandonConnectingCall(
|
|
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
|
-
|
|
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
|
|
196
|
-
if (!
|
|
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 (!
|
|
224
|
+
if (!B(e))
|
|
199
225
|
throw this.raiseError("WEBRTC_UNSUPPORTED", "error");
|
|
200
|
-
const
|
|
201
|
-
if (
|
|
226
|
+
const c = U(i);
|
|
227
|
+
if (c !== null && c * 1e3 <= Date.now())
|
|
202
228
|
throw this.raiseError("TOKEN_EXPIRED", "error");
|
|
203
|
-
return { accessToken:
|
|
229
|
+
return { accessToken: i, url: n };
|
|
204
230
|
}
|
|
205
|
-
async connectRoom(t, e,
|
|
231
|
+
async connectRoom(t, e, i) {
|
|
206
232
|
try {
|
|
207
233
|
await t.connect(
|
|
208
234
|
e,
|
|
209
|
-
|
|
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 (
|
|
216
|
-
|
|
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),
|
|
254
|
+
await t.localParticipant.setMicrophoneEnabled(!0), this.room === t && !this.destroyed && this.confirmMicrophone(t.localParticipant.isMicrophoneEnabled);
|
|
228
255
|
} catch (e) {
|
|
229
|
-
|
|
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(
|
|
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 || !
|
|
314
|
+
if (!e || !A.has(this.connectionState))
|
|
242
315
|
throw this.raiseError("NOT_IN_CALL", this.connectionState);
|
|
243
|
-
const
|
|
244
|
-
if (
|
|
316
|
+
const i = t.trim().slice(0, j);
|
|
317
|
+
if (i)
|
|
245
318
|
try {
|
|
246
|
-
await e.localParticipant.sendText(
|
|
247
|
-
} catch (
|
|
248
|
-
throw this.raiseError("SERVER_ERROR", this.connectionState,
|
|
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 || !
|
|
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 = (
|
|
333
|
+
const e = (r, h, d) => {
|
|
261
334
|
this.log.debug("[voice-client] remote track subscribed", {
|
|
262
|
-
participant:
|
|
263
|
-
participantID:
|
|
264
|
-
trackID:
|
|
265
|
-
streamTrackID:
|
|
266
|
-
kind:
|
|
267
|
-
source:
|
|
268
|
-
muted:
|
|
269
|
-
}),
|
|
270
|
-
},
|
|
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:
|
|
283
|
-
|
|
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
|
-
},
|
|
358
|
+
}, c = (r) => {
|
|
286
359
|
this.log.debug("[voice-client] remote participant connected", {
|
|
287
360
|
room: t.name,
|
|
288
|
-
participant:
|
|
289
|
-
participantID:
|
|
361
|
+
participant: r.identity,
|
|
362
|
+
participantID: r.sid
|
|
290
363
|
}), !this.destroyed && this.syncAgentState(t);
|
|
291
|
-
},
|
|
292
|
-
this.destroyed || this.syncAgentState(t);
|
|
293
|
-
},
|
|
294
|
-
this.destroyed || this.applyLkConnectionState(
|
|
295
|
-
},
|
|
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:
|
|
371
|
+
reason: r,
|
|
299
372
|
stoppingLocally: this.stoppingLocally
|
|
300
|
-
}), !this.destroyed && this.handleDisconnected(
|
|
373
|
+
}), !this.destroyed && this.handleDisconnected(r);
|
|
301
374
|
};
|
|
302
|
-
t.registerTextStreamHandler(R, (
|
|
375
|
+
t.registerTextStreamHandler(R, (r, h) => {
|
|
303
376
|
if (this.destroyed) return;
|
|
304
|
-
const
|
|
305
|
-
this.readTranscriptSegment(
|
|
306
|
-
})
|
|
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,
|
|
310
|
-
() => t.off(a.TrackSubscriptionFailed,
|
|
311
|
-
() => t.off(a.ParticipantAttributesChanged,
|
|
312
|
-
() => t.off(a.ParticipantConnected,
|
|
313
|
-
() => t.off(a.ParticipantDisconnected,
|
|
314
|
-
() => t.off(a.ConnectionStateChanged,
|
|
315
|
-
() => t.off(a.Disconnected,
|
|
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
|
|
337
|
-
const n = ((
|
|
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
|
|
340
|
-
let
|
|
417
|
+
var u;
|
|
418
|
+
let o = "";
|
|
341
419
|
try {
|
|
342
|
-
for await (const
|
|
343
|
-
|
|
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
|
-
((
|
|
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
|
-
|
|
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,
|
|
355
|
-
if (this.teardownRoom(),
|
|
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 (
|
|
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
|
|
364
|
-
this.setConnectionState("disconnected"), this.emitter.emit("callEnded", { callId: e, reason:
|
|
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
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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,
|
|
495
|
+
raiseError(t, e, i) {
|
|
392
496
|
e !== this.connectionState && this.setConnectionState(e);
|
|
393
|
-
const
|
|
394
|
-
return this.log.error(`[voice-client] ${t}`,
|
|
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
|
-
|
|
399
|
-
|
|
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;
|