@craftedxp/voice-js 0.10.0 → 0.12.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.
Files changed (41) hide show
  1. package/README.md +21 -19
  2. package/dist/assistant.d.mts +3 -3
  3. package/dist/assistant.d.ts +3 -3
  4. package/dist/assistant.js +210 -22
  5. package/dist/assistant.js.map +1 -1
  6. package/dist/assistant.mjs +2 -1
  7. package/dist/browser.d.mts +3 -3
  8. package/dist/browser.d.ts +3 -3
  9. package/dist/browser.js +201 -34
  10. package/dist/browser.js.map +1 -1
  11. package/dist/browser.mjs +3 -2
  12. package/dist/browser.mjs.map +1 -1
  13. package/dist/{chunk-3BYNC25N.mjs → chunk-C2U6V5NI.mjs} +191 -23
  14. package/dist/chunk-C2U6V5NI.mjs.map +1 -0
  15. package/dist/chunk-OS2GZAR7.mjs +18 -0
  16. package/dist/chunk-OS2GZAR7.mjs.map +1 -0
  17. package/dist/{chunk-LV7JGPYW.mjs → chunk-VSY43NGD.mjs} +6 -13
  18. package/dist/chunk-VSY43NGD.mjs.map +1 -0
  19. package/dist/{config-D2TbvIqT.d.mts → config-DfbPB1Tq.d.mts} +22 -3
  20. package/dist/{config-D2TbvIqT.d.ts → config-DfbPB1Tq.d.ts} +22 -3
  21. package/dist/embed.iife.js +42 -5
  22. package/dist/{incomingCall-CfRRzj2P.d.ts → incomingCall-2B3XB8pM.d.mts} +3 -1
  23. package/dist/{incomingCall-CfRRzj2P.d.mts → incomingCall-CYmeQkIw.d.ts} +3 -1
  24. package/dist/node.d.mts +26 -3
  25. package/dist/node.d.ts +26 -3
  26. package/dist/node.js +11 -2
  27. package/dist/node.js.map +1 -1
  28. package/dist/node.mjs +11 -2
  29. package/dist/node.mjs.map +1 -1
  30. package/dist/room.js +15 -12
  31. package/dist/room.js.map +1 -1
  32. package/dist/room.mjs +2 -1
  33. package/dist/transcribe.d.mts +2 -2
  34. package/dist/transcribe.d.ts +2 -2
  35. package/dist/transcribe.js +209 -21
  36. package/dist/transcribe.js.map +1 -1
  37. package/dist/transcribe.mjs +2 -1
  38. package/dist/transcribe.mjs.map +1 -1
  39. package/package.json +1 -1
  40. package/dist/chunk-3BYNC25N.mjs.map +0 -1
  41. package/dist/chunk-LV7JGPYW.mjs.map +0 -1
package/dist/browser.mjs CHANGED
@@ -9,10 +9,11 @@ import {
9
9
  normalizeConfig,
10
10
  parseIncomingCall,
11
11
  startTextSession
12
- } from "./chunk-3BYNC25N.mjs";
12
+ } from "./chunk-C2U6V5NI.mjs";
13
13
  import {
14
14
  joinRoom
15
- } from "./chunk-LV7JGPYW.mjs";
15
+ } from "./chunk-VSY43NGD.mjs";
16
+ import "./chunk-OS2GZAR7.mjs";
16
17
 
17
18
  // src/browser.ts
