@lorikeetai/react-native-sdk 2.1.0 → 2.2.0-beta.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 +159 -0
- package/dist/voice.cjs +175 -0
- package/dist/voice.cjs.map +1 -0
- package/dist/voice.d.cts +74 -0
- package/dist/voice.d.cts.map +1 -0
- package/dist/voice.d.ts +74 -0
- package/dist/voice.d.ts.map +1 -0
- package/dist/voice.js +174 -0
- package/dist/voice.js.map +1 -0
- package/package.json +32 -3
package/README.md
CHANGED
|
@@ -397,6 +397,165 @@ WebView component that handles the widget content.
|
|
|
397
397
|
- `webviewProps` (optional): Passed through to the webview component. The SDK
|
|
398
398
|
adds external-link handling on `onNavigationStateChange` and then calls yours
|
|
399
399
|
|
|
400
|
+
## Voice calls (beta)
|
|
401
|
+
|
|
402
|
+
`@lorikeetai/react-native-sdk/voice` runs a Lorikeet voice call inside your
|
|
403
|
+
app with native audio, so the customer can keep talking while the app is in
|
|
404
|
+
the background. It is a beta. Install it from the `beta` tag, and expect its
|
|
405
|
+
API to change before it is stable.
|
|
406
|
+
|
|
407
|
+
Nothing here touches the chat widget. Only the `voice` entry point loads
|
|
408
|
+
LiveKit, so an app that never imports it installs nothing new and needs no
|
|
409
|
+
native rebuild.
|
|
410
|
+
|
|
411
|
+
### Install
|
|
412
|
+
|
|
413
|
+
The LiveKit packages are optional peer dependencies, so install them yourself:
|
|
414
|
+
|
|
415
|
+
```bash
|
|
416
|
+
pnpm add @lorikeetai/react-native-sdk@beta \
|
|
417
|
+
@livekit/react-native@^3.1.0 \
|
|
418
|
+
@livekit/react-native-webrtc@^150.1.0 \
|
|
419
|
+
livekit-client@^2.20.0
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
`@livekit/react-native` depends on `@livekit/components-react`, which needs
|
|
423
|
+
`react-dom`. Install the `react-dom` version that matches your `react`, or the
|
|
424
|
+
package manager may pick a newer one that your `react` does not satisfy.
|
|
425
|
+
|
|
426
|
+
Do not install `@livekit/krisp-noise-filter`. The call does not use it, and
|
|
427
|
+
its license is not open source.
|
|
428
|
+
|
|
429
|
+
Voice requires React Native 0.79 or later, though the chat widget's peer range
|
|
430
|
+
is unchanged. The `voice` subpath is in the package's `exports` map, and 0.79
|
|
431
|
+
ships the first Metro (0.82) that reads that map by default. TypeScript reads
|
|
432
|
+
it with `moduleResolution` set to `bundler` or `node16`, and Expo's base
|
|
433
|
+
`tsconfig` uses `bundler`.
|
|
434
|
+
|
|
435
|
+
### Native setup
|
|
436
|
+
|
|
437
|
+
These are native modules. Expo Go does not include them, so use a
|
|
438
|
+
[development build](https://docs.expo.dev/develop/development-builds/introduction/)
|
|
439
|
+
on Expo, and rebuild native code after installing.
|
|
440
|
+
|
|
441
|
+
With Expo SDK 56 or later, add the two config plugins, the iOS microphone
|
|
442
|
+
string and audio background mode, and the Android audio permissions to
|
|
443
|
+
`app.json`, then run `expo prebuild`:
|
|
444
|
+
|
|
445
|
+
```bash
|
|
446
|
+
npx expo install @livekit/react-native-expo-plugin @config-plugins/react-native-webrtc
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
```json
|
|
450
|
+
{
|
|
451
|
+
"expo": {
|
|
452
|
+
"platforms": ["ios", "android"],
|
|
453
|
+
"plugins": [
|
|
454
|
+
"@livekit/react-native-expo-plugin",
|
|
455
|
+
"@config-plugins/react-native-webrtc"
|
|
456
|
+
],
|
|
457
|
+
"ios": {
|
|
458
|
+
"infoPlist": {
|
|
459
|
+
"NSMicrophoneUsageDescription": "Talk to our support team.",
|
|
460
|
+
"UIBackgroundModes": ["audio"]
|
|
461
|
+
}
|
|
462
|
+
},
|
|
463
|
+
"android": {
|
|
464
|
+
"permissions": [
|
|
465
|
+
"android.permission.RECORD_AUDIO",
|
|
466
|
+
"android.permission.MODIFY_AUDIO_SETTINGS"
|
|
467
|
+
]
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
Set `platforms` to iOS and Android. Expo treats an app with `react-dom`
|
|
474
|
+
installed as web-capable, so `expo export` would also try a web bundle, which
|
|
475
|
+
LiveKit's native modules cannot build.
|
|
476
|
+
|
|
477
|
+
Use version 1.1.0 or later of `@livekit/react-native-expo-plugin`. It adds the
|
|
478
|
+
`livekit/podspecs` CocoaPods source, which LiveKit's iOS pods need once
|
|
479
|
+
CocoaPods trunk stops accepting new pods on 2 December 2026.
|
|
480
|
+
|
|
481
|
+
Without Expo, follow LiveKit's
|
|
482
|
+
[installation steps](https://github.com/livekit/client-sdk-react-native#installation).
|
|
483
|
+
Add the `livekit/podspecs` source to your `Podfile`, call
|
|
484
|
+
`LiveKitReactNative.setup` in `MainApplication` and `LivekitReactNative.setup`
|
|
485
|
+
in `AppDelegate`, and add the same permissions and background mode as above.
|
|
486
|
+
|
|
487
|
+
On Android, the microphone keeps working in the background only while your
|
|
488
|
+
app runs a foreground service of type `microphone`, which the SDK does not
|
|
489
|
+
start for you. Without one, the agent can still be heard, but the customer's
|
|
490
|
+
microphone goes silent when the app leaves the foreground.
|
|
491
|
+
|
|
492
|
+
### Usage
|
|
493
|
+
|
|
494
|
+
Your backend creates the session with
|
|
495
|
+
`POST /v1/conversation/voice/web/create`, using your API credentials, and
|
|
496
|
+
returns its `{ conversationId, wsUrl, token }` to the app. Never put your API
|
|
497
|
+
credentials in the app.
|
|
498
|
+
|
|
499
|
+
```tsx
|
|
500
|
+
import { Button, Text, View } from 'react-native'
|
|
501
|
+
|
|
502
|
+
import { useLorikeetCall } from '@lorikeetai/react-native-sdk/voice'
|
|
503
|
+
|
|
504
|
+
export function SupportCall() {
|
|
505
|
+
const call = useLorikeetCall({
|
|
506
|
+
getSession: async () => {
|
|
507
|
+
const response = await fetch('https://api.example.com/support/call', {
|
|
508
|
+
method: 'POST',
|
|
509
|
+
})
|
|
510
|
+
if (response.status === 404) return null
|
|
511
|
+
if (!response.ok) throw new Error(`Call session: ${response.status}`)
|
|
512
|
+
return response.json()
|
|
513
|
+
},
|
|
514
|
+
onError: (cause) => console.warn('Support call failed', cause),
|
|
515
|
+
})
|
|
516
|
+
|
|
517
|
+
return (
|
|
518
|
+
<View>
|
|
519
|
+
<Text>{call.state}</Text>
|
|
520
|
+
{call.state === 'idle' || call.state === 'error' ? (
|
|
521
|
+
<Button title="Start call" onPress={() => void call.start()} />
|
|
522
|
+
) : (
|
|
523
|
+
<Button title="End call" onPress={call.end} />
|
|
524
|
+
)}
|
|
525
|
+
</View>
|
|
526
|
+
)
|
|
527
|
+
}
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
`useLorikeetCall({ getSession, onError })` returns:
|
|
531
|
+
|
|
532
|
+
- `state`: `idle`, `starting`, `connecting`, `active` or `error`, as in the web
|
|
533
|
+
Voice Widget. `starting` covers asking for the microphone and fetching the
|
|
534
|
+
session, and `connecting` covers joining the call.
|
|
535
|
+
- `error`: why the call is in `error`. `microphone_denied` means the customer
|
|
536
|
+
refused the microphone, or it is off for the app in the device's settings.
|
|
537
|
+
`not_available` means `getSession` returned `null`. `failed` is anything
|
|
538
|
+
else, and `onError` receives its cause. `microphone_system_denied` is the web
|
|
539
|
+
Voice Widget's code for a browser blocked by the operating system, and a
|
|
540
|
+
mobile device never reports it.
|
|
541
|
+
- `start()`: start a call from `idle` or `error`. The hook asks for microphone
|
|
542
|
+
access before it calls `getSession`, so a refusal starts no conversation.
|
|
543
|
+
- `end()`: hang up, or abandon a call still starting. The call also ends, and
|
|
544
|
+
returns to `idle`, when the agent leaves.
|
|
545
|
+
- `ended`: whether the last call ended after it began connecting.
|
|
546
|
+
- `microphoneMuted` and `setMicrophoneMuted(muted)`: mute the customer.
|
|
547
|
+
- `speakerMuted` and `setSpeakerMuted(muted)`: mute the agent.
|
|
548
|
+
|
|
549
|
+
The call has no live transcript. The conversation is in Lorikeet under its
|
|
550
|
+
`conversationId`.
|
|
551
|
+
|
|
552
|
+
If your app already calls LiveKit's `registerGlobals()`, the hook leaves your
|
|
553
|
+
configuration alone. Otherwise the hook calls it when the first call starts.
|
|
554
|
+
|
|
555
|
+
The hook owns the native audio session for the length of a call and stops it
|
|
556
|
+
when the call ends. An app that runs its own LiveKit call at the same time
|
|
557
|
+
should not use the hook.
|
|
558
|
+
|
|
400
559
|
## Development
|
|
401
560
|
|
|
402
561
|
This package is outside the root pnpm workspace, on its own lockfile, so run these from `packages/react-native-widget` after `pnpm install` there.
|
package/dist/voice.cjs
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
let react = require("react");
|
|
3
|
+
let _livekit_react_native = require("@livekit/react-native");
|
|
4
|
+
let livekit_client = require("livekit-client");
|
|
5
|
+
|
|
6
|
+
// src/voice/use-lorikeet-call.ts
|
|
7
|
+
/** The server's `LORIKEET_AGENT_IDENTITY`. The call ends when it leaves. */
|
|
8
|
+
const LORIKEET_AGENT_IDENTITY = "lorikeet-agent";
|
|
9
|
+
const errorFor = (error) => {
|
|
10
|
+
if (typeof error !== "object" || error === null || !("name" in error)) return "failed";
|
|
11
|
+
return error.name === "SecurityError" || error.name === "NotAllowedError" ? "microphone_denied" : "failed";
|
|
12
|
+
};
|
|
13
|
+
const isSession = (value) => typeof value === "object" && value !== null && "conversationId" in value && typeof value.conversationId === "string" && "token" in value && typeof value.token === "string" && "wsUrl" in value && typeof value.wsUrl === "string";
|
|
14
|
+
/**
|
|
15
|
+
* Runs a Lorikeet voice call over LiveKit's native audio, with the web Voice
|
|
16
|
+
* Widget's states and errors.
|
|
17
|
+
*/
|
|
18
|
+
function useLorikeetCall({ getSession, onError }) {
|
|
19
|
+
const [state, setState] = (0, react.useState)("idle");
|
|
20
|
+
const [error, setError] = (0, react.useState)();
|
|
21
|
+
const [ended, setEnded] = (0, react.useState)(false);
|
|
22
|
+
const [microphoneMuted, setMicrophoneMutedState] = (0, react.useState)(false);
|
|
23
|
+
const [speakerMuted, setSpeakerMutedState] = (0, react.useState)(false);
|
|
24
|
+
const stateRef = (0, react.useRef)(state);
|
|
25
|
+
const generationRef = (0, react.useRef)(0);
|
|
26
|
+
const localTrackRef = (0, react.useRef)(null);
|
|
27
|
+
const roomRef = (0, react.useRef)(null);
|
|
28
|
+
const speakerMutedRef = (0, react.useRef)(false);
|
|
29
|
+
const audioSessionActiveRef = (0, react.useRef)(false);
|
|
30
|
+
const audioSessionStoppedRef = (0, react.useRef)(Promise.resolve());
|
|
31
|
+
const optionsRef = (0, react.useRef)({
|
|
32
|
+
getSession,
|
|
33
|
+
onError
|
|
34
|
+
});
|
|
35
|
+
optionsRef.current = {
|
|
36
|
+
getSession,
|
|
37
|
+
onError
|
|
38
|
+
};
|
|
39
|
+
const transitionTo = (0, react.useCallback)((next) => {
|
|
40
|
+
stateRef.current = next;
|
|
41
|
+
setState(next);
|
|
42
|
+
}, []);
|
|
43
|
+
const cleanup = (0, react.useCallback)(() => {
|
|
44
|
+
var _localTrackRef$curren;
|
|
45
|
+
(_localTrackRef$curren = localTrackRef.current) === null || _localTrackRef$curren === void 0 || _localTrackRef$curren.stop();
|
|
46
|
+
localTrackRef.current = null;
|
|
47
|
+
const room = roomRef.current;
|
|
48
|
+
roomRef.current = null;
|
|
49
|
+
const disconnected = (room ? room.disconnect() : Promise.resolve()).catch(() => void 0);
|
|
50
|
+
if (audioSessionActiveRef.current) {
|
|
51
|
+
audioSessionActiveRef.current = false;
|
|
52
|
+
audioSessionStoppedRef.current = disconnected.then(() => _livekit_react_native.AudioSession.stopAudioSession()).catch(() => void 0);
|
|
53
|
+
}
|
|
54
|
+
}, []);
|
|
55
|
+
const resetMutes = (0, react.useCallback)(() => {
|
|
56
|
+
setMicrophoneMutedState(false);
|
|
57
|
+
setSpeakerMutedState(false);
|
|
58
|
+
speakerMutedRef.current = false;
|
|
59
|
+
}, []);
|
|
60
|
+
const end = (0, react.useCallback)(() => {
|
|
61
|
+
generationRef.current += 1;
|
|
62
|
+
cleanup();
|
|
63
|
+
setEnded(stateRef.current === "connecting" || stateRef.current === "active");
|
|
64
|
+
setError(void 0);
|
|
65
|
+
resetMutes();
|
|
66
|
+
transitionTo("idle");
|
|
67
|
+
}, [
|
|
68
|
+
cleanup,
|
|
69
|
+
resetMutes,
|
|
70
|
+
transitionTo
|
|
71
|
+
]);
|
|
72
|
+
const fail = (0, react.useCallback)((callError) => {
|
|
73
|
+
generationRef.current += 1;
|
|
74
|
+
cleanup();
|
|
75
|
+
setEnded(false);
|
|
76
|
+
setError(callError);
|
|
77
|
+
transitionTo("error");
|
|
78
|
+
}, [cleanup, transitionTo]);
|
|
79
|
+
(0, react.useEffect)(() => {
|
|
80
|
+
return () => {
|
|
81
|
+
generationRef.current += 1;
|
|
82
|
+
cleanup();
|
|
83
|
+
};
|
|
84
|
+
}, [cleanup]);
|
|
85
|
+
const start = (0, react.useCallback)(async () => {
|
|
86
|
+
if (stateRef.current !== "idle" && stateRef.current !== "error") return;
|
|
87
|
+
setError(void 0);
|
|
88
|
+
setEnded(false);
|
|
89
|
+
resetMutes();
|
|
90
|
+
transitionTo("starting");
|
|
91
|
+
const generation = ++generationRef.current;
|
|
92
|
+
const isStale = () => generationRef.current !== generation;
|
|
93
|
+
try {
|
|
94
|
+
if (!("LiveKitReactNativeGlobal" in globalThis)) (0, _livekit_react_native.registerGlobals)();
|
|
95
|
+
await audioSessionStoppedRef.current;
|
|
96
|
+
if (isStale()) return;
|
|
97
|
+
audioSessionActiveRef.current = true;
|
|
98
|
+
await _livekit_react_native.AudioSession.startAudioSession();
|
|
99
|
+
if (isStale()) return;
|
|
100
|
+
const localTrack = await (0, livekit_client.createLocalAudioTrack)();
|
|
101
|
+
if (isStale()) {
|
|
102
|
+
localTrack.stop();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
localTrackRef.current = localTrack;
|
|
106
|
+
const session = await optionsRef.current.getSession();
|
|
107
|
+
if (isStale()) return;
|
|
108
|
+
if (session === null) {
|
|
109
|
+
fail("not_available");
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (!isSession(session)) throw new TypeError("getSession must resolve to { conversationId, wsUrl, token } or null");
|
|
113
|
+
transitionTo("connecting");
|
|
114
|
+
const room = new livekit_client.Room();
|
|
115
|
+
roomRef.current = room;
|
|
116
|
+
const isCurrentRoom = () => roomRef.current === room;
|
|
117
|
+
room.on(livekit_client.RoomEvent.ParticipantDisconnected, (participant) => {
|
|
118
|
+
if (isCurrentRoom() && participant.identity === "lorikeet-agent") end();
|
|
119
|
+
});
|
|
120
|
+
room.on(livekit_client.RoomEvent.TrackSubscribed, (track) => {
|
|
121
|
+
if (isCurrentRoom() && track instanceof livekit_client.RemoteAudioTrack) track.setVolume(speakerMutedRef.current ? 0 : 1);
|
|
122
|
+
});
|
|
123
|
+
let joined = false;
|
|
124
|
+
room.on(livekit_client.RoomEvent.Disconnected, () => {
|
|
125
|
+
if (isCurrentRoom() && joined) end();
|
|
126
|
+
});
|
|
127
|
+
await room.connect(session.wsUrl, session.token, { autoSubscribe: true });
|
|
128
|
+
if (isStale()) return;
|
|
129
|
+
joined = true;
|
|
130
|
+
await room.localParticipant.publishTrack(localTrack, { source: livekit_client.Track.Source.Microphone });
|
|
131
|
+
if (isStale()) return;
|
|
132
|
+
transitionTo("active");
|
|
133
|
+
} catch (caught) {
|
|
134
|
+
if (isStale()) return;
|
|
135
|
+
const callError = errorFor(caught);
|
|
136
|
+
fail(callError);
|
|
137
|
+
if (callError === "failed") try {
|
|
138
|
+
var _optionsRef$current$o, _optionsRef$current;
|
|
139
|
+
(_optionsRef$current$o = (_optionsRef$current = optionsRef.current).onError) === null || _optionsRef$current$o === void 0 || _optionsRef$current$o.call(_optionsRef$current, caught);
|
|
140
|
+
} catch {}
|
|
141
|
+
}
|
|
142
|
+
}, [
|
|
143
|
+
end,
|
|
144
|
+
fail,
|
|
145
|
+
resetMutes,
|
|
146
|
+
transitionTo
|
|
147
|
+
]);
|
|
148
|
+
return {
|
|
149
|
+
end,
|
|
150
|
+
ended,
|
|
151
|
+
error,
|
|
152
|
+
microphoneMuted,
|
|
153
|
+
setMicrophoneMuted: (0, react.useCallback)((muted) => {
|
|
154
|
+
const track = localTrackRef.current;
|
|
155
|
+
if (!track) return;
|
|
156
|
+
const reconcile = () => {
|
|
157
|
+
if (localTrackRef.current === track) setMicrophoneMutedState(track.isMuted);
|
|
158
|
+
};
|
|
159
|
+
(muted ? track.mute() : track.unmute()).then(reconcile, reconcile);
|
|
160
|
+
}, []),
|
|
161
|
+
setSpeakerMuted: (0, react.useCallback)((muted) => {
|
|
162
|
+
var _roomRef$current$remo, _roomRef$current;
|
|
163
|
+
speakerMutedRef.current = muted;
|
|
164
|
+
const participants = (_roomRef$current$remo = (_roomRef$current = roomRef.current) === null || _roomRef$current === void 0 ? void 0 : _roomRef$current.remoteParticipants.values()) !== null && _roomRef$current$remo !== void 0 ? _roomRef$current$remo : [];
|
|
165
|
+
for (const participant of participants) for (const { track } of participant.audioTrackPublications.values()) if (track instanceof livekit_client.RemoteAudioTrack) track.setVolume(muted ? 0 : 1);
|
|
166
|
+
setSpeakerMutedState(muted);
|
|
167
|
+
}, []),
|
|
168
|
+
speakerMuted,
|
|
169
|
+
start,
|
|
170
|
+
state
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
exports.useLorikeetCall = useLorikeetCall;
|
|
175
|
+
//# sourceMappingURL=voice.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"voice.cjs","names":["useState","useRef","useCallback","AudioSession","createLocalAudioTrack","Room","RoomEvent","RemoteAudioTrack","Track"],"sources":["../src/voice/use-lorikeet-call.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react'\n\nimport { AudioSession, registerGlobals } from '@livekit/react-native'\nimport {\n createLocalAudioTrack,\n type LocalAudioTrack,\n RemoteAudioTrack,\n Room,\n RoomEvent,\n Track,\n} from 'livekit-client'\n\nimport type {\n LorikeetCall,\n LorikeetCallError,\n LorikeetCallSession,\n LorikeetCallState,\n UseLorikeetCallOptions,\n} from './types'\n\n/** The server's `LORIKEET_AGENT_IDENTITY`. The call ends when it leaves. */\nexport const LORIKEET_AGENT_IDENTITY = 'lorikeet-agent'\n\n// react-native-webrtc rejects a refused permission with a plain object named\n// `SecurityError`, not a DOMException, so match on the name alone.\nconst errorFor = (error: unknown): LorikeetCallError => {\n if (typeof error !== 'object' || error === null || !('name' in error)) {\n return 'failed'\n }\n return error.name === 'SecurityError' || error.name === 'NotAllowedError'\n ? 'microphone_denied'\n : 'failed'\n}\n\nconst isSession = (value: unknown): value is LorikeetCallSession =>\n typeof value === 'object' &&\n value !== null &&\n 'conversationId' in value &&\n typeof value.conversationId === 'string' &&\n 'token' in value &&\n typeof value.token === 'string' &&\n 'wsUrl' in value &&\n typeof value.wsUrl === 'string'\n\n/**\n * Runs a Lorikeet voice call over LiveKit's native audio, with the web Voice\n * Widget's states and errors.\n */\nexport function useLorikeetCall({\n getSession,\n onError,\n}: UseLorikeetCallOptions): LorikeetCall {\n const [state, setState] = useState<LorikeetCallState>('idle')\n const [error, setError] = useState<LorikeetCallError | undefined>()\n const [ended, setEnded] = useState(false)\n const [microphoneMuted, setMicrophoneMutedState] = useState(false)\n const [speakerMuted, setSpeakerMutedState] = useState(false)\n\n const stateRef = useRef(state)\n const generationRef = useRef(0)\n const localTrackRef = useRef<LocalAudioTrack | null>(null)\n const roomRef = useRef<Room | null>(null)\n const speakerMutedRef = useRef(false)\n const audioSessionActiveRef = useRef(false)\n // The previous call's teardown, so the next call cannot start its audio\n // session only to have that teardown stop it.\n const audioSessionStoppedRef = useRef<Promise<void>>(Promise.resolve())\n // Read at call time, so a host passing inline functions keeps `start` stable.\n const optionsRef = useRef({ getSession, onError })\n optionsRef.current = { getSession, onError }\n\n const transitionTo = useCallback((next: LorikeetCallState) => {\n stateRef.current = next\n setState(next)\n }, [])\n\n const cleanup = useCallback(() => {\n localTrackRef.current?.stop()\n localTrackRef.current = null\n const room = roomRef.current\n roomRef.current = null\n // Nothing is left to report a teardown failure to, and a failed disconnect\n // must not leave the native audio session running.\n const disconnected = (room ? room.disconnect() : Promise.resolve()).catch(\n () => undefined,\n )\n if (audioSessionActiveRef.current) {\n audioSessionActiveRef.current = false\n audioSessionStoppedRef.current = disconnected\n .then(() => AudioSession.stopAudioSession())\n .catch(() => undefined)\n }\n }, [])\n\n const resetMutes = useCallback(() => {\n setMicrophoneMutedState(false)\n setSpeakerMutedState(false)\n speakerMutedRef.current = false\n }, [])\n\n const end = useCallback(() => {\n generationRef.current += 1\n cleanup()\n setEnded(stateRef.current === 'connecting' || stateRef.current === 'active')\n setError(undefined)\n resetMutes()\n transitionTo('idle')\n }, [cleanup, resetMutes, transitionTo])\n\n const fail = useCallback(\n (callError: LorikeetCallError) => {\n generationRef.current += 1\n cleanup()\n setEnded(false)\n setError(callError)\n transitionTo('error')\n },\n [cleanup, transitionTo],\n )\n\n useEffect(() => {\n return () => {\n generationRef.current += 1\n cleanup()\n }\n }, [cleanup])\n\n const start = useCallback(async () => {\n if (stateRef.current !== 'idle' && stateRef.current !== 'error') {\n return\n }\n\n setError(undefined)\n setEnded(false)\n resetMutes()\n transitionTo('starting')\n const generation = ++generationRef.current\n const isStale = () => generationRef.current !== generation\n\n try {\n // `registerGlobals` sets this. Calling it again would replace any iOS\n // audio setup a host that already uses LiveKit configured itself.\n if (!('LiveKitReactNativeGlobal' in globalThis)) {\n registerGlobals()\n }\n await audioSessionStoppedRef.current\n if (isStale()) {\n return\n }\n audioSessionActiveRef.current = true\n await AudioSession.startAudioSession()\n if (isStale()) {\n return\n }\n\n const localTrack = await createLocalAudioTrack()\n if (isStale()) {\n localTrack.stop()\n return\n }\n localTrackRef.current = localTrack\n\n const session = await optionsRef.current.getSession()\n if (isStale()) {\n return\n }\n if (session === null) {\n fail('not_available')\n return\n }\n if (!isSession(session)) {\n throw new TypeError(\n 'getSession must resolve to { conversationId, wsUrl, token } or null',\n )\n }\n transitionTo('connecting')\n\n const room = new Room()\n roomRef.current = room\n const isCurrentRoom = () => roomRef.current === room\n room.on(RoomEvent.ParticipantDisconnected, (participant) => {\n if (\n isCurrentRoom() &&\n participant.identity === LORIKEET_AGENT_IDENTITY\n ) {\n end()\n }\n })\n room.on(RoomEvent.TrackSubscribed, (track) => {\n if (isCurrentRoom() && track instanceof RemoteAudioTrack) {\n track.setVolume(speakerMutedRef.current ? 0 : 1)\n }\n })\n // A failed join also emits Disconnected, just before `connect` rejects,\n // and has to surface as `failed` rather than as a hang-up.\n let joined = false\n room.on(RoomEvent.Disconnected, () => {\n if (isCurrentRoom() && joined) {\n end()\n }\n })\n\n await room.connect(session.wsUrl, session.token, { autoSubscribe: true })\n if (isStale()) {\n return\n }\n joined = true\n await room.localParticipant.publishTrack(localTrack, {\n source: Track.Source.Microphone,\n })\n if (isStale()) {\n return\n }\n transitionTo('active')\n } catch (caught) {\n if (isStale()) {\n return\n }\n const callError = errorFor(caught)\n fail(callError)\n if (callError === 'failed') {\n try {\n optionsRef.current.onError?.(caught)\n } catch {\n // A host's failing error handler must not turn into an unhandled\n // rejection from a `start()` that callers fire and forget.\n }\n }\n }\n }, [end, fail, resetMutes, transitionTo])\n\n const setMicrophoneMuted = useCallback((muted: boolean) => {\n const track = localTrackRef.current\n if (!track) {\n return\n }\n // Report what the track did rather than what was asked, so a mute that\n // fails is not shown as muted while the customer is still heard.\n const reconcile = () => {\n if (localTrackRef.current === track) {\n setMicrophoneMutedState(track.isMuted)\n }\n }\n void (muted ? track.mute() : track.unmute()).then(reconcile, reconcile)\n }, [])\n\n const setSpeakerMuted = useCallback((muted: boolean) => {\n speakerMutedRef.current = muted\n const participants = roomRef.current?.remoteParticipants.values() ?? []\n for (const participant of participants) {\n for (const { track } of participant.audioTrackPublications.values()) {\n if (track instanceof RemoteAudioTrack) {\n track.setVolume(muted ? 0 : 1)\n }\n }\n }\n setSpeakerMutedState(muted)\n }, [])\n\n return {\n end,\n ended,\n error,\n microphoneMuted,\n setMicrophoneMuted,\n setSpeakerMuted,\n speakerMuted,\n start,\n state,\n }\n}\n"],"mappings":"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"}
|
package/dist/voice.d.cts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `POST /v1/conversation/voice/web/create` returns. Your backend calls it
|
|
3
|
+
* with your API credentials and hands the result to the app, which must never
|
|
4
|
+
* hold those credentials itself.
|
|
5
|
+
*/
|
|
6
|
+
interface LorikeetCallSession {
|
|
7
|
+
/** The conversation the call belongs to. */
|
|
8
|
+
conversationId: string;
|
|
9
|
+
/** The token that admits this customer to the call's room. */
|
|
10
|
+
token: string;
|
|
11
|
+
/** The voice server to connect to. */
|
|
12
|
+
wsUrl: string;
|
|
13
|
+
}
|
|
14
|
+
/** The web Voice Widget's call states. */
|
|
15
|
+
type LorikeetCallState = 'idle' | 'starting' | 'connecting' | 'active' | 'error';
|
|
16
|
+
/**
|
|
17
|
+
* Why a call is in `error`, from the web Voice Widget's set.
|
|
18
|
+
*
|
|
19
|
+
* - `not_available`: `getSession` returned `null`.
|
|
20
|
+
* - `microphone_denied`: the customer refused microphone access, or it is off
|
|
21
|
+
* for the app in the device's settings.
|
|
22
|
+
* - `microphone_system_denied`: the web Voice Widget's code for a browser
|
|
23
|
+
* blocked by the operating system. Kept so one handler covers both
|
|
24
|
+
* platforms. A mobile device reports `microphone_denied` instead.
|
|
25
|
+
* - `failed`: anything else. `onError` receives the cause.
|
|
26
|
+
*/
|
|
27
|
+
type LorikeetCallError = 'not_available' | 'microphone_denied' | 'microphone_system_denied' | 'failed';
|
|
28
|
+
interface UseLorikeetCallOptions {
|
|
29
|
+
/**
|
|
30
|
+
* Fetches a session from your backend for a new call. Called each time the
|
|
31
|
+
* customer starts one, after microphone access is granted, so a refused
|
|
32
|
+
* microphone starts no conversation.
|
|
33
|
+
*
|
|
34
|
+
* Return `null` when calling is not available to this customer, which puts
|
|
35
|
+
* the call in `error` with `not_available`. Throw, or return anything that
|
|
36
|
+
* is not a session, for `failed`.
|
|
37
|
+
*/
|
|
38
|
+
getSession: () => Promise<LorikeetCallSession | null>;
|
|
39
|
+
/**
|
|
40
|
+
* Receives the cause whenever a call fails with `failed`. Anything it throws
|
|
41
|
+
* is ignored.
|
|
42
|
+
*/
|
|
43
|
+
onError?: (cause: unknown) => void;
|
|
44
|
+
}
|
|
45
|
+
interface LorikeetCall {
|
|
46
|
+
/** End the call, or abandon one still starting. The state returns to `idle`. */
|
|
47
|
+
end: () => void;
|
|
48
|
+
/**
|
|
49
|
+
* Whether the last call ended, by either side, after it began connecting.
|
|
50
|
+
* Reset when the next call starts.
|
|
51
|
+
*/
|
|
52
|
+
ended: boolean;
|
|
53
|
+
/** Set while `state` is `error`. */
|
|
54
|
+
error: LorikeetCallError | undefined;
|
|
55
|
+
microphoneMuted: boolean;
|
|
56
|
+
/** Mute or unmute the customer's microphone. Only applies to a call in progress. */
|
|
57
|
+
setMicrophoneMuted: (muted: boolean) => void;
|
|
58
|
+
/** Mute or unmute the agent's audio. */
|
|
59
|
+
setSpeakerMuted: (muted: boolean) => void;
|
|
60
|
+
speakerMuted: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Start a call from `idle` or `error`. Does nothing in any other state.
|
|
63
|
+
* Never rejects: a failure is reported through `state` and `error`.
|
|
64
|
+
*/
|
|
65
|
+
start: () => Promise<void>;
|
|
66
|
+
state: LorikeetCallState;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Runs a Lorikeet voice call over LiveKit's native audio, with the web Voice
|
|
70
|
+
* Widget's states and errors.
|
|
71
|
+
*/
|
|
72
|
+
declare function useLorikeetCall({ getSession, onError }: UseLorikeetCallOptions): LorikeetCall;
|
|
73
|
+
export { type LorikeetCall, type LorikeetCallError, type LorikeetCallSession, type LorikeetCallState, type UseLorikeetCallOptions, useLorikeetCall };
|
|
74
|
+
//# sourceMappingURL=voice.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"voice.d.cts","names":[],"sources":["../src/voice/types.ts","../src/voice/use-lorikeet-call.ts"],"mappings":";;;;;UAKiB;;EAEf;;EAEA;;EAEA;;;KAIU;;;;;;;;;;;;KAkBA;UAMK;;;;;;;;;;EAUf,kBAAkB,QAAQ;;;;;EAK1B,WAAW;;UAGI;;EAEf;;;;;EAKA;;EAEA,OAAO;EACP;;EAEA,qBAAqB;;EAErB,kBAAkB;EAClB;;;;;EAKA,aAAa;EACb,OAAO;;;;;;iBC9BO,kBACd,YACA,WACC,yBAAyB"}
|
package/dist/voice.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `POST /v1/conversation/voice/web/create` returns. Your backend calls it
|
|
3
|
+
* with your API credentials and hands the result to the app, which must never
|
|
4
|
+
* hold those credentials itself.
|
|
5
|
+
*/
|
|
6
|
+
interface LorikeetCallSession {
|
|
7
|
+
/** The conversation the call belongs to. */
|
|
8
|
+
conversationId: string;
|
|
9
|
+
/** The token that admits this customer to the call's room. */
|
|
10
|
+
token: string;
|
|
11
|
+
/** The voice server to connect to. */
|
|
12
|
+
wsUrl: string;
|
|
13
|
+
}
|
|
14
|
+
/** The web Voice Widget's call states. */
|
|
15
|
+
type LorikeetCallState = 'idle' | 'starting' | 'connecting' | 'active' | 'error';
|
|
16
|
+
/**
|
|
17
|
+
* Why a call is in `error`, from the web Voice Widget's set.
|
|
18
|
+
*
|
|
19
|
+
* - `not_available`: `getSession` returned `null`.
|
|
20
|
+
* - `microphone_denied`: the customer refused microphone access, or it is off
|
|
21
|
+
* for the app in the device's settings.
|
|
22
|
+
* - `microphone_system_denied`: the web Voice Widget's code for a browser
|
|
23
|
+
* blocked by the operating system. Kept so one handler covers both
|
|
24
|
+
* platforms. A mobile device reports `microphone_denied` instead.
|
|
25
|
+
* - `failed`: anything else. `onError` receives the cause.
|
|
26
|
+
*/
|
|
27
|
+
type LorikeetCallError = 'not_available' | 'microphone_denied' | 'microphone_system_denied' | 'failed';
|
|
28
|
+
interface UseLorikeetCallOptions {
|
|
29
|
+
/**
|
|
30
|
+
* Fetches a session from your backend for a new call. Called each time the
|
|
31
|
+
* customer starts one, after microphone access is granted, so a refused
|
|
32
|
+
* microphone starts no conversation.
|
|
33
|
+
*
|
|
34
|
+
* Return `null` when calling is not available to this customer, which puts
|
|
35
|
+
* the call in `error` with `not_available`. Throw, or return anything that
|
|
36
|
+
* is not a session, for `failed`.
|
|
37
|
+
*/
|
|
38
|
+
getSession: () => Promise<LorikeetCallSession | null>;
|
|
39
|
+
/**
|
|
40
|
+
* Receives the cause whenever a call fails with `failed`. Anything it throws
|
|
41
|
+
* is ignored.
|
|
42
|
+
*/
|
|
43
|
+
onError?: (cause: unknown) => void;
|
|
44
|
+
}
|
|
45
|
+
interface LorikeetCall {
|
|
46
|
+
/** End the call, or abandon one still starting. The state returns to `idle`. */
|
|
47
|
+
end: () => void;
|
|
48
|
+
/**
|
|
49
|
+
* Whether the last call ended, by either side, after it began connecting.
|
|
50
|
+
* Reset when the next call starts.
|
|
51
|
+
*/
|
|
52
|
+
ended: boolean;
|
|
53
|
+
/** Set while `state` is `error`. */
|
|
54
|
+
error: LorikeetCallError | undefined;
|
|
55
|
+
microphoneMuted: boolean;
|
|
56
|
+
/** Mute or unmute the customer's microphone. Only applies to a call in progress. */
|
|
57
|
+
setMicrophoneMuted: (muted: boolean) => void;
|
|
58
|
+
/** Mute or unmute the agent's audio. */
|
|
59
|
+
setSpeakerMuted: (muted: boolean) => void;
|
|
60
|
+
speakerMuted: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Start a call from `idle` or `error`. Does nothing in any other state.
|
|
63
|
+
* Never rejects: a failure is reported through `state` and `error`.
|
|
64
|
+
*/
|
|
65
|
+
start: () => Promise<void>;
|
|
66
|
+
state: LorikeetCallState;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Runs a Lorikeet voice call over LiveKit's native audio, with the web Voice
|
|
70
|
+
* Widget's states and errors.
|
|
71
|
+
*/
|
|
72
|
+
declare function useLorikeetCall({ getSession, onError }: UseLorikeetCallOptions): LorikeetCall;
|
|
73
|
+
export { type LorikeetCall, type LorikeetCallError, type LorikeetCallSession, type LorikeetCallState, type UseLorikeetCallOptions, useLorikeetCall };
|
|
74
|
+
//# sourceMappingURL=voice.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"voice.d.ts","names":[],"sources":["../src/voice/types.ts","../src/voice/use-lorikeet-call.ts"],"mappings":";;;;;UAKiB;;EAEf;;EAEA;;EAEA;;;KAIU;;;;;;;;;;;;KAkBA;UAMK;;;;;;;;;;EAUf,kBAAkB,QAAQ;;;;;EAK1B,WAAW;;UAGI;;EAEf;;;;;EAKA;;EAEA,OAAO;EACP;;EAEA,qBAAqB;;EAErB,kBAAkB;EAClB;;;;;EAKA,aAAa;EACb,OAAO;;;;;;iBC9BO,kBACd,YACA,WACC,yBAAyB"}
|
package/dist/voice.js
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { AudioSession, registerGlobals } from "@livekit/react-native";
|
|
3
|
+
import { RemoteAudioTrack, Room, RoomEvent, Track, createLocalAudioTrack } from "livekit-client";
|
|
4
|
+
|
|
5
|
+
// src/voice/use-lorikeet-call.ts
|
|
6
|
+
/** The server's `LORIKEET_AGENT_IDENTITY`. The call ends when it leaves. */
|
|
7
|
+
const LORIKEET_AGENT_IDENTITY = "lorikeet-agent";
|
|
8
|
+
const errorFor = (error) => {
|
|
9
|
+
if (typeof error !== "object" || error === null || !("name" in error)) return "failed";
|
|
10
|
+
return error.name === "SecurityError" || error.name === "NotAllowedError" ? "microphone_denied" : "failed";
|
|
11
|
+
};
|
|
12
|
+
const isSession = (value) => typeof value === "object" && value !== null && "conversationId" in value && typeof value.conversationId === "string" && "token" in value && typeof value.token === "string" && "wsUrl" in value && typeof value.wsUrl === "string";
|
|
13
|
+
/**
|
|
14
|
+
* Runs a Lorikeet voice call over LiveKit's native audio, with the web Voice
|
|
15
|
+
* Widget's states and errors.
|
|
16
|
+
*/
|
|
17
|
+
function useLorikeetCall({ getSession, onError }) {
|
|
18
|
+
const [state, setState] = useState("idle");
|
|
19
|
+
const [error, setError] = useState();
|
|
20
|
+
const [ended, setEnded] = useState(false);
|
|
21
|
+
const [microphoneMuted, setMicrophoneMutedState] = useState(false);
|
|
22
|
+
const [speakerMuted, setSpeakerMutedState] = useState(false);
|
|
23
|
+
const stateRef = useRef(state);
|
|
24
|
+
const generationRef = useRef(0);
|
|
25
|
+
const localTrackRef = useRef(null);
|
|
26
|
+
const roomRef = useRef(null);
|
|
27
|
+
const speakerMutedRef = useRef(false);
|
|
28
|
+
const audioSessionActiveRef = useRef(false);
|
|
29
|
+
const audioSessionStoppedRef = useRef(Promise.resolve());
|
|
30
|
+
const optionsRef = useRef({
|
|
31
|
+
getSession,
|
|
32
|
+
onError
|
|
33
|
+
});
|
|
34
|
+
optionsRef.current = {
|
|
35
|
+
getSession,
|
|
36
|
+
onError
|
|
37
|
+
};
|
|
38
|
+
const transitionTo = useCallback((next) => {
|
|
39
|
+
stateRef.current = next;
|
|
40
|
+
setState(next);
|
|
41
|
+
}, []);
|
|
42
|
+
const cleanup = useCallback(() => {
|
|
43
|
+
var _localTrackRef$curren;
|
|
44
|
+
(_localTrackRef$curren = localTrackRef.current) === null || _localTrackRef$curren === void 0 || _localTrackRef$curren.stop();
|
|
45
|
+
localTrackRef.current = null;
|
|
46
|
+
const room = roomRef.current;
|
|
47
|
+
roomRef.current = null;
|
|
48
|
+
const disconnected = (room ? room.disconnect() : Promise.resolve()).catch(() => void 0);
|
|
49
|
+
if (audioSessionActiveRef.current) {
|
|
50
|
+
audioSessionActiveRef.current = false;
|
|
51
|
+
audioSessionStoppedRef.current = disconnected.then(() => AudioSession.stopAudioSession()).catch(() => void 0);
|
|
52
|
+
}
|
|
53
|
+
}, []);
|
|
54
|
+
const resetMutes = useCallback(() => {
|
|
55
|
+
setMicrophoneMutedState(false);
|
|
56
|
+
setSpeakerMutedState(false);
|
|
57
|
+
speakerMutedRef.current = false;
|
|
58
|
+
}, []);
|
|
59
|
+
const end = useCallback(() => {
|
|
60
|
+
generationRef.current += 1;
|
|
61
|
+
cleanup();
|
|
62
|
+
setEnded(stateRef.current === "connecting" || stateRef.current === "active");
|
|
63
|
+
setError(void 0);
|
|
64
|
+
resetMutes();
|
|
65
|
+
transitionTo("idle");
|
|
66
|
+
}, [
|
|
67
|
+
cleanup,
|
|
68
|
+
resetMutes,
|
|
69
|
+
transitionTo
|
|
70
|
+
]);
|
|
71
|
+
const fail = useCallback((callError) => {
|
|
72
|
+
generationRef.current += 1;
|
|
73
|
+
cleanup();
|
|
74
|
+
setEnded(false);
|
|
75
|
+
setError(callError);
|
|
76
|
+
transitionTo("error");
|
|
77
|
+
}, [cleanup, transitionTo]);
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
return () => {
|
|
80
|
+
generationRef.current += 1;
|
|
81
|
+
cleanup();
|
|
82
|
+
};
|
|
83
|
+
}, [cleanup]);
|
|
84
|
+
const start = useCallback(async () => {
|
|
85
|
+
if (stateRef.current !== "idle" && stateRef.current !== "error") return;
|
|
86
|
+
setError(void 0);
|
|
87
|
+
setEnded(false);
|
|
88
|
+
resetMutes();
|
|
89
|
+
transitionTo("starting");
|
|
90
|
+
const generation = ++generationRef.current;
|
|
91
|
+
const isStale = () => generationRef.current !== generation;
|
|
92
|
+
try {
|
|
93
|
+
if (!("LiveKitReactNativeGlobal" in globalThis)) registerGlobals();
|
|
94
|
+
await audioSessionStoppedRef.current;
|
|
95
|
+
if (isStale()) return;
|
|
96
|
+
audioSessionActiveRef.current = true;
|
|
97
|
+
await AudioSession.startAudioSession();
|
|
98
|
+
if (isStale()) return;
|
|
99
|
+
const localTrack = await createLocalAudioTrack();
|
|
100
|
+
if (isStale()) {
|
|
101
|
+
localTrack.stop();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
localTrackRef.current = localTrack;
|
|
105
|
+
const session = await optionsRef.current.getSession();
|
|
106
|
+
if (isStale()) return;
|
|
107
|
+
if (session === null) {
|
|
108
|
+
fail("not_available");
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (!isSession(session)) throw new TypeError("getSession must resolve to { conversationId, wsUrl, token } or null");
|
|
112
|
+
transitionTo("connecting");
|
|
113
|
+
const room = new Room();
|
|
114
|
+
roomRef.current = room;
|
|
115
|
+
const isCurrentRoom = () => roomRef.current === room;
|
|
116
|
+
room.on(RoomEvent.ParticipantDisconnected, (participant) => {
|
|
117
|
+
if (isCurrentRoom() && participant.identity === "lorikeet-agent") end();
|
|
118
|
+
});
|
|
119
|
+
room.on(RoomEvent.TrackSubscribed, (track) => {
|
|
120
|
+
if (isCurrentRoom() && track instanceof RemoteAudioTrack) track.setVolume(speakerMutedRef.current ? 0 : 1);
|
|
121
|
+
});
|
|
122
|
+
let joined = false;
|
|
123
|
+
room.on(RoomEvent.Disconnected, () => {
|
|
124
|
+
if (isCurrentRoom() && joined) end();
|
|
125
|
+
});
|
|
126
|
+
await room.connect(session.wsUrl, session.token, { autoSubscribe: true });
|
|
127
|
+
if (isStale()) return;
|
|
128
|
+
joined = true;
|
|
129
|
+
await room.localParticipant.publishTrack(localTrack, { source: Track.Source.Microphone });
|
|
130
|
+
if (isStale()) return;
|
|
131
|
+
transitionTo("active");
|
|
132
|
+
} catch (caught) {
|
|
133
|
+
if (isStale()) return;
|
|
134
|
+
const callError = errorFor(caught);
|
|
135
|
+
fail(callError);
|
|
136
|
+
if (callError === "failed") try {
|
|
137
|
+
var _optionsRef$current$o, _optionsRef$current;
|
|
138
|
+
(_optionsRef$current$o = (_optionsRef$current = optionsRef.current).onError) === null || _optionsRef$current$o === void 0 || _optionsRef$current$o.call(_optionsRef$current, caught);
|
|
139
|
+
} catch {}
|
|
140
|
+
}
|
|
141
|
+
}, [
|
|
142
|
+
end,
|
|
143
|
+
fail,
|
|
144
|
+
resetMutes,
|
|
145
|
+
transitionTo
|
|
146
|
+
]);
|
|
147
|
+
return {
|
|
148
|
+
end,
|
|
149
|
+
ended,
|
|
150
|
+
error,
|
|
151
|
+
microphoneMuted,
|
|
152
|
+
setMicrophoneMuted: useCallback((muted) => {
|
|
153
|
+
const track = localTrackRef.current;
|
|
154
|
+
if (!track) return;
|
|
155
|
+
const reconcile = () => {
|
|
156
|
+
if (localTrackRef.current === track) setMicrophoneMutedState(track.isMuted);
|
|
157
|
+
};
|
|
158
|
+
(muted ? track.mute() : track.unmute()).then(reconcile, reconcile);
|
|
159
|
+
}, []),
|
|
160
|
+
setSpeakerMuted: useCallback((muted) => {
|
|
161
|
+
var _roomRef$current$remo, _roomRef$current;
|
|
162
|
+
speakerMutedRef.current = muted;
|
|
163
|
+
const participants = (_roomRef$current$remo = (_roomRef$current = roomRef.current) === null || _roomRef$current === void 0 ? void 0 : _roomRef$current.remoteParticipants.values()) !== null && _roomRef$current$remo !== void 0 ? _roomRef$current$remo : [];
|
|
164
|
+
for (const participant of participants) for (const { track } of participant.audioTrackPublications.values()) if (track instanceof RemoteAudioTrack) track.setVolume(muted ? 0 : 1);
|
|
165
|
+
setSpeakerMutedState(muted);
|
|
166
|
+
}, []),
|
|
167
|
+
speakerMuted,
|
|
168
|
+
start,
|
|
169
|
+
state
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export { useLorikeetCall };
|
|
174
|
+
//# sourceMappingURL=voice.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"voice.js","names":[],"sources":["../src/voice/use-lorikeet-call.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react'\n\nimport { AudioSession, registerGlobals } from '@livekit/react-native'\nimport {\n createLocalAudioTrack,\n type LocalAudioTrack,\n RemoteAudioTrack,\n Room,\n RoomEvent,\n Track,\n} from 'livekit-client'\n\nimport type {\n LorikeetCall,\n LorikeetCallError,\n LorikeetCallSession,\n LorikeetCallState,\n UseLorikeetCallOptions,\n} from './types'\n\n/** The server's `LORIKEET_AGENT_IDENTITY`. The call ends when it leaves. */\nexport const LORIKEET_AGENT_IDENTITY = 'lorikeet-agent'\n\n// react-native-webrtc rejects a refused permission with a plain object named\n// `SecurityError`, not a DOMException, so match on the name alone.\nconst errorFor = (error: unknown): LorikeetCallError => {\n if (typeof error !== 'object' || error === null || !('name' in error)) {\n return 'failed'\n }\n return error.name === 'SecurityError' || error.name === 'NotAllowedError'\n ? 'microphone_denied'\n : 'failed'\n}\n\nconst isSession = (value: unknown): value is LorikeetCallSession =>\n typeof value === 'object' &&\n value !== null &&\n 'conversationId' in value &&\n typeof value.conversationId === 'string' &&\n 'token' in value &&\n typeof value.token === 'string' &&\n 'wsUrl' in value &&\n typeof value.wsUrl === 'string'\n\n/**\n * Runs a Lorikeet voice call over LiveKit's native audio, with the web Voice\n * Widget's states and errors.\n */\nexport function useLorikeetCall({\n getSession,\n onError,\n}: UseLorikeetCallOptions): LorikeetCall {\n const [state, setState] = useState<LorikeetCallState>('idle')\n const [error, setError] = useState<LorikeetCallError | undefined>()\n const [ended, setEnded] = useState(false)\n const [microphoneMuted, setMicrophoneMutedState] = useState(false)\n const [speakerMuted, setSpeakerMutedState] = useState(false)\n\n const stateRef = useRef(state)\n const generationRef = useRef(0)\n const localTrackRef = useRef<LocalAudioTrack | null>(null)\n const roomRef = useRef<Room | null>(null)\n const speakerMutedRef = useRef(false)\n const audioSessionActiveRef = useRef(false)\n // The previous call's teardown, so the next call cannot start its audio\n // session only to have that teardown stop it.\n const audioSessionStoppedRef = useRef<Promise<void>>(Promise.resolve())\n // Read at call time, so a host passing inline functions keeps `start` stable.\n const optionsRef = useRef({ getSession, onError })\n optionsRef.current = { getSession, onError }\n\n const transitionTo = useCallback((next: LorikeetCallState) => {\n stateRef.current = next\n setState(next)\n }, [])\n\n const cleanup = useCallback(() => {\n localTrackRef.current?.stop()\n localTrackRef.current = null\n const room = roomRef.current\n roomRef.current = null\n // Nothing is left to report a teardown failure to, and a failed disconnect\n // must not leave the native audio session running.\n const disconnected = (room ? room.disconnect() : Promise.resolve()).catch(\n () => undefined,\n )\n if (audioSessionActiveRef.current) {\n audioSessionActiveRef.current = false\n audioSessionStoppedRef.current = disconnected\n .then(() => AudioSession.stopAudioSession())\n .catch(() => undefined)\n }\n }, [])\n\n const resetMutes = useCallback(() => {\n setMicrophoneMutedState(false)\n setSpeakerMutedState(false)\n speakerMutedRef.current = false\n }, [])\n\n const end = useCallback(() => {\n generationRef.current += 1\n cleanup()\n setEnded(stateRef.current === 'connecting' || stateRef.current === 'active')\n setError(undefined)\n resetMutes()\n transitionTo('idle')\n }, [cleanup, resetMutes, transitionTo])\n\n const fail = useCallback(\n (callError: LorikeetCallError) => {\n generationRef.current += 1\n cleanup()\n setEnded(false)\n setError(callError)\n transitionTo('error')\n },\n [cleanup, transitionTo],\n )\n\n useEffect(() => {\n return () => {\n generationRef.current += 1\n cleanup()\n }\n }, [cleanup])\n\n const start = useCallback(async () => {\n if (stateRef.current !== 'idle' && stateRef.current !== 'error') {\n return\n }\n\n setError(undefined)\n setEnded(false)\n resetMutes()\n transitionTo('starting')\n const generation = ++generationRef.current\n const isStale = () => generationRef.current !== generation\n\n try {\n // `registerGlobals` sets this. Calling it again would replace any iOS\n // audio setup a host that already uses LiveKit configured itself.\n if (!('LiveKitReactNativeGlobal' in globalThis)) {\n registerGlobals()\n }\n await audioSessionStoppedRef.current\n if (isStale()) {\n return\n }\n audioSessionActiveRef.current = true\n await AudioSession.startAudioSession()\n if (isStale()) {\n return\n }\n\n const localTrack = await createLocalAudioTrack()\n if (isStale()) {\n localTrack.stop()\n return\n }\n localTrackRef.current = localTrack\n\n const session = await optionsRef.current.getSession()\n if (isStale()) {\n return\n }\n if (session === null) {\n fail('not_available')\n return\n }\n if (!isSession(session)) {\n throw new TypeError(\n 'getSession must resolve to { conversationId, wsUrl, token } or null',\n )\n }\n transitionTo('connecting')\n\n const room = new Room()\n roomRef.current = room\n const isCurrentRoom = () => roomRef.current === room\n room.on(RoomEvent.ParticipantDisconnected, (participant) => {\n if (\n isCurrentRoom() &&\n participant.identity === LORIKEET_AGENT_IDENTITY\n ) {\n end()\n }\n })\n room.on(RoomEvent.TrackSubscribed, (track) => {\n if (isCurrentRoom() && track instanceof RemoteAudioTrack) {\n track.setVolume(speakerMutedRef.current ? 0 : 1)\n }\n })\n // A failed join also emits Disconnected, just before `connect` rejects,\n // and has to surface as `failed` rather than as a hang-up.\n let joined = false\n room.on(RoomEvent.Disconnected, () => {\n if (isCurrentRoom() && joined) {\n end()\n }\n })\n\n await room.connect(session.wsUrl, session.token, { autoSubscribe: true })\n if (isStale()) {\n return\n }\n joined = true\n await room.localParticipant.publishTrack(localTrack, {\n source: Track.Source.Microphone,\n })\n if (isStale()) {\n return\n }\n transitionTo('active')\n } catch (caught) {\n if (isStale()) {\n return\n }\n const callError = errorFor(caught)\n fail(callError)\n if (callError === 'failed') {\n try {\n optionsRef.current.onError?.(caught)\n } catch {\n // A host's failing error handler must not turn into an unhandled\n // rejection from a `start()` that callers fire and forget.\n }\n }\n }\n }, [end, fail, resetMutes, transitionTo])\n\n const setMicrophoneMuted = useCallback((muted: boolean) => {\n const track = localTrackRef.current\n if (!track) {\n return\n }\n // Report what the track did rather than what was asked, so a mute that\n // fails is not shown as muted while the customer is still heard.\n const reconcile = () => {\n if (localTrackRef.current === track) {\n setMicrophoneMutedState(track.isMuted)\n }\n }\n void (muted ? track.mute() : track.unmute()).then(reconcile, reconcile)\n }, [])\n\n const setSpeakerMuted = useCallback((muted: boolean) => {\n speakerMutedRef.current = muted\n const participants = roomRef.current?.remoteParticipants.values() ?? []\n for (const participant of participants) {\n for (const { track } of participant.audioTrackPublications.values()) {\n if (track instanceof RemoteAudioTrack) {\n track.setVolume(muted ? 0 : 1)\n }\n }\n }\n setSpeakerMutedState(muted)\n }, [])\n\n return {\n end,\n ended,\n error,\n microphoneMuted,\n setMicrophoneMuted,\n setSpeakerMuted,\n speakerMuted,\n start,\n state,\n }\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lorikeetai/react-native-sdk",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0-beta.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"private": false,
|
|
6
|
-
"description": "LorikeetCX React Native
|
|
6
|
+
"description": "LorikeetCX React Native SDK: the chat widget in a WebView, and in-app voice calls",
|
|
7
7
|
"keywords": [
|
|
8
8
|
"lorikeet",
|
|
9
9
|
"react-native",
|
|
10
10
|
"webview",
|
|
11
|
-
"widget"
|
|
11
|
+
"widget",
|
|
12
|
+
"voice"
|
|
12
13
|
],
|
|
13
14
|
"author": "LorikeetCX.ai",
|
|
14
15
|
"license": "MIT",
|
|
@@ -27,20 +28,48 @@
|
|
|
27
28
|
"react-native": "./dist/index.js",
|
|
28
29
|
"import": "./dist/index.js",
|
|
29
30
|
"require": "./dist/index.cjs"
|
|
31
|
+
},
|
|
32
|
+
"./voice": {
|
|
33
|
+
"react-native": "./dist/voice.js",
|
|
34
|
+
"import": {
|
|
35
|
+
"types": "./dist/voice.d.ts",
|
|
36
|
+
"default": "./dist/voice.js"
|
|
37
|
+
},
|
|
38
|
+
"require": {
|
|
39
|
+
"types": "./dist/voice.d.cts",
|
|
40
|
+
"default": "./dist/voice.cjs"
|
|
41
|
+
}
|
|
30
42
|
}
|
|
31
43
|
},
|
|
32
44
|
"peerDependencies": {
|
|
45
|
+
"@livekit/react-native": "^3.1.0",
|
|
46
|
+
"@livekit/react-native-webrtc": "^150.1.0",
|
|
47
|
+
"livekit-client": "^2.20.0",
|
|
33
48
|
"react": ">=18.0.0",
|
|
34
49
|
"react-native": ">=0.70.0",
|
|
35
50
|
"react-native-safe-area-context": ">=4.0.0 <6.0.0",
|
|
36
51
|
"react-native-svg": ">=13.0.0 <16.0.0",
|
|
37
52
|
"react-native-webview": ">=13.0.0 <14.0.0"
|
|
38
53
|
},
|
|
54
|
+
"peerDependenciesMeta": {
|
|
55
|
+
"@livekit/react-native": {
|
|
56
|
+
"optional": true
|
|
57
|
+
},
|
|
58
|
+
"@livekit/react-native-webrtc": {
|
|
59
|
+
"optional": true
|
|
60
|
+
},
|
|
61
|
+
"livekit-client": {
|
|
62
|
+
"optional": true
|
|
63
|
+
}
|
|
64
|
+
},
|
|
39
65
|
"devDependencies": {
|
|
66
|
+
"@livekit/react-native": "3.1.0",
|
|
67
|
+
"@livekit/react-native-webrtc": "150.1.1",
|
|
40
68
|
"@testing-library/react": "^16.1.0",
|
|
41
69
|
"@types/react": "^19.0.0",
|
|
42
70
|
"@typescript/native-preview": "7.0.0-dev.20260707.2",
|
|
43
71
|
"happy-dom": "^21.0.4",
|
|
72
|
+
"livekit-client": "^2.23.0",
|
|
44
73
|
"react": "^19.1.0",
|
|
45
74
|
"react-dom": "^19.1.0",
|
|
46
75
|
"react-native": "0.81.4",
|