@craftedxp/voice-js 0.4.2 → 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/CONSUMING.md +10 -2
- package/README.md +69 -11
- package/dist/browser.d.mts +242 -1
- package/dist/browser.d.ts +242 -1
- package/dist/browser.js +301 -2
- package/dist/browser.js.map +1 -1
- package/dist/browser.mjs +301 -1
- package/dist/browser.mjs.map +1 -1
- package/dist/embed.iife.js +43 -3
- package/dist/node.d.mts +240 -1
- package/dist/node.d.ts +240 -1
- package/dist/node.js +27 -2
- package/dist/node.js.map +1 -1
- package/dist/node.mjs +25 -1
- package/dist/node.mjs.map +1 -1
- package/package.json +5 -1
package/dist/browser.mjs
CHANGED
|
@@ -1015,6 +1015,288 @@ async function createWebRtcCall(opts) {
|
|
|
1015
1015
|
};
|
|
1016
1016
|
}
|
|
1017
1017
|
|
|
1018
|
+
// src/room.ts
|
|
1019
|
+
import {
|
|
1020
|
+
Room,
|
|
1021
|
+
RoomEvent,
|
|
1022
|
+
Track
|
|
1023
|
+
} from "livekit-client";
|
|
1024
|
+
|
|
1025
|
+
// src/roomProtocol.ts
|
|
1026
|
+
var SYSTEM_TOPIC = "system";
|
|
1027
|
+
var TRANSCRIPT_TOPIC = "transcript";
|
|
1028
|
+
var decodeSystem = (bytes) => {
|
|
1029
|
+
try {
|
|
1030
|
+
const v = JSON.parse(new TextDecoder().decode(bytes));
|
|
1031
|
+
if (v && typeof v.kind === "string") return v;
|
|
1032
|
+
return null;
|
|
1033
|
+
} catch {
|
|
1034
|
+
return null;
|
|
1035
|
+
}
|
|
1036
|
+
};
|
|
1037
|
+
var decodeTranscript = (bytes) => {
|
|
1038
|
+
try {
|
|
1039
|
+
const v = JSON.parse(new TextDecoder().decode(bytes));
|
|
1040
|
+
if (v && v.kind === "partial") return v;
|
|
1041
|
+
return null;
|
|
1042
|
+
} catch {
|
|
1043
|
+
return null;
|
|
1044
|
+
}
|
|
1045
|
+
};
|
|
1046
|
+
|
|
1047
|
+
// src/room.ts
|
|
1048
|
+
var identityToPid = (identity) => identity.startsWith("guest:") ? identity.slice("guest:".length) : identity;
|
|
1049
|
+
var joinRoom = async (opts) => {
|
|
1050
|
+
const exchangeUrl = `${opts.apiBase.replace(/\/+$/, "")}/v1/rooms/${encodeURIComponent(
|
|
1051
|
+
opts.roomId
|
|
1052
|
+
)}/join`;
|
|
1053
|
+
const exchangeRes = await fetch(exchangeUrl, {
|
|
1054
|
+
method: "POST",
|
|
1055
|
+
headers: { "Content-Type": "application/json" },
|
|
1056
|
+
body: JSON.stringify({ code: opts.joinCode, name: opts.name })
|
|
1057
|
+
});
|
|
1058
|
+
if (!exchangeRes.ok) {
|
|
1059
|
+
const err = await exchangeRes.json().catch(() => ({}));
|
|
1060
|
+
throw new Error(err.error?.code ?? `join_failed_${exchangeRes.status}`);
|
|
1061
|
+
}
|
|
1062
|
+
const exchange = await exchangeRes.json();
|
|
1063
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
1064
|
+
const emit = (e, payload) => {
|
|
1065
|
+
handlers.get(e)?.forEach((h) => {
|
|
1066
|
+
try {
|
|
1067
|
+
h(payload);
|
|
1068
|
+
} catch {
|
|
1069
|
+
}
|
|
1070
|
+
});
|
|
1071
|
+
};
|
|
1072
|
+
const room = new Room({ adaptiveStream: true, dynacast: true });
|
|
1073
|
+
room.on(
|
|
1074
|
+
RoomEvent.ParticipantConnected,
|
|
1075
|
+
(p) => emit("participant.joined", {
|
|
1076
|
+
participantId: identityToPid(p.identity),
|
|
1077
|
+
name: p.name ?? ""
|
|
1078
|
+
})
|
|
1079
|
+
);
|
|
1080
|
+
room.on(
|
|
1081
|
+
RoomEvent.ParticipantDisconnected,
|
|
1082
|
+
(p) => emit("participant.left", {
|
|
1083
|
+
participantId: identityToPid(p.identity),
|
|
1084
|
+
name: p.name ?? ""
|
|
1085
|
+
})
|
|
1086
|
+
);
|
|
1087
|
+
room.on(RoomEvent.Disconnected, () => emit("room.ended", void 0));
|
|
1088
|
+
room.on(RoomEvent.DataReceived, (data, _participant, _kind, topic) => {
|
|
1089
|
+
if (topic === SYSTEM_TOPIC) {
|
|
1090
|
+
const m = decodeSystem(data);
|
|
1091
|
+
if (m) emit("system.message", m);
|
|
1092
|
+
} else if (topic === TRANSCRIPT_TOPIC) {
|
|
1093
|
+
const m = decodeTranscript(data);
|
|
1094
|
+
if (m) emit("transcript.partial", m);
|
|
1095
|
+
}
|
|
1096
|
+
});
|
|
1097
|
+
const trackKind = (t) => t.kind === Track.Kind.Video ? "video" : "audio";
|
|
1098
|
+
const trackSource = (s) => {
|
|
1099
|
+
switch (s) {
|
|
1100
|
+
case Track.Source.Camera:
|
|
1101
|
+
return "camera";
|
|
1102
|
+
case Track.Source.Microphone:
|
|
1103
|
+
return "microphone";
|
|
1104
|
+
case Track.Source.ScreenShare:
|
|
1105
|
+
return "screen_share";
|
|
1106
|
+
case Track.Source.ScreenShareAudio:
|
|
1107
|
+
return "screen_share_audio";
|
|
1108
|
+
default:
|
|
1109
|
+
return "unknown";
|
|
1110
|
+
}
|
|
1111
|
+
};
|
|
1112
|
+
room.on(
|
|
1113
|
+
RoomEvent.TrackSubscribed,
|
|
1114
|
+
(track, pub, participant) => emit("track.subscribed", {
|
|
1115
|
+
participantId: identityToPid(participant.identity),
|
|
1116
|
+
kind: trackKind(track),
|
|
1117
|
+
source: trackSource(pub.source),
|
|
1118
|
+
track
|
|
1119
|
+
})
|
|
1120
|
+
);
|
|
1121
|
+
room.on(
|
|
1122
|
+
RoomEvent.TrackUnsubscribed,
|
|
1123
|
+
(track, pub, participant) => emit("track.unsubscribed", {
|
|
1124
|
+
participantId: identityToPid(participant.identity),
|
|
1125
|
+
kind: trackKind(track),
|
|
1126
|
+
source: trackSource(pub.source),
|
|
1127
|
+
track
|
|
1128
|
+
})
|
|
1129
|
+
);
|
|
1130
|
+
room.on(
|
|
1131
|
+
RoomEvent.ActiveSpeakersChanged,
|
|
1132
|
+
(speakers) => emit(
|
|
1133
|
+
"active.speakers",
|
|
1134
|
+
speakers.map((p) => identityToPid(p.identity))
|
|
1135
|
+
)
|
|
1136
|
+
);
|
|
1137
|
+
await room.connect(exchange.livekit.url, exchange.livekit.token);
|
|
1138
|
+
return {
|
|
1139
|
+
participantId: exchange.participantId,
|
|
1140
|
+
get participants() {
|
|
1141
|
+
return [...room.remoteParticipants.values()].map((p) => ({
|
|
1142
|
+
participantId: identityToPid(p.identity),
|
|
1143
|
+
name: p.name ?? ""
|
|
1144
|
+
}));
|
|
1145
|
+
},
|
|
1146
|
+
on(event, handler) {
|
|
1147
|
+
const set = handlers.get(event) ?? /* @__PURE__ */ new Set();
|
|
1148
|
+
set.add(handler);
|
|
1149
|
+
handlers.set(event, set);
|
|
1150
|
+
},
|
|
1151
|
+
publishMic: async () => {
|
|
1152
|
+
await room.localParticipant.setMicrophoneEnabled(true);
|
|
1153
|
+
},
|
|
1154
|
+
publishCamera: async () => {
|
|
1155
|
+
await room.localParticipant.setCameraEnabled(true);
|
|
1156
|
+
},
|
|
1157
|
+
setMicEnabled: async (on) => {
|
|
1158
|
+
await room.localParticipant.setMicrophoneEnabled(on);
|
|
1159
|
+
},
|
|
1160
|
+
setCameraEnabled: async (on) => {
|
|
1161
|
+
await room.localParticipant.setCameraEnabled(on);
|
|
1162
|
+
},
|
|
1163
|
+
isMicEnabled: () => room.localParticipant.isMicrophoneEnabled,
|
|
1164
|
+
isCameraEnabled: () => room.localParticipant.isCameraEnabled,
|
|
1165
|
+
getLocalCameraTrack: () => room.localParticipant.getTrackPublication(Track.Source.Camera)?.videoTrack ?? null,
|
|
1166
|
+
getRemoteTracks: () => {
|
|
1167
|
+
const out = [];
|
|
1168
|
+
for (const p of room.remoteParticipants.values()) {
|
|
1169
|
+
for (const pub of p.trackPublications.values()) {
|
|
1170
|
+
const track = pub.track;
|
|
1171
|
+
if (!track) continue;
|
|
1172
|
+
out.push({
|
|
1173
|
+
participantId: identityToPid(p.identity),
|
|
1174
|
+
kind: trackKind(track),
|
|
1175
|
+
source: trackSource(pub.source),
|
|
1176
|
+
track
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
return out;
|
|
1181
|
+
},
|
|
1182
|
+
setScreenShareEnabled: async (on, opts2) => {
|
|
1183
|
+
await room.localParticipant.setScreenShareEnabled(on, { audio: opts2?.audio ?? false });
|
|
1184
|
+
},
|
|
1185
|
+
isScreenShareEnabled: () => room.localParticipant.isScreenShareEnabled,
|
|
1186
|
+
getLocalScreenTrack: () => room.localParticipant.getTrackPublication(Track.Source.ScreenShare)?.videoTrack ?? null,
|
|
1187
|
+
leave: async () => {
|
|
1188
|
+
await room.disconnect();
|
|
1189
|
+
}
|
|
1190
|
+
};
|
|
1191
|
+
};
|
|
1192
|
+
|
|
1193
|
+
// src/textSession.ts
|
|
1194
|
+
async function startTextSession(opts) {
|
|
1195
|
+
const f = opts.fetch ?? fetch;
|
|
1196
|
+
const tokenQs = `?token=${encodeURIComponent(opts.token)}`;
|
|
1197
|
+
const startUrl = `${opts.baseUrl}/v1/agents/${opts.agentId}/chat${tokenQs}`;
|
|
1198
|
+
const startBody = opts.text ? JSON.stringify({ text: opts.text }) : "{}";
|
|
1199
|
+
const res = await f(startUrl, {
|
|
1200
|
+
method: "POST",
|
|
1201
|
+
headers: { "Content-Type": "application/json", Accept: "text/event-stream" },
|
|
1202
|
+
body: startBody
|
|
1203
|
+
});
|
|
1204
|
+
if (!res.ok || !res.body) {
|
|
1205
|
+
const text = await res.text().catch(() => "");
|
|
1206
|
+
throw new Error(`startTextSession failed: ${res.status} ${text}`);
|
|
1207
|
+
}
|
|
1208
|
+
const iter = parseSse(res.body);
|
|
1209
|
+
let chatId = "";
|
|
1210
|
+
let callId = "";
|
|
1211
|
+
const buffered = [];
|
|
1212
|
+
const it = iter[Symbol.asyncIterator]();
|
|
1213
|
+
while (true) {
|
|
1214
|
+
const { value, done } = await it.next();
|
|
1215
|
+
if (done) break;
|
|
1216
|
+
if (value.type === "chat.started") {
|
|
1217
|
+
chatId = value.chatId;
|
|
1218
|
+
callId = value.callId;
|
|
1219
|
+
break;
|
|
1220
|
+
}
|
|
1221
|
+
buffered.push(value);
|
|
1222
|
+
}
|
|
1223
|
+
return {
|
|
1224
|
+
id: chatId,
|
|
1225
|
+
callId,
|
|
1226
|
+
greeting: replayThen(buffered, { [Symbol.asyncIterator]: () => it }),
|
|
1227
|
+
async send(text) {
|
|
1228
|
+
const r = await f(`${opts.baseUrl}/v1/chats/${chatId}/messages${tokenQs}`, {
|
|
1229
|
+
method: "POST",
|
|
1230
|
+
headers: { "Content-Type": "application/json", Accept: "text/event-stream" },
|
|
1231
|
+
body: JSON.stringify({ text })
|
|
1232
|
+
});
|
|
1233
|
+
if (!r.ok || !r.body) {
|
|
1234
|
+
const errText = await r.text().catch(() => "");
|
|
1235
|
+
throw new Error(`send failed: ${r.status} ${errText}`);
|
|
1236
|
+
}
|
|
1237
|
+
return parseSse(r.body);
|
|
1238
|
+
},
|
|
1239
|
+
async end() {
|
|
1240
|
+
await f(`${opts.baseUrl}/v1/calls/${callId}`, { method: "DELETE" });
|
|
1241
|
+
}
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
async function* parseSse(body) {
|
|
1245
|
+
const reader = body.getReader();
|
|
1246
|
+
const decoder = new TextDecoder();
|
|
1247
|
+
let buf = "";
|
|
1248
|
+
while (true) {
|
|
1249
|
+
const { value, done } = await reader.read();
|
|
1250
|
+
if (done) return;
|
|
1251
|
+
buf += decoder.decode(value, { stream: true });
|
|
1252
|
+
let idx;
|
|
1253
|
+
while ((idx = buf.indexOf("\n\n")) >= 0) {
|
|
1254
|
+
const chunk = buf.slice(0, idx);
|
|
1255
|
+
buf = buf.slice(idx + 2);
|
|
1256
|
+
let event = "message";
|
|
1257
|
+
let data = "";
|
|
1258
|
+
for (const line of chunk.split("\n")) {
|
|
1259
|
+
if (line.startsWith(":")) continue;
|
|
1260
|
+
if (line.startsWith("event:")) event = line.slice(6).trim();
|
|
1261
|
+
else if (line.startsWith("data:")) data += line.slice(5).trim();
|
|
1262
|
+
}
|
|
1263
|
+
if (!data) continue;
|
|
1264
|
+
try {
|
|
1265
|
+
const parsed = JSON.parse(data);
|
|
1266
|
+
yield { type: event, ...parsed };
|
|
1267
|
+
} catch {
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
async function* replayThen(buffered, rest) {
|
|
1273
|
+
for (const x of buffered) yield x;
|
|
1274
|
+
for await (const x of rest) yield x;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
// src/incomingCall.ts
|
|
1278
|
+
var parseIncomingCall = (raw) => {
|
|
1279
|
+
if (typeof raw !== "object" || raw === null) {
|
|
1280
|
+
throw new Error("parseIncomingCall: payload must be an object");
|
|
1281
|
+
}
|
|
1282
|
+
const p = raw;
|
|
1283
|
+
if (typeof p.token !== "string" || !p.token.startsWith("ct_")) {
|
|
1284
|
+
throw new Error("parseIncomingCall: missing or invalid `token` (expected a ct_ string)");
|
|
1285
|
+
}
|
|
1286
|
+
if (typeof p.agentId !== "string" || p.agentId.length === 0) {
|
|
1287
|
+
throw new Error("parseIncomingCall: missing `agentId`");
|
|
1288
|
+
}
|
|
1289
|
+
const transport = p.transport === "webrtc" ? "webrtc" : "ws";
|
|
1290
|
+
const out = { token: p.token, agentId: p.agentId, transport };
|
|
1291
|
+
if (transport === "webrtc" && typeof p.webrtcGatewayBase === "string") {
|
|
1292
|
+
out.webrtcGatewayBase = p.webrtcGatewayBase;
|
|
1293
|
+
}
|
|
1294
|
+
if (typeof p.expiresAt === "number") out.expiresAt = p.expiresAt;
|
|
1295
|
+
if (typeof p.agentName === "string") out.agentName = p.agentName;
|
|
1296
|
+
if (typeof p.agentAvatarUrl === "string") out.agentAvatarUrl = p.agentAvatarUrl;
|
|
1297
|
+
return out;
|
|
1298
|
+
};
|
|
1299
|
+
|
|
1018
1300
|
// src/browser.ts
|
|
1019
1301
|
var browserWsFactory = (url) => new globalThis.WebSocket(url);
|
|
1020
1302
|
var BrowserVoiceFactory = class {
|
|
@@ -1072,6 +1354,21 @@ var BrowserVoiceFactory = class {
|
|
|
1072
1354
|
await client.start();
|
|
1073
1355
|
return client;
|
|
1074
1356
|
};
|
|
1357
|
+
// Multi-party rooms (Phase 7 video).
|
|
1358
|
+
//
|
|
1359
|
+
// The guest's browser calls this with the roomId + joinCode it parsed
|
|
1360
|
+
// out of the invite link. The SDK exchanges the code for a LiveKit
|
|
1361
|
+
// JWT against `${apiBase}/v1/rooms/:roomId/join` (an AUTH-EXEMPT
|
|
1362
|
+
// endpoint — the opaque code is the only credential), then connects
|
|
1363
|
+
// to LiveKit and returns a typed event surface.
|
|
1364
|
+
this.joinRoom = (opts) => joinRoom({ apiBase: this.config.apiBase, ...opts });
|
|
1365
|
+
// Text-channel chat session (no microphone / audio).
|
|
1366
|
+
// Mint a `ct_` token with `channel: 'text'` on your backend, then call
|
|
1367
|
+
// this to open an SSE stream against the chat API.
|
|
1368
|
+
this.startTextSession = (opts) => startTextSession({
|
|
1369
|
+
...opts,
|
|
1370
|
+
baseUrl: this.config.apiBase
|
|
1371
|
+
});
|
|
1075
1372
|
this.config = config;
|
|
1076
1373
|
}
|
|
1077
1374
|
};
|
|
@@ -1085,6 +1382,9 @@ export {
|
|
|
1085
1382
|
createAudioPlayback,
|
|
1086
1383
|
createProtocolState,
|
|
1087
1384
|
createReconnectingWebSocket,
|
|
1088
|
-
handleServerMessage
|
|
1385
|
+
handleServerMessage,
|
|
1386
|
+
joinRoom,
|
|
1387
|
+
parseIncomingCall,
|
|
1388
|
+
startTextSession
|
|
1089
1389
|
};
|
|
1090
1390
|
//# sourceMappingURL=browser.mjs.map
|