18
19
  function configureVoiceClient2(config) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/browser.ts"],"sourcesContent":["// Browser entry — `import { configureVoiceClient } from '@craftedxp/voice-js'`\n//\n// @deprecated Import from '@craftedxp/voice-js/assistant', '@craftedxp/voice-js/room',\n// or '@craftedxp/voice-js/transcribe' instead. This barrel will be removed in 1.0.\n//\n// Backwards-compatible re-export barrel. The factory returned here also\n// exposes `joinRoom` as a method so consumers who call `voice.joinRoom()`\n// keep working unchanged.\n//\n// Pre-injects:\n// - native `WebSocket` (globalThis.WebSocket)\n// - browser audio adapter (AudioContext + AudioWorklet + getUserMedia)\n//\n// Electron renderer + browser tabs both pick this entry via the\n// `browser` condition in package.json `exports`.\n\nimport { configureVoiceClient as configureAssistant } from './assistant'\nimport { joinRoom as joinRoomImpl } from './room'\nimport { normalizeConfig } from './config'\nimport type { VoiceClientConfig, VoiceClientFactory } from './config'\nimport type { JoinRoomOptions, RoomSession } from './room'\n\n/**\n * One-time SDK setup. Returns a factory you call `startCall` on for\n * every voice call. Also exposes `joinRoom` as a method for multi-party\n * room joining (back-compat; prefer `@craftedxp/voice-js/room` for new code).\n */\nexport function configureVoiceClient(config: VoiceClientConfig): VoiceClientFactory & {\n joinRoom: (opts: Omit<JoinRoomOptions, 'apiBase'>) => Promise<RoomSession>\n} {\n const normalized = normalizeConfig(config)\n const factory = configureAssistant(normalized)\n return {\n ...factory,\n // Re-compose joinRoom onto the factory so barrel consumers who call\n // `voice.joinRoom(...)` keep working unchanged.\n joinRoom: (opts: Omit<JoinRoomOptions, 'apiBase'>) =>\n joinRoomImpl({ apiBase: normalized.apiBase, ...opts }),\n }\n}\n\n// ---------------------------------------------------------------------------\n// Public re-exports — assistant surface (types + advanced primitives).\n// ---------------------------------------------------------------------------\n\nexport type {\n Call,\n ClientTool,\n ClientToolMap,\n FetchToken,\n FetchTokenArgs,\n FetchTokenResult,\n StartCallOptions,\n VoiceClientConfig,\n VoiceClientFactory,\n} from './config'\n\nexport type {\n CallEndEvent,\n CallEndReason,\n CallError,\n CallErrorCode,\n CallState,\n TranscriptEntry,\n VolumeEvent,\n} from './protocol'\n\n// Low-level primitives — exposed for advanced consumers who want to\n// drive just the mic, just the playback, or build a custom transport on\n// top of the protocol decoder. Most users should stick to\n// `configureVoiceClient().startCall()`.\nexport { createAudioCapture } from './AudioCapture'\nexport type { CaptureController, CaptureOptions, OnChunk, OnError, OnVolume } from './AudioCapture'\n\nexport { createAudioPlayback } from './AudioPlayback'\nexport type { PlaybackController, PlaybackOptions, OnAgentSpeakingChange } from './AudioPlayback'\n\nexport { createReconnectingWebSocket } from './ReconnectingWebSocket'\nexport type {\n ReconnectingWebSocket,\n RWSEvent,\n RWSOptions,\n WebSocketFactory,\n WebSocketLike,\n} from './ReconnectingWebSocket'\n\nexport { handleServerMessage, createProtocolState, buildWsUrl } from './protocol'\nexport type { ProtocolState, ProtocolCallbacks, ServerMessage } from './protocol'\n\n// Multi-party room (Phase 7 video) surface. Exposed as a method on the\n// factory (`voice.joinRoom({...})`) and re-exported here as a free\n// function for consumers that prefer not to instantiate a full factory\n// just to attend a room as a guest.\nexport { joinRoom } from './room'\nexport type {\n JoinRoomOptions,\n RoomEventName,\n RoomEventPayloads,\n RoomParticipantInfo,\n RoomSession,\n} from './room'\nexport type { SystemMessage, TranscriptMessage, AnalysisMessage } from './roomProtocol'\n\nexport { parseIncomingCall } from './incomingCall'\nexport type { IncomingCallPayload } from './incomingCall'\n\nexport { startTextSession } from './textSession'\nexport type { ChatEvent, StartTextSessionOpts, TextSession } from './textSession'\n"],"mappings":";;;;;;;;;;;;;;;;;AA2BO,SAASA,sBAAqB,QAEnC;AACA,QAAM,aAAa,gBAAgB,MAAM;AACzC,QAAM,UAAU,qBAAmB,UAAU;AAC7C,SAAO;AAAA,IACL,GAAG;AAAA;AAAA;AAAA,IAGH,UAAU,CAAC,SACT,SAAa,EAAE,SAAS,WAAW,SAAS,GAAG,KAAK,CAAC;AAAA,EACzD;AACF;","names":["configureVoiceClient"]}
