@craftedxp/voice-js 0.4.1 → 0.5.4

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/browser.js CHANGED
@@ -29,6 +29,8 @@ __export(browser_exports, {
29
29
  createProtocolState: () => createProtocolState,
30
30
  createReconnectingWebSocket: () => createReconnectingWebSocket,
31
31
  handleServerMessage: () => handleServerMessage,
32
+ joinRoom: () => joinRoom,
33
+ parseIncomingCall: () => parseIncomingCall,
32
34
  })
33
35
  module.exports = __toCommonJS(browser_exports)
34
36
 
@@ -958,8 +960,45 @@ async function createWebRtcCall(opts) {
958
960
  const iceUrl = gateway
959
961
  ? `${gateway}/webrtc/ice?token=${encodeURIComponent(opts.token)}`
960
962
  : `${opts.apiBase}/v1/agents/${encodeURIComponent(opts.agentId)}/webrtc/ice?token=${encodeURIComponent(opts.token)}`
963
+ const teardown = () => {
964
+ if (ended) return
965
+ ended = true
966
+ try {
967
+ mic.getTracks().forEach((t) => t.stop())
968
+ } catch {}
969
+ try {
970
+ pc.close()
971
+ } catch {}
972
+ try {
973
+ audioEl.remove()
974
+ } catch {}
975
+ fireState('ended')
976
+ opts.onEnd?.()
977
+ }
978
+ let callId = null
979
+ const pendingCandidates = []
980
+ const postCandidate = (candidate) => {
981
+ void fetch(iceUrl, {
982
+ method: 'POST',
983
+ headers: { 'content-type': 'application/json' },
984
+ body: JSON.stringify({ callId, candidate }),
985
+ }).catch(() => {})
986
+ }
987
+ pc.onicecandidate = (e) => {
988
+ if (!e.candidate) return
989
+ if (callId) postCandidate(e.candidate)
990
+ else pendingCandidates.push(e.candidate)
991
+ }
992
+ pc.onconnectionstatechange = () => {
993
+ const s = pc.connectionState
994
+ if (s === 'connected') fireState('listening')
995
+ if (s === 'failed' || s === 'disconnected') {
996
+ opts.onError?.({ code: 'socket_error', message: `webrtc connection ${s}` })
997
+ teardown()
998
+ }
999
+ if (s === 'closed' && !ended) teardown()
1000
+ }
961
1001
  await pc.setLocalDescription(await pc.createOffer())
962
- let callId
963
1002
  try {
964
1003
  const offerRes = await fetch(offerUrl, {
965
1004
  method: 'POST',
@@ -978,6 +1017,7 @@ async function createWebRtcCall(opts) {
978
1017
  const body = await offerRes.json()
979
1018
  callId = body.callId
980
1019
  await pc.setRemoteDescription({ type: 'answer', sdp: body.sdp })
1020
+ while (pendingCandidates.length > 0) postCandidate(pendingCandidates.shift())
981
1021
  } catch (err) {
982
1022
  if (!ended) {
983
1023
  opts.onError?.({
@@ -991,38 +1031,6 @@ async function createWebRtcCall(opts) {
991
1031
  }
992
1032
  throw err
993
1033
  }
994
- pc.onicecandidate = (e) => {
995
- if (!e.candidate) return
996
- void fetch(iceUrl, {
997
- method: 'POST',
998
- headers: { 'content-type': 'application/json' },
999
- body: JSON.stringify({ callId, candidate: e.candidate }),
1000
- }).catch(() => {})
1001
- }
1002
- pc.onconnectionstatechange = () => {
1003
- const s = pc.connectionState
1004
- if (s === 'connected') fireState('listening')
1005
- if (s === 'failed' || s === 'disconnected') {
1006
- opts.onError?.({ code: 'socket_error', message: `webrtc connection ${s}` })
1007
- teardown()
1008
- }
1009
- if (s === 'closed' && !ended) teardown()
1010
- }
1011
- const teardown = () => {
1012
- if (ended) return
1013
- ended = true
1014
- try {
1015
- mic.getTracks().forEach((t) => t.stop())
1016
- } catch {}
1017
- try {
1018
- pc.close()
1019
- } catch {}
1020
- try {
1021
- audioEl.remove()
1022
- } catch {}
1023
- fireState('ended')
1024
- opts.onEnd?.()
1025
- }
1026
1034
  return {
1027
1035
  get state() {
1028
1036
  return proto.state
@@ -1047,6 +1055,199 @@ async function createWebRtcCall(opts) {
1047
1055
  }
1048
1056
  }
1049
1057
 
1058
+ // src/room.ts
1059
+ var import_livekit_client = require('livekit-client')
1060
+
1061
+ // src/roomProtocol.ts
1062
+ var SYSTEM_TOPIC = 'system'
1063
+ var TRANSCRIPT_TOPIC = 'transcript'
1064
+ var decodeSystem = (bytes) => {
1065
+ try {
1066
+ const v = JSON.parse(new TextDecoder().decode(bytes))
1067
+ if (v && typeof v.kind === 'string') return v
1068
+ return null
1069
+ } catch {
1070
+ return null
1071
+ }
1072
+ }
1073
+ var decodeTranscript = (bytes) => {
1074
+ try {
1075
+ const v = JSON.parse(new TextDecoder().decode(bytes))
1076
+ if (v && v.kind === 'partial') return v
1077
+ return null
1078
+ } catch {
1079
+ return null
1080
+ }
1081
+ }
1082
+
1083
+ // src/room.ts
1084
+ var identityToPid = (identity) =>
1085
+ identity.startsWith('guest:') ? identity.slice('guest:'.length) : identity
1086
+ var joinRoom = async (opts) => {
1087
+ const exchangeUrl = `${opts.apiBase.replace(/\/+$/, '')}/v1/rooms/${encodeURIComponent(
1088
+ opts.roomId,
1089
+ )}/join`
1090
+ const exchangeRes = await fetch(exchangeUrl, {
1091
+ method: 'POST',
1092
+ headers: { 'Content-Type': 'application/json' },
1093
+ body: JSON.stringify({ code: opts.joinCode, name: opts.name }),
1094
+ })
1095
+ if (!exchangeRes.ok) {
1096
+ const err = await exchangeRes.json().catch(() => ({}))
1097
+ throw new Error(err.error?.code ?? `join_failed_${exchangeRes.status}`)
1098
+ }
1099
+ const exchange = await exchangeRes.json()
1100
+ const handlers = /* @__PURE__ */ new Map()
1101
+ const emit = (e, payload) => {
1102
+ handlers.get(e)?.forEach((h) => {
1103
+ try {
1104
+ h(payload)
1105
+ } catch {}
1106
+ })
1107
+ }
1108
+ const room = new import_livekit_client.Room({ adaptiveStream: true, dynacast: true })
1109
+ room.on(import_livekit_client.RoomEvent.ParticipantConnected, (p) =>
1110
+ emit('participant.joined', {
1111
+ participantId: identityToPid(p.identity),
1112
+ name: p.name ?? '',
1113
+ }),
1114
+ )
1115
+ room.on(import_livekit_client.RoomEvent.ParticipantDisconnected, (p) =>
1116
+ emit('participant.left', {
1117
+ participantId: identityToPid(p.identity),
1118
+ name: p.name ?? '',
1119
+ }),
1120
+ )
1121
+ room.on(import_livekit_client.RoomEvent.Disconnected, () => emit('room.ended', void 0))
1122
+ room.on(import_livekit_client.RoomEvent.DataReceived, (data, _participant, _kind, topic) => {
1123
+ if (topic === SYSTEM_TOPIC) {
1124
+ const m = decodeSystem(data)
1125
+ if (m) emit('system.message', m)
1126
+ } else if (topic === TRANSCRIPT_TOPIC) {
1127
+ const m = decodeTranscript(data)
1128
+ if (m) emit('transcript.partial', m)
1129
+ }
1130
+ })
1131
+ const trackKind = (t) => (t.kind === import_livekit_client.Track.Kind.Video ? 'video' : 'audio')
1132
+ const trackSource = (s) => {
1133
+ switch (s) {
1134
+ case import_livekit_client.Track.Source.Camera:
1135
+ return 'camera'
1136
+ case import_livekit_client.Track.Source.Microphone:
1137
+ return 'microphone'
1138
+ case import_livekit_client.Track.Source.ScreenShare:
1139
+ return 'screen_share'
1140
+ case import_livekit_client.Track.Source.ScreenShareAudio:
1141
+ return 'screen_share_audio'
1142
+ default:
1143
+ return 'unknown'
1144
+ }
1145
+ }
1146
+ room.on(import_livekit_client.RoomEvent.TrackSubscribed, (track, pub, participant) =>
1147
+ emit('track.subscribed', {
1148
+ participantId: identityToPid(participant.identity),
1149
+ kind: trackKind(track),
1150
+ source: trackSource(pub.source),
1151
+ track,
1152
+ }),
1153
+ )
1154
+ room.on(import_livekit_client.RoomEvent.TrackUnsubscribed, (track, pub, participant) =>
1155
+ emit('track.unsubscribed', {
1156
+ participantId: identityToPid(participant.identity),
1157
+ kind: trackKind(track),
1158
+ source: trackSource(pub.source),
1159
+ track,
1160
+ }),
1161
+ )
1162
+ room.on(import_livekit_client.RoomEvent.ActiveSpeakersChanged, (speakers) =>
1163
+ emit(
1164
+ 'active.speakers',
1165
+ speakers.map((p) => identityToPid(p.identity)),
1166
+ ),
1167
+ )
1168
+ await room.connect(exchange.livekit.url, exchange.livekit.token)
1169
+ return {
1170
+ participantId: exchange.participantId,
1171
+ get participants() {
1172
+ return [...room.remoteParticipants.values()].map((p) => ({
1173
+ participantId: identityToPid(p.identity),
1174
+ name: p.name ?? '',
1175
+ }))
1176
+ },
1177
+ on(event, handler) {
1178
+ const set = handlers.get(event) ?? /* @__PURE__ */ new Set()
1179
+ set.add(handler)
1180
+ handlers.set(event, set)
1181
+ },
1182
+ publishMic: async () => {
1183
+ await room.localParticipant.setMicrophoneEnabled(true)
1184
+ },
1185
+ publishCamera: async () => {
1186
+ await room.localParticipant.setCameraEnabled(true)
1187
+ },
1188
+ setMicEnabled: async (on) => {
1189
+ await room.localParticipant.setMicrophoneEnabled(on)
1190
+ },
1191
+ setCameraEnabled: async (on) => {
1192
+ await room.localParticipant.setCameraEnabled(on)
1193
+ },
1194
+ isMicEnabled: () => room.localParticipant.isMicrophoneEnabled,
1195
+ isCameraEnabled: () => room.localParticipant.isCameraEnabled,
1196
+ getLocalCameraTrack: () =>
1197
+ room.localParticipant.getTrackPublication(import_livekit_client.Track.Source.Camera)
1198
+ ?.videoTrack ?? null,
1199
+ getRemoteTracks: () => {
1200
+ const out = []
1201
+ for (const p of room.remoteParticipants.values()) {
1202
+ for (const pub of p.trackPublications.values()) {
1203
+ const track = pub.track
1204
+ if (!track) continue
1205
+ out.push({
1206
+ participantId: identityToPid(p.identity),
1207
+ kind: trackKind(track),
1208
+ source: trackSource(pub.source),
1209
+ track,
1210
+ })
1211
+ }
1212
+ }
1213
+ return out
1214
+ },
1215
+ setScreenShareEnabled: async (on, opts2) => {
1216
+ await room.localParticipant.setScreenShareEnabled(on, { audio: opts2?.audio ?? false })
1217
+ },
1218
+ isScreenShareEnabled: () => room.localParticipant.isScreenShareEnabled,
1219
+ getLocalScreenTrack: () =>
1220
+ room.localParticipant.getTrackPublication(import_livekit_client.Track.Source.ScreenShare)
1221
+ ?.videoTrack ?? null,
1222
+ leave: async () => {
1223
+ await room.disconnect()
1224
+ },
1225
+ }
1226
+ }
1227
+
1228
+ // src/incomingCall.ts
1229
+ var parseIncomingCall = (raw) => {
1230
+ if (typeof raw !== 'object' || raw === null) {
1231
+ throw new Error('parseIncomingCall: payload must be an object')
1232
+ }
1233
+ const p = raw
1234
+ if (typeof p.token !== 'string' || !p.token.startsWith('ct_')) {
1235
+ throw new Error('parseIncomingCall: missing or invalid `token` (expected a ct_ string)')
1236
+ }
1237
+ if (typeof p.agentId !== 'string' || p.agentId.length === 0) {
1238
+ throw new Error('parseIncomingCall: missing `agentId`')
1239
+ }
1240
+ const transport = p.transport === 'webrtc' ? 'webrtc' : 'ws'
1241
+ const out = { token: p.token, agentId: p.agentId, transport }
1242
+ if (transport === 'webrtc' && typeof p.webrtcGatewayBase === 'string') {
1243
+ out.webrtcGatewayBase = p.webrtcGatewayBase
1244
+ }
1245
+ if (typeof p.expiresAt === 'number') out.expiresAt = p.expiresAt
1246
+ if (typeof p.agentName === 'string') out.agentName = p.agentName
1247
+ if (typeof p.agentAvatarUrl === 'string') out.agentAvatarUrl = p.agentAvatarUrl
1248
+ return out
1249
+ }
1250
+
1050
1251
  // src/browser.ts
1051
1252
  var browserWsFactory = (url) => new globalThis.WebSocket(url)
1052
1253
  var BrowserVoiceFactory = class {
@@ -1106,6 +1307,14 @@ var BrowserVoiceFactory = class {
1106
1307
  await client.start()
1107
1308
  return client
1108
1309
  }
1310
+ // Multi-party rooms (Phase 7 video).
1311
+ //
1312
+ // The guest's browser calls this with the roomId + joinCode it parsed
1313
+ // out of the invite link. The SDK exchanges the code for a LiveKit
1314
+ // JWT against `${apiBase}/v1/rooms/:roomId/join` (an AUTH-EXEMPT
1315
+ // endpoint — the opaque code is the only credential), then connects
1316
+ // to LiveKit and returns a typed event surface.
1317
+ this.joinRoom = (opts) => joinRoom({ apiBase: this.config.apiBase, ...opts })
1109
1318
  this.config = config
1110
1319
  }
1111
1320
  }
@@ -1122,5 +1331,7 @@ function configureVoiceClient(config) {
1122
1331
  createProtocolState,
1123
1332
  createReconnectingWebSocket,
1124
1333
  handleServerMessage,
1334
+ joinRoom,
1335
+ parseIncomingCall,
1125
1336
  })
1126
1337
  //# sourceMappingURL=browser.js.map