1
+ {"version":3,"sources":["../src/browser.ts"],"sourcesContent":["// Browser entry — `import { configureVoiceClient } from '@craftedxp/voice-js'`\n//\n// @deprecated Import from '@craftedxp/voice-js/assistant', '@craftedxp/voice-js/room',\n// or '@craftedxp/voice-js/transcribe' instead. This barrel will be removed in 1.0.\n//\n// Backwards-compatible re-export barrel. The factory returned here also\n// exposes `joinRoom` as a method so consumers who call `voice.joinRoom()`\n// keep working unchanged.\n//\n// Pre-injects:\n// - native `WebSocket` (globalThis.WebSocket)\n// - browser audio adapter (AudioContext + AudioWorklet + getUserMedia)\n//\n// Electron renderer + browser tabs both pick this entry via the\n// `browser` condition in package.json `exports`.\n\nimport { configureVoiceClient as configureAssistant } from './assistant'\nimport { joinRoom as joinRoomImpl } from './room'\nimport { normalizeConfig } from './config'\nimport type { VoiceClientConfig, VoiceClientFactory } from './config'\nimport type { JoinRoomOptions, RoomSession } from './room'\n\n/**\n * One-time SDK setup. Returns a factory you call `startCall` on for\n * every voice call. Also exposes `joinRoom` as a method for multi-party\n * room joining (back-compat; prefer `@craftedxp/voice-js/room` for new code).\n */\nexport function configureVoiceClient(config: VoiceClientConfig): VoiceClientFactory & {\n joinRoom: (opts: Omit<JoinRoomOptions, 'apiBase'>) => Promise<RoomSession>\n} {\n const normalized = normalizeConfig(config)\n const factory = configureAssistant(normalized)\n return {\n ...factory,\n // Re-compose joinRoom onto the factory so barrel consumers who call\n // `voice.joinRoom(...)` keep working unchanged.\n joinRoom: (opts: Omit<JoinRoomOptions, 'apiBase'>) =>\n joinRoomImpl({ apiBase: normalized.apiBase, ...opts }),\n }\n}\n\n// ---------------------------------------------------------------------------\n// Public re-exports — assistant surface (types + advanced primitives).\n// ---------------------------------------------------------------------------\n\nexport type {\n Call,\n CallTransport,\n ClientTool,\n ClientToolMap,\n FetchToken,\n FetchTokenArgs,\n FetchTokenResult,\n StartCallOptions,\n TransportChangeEvent,\n VoiceClientConfig,\n VoiceClientFactory,\n} from './config'\n\nexport type {\n CallEndEvent,\n CallEndReason,\n CallError,\n CallErrorCode,\n CallState,\n TranscriptEntry,\n VolumeEvent,\n} from './protocol'\n\n// Low-level primitives — exposed for advanced consumers who want to\n// drive just the mic, just the playback, or build a custom transport on\n// top of the protocol decoder. Most users should stick to\n// `configureVoiceClient().startCall()`.\nexport { createAudioCapture } from './AudioCapture'\nexport type { CaptureController, CaptureOptions, OnChunk, OnError, OnVolume } from './AudioCapture'\n\nexport { createAudioPlayback } from './AudioPlayback'\nexport type { PlaybackController, PlaybackOptions, OnAgentSpeakingChange } from './AudioPlayback'\n\nexport { createReconnectingWebSocket } from './ReconnectingWebSocket'\nexport type {\n ReconnectingWebSocket,\n RWSEvent,\n RWSOptions,\n WebSocketFactory,\n WebSocketLike,\n} from './ReconnectingWebSocket'\n\nexport { handleServerMessage, createProtocolState, buildWsUrl } from './protocol'\nexport type { ProtocolState, ProtocolCallbacks, ServerMessage } from './protocol'\n\n// Multi-party room (Phase 7 video) surface. Exposed as a method on the\n// factory (`voice.joinRoom({...})`) and re-exported here as a free\n// function for consumers that prefer not to instantiate a full factory\n// just to attend a room as a guest.\nexport { joinRoom } from './room'\nexport type {\n JoinRoomOptions,\n RoomEventName,\n RoomEventPayloads,\n RoomParticipantInfo,\n RoomSession,\n} from './room'\nexport type { SystemMessage, TranscriptMessage, AnalysisMessage } from './roomProtocol'\n\nexport { parseIncomingCall } from './incomingCall'\nexport type { IncomingCallPayload } from './incomingCall'\n\nexport { startTextSession } from './textSession'\nexport type { ChatEvent, StartTextSessionOpts, TextSession } from './textSession'\n"],"mappings":";;;;;;;;;;;;;;;;;;AA2BO,SAASA,sBAAqB,QAEnC;AACA,QAAM,aAAa,gBAAgB,MAAM;AACzC,QAAM,UAAU,qBAAmB,UAAU;AAC7C,SAAO;AAAA,IACL,GAAG;AAAA;AAAA;AAAA,IAGH,UAAU,CAAC,SACT,SAAa,EAAE,SAAS,WAAW,SAAS,GAAG,KAAK,CAAC;AAAA,EACzD;AACF;","names":["configureVoiceClient"]}
@@ -1,3 +1,7 @@
1
+ import {
2
+ loadLiveKit
3
+ } from "./chunk-OS2GZAR7.mjs";
4
+
1
5
  // src/config.ts
2
6
  function normalizeConfig(config) {
3
7
  if (!config) throw new Error("configureVoiceClient: config is required");
@@ -478,7 +482,8 @@ function buildWsUrl(args) {
478
482
  const base = new URL(args.apiBase);
479
483
  const proto = base.protocol === "https:" ? "wss:" : "ws:";
480
484
  const bargeQS = args.bargeIn === false ? "&barge=off" : "";
481
- return `${proto}//${base.host}/v1/agents/${encodeURIComponent(args.agentId)}/call?token=${encodeURIComponent(args.token)}${bargeQS}`;
485
+ const mediaQS = args.media === "livekit" ? "&media=livekit" : "";
486
+ return `${proto}//${base.host}/v1/agents/${encodeURIComponent(args.agentId)}/call?token=${encodeURIComponent(args.token)}${bargeQS}${mediaQS}`;
482
487
  }
483
488
 
484
489
  // src/clientTools.ts
@@ -632,7 +637,75 @@ var createClientMarksBuffer = (args) => {
632
637
  };
633
638
  };
634
639
 
640
+ // src/livekit/createLiveKitMedia.ts
641
+ var toError = (e) => e instanceof Error ? e : new Error(String(e));
642
+ function createLiveKitMedia(lk, opts = {}) {
643
+ let room = null;
644
+ let audioEl = null;
645
+ let muted = false;
646
+ const attach = (track) => {
647
+ if (track.kind !== lk.Track.Kind.Audio) return;
648
+ if (typeof document === "undefined") return;
649
+ if (!audioEl) {
650
+ audioEl = document.createElement("audio");
651
+ audioEl.autoplay = true;
652
+ audioEl.style.display = "none";
653
+ document.body.appendChild(audioEl);
654
+ }
655
+ track.attach(audioEl);
656
+ };
657
+ return {
658
+ connect: async (url, token) => {
659
+ const r = new lk.Room({ adaptiveStream: false, dynacast: false });
660
+ room = r;
661
+ r.on(lk.RoomEvent.TrackSubscribed, (track) => attach(track));
662
+ await r.connect(url, token);
663
+ if (room !== r) {
664
+ return;
665
+ }
666
+ await r.localParticipant.setMicrophoneEnabled(!muted);
667
+ if (room !== r) {
668
+ return;
669
+ }
670
+ try {
671
+ await r.startAudio();
672
+ } catch {
673
+ }
674
+ },
675
+ disconnect: () => {
676
+ const r = room;
677
+ room = null;
678
+ if (r) void r.disconnect().catch(() => void 0);
679
+ audioEl?.remove();
680
+ audioEl = null;
681
+ },
682
+ setMuted: (m) => {
683
+ muted = m;
684
+ const r = room;
685
+ if (!r) return;
686
+ void r.localParticipant.setMicrophoneEnabled(!m).catch((e) => opts.onError?.(toError(e)));
687
+ }
688
+ };
689
+ }
690
+
635
691
  // src/VoiceClient.ts
692
+ var parseMediaFrame = (raw) => {
693
+ if (!raw.includes('"livekit_')) return null;
694
+ try {
695
+ const m = JSON.parse(raw);
696
+ if (m.type === "livekit_join" && typeof m.url === "string" && typeof m.token === "string") {
697
+ return { type: "livekit_join", url: m.url, token: m.token };
698
+ }
699
+ if (m.type === "livekit_fallback") {
700
+ return {
701
+ type: "livekit_fallback",
702
+ reason: typeof m.reason === "string" ? m.reason : void 0
703
+ };
704
+ }
705
+ } catch {
706
+ }
707
+ return null;
708
+ };
636
709
  var BrowserVoiceClient = class {
637
710
  constructor(args) {
638
711
  this.rws = null;
@@ -644,6 +717,9 @@ var BrowserVoiceClient = class {
644
717
  this.startedAt = null;
645
718
  this.endedFired = false;
646
719
  this.lastError = null;
720
+ this.mediaMode = "ws";
721
+ this.lkMedia = null;
722
+ this.lkJoinSeen = false;
647
723
  this.end = () => {
648
724
  this.teardown("user_hangup");
649
725
  };
@@ -651,11 +727,73 @@ var BrowserVoiceClient = class {
651
727
  if (this.muted) return;
652
728
  this.muted = true;
653
729
  this.capture?.mute(true);
730
+ this.lkMedia?.setMuted(true);
654
731
  };
655
732
  this.unmute = () => {
656
733
  if (!this.muted) return;
657
734
  this.muted = false;
658
735
  this.capture?.mute(false);
736
+ this.lkMedia?.setMuted(false);
737
+ };
738
+ this.ensureWsPlayback = async () => {
739
+ if (this.playback) return;
740
+ const createPlayback = this.args.audio?.createPlayback ?? createAudioPlayback;
741
+ this.playback = createPlayback({
742
+ onVolume: (v) => {
743
+ this.outputVolume = v;
744
+ this.args.options.onVolume?.({ input: this.inputVolume, output: v });
745
+ }
746
+ });
747
+ try {
748
+ await this.playback.resume();
749
+ } catch {
750
+ }
751
+ };
752
+ // ---------------------------------------------------------------
753
+ // LiveKit media lifecycle (phase 35)
754
+ // ---------------------------------------------------------------
755
+ this.joinLiveKit = async (url, token) => {
756
+ this.lkJoinSeen = true;
757
+ const lk = this.args.media?.lk;
758
+ if (!lk) {
759
+ this.fallbackToWs("livekit-client unavailable");
760
+ return;
761
+ }
762
+ const media = createLiveKitMedia(lk, {
763
+ onError: (e) => this.emitError({ code: "mic_start_failed", message: e.message })
764
+ });
765
+ this.lkMedia = media;
766
+ if (this.muted) media.setMuted(true);
767
+ try {
768
+ await media.connect(url, token);
769
+ if (this.lkMedia !== media) return;
770
+ this.mediaMode = "livekit";
771
+ this.rws?.send(JSON.stringify({ type: "livekit_ready" }));
772
+ } catch (err) {
773
+ const reason = err instanceof Error ? err.message : String(err);
774
+ media.disconnect();
775
+ if (this.lkMedia === media) this.lkMedia = null;
776
+ this.rws?.send(JSON.stringify({ type: "livekit_failed", reason }));
777
+ this.fallbackToWs(reason);
778
+ }
779
+ };
780
+ // Idempotent: whichever side gives up first (client join error, server
781
+ // livekit_fallback, `connected` without a join) lands here once.
782
+ this.fallbackToWs = (reason) => {
783
+ if (this.mediaMode === "ws") return;
784
+ this.mediaMode = "ws";
785
+ this.lkMedia?.disconnect();
786
+ this.lkMedia = null;
787
+ void this.ensureWsPlayback();
788
+ void this.startCapture();
789
+ this.args.options.onTransportChange?.({ requested: "livekit", actual: "ws", reason });
790
+ };
791
+ this.resetLiveKitForReconnect = () => {
792
+ if (this.args.media?.kind !== "livekit") return;
793
+ this.lkMedia?.disconnect();
794
+ this.lkMedia = null;
795
+ this.lkJoinSeen = false;
796
+ this.mediaMode = "livekit-pending";
659
797
  };
660
798
  // ---------------------------------------------------------------
661
799
  // Internal
@@ -676,17 +814,27 @@ var BrowserVoiceClient = class {
676
814
  this.handleSocketEvent = (ev) => {
677
815
  switch (ev.type) {
678
816
  case "open":
679
- void this.startCapture();
817
+ if (this.mediaMode === "ws") void this.startCapture();
680
818
  break;
681
819
  case "reconnected":
682
820
  this.proto.transcript = [];
683
821
  this.proto.agentBubbleId = null;
684
822
  this.args.options.onTranscript?.(this.proto.transcript);
685
- void this.startCapture();
823
+ this.resetLiveKitForReconnect();
824
+ if (this.mediaMode === "ws") void this.startCapture();
686
825
  this.setState("listening");
687
826
  break;
688
827
  case "message":
689
828
  if (typeof ev.data === "string") {
829
+ const media = parseMediaFrame(ev.data);
830
+ if (media?.type === "livekit_join") {
831
+ if (this.mediaMode === "livekit-pending") void this.joinLiveKit(media.url, media.token);
832
+ break;
833
+ }
834
+ if (media?.type === "livekit_fallback") {
835
+ this.fallbackToWs(media.reason ?? "server_fallback");
836
+ break;
837
+ }
690
838
  handleServerMessage(ev.data, this.proto, {
691
839
  onState: this.setState,
692
840
  onTranscript: (entries) => this.args.options.onTranscript?.(entries),
@@ -703,7 +851,12 @@ var BrowserVoiceClient = class {
703
851
  if (typeof seq === "number") this.marks.onAgentTurnEnd(seq);
704
852
  },
705
853
  onCallEnd: (reason) => this.teardown(reason),
706
- onConnected: () => this.sendClientToolsRegister(),
854
+ onConnected: () => {
855
+ if (this.mediaMode === "livekit-pending" && !this.lkJoinSeen) {
856
+ this.fallbackToWs("server_declined");
857
+ }
858
+ this.sendClientToolsRegister();
859
+ },
707
860
  onClientToolCall: (frame) => dispatchClientToolCall(
708
861
  (f) => this.rws?.send(JSON.stringify(f)),
709
862
  this.args.options.clientTools ?? {},
@@ -711,6 +864,8 @@ var BrowserVoiceClient = class {
711
864
  )
712
865
  });
713
866
  } else {
867
+ if (this.mediaMode === "livekit") break;
868
+ if (!this.playback) void this.ensureWsPlayback();
714
869
  this.marks.markFirstAudibleOutput();
715
870
  this.playback?.enqueue(ev.data);
716
871
  }
@@ -728,7 +883,8 @@ var BrowserVoiceClient = class {
728
883
  };
729
884
  this.startCapture = async () => {
730
885
  if (this.capture?.isCapturing()) return;
731
- this.capture = createAudioCapture({
886
+ const createCapture = this.args.audio?.createCapture ?? createAudioCapture;
887
+ this.capture = createCapture({
732
888
  onChunk: (pcm) => {
733
889
  this.marks.markFirstOutboundAudio();
734
890
  this.rws?.send(pcm);
@@ -757,6 +913,8 @@ var BrowserVoiceClient = class {
757
913
  }
758
914
  this.capture?.stop();
759
915
  this.capture = null;
916
+ this.lkMedia?.disconnect();
917
+ this.lkMedia = null;
760
918
  this.playback?.close();
761
919
  this.playback = null;
762
920
  try {
@@ -809,28 +967,17 @@ var BrowserVoiceClient = class {
809
967
  async start() {
810
968
  this.setState("connecting");
811
969
  this.startedAt = Date.now();
970
+ this.mediaMode = this.args.media?.kind === "livekit" ? "livekit-pending" : "ws";
812
971
  const url = buildWsUrl({
813
972
  apiBase: this.args.config.apiBase,
814
973
  agentId: this.args.options.agentId,
815
974
  token: this.args.token,
816
- bargeIn: this.args.options.bargeIn
817
- });
818
- this.playback = createAudioPlayback({
819
- onVolume: (v) => {
820
- this.outputVolume = v;
821
- this.args.options.onVolume?.({ input: this.inputVolume, output: v });
822
- }
975
+ bargeIn: this.args.options.bargeIn,
976
+ media: this.mediaMode === "ws" ? void 0 : "livekit"
823
977
  });
824
- try {
825
- await this.playback.resume();
826
- } catch {
827
- }
978
+ if (this.mediaMode === "ws") await this.ensureWsPlayback();
828
979
  this.rws = createReconnectingWebSocket(
829
- {
830
- url,
831
- wsFactory: this.args.wsFactory,
832
- maxRetries: 3
833
- },
980
+ { url, wsFactory: this.args.wsFactory, maxRetries: 3 },
834
981
  (ev) => this.handleSocketEvent(ev)
835
982
  );
836
983
  }
@@ -1111,7 +1258,7 @@ var parseIncomingCall = (raw) => {
1111
1258
  if (typeof p.agentId !== "string" || p.agentId.length === 0) {
1112
1259
  throw new Error("parseIncomingCall: missing `agentId`");
1113
1260
  }
1114
- const transport = p.transport === "webrtc" ? "webrtc" : "ws";
1261
+ const transport = p.transport === "webrtc" ? "webrtc" : p.transport === "livekit" ? "livekit" : "ws";
1115
1262
  const out = { token: p.token, agentId: p.agentId, transport };
1116
1263
  if (transport === "webrtc" && typeof p.webrtcGatewayBase === "string") {
1117
1264
  out.webrtcGatewayBase = p.webrtcGatewayBase;
@@ -1168,6 +1315,27 @@ var AssistantVoiceFactory = class {
1168
1315
  clientTools: options.clientTools
1169
1316
  });
1170
1317
  }
1318
+ if (resolved.transport === "livekit") {
1319
+ let lk = null;
1320
+ let loadError = "";
1321
+ try {
1322
+ lk = await loadLiveKit("transport 'livekit'");
1323
+ } catch (e) {
1324
+ loadError = e instanceof Error ? e.message : String(e);
1325
+ }
1326
+ if (!lk) {
1327
+ options.onTransportChange?.({ requested: "livekit", actual: "ws", reason: loadError });
1328
+ }
1329
+ const client2 = new BrowserVoiceClient({
1330
+ config: this.config,
1331
+ options: { ...options, context, metadata },
1332
+ token: resolved.token,
1333
+ wsFactory: browserWsFactory,
1334
+ media: lk ? { kind: "livekit", lk } : void 0
1335
+ });
1336
+ await client2.start();
1337
+ return client2;
1338
+ }
1171
1339
  const client = new BrowserVoiceClient({
1172
1340
  config: this.config,
1173
1341
  // Carry merged context/metadata through to startCall so server can
@@ -1205,4 +1373,4 @@ export {
1205
1373
  parseIncomingCall,
1206
1374
  configureVoiceClient
1207
1375
  };
1208
- //# sourceMappingURL=chunk-3BYNC25N.mjs.map
1376
+ //# sourceMappingURL=chunk-C2U6V5NI.mjs.map