@facepe/widget-mobile-app 0.0.0-stage → 1.1.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/README.md CHANGED
@@ -1,3 +1,289 @@
1
- # Temporary Holding Version
1
+ # @facepe/widget-mobile-app
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The official **FacePe AI Digital Human Live Streaming SDK** for **React Native** (supporting both **iOS** and **Android**).
4
+
5
+ Embed interactive, photorealistic, lip-synced 3D digital avatars directly inside your native mobile apps with pure WebRTC media streaming, native microphone capture, bidirectional voice dialog, and live data events — **without webviews**.
6
+
7
+ ---
8
+
9
+ ## 📱 Features
10
+
11
+ - **⚡ Native WebRTC Streaming**: Pure native media streaming powered by `@livekit/react-native` (no WebViews or embedded iframes).
12
+ - **🗣️ Natural Two-Way Voice Dialog**: Real-time microphone capture with hardware echo cancellation, noise suppression, and server VAD.
13
+ - **🍎 Full iOS Support**: Hardware-accelerated video decoding on iOS 13.0+ (iPhone & iPad).
14
+ - **🤖 Full Android Support**: Hardware-accelerated video decoding on Android 7.0+ (API 24+).
15
+ - **💬 Real-Time Transcriptions**: Bidirectional captions for avatar speech and user speech.
16
+ - **🛒 Conversational Commerce**: Automatic detection of cart items, orders, and payment intents.
17
+ - **🎛️ Imperative Controls**: Ref methods to start/stop stream, mute/unmute mic, speak text, or interrupt speech.
18
+
19
+ ---
20
+
21
+ ## 📦 Installation
22
+
23
+ In your React Native project:
24
+
25
+ ```bash
26
+ npm install @facepe/widget-mobile-app @livekit/react-native @livekit/react-native-webrtc
27
+ ```
28
+
29
+ *(or using yarn / pnpm)*:
30
+
31
+ ```bash
32
+ yarn add @facepe/widget-mobile-app @livekit/react-native @livekit/react-native-webrtc
33
+ ```
34
+
35
+ ---
36
+
37
+ ## 🛠️ Platform Permissions Setup
38
+
39
+ ### iOS Setup (`ios/Podfile` & `Info.plist`)
40
+
41
+ 1. Open `ios/YourAppName/Info.plist` and add permission strings for microphone and audio:
42
+
43
+ ```xml
44
+ <key>NSMicrophoneUsageDescription</key>
45
+ <string>We need access to your microphone to converse with the FacePe AI avatar.</string>
46
+ <key>NSCameraUsageDescription</key>
47
+ <string>Camera access for interactive features.</string>
48
+ ```
49
+
50
+ 2. Run CocoaPods:
51
+ ```bash
52
+ cd ios && pod install && cd ..
53
+ ```
54
+
55
+ ---
56
+
57
+ ### Android Setup (`android/app/src/main/AndroidManifest.xml`)
58
+
59
+ Open `android/app/src/main/AndroidManifest.xml` and include permissions:
60
+
61
+ ```xml
62
+ <uses-permission android:name="android.permission.INTERNET" />
63
+ <uses-permission android:name="android.permission.RECORD_AUDIO" />
64
+ <uses-permission android:name="android.permission.CAMERA" />
65
+ <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
66
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
67
+ ```
68
+
69
+ ---
70
+
71
+ ## 🚀 Quick Start Example
72
+
73
+ ### 1. Server-side Token Endpoint
74
+
75
+ Exchange your `accessKey` and `secretKey` on your secure backend (Node.js / Express / Next.js / Python / Go) so credentials are never exposed inside mobile apps:
76
+
77
+ ```javascript
78
+ // Express server route: GET /api/facepe-token
79
+ app.get('/api/facepe-token', async (req, res) => {
80
+ const response = await fetch('https://api.facepe.ai/api/v1/sdk/token', {
81
+ method: 'POST',
82
+ headers: { 'Content-Type': 'application/json' },
83
+ body: JSON.stringify({
84
+ accessKey: process.env.FACEPE_SDK_ACCESS_KEY,
85
+ secretKey: process.env.FACEPE_SDK_SECRET_KEY,
86
+ channel: 'sdk',
87
+ }),
88
+ });
89
+ const data = await response.json();
90
+ res.send(data.data.token);
91
+ });
92
+ ```
93
+
94
+ ---
95
+
96
+ ### 2. React Native Component
97
+
98
+ ```tsx
99
+ import React, { useRef } from 'react';
100
+ import { StyleSheet, View, SafeAreaView, Text, TouchableOpacity } from 'react-native';
101
+ import {
102
+ FacePeMobileProvider,
103
+ FacePeMobileAvatar,
104
+ FacePeMobileAvatarRef,
105
+ useFacePeAvatar,
106
+ } from '@facepe/widget-mobile-app';
107
+
108
+ export default function App() {
109
+ const avatarRef = useRef<FacePeMobileAvatarRef>(null);
110
+
111
+ return (
112
+ <SafeAreaView style={styles.container}>
113
+ <Text style={styles.title}>FacePe AI Native Digital Human</Text>
114
+
115
+ <FacePeMobileProvider
116
+ apiBaseUrl="https://api.facepe.ai/api/v1/sdk"
117
+ getToken={async () => {
118
+ const res = await fetch('https://your-backend.com/api/facepe-token');
119
+ return await res.text();
120
+ }}
121
+ >
122
+ <AvatarScreen avatarRef={avatarRef} />
123
+ </FacePeMobileProvider>
124
+ </SafeAreaView>
125
+ );
126
+ }
127
+
128
+ function AvatarScreen({ avatarRef }: { avatarRef: React.RefObject<FacePeMobileAvatarRef> }) {
129
+ const { transcripts, isAvatarSpeaking, avatarProps } = useFacePeAvatar();
130
+
131
+ return (
132
+ <View style={styles.content}>
133
+ <View style={styles.avatarContainer}>
134
+ <FacePeMobileAvatar
135
+ ref={avatarRef}
136
+ language="en"
137
+ objectFit="cover"
138
+ autoStart={true}
139
+ enableMic={true}
140
+ {...avatarProps}
141
+ />
142
+ </View>
143
+
144
+ {/* Control bar */}
145
+ <View style={styles.controls}>
146
+ <TouchableOpacity
147
+ style={styles.button}
148
+ onPress={() => avatarRef.current?.toggleMic()}
149
+ >
150
+ <Text style={styles.buttonText}>Toggle Mic</Text>
151
+ </TouchableOpacity>
152
+
153
+ <TouchableOpacity
154
+ style={styles.button}
155
+ onPress={() => avatarRef.current?.interrupt()}
156
+ >
157
+ <Text style={styles.buttonText}>Interrupt</Text>
158
+ </TouchableOpacity>
159
+ </View>
160
+
161
+ {/* Real-time speech transcript log */}
162
+ <View style={styles.transcriptBox}>
163
+ {transcripts.slice(-3).map((item, idx) => (
164
+ <Text key={idx} style={styles.transcriptLine}>
165
+ <Text style={item.role === 'assistant' ? styles.avatarTag : styles.userTag}>
166
+ {item.role === 'assistant' ? 'Avatar: ' : 'You: '}
167
+ </Text>
168
+ {item.text}
169
+ </Text>
170
+ ))}
171
+ </View>
172
+ </View>
173
+ );
174
+ }
175
+
176
+ const styles = StyleSheet.create({
177
+ container: {
178
+ flex: 1,
179
+ backgroundColor: '#0F172A',
180
+ },
181
+ content: {
182
+ flex: 1,
183
+ padding: 16,
184
+ },
185
+ title: {
186
+ color: '#FFFFFF',
187
+ fontSize: 20,
188
+ fontWeight: 'bold',
189
+ textAlign: 'center',
190
+ marginVertical: 12,
191
+ },
192
+ avatarContainer: {
193
+ flex: 1,
194
+ borderRadius: 24,
195
+ overflow: 'hidden',
196
+ backgroundColor: '#1E293B',
197
+ },
198
+ controls: {
199
+ flexDirection: 'row',
200
+ justifyContent: 'center',
201
+ gap: 12,
202
+ marginVertical: 14,
203
+ },
204
+ button: {
205
+ backgroundColor: '#7C3AED',
206
+ paddingHorizontal: 20,
207
+ paddingVertical: 10,
208
+ borderRadius: 12,
209
+ },
210
+ buttonText: {
211
+ color: '#FFFFFF',
212
+ fontWeight: '600',
213
+ fontSize: 14,
214
+ },
215
+ transcriptBox: {
216
+ backgroundColor: '#1E293B',
217
+ padding: 12,
218
+ borderRadius: 12,
219
+ minHeight: 80,
220
+ },
221
+ transcriptLine: {
222
+ color: '#E2E8F0',
223
+ fontSize: 13,
224
+ marginBottom: 4,
225
+ },
226
+ avatarTag: {
227
+ color: '#A78BFA',
228
+ fontWeight: '700',
229
+ },
230
+ userTag: {
231
+ color: '#60A5FA',
232
+ fontWeight: '700',
233
+ },
234
+ });
235
+ ```
236
+
237
+ ---
238
+
239
+ ## ⚙️ Props & Configuration
240
+
241
+ ### `<FacePeMobileAvatar />`
242
+
243
+ | Prop | Type | Default | Description |
244
+ | :--- | :--- | :--- | :--- |
245
+ | `avatarKey` | `string` | *(optional)* | Specific avatar key or ID assigned to your company. |
246
+ | `language` | `string` | `'en'` | Spoken language code (`'en'`, `'hi'`, `'es'`, `'ta'`, `'te'`, etc.). |
247
+ | `style` | `ViewStyle` | `undefined` | Style applied to avatar container. |
248
+ | `videoStyle` | `ViewStyle` | `undefined` | Style applied to native video view. |
249
+ | `objectFit` | `'cover' \| 'contain'` | `'cover'` | Video aspect scaling mode. |
250
+ | `mirror` | `boolean` | `false` | Mirror video orientation. |
251
+ | `autoStart` | `boolean` | `true` | Automatically connect and start stream on mount. |
252
+ | `enableMic` | `boolean` | `true` | Automatically enable microphone on connect. |
253
+ | `onTranscript` | `function` | `undefined` | Fires when speech transcription arrives. |
254
+ | `onSpeakStart` | `function` | `undefined` | Fires when avatar starts speaking. |
255
+ | `onSpeakEnd` | `function` | `undefined` | Fires when avatar finishes speaking. |
256
+ | `onUserSpeakStart` | `function` | `undefined` | Fires when user voice activity begins. |
257
+ | `onUserSpeakEnd` | `function` | `undefined` | Fires when user voice activity ends. |
258
+ | `onOrderUpdate` | `function` | `undefined` | Fires when cart items or intents are recognized. |
259
+ | `onCheckout` | `function` | `undefined` | Fires when customer requests checkout / payment. |
260
+ | `onStatusChange` | `function` | `undefined` | Stream lifecycle status: `'idle' \| 'connecting' \| 'connected' \| 'reconnecting' \| 'disconnected' \| 'error'`. |
261
+ | `onError` | `function` | `undefined` | Stream error callback `{ code, message }`. |
262
+
263
+ ---
264
+
265
+ ### Imperative Ref (`FacePeMobileAvatarRef`)
266
+
267
+ ```tsx
268
+ const avatarRef = useRef<FacePeMobileAvatarRef>(null);
269
+
270
+ // Start streaming session
271
+ await avatarRef.current?.start();
272
+
273
+ // Stop streaming session & finalize billing usage
274
+ await avatarRef.current?.stop();
275
+
276
+ // Toggle mic on/off (returns new state boolean)
277
+ const isNowMuted = await avatarRef.current?.toggleMic();
278
+
279
+ // Make avatar speak programmatically
280
+ await avatarRef.current?.speak("Hello! How can I assist you today?");
281
+
282
+ // Interrupt avatar speech immediately
283
+ await avatarRef.current?.interrupt();
284
+ ```
285
+
286
+ ---
287
+
288
+ ## 📄 License
289
+ MIT © FacePe AI
@@ -0,0 +1,3 @@
1
+ import React from 'react';
2
+ import type { FacePeMobileAvatarProps, FacePeMobileAvatarRef } from './types';
3
+ export declare const FacePeMobileAvatar: React.ForwardRefExoticComponent<FacePeMobileAvatarProps & React.RefAttributes<FacePeMobileAvatarRef>>;
@@ -0,0 +1,576 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState, useImperativeHandle, forwardRef, useCallback, } from 'react';
3
+ import { View, StyleSheet, ActivityIndicator, Text, TouchableOpacity } from 'react-native';
4
+ import { useFacePeMobileContext } from './FacePeMobileProvider';
5
+ // Resolve @livekit/react-native dynamically at runtime so bare/Expo projects can run smoothly
6
+ function getLiveKitRN() {
7
+ try {
8
+ return require('@livekit/react-native');
9
+ }
10
+ catch {
11
+ return null;
12
+ }
13
+ }
14
+ let globalsRegistered = false;
15
+ function ensureLiveKitGlobals(LiveKitRN) {
16
+ if (globalsRegistered || !LiveKitRN)
17
+ return;
18
+ try {
19
+ if (typeof LiveKitRN.registerGlobals === 'function') {
20
+ LiveKitRN.registerGlobals();
21
+ globalsRegistered = true;
22
+ }
23
+ }
24
+ catch (err) {
25
+ console.warn('[FacePeMobileAvatar] registerGlobals warning:', err);
26
+ }
27
+ }
28
+ export const FacePeMobileAvatar = forwardRef(({ avatarKey, language = 'en', style, videoStyle, objectFit = 'cover', mirror = false, autoStart = true, enableMic = true, onTranscript, onSpeakStart, onSpeakEnd, onUserSpeakStart, onUserSpeakEnd, onOrderUpdate, onCheckout, onStatusChange, onError, }, ref) => {
29
+ const { token, loading: tokenLoading, error: tokenError, apiBaseUrl, refreshToken } = useFacePeMobileContext();
30
+ const [status, setStatus] = useState('idle');
31
+ const [videoTrack, setVideoTrack] = useState(null);
32
+ const [isMicEnabled, setIsMicEnabled] = useState(enableMic);
33
+ const [isAvatarSpeaking, setIsAvatarSpeaking] = useState(false);
34
+ const [isUserSpeaking, setIsUserSpeaking] = useState(false);
35
+ const [errorMessage, setErrorMessage] = useState(null);
36
+ const LiveKitRN = getLiveKitRN();
37
+ const hasLiveKit = !!LiveKitRN;
38
+ const roomRef = useRef(null);
39
+ const usageIdRef = useRef(null);
40
+ const mountedRef = useRef(true);
41
+ const isConnectingRef = useRef(false);
42
+ const tokenRef = useRef(token);
43
+ tokenRef.current = token;
44
+ // Helper: Safely emit status changes
45
+ const updateStatus = useCallback((newStatus) => {
46
+ if (!mountedRef.current)
47
+ return;
48
+ setStatus(newStatus);
49
+ onStatusChange?.(newStatus);
50
+ }, [onStatusChange]);
51
+ // Helper: Safely publish data channel packet
52
+ const sendData = (msg) => {
53
+ const room = roomRef.current;
54
+ if (!room || room.state !== 'connected')
55
+ return;
56
+ try {
57
+ const jsonStr = JSON.stringify(msg);
58
+ let payload;
59
+ if (typeof TextEncoder !== 'undefined') {
60
+ payload = new TextEncoder().encode(jsonStr);
61
+ }
62
+ else {
63
+ const arr = new Uint8Array(jsonStr.length);
64
+ for (let i = 0; i < jsonStr.length; i++) {
65
+ arr[i] = jsonStr.charCodeAt(i);
66
+ }
67
+ payload = arr;
68
+ }
69
+ room.localParticipant.publishData(payload, { reliable: true });
70
+ }
71
+ catch (err) {
72
+ console.warn('[FacePeMobileAvatar] Error publishing data packet:', err);
73
+ }
74
+ };
75
+ // Helper: Notify FacePe backend that session ended (for billing/usage)
76
+ const endUsage = async () => {
77
+ const uId = usageIdRef.current;
78
+ usageIdRef.current = null;
79
+ const currentToken = tokenRef.current;
80
+ if (!uId || !currentToken)
81
+ return;
82
+ try {
83
+ const cleanBase = apiBaseUrl.replace(/\/+$/, '');
84
+ await fetch(`${cleanBase}/avatar-sessions/${uId}/end`, {
85
+ method: 'POST',
86
+ headers: {
87
+ 'Content-Type': 'application/json',
88
+ Authorization: `Bearer ${currentToken}`,
89
+ },
90
+ });
91
+ }
92
+ catch {
93
+ // ignore background teardown error
94
+ }
95
+ };
96
+ // Start streaming session
97
+ const startStream = useCallback(async () => {
98
+ if (isConnectingRef.current || roomRef.current)
99
+ return;
100
+ if (!hasLiveKit) {
101
+ const msg = 'Please install `@livekit/react-native` and `@livekit/react-native-webrtc` to enable native AI avatar streaming.';
102
+ setErrorMessage(msg);
103
+ updateStatus('error');
104
+ onError?.({ code: 'MISSING_DEPENDENCY', message: msg });
105
+ return;
106
+ }
107
+ isConnectingRef.current = true;
108
+ setErrorMessage(null);
109
+ updateStatus('connecting');
110
+ try {
111
+ ensureLiveKitGlobals(LiveKitRN);
112
+ // Ensure active SDK auth token
113
+ let currentToken = tokenRef.current;
114
+ if (!currentToken) {
115
+ currentToken = await refreshToken();
116
+ }
117
+ if (!currentToken) {
118
+ throw new Error('Authentication token not available. Please verify your token provider.');
119
+ }
120
+ const cleanBase = apiBaseUrl.replace(/\/+$/, '');
121
+ // Resolve target avatar ID
122
+ let targetAvatarId = avatarKey;
123
+ if (!targetAvatarId) {
124
+ // If no avatar key passed, retrieve assigned avatars from /sdk/session
125
+ const sessionRes = await fetch(`${cleanBase}/session`, {
126
+ method: 'GET',
127
+ headers: {
128
+ Authorization: `Bearer ${currentToken}`,
129
+ },
130
+ });
131
+ if (sessionRes.ok) {
132
+ const sessionData = await sessionRes.json();
133
+ const avatars = sessionData?.data?.avatars || sessionData?.avatars || [];
134
+ if (avatars.length > 0) {
135
+ targetAvatarId = avatars[0].id || avatars[0].key;
136
+ }
137
+ }
138
+ }
139
+ if (!targetAvatarId) {
140
+ throw new Error('No Avatar configured or assigned to this token.');
141
+ }
142
+ // Request live streaming session from FacePe SDK backend
143
+ const sessionResp = await fetch(`${cleanBase}/avatars/${targetAvatarId}/sessions`, {
144
+ method: 'POST',
145
+ headers: {
146
+ 'Content-Type': 'application/json',
147
+ Authorization: `Bearer ${currentToken}`,
148
+ },
149
+ body: JSON.stringify({ language }),
150
+ });
151
+ if (!sessionResp.ok) {
152
+ const errBody = await sessionResp.json().catch(() => ({}));
153
+ const errMsg = errBody?.error?.message || errBody?.message || 'Failed to initialize avatar session';
154
+ throw new Error(errMsg);
155
+ }
156
+ const sessionPayload = await sessionResp.json();
157
+ const sessionInfo = sessionPayload?.data || sessionPayload;
158
+ usageIdRef.current = sessionInfo?.usageId || null;
159
+ const connection = sessionInfo?.connection;
160
+ if (!connection || connection.type !== 'room' || !connection.url || !connection.token) {
161
+ throw new Error('Avatar server did not return a valid LiveKit room connection.');
162
+ }
163
+ // Initialize LiveKit Room natively
164
+ const room = new LiveKitRN.Room({
165
+ adaptiveStream: true,
166
+ dynacast: true,
167
+ });
168
+ roomRef.current = room;
169
+ // Start Audio Session on iOS/Android
170
+ if (LiveKitRN.AudioSession?.startAudioSession) {
171
+ try {
172
+ await LiveKitRN.AudioSession.startAudioSession();
173
+ }
174
+ catch {
175
+ // non-fatal
176
+ }
177
+ }
178
+ // 1. Remote video track subscription
179
+ room.on(LiveKitRN.RoomEvent.TrackSubscribed, (track) => {
180
+ if (!mountedRef.current)
181
+ return;
182
+ const isVideo = track.kind === 'video' ||
183
+ (LiveKitRN.Track?.Kind?.Video && track.kind === LiveKitRN.Track.Kind.Video);
184
+ if (isVideo) {
185
+ setVideoTrack(track);
186
+ updateStatus('connected');
187
+ }
188
+ });
189
+ room.on(LiveKitRN.RoomEvent.TrackUnsubscribed, (track) => {
190
+ if (!mountedRef.current)
191
+ return;
192
+ const isVideo = track.kind === 'video' ||
193
+ (LiveKitRN.Track?.Kind?.Video && track.kind === LiveKitRN.Track.Kind.Video);
194
+ if (isVideo) {
195
+ setVideoTrack(null);
196
+ }
197
+ });
198
+ // 2. Active speaker detection
199
+ room.on(LiveKitRN.RoomEvent.ActiveSpeakersChanged, (speakers) => {
200
+ if (!mountedRef.current)
201
+ return;
202
+ const hasAssistant = speakers.some((p) => !p.isLocal);
203
+ const hasUser = speakers.some((p) => p.isLocal);
204
+ setIsAvatarSpeaking(hasAssistant);
205
+ if (hasAssistant)
206
+ onSpeakStart?.();
207
+ else
208
+ onSpeakEnd?.();
209
+ setIsUserSpeaking(hasUser);
210
+ if (hasUser)
211
+ onUserSpeakStart?.();
212
+ else
213
+ onUserSpeakEnd?.();
214
+ });
215
+ // 3. Bidirectional data-channel events (transcriptions, orders, interrupts)
216
+ room.on(LiveKitRN.RoomEvent.DataReceived, (payload) => {
217
+ if (!mountedRef.current)
218
+ return;
219
+ try {
220
+ let text = '';
221
+ if (typeof TextDecoder !== 'undefined') {
222
+ text = new TextDecoder().decode(payload);
223
+ }
224
+ else {
225
+ text = String.fromCharCode.apply(null, Array.from(payload));
226
+ }
227
+ const msg = JSON.parse(text);
228
+ const type = String(msg.type ?? msg.event_type ?? '');
229
+ // Speaking indicators
230
+ if (type === 'avatar.speak_started' || type === 'response.created') {
231
+ setIsAvatarSpeaking(true);
232
+ onSpeakStart?.();
233
+ }
234
+ else if (type === 'avatar.speak_ended' || type === 'response.done') {
235
+ setIsAvatarSpeaking(false);
236
+ onSpeakEnd?.();
237
+ }
238
+ if (type === 'user.speak_started' || type === 'input_audio_buffer.speech_started') {
239
+ setIsUserSpeaking(true);
240
+ onUserSpeakStart?.();
241
+ }
242
+ else if (type === 'user.speak_ended' || type === 'input_audio_buffer.speech_stopped') {
243
+ setIsUserSpeaking(false);
244
+ onUserSpeakEnd?.();
245
+ }
246
+ // Real-time transcriptions
247
+ if (type === 'avatar.transcription' || type === 'response.audio_transcript.done') {
248
+ const str = msg.text || msg.transcript || msg.payload?.text;
249
+ if (str)
250
+ onTranscript?.({ payload: { role: 'assistant', text: String(str).trim() } });
251
+ }
252
+ else if (type === 'user.transcription' ||
253
+ type === 'conversation.item.input_audio_transcription.completed') {
254
+ const str = msg.text || msg.transcript || msg.payload?.text;
255
+ if (str)
256
+ onTranscript?.({ payload: { role: 'user', text: String(str).trim() } });
257
+ }
258
+ // Order & checkout intents
259
+ if (type === 'order.update') {
260
+ const items = Array.isArray(msg.items) ? msg.items : msg.payload?.items || [];
261
+ onOrderUpdate?.({ items });
262
+ }
263
+ else if (type === 'order.checkout') {
264
+ onCheckout?.();
265
+ }
266
+ }
267
+ catch {
268
+ // ignore non-json data frames
269
+ }
270
+ });
271
+ // 4. Connection state transitions
272
+ room.on(LiveKitRN.RoomEvent.Disconnected, () => {
273
+ if (!mountedRef.current)
274
+ return;
275
+ setVideoTrack(null);
276
+ setIsAvatarSpeaking(false);
277
+ setIsUserSpeaking(false);
278
+ updateStatus('disconnected');
279
+ });
280
+ room.on(LiveKitRN.RoomEvent.Reconnecting, () => {
281
+ updateStatus('reconnecting');
282
+ });
283
+ room.on(LiveKitRN.RoomEvent.Reconnected, () => {
284
+ updateStatus('connected');
285
+ });
286
+ // Connect room over WebRTC
287
+ await room.connect(connection.url, connection.token, { autoSubscribe: true });
288
+ // Enable microphone capture if requested
289
+ if (enableMic) {
290
+ try {
291
+ await room.localParticipant.setMicrophoneEnabled(true);
292
+ setIsMicEnabled(true);
293
+ }
294
+ catch (micErr) {
295
+ console.warn('[FacePeMobileAvatar] Microphone setup warning:', micErr);
296
+ }
297
+ }
298
+ // Configure server VAD and Whisper transcription
299
+ const sessionUpdate = {
300
+ type: 'session.update',
301
+ session: {
302
+ input_audio_transcription: { model: 'whisper-1' },
303
+ turn_detection: { type: 'server_vad' },
304
+ },
305
+ };
306
+ sendData(sessionUpdate);
307
+ // Fallback status if video arrives quickly
308
+ if (mountedRef.current && status !== 'connected') {
309
+ updateStatus('connected');
310
+ }
311
+ }
312
+ catch (err) {
313
+ if (!mountedRef.current)
314
+ return;
315
+ const msg = err?.message || 'Connection failed';
316
+ setErrorMessage(msg);
317
+ updateStatus('error');
318
+ onError?.({ code: 'CONNECT_ERROR', message: msg });
319
+ await endUsage();
320
+ }
321
+ finally {
322
+ isConnectingRef.current = false;
323
+ }
324
+ }, [
325
+ hasLiveKit,
326
+ avatarKey,
327
+ language,
328
+ enableMic,
329
+ apiBaseUrl,
330
+ refreshToken,
331
+ updateStatus,
332
+ onSpeakStart,
333
+ onSpeakEnd,
334
+ onUserSpeakStart,
335
+ onUserSpeakEnd,
336
+ onTranscript,
337
+ onOrderUpdate,
338
+ onCheckout,
339
+ onError,
340
+ ]);
341
+ // Stop streaming session
342
+ const stopStream = useCallback(async () => {
343
+ const room = roomRef.current;
344
+ roomRef.current = null;
345
+ if (room) {
346
+ try {
347
+ if (room.localParticipant) {
348
+ await room.localParticipant.setMicrophoneEnabled(false);
349
+ }
350
+ await room.disconnect();
351
+ }
352
+ catch {
353
+ // ignore
354
+ }
355
+ }
356
+ setVideoTrack(null);
357
+ setIsAvatarSpeaking(false);
358
+ setIsUserSpeaking(false);
359
+ updateStatus('idle');
360
+ await endUsage();
361
+ }, [updateStatus]);
362
+ // Imperative methods exposed via ref
363
+ useImperativeHandle(ref, () => ({
364
+ start: async () => {
365
+ await startStream();
366
+ },
367
+ stop: async () => {
368
+ await stopStream();
369
+ },
370
+ toggleMic: async () => {
371
+ const room = roomRef.current;
372
+ if (!room || !room.localParticipant)
373
+ return isMicEnabled;
374
+ const nextState = !isMicEnabled;
375
+ try {
376
+ await room.localParticipant.setMicrophoneEnabled(nextState);
377
+ setIsMicEnabled(nextState);
378
+ return nextState;
379
+ }
380
+ catch (err) {
381
+ console.warn('[FacePeMobileAvatar] toggleMic error:', err);
382
+ return isMicEnabled;
383
+ }
384
+ },
385
+ setMicEnabled: async (enabled) => {
386
+ const room = roomRef.current;
387
+ if (!room || !room.localParticipant)
388
+ return;
389
+ try {
390
+ await room.localParticipant.setMicrophoneEnabled(enabled);
391
+ setIsMicEnabled(enabled);
392
+ }
393
+ catch (err) {
394
+ console.warn('[FacePeMobileAvatar] setMicEnabled error:', err);
395
+ }
396
+ },
397
+ speak: async (text) => {
398
+ sendData({ event_type: 'avatar.speak_text', text });
399
+ },
400
+ interrupt: async () => {
401
+ sendData({ event_type: 'avatar.interrupt' });
402
+ setIsAvatarSpeaking(false);
403
+ onSpeakEnd?.();
404
+ },
405
+ get status() {
406
+ return status;
407
+ },
408
+ get isMicEnabled() {
409
+ return isMicEnabled;
410
+ },
411
+ }), [startStream, stopStream, isMicEnabled, status, onSpeakEnd]);
412
+ // Auto-start on mount once token is resolved
413
+ useEffect(() => {
414
+ mountedRef.current = true;
415
+ if (autoStart && token && !tokenLoading && !tokenError && status === 'idle') {
416
+ void startStream();
417
+ }
418
+ return () => {
419
+ mountedRef.current = false;
420
+ void stopStream();
421
+ };
422
+ }, [autoStart, token, tokenLoading, tokenError]);
423
+ // Handle token loading
424
+ if (tokenLoading) {
425
+ return (_jsxs(View, { style: [styles.container, styles.center, style], children: [_jsx(ActivityIndicator, { size: "large", color: "#8B5CF6" }), _jsx(Text, { style: styles.loadingText, children: "Authenticating with FacePe AI\u2026" })] }));
426
+ }
427
+ // Handle token error
428
+ if (tokenError) {
429
+ return (_jsxs(View, { style: [styles.container, styles.center, style], children: [_jsx(Text, { style: styles.errorTitle, children: "Authentication Failed" }), _jsx(Text, { style: styles.errorText, children: tokenError.message }), _jsx(TouchableOpacity, { style: styles.retryButton, onPress: () => void refreshToken(true), children: _jsx(Text, { style: styles.retryText, children: "Retry" }) })] }));
430
+ }
431
+ // If @livekit/react-native is not installed in the host app, render native guidance banner
432
+ if (!hasLiveKit) {
433
+ return (_jsxs(View, { style: [styles.container, styles.center, styles.fallbackBox, style], children: [_jsx(View, { style: styles.badge, children: _jsx(Text, { style: styles.badgeText, children: "Native Streaming Required" }) }), _jsx(Text, { style: styles.noticeTitle, children: "FacePe Mobile SDK" }), _jsx(Text, { style: styles.noticeBody, children: "To stream interactive 3D digital avatars natively without webviews, please install LiveKit WebRTC peer dependencies:" }), _jsx(Text, { style: styles.codeSnippet, children: "npm install @livekit/react-native @livekit/react-native-webrtc" })] }));
434
+ }
435
+ // Native LiveKit VideoView component
436
+ const VideoViewComponent = LiveKitRN.VideoView;
437
+ return (_jsxs(View, { style: [styles.container, style], children: [videoTrack && VideoViewComponent ? (_jsx(VideoViewComponent, { videoTrack: videoTrack, style: [styles.video, videoStyle], objectFit: objectFit, mirror: mirror })) : (_jsx(View, { style: [styles.container, styles.center], children: status === 'connecting' || status === 'reconnecting' ? (_jsxs(_Fragment, { children: [_jsx(ActivityIndicator, { size: "large", color: "#8B5CF6" }), _jsx(Text, { style: styles.loadingText, children: status === 'connecting'
438
+ ? 'Connecting to FacePe AI Avatar…'
439
+ : 'Reconnecting stream…' })] })) : status === 'error' ? (_jsxs(_Fragment, { children: [_jsx(Text, { style: styles.errorTitle, children: "Connection Failed" }), _jsx(Text, { style: styles.errorText, children: errorMessage || 'Unable to connect to stream' }), _jsx(TouchableOpacity, { style: styles.retryButton, onPress: () => void startStream(), children: _jsx(Text, { style: styles.retryText, children: "Reconnect" }) })] })) : (_jsx(TouchableOpacity, { style: styles.startButton, onPress: () => void startStream(), children: _jsx(Text, { style: styles.startText, children: "Start Avatar Session" }) })) })), status === 'connected' && (isAvatarSpeaking || isUserSpeaking) && (_jsxs(View, { style: styles.speakerPill, children: [_jsx(View, { style: [
440
+ styles.speakerDot,
441
+ isAvatarSpeaking ? styles.dotAvatar : styles.dotUser,
442
+ ] }), _jsx(Text, { style: styles.speakerText, children: isAvatarSpeaking ? 'Avatar speaking' : 'Listening to you…' })] }))] }));
443
+ });
444
+ FacePeMobileAvatar.displayName = 'FacePeMobileAvatar';
445
+ const styles = StyleSheet.create({
446
+ container: {
447
+ width: '100%',
448
+ height: '100%',
449
+ backgroundColor: '#0F172A',
450
+ borderRadius: 20,
451
+ overflow: 'hidden',
452
+ position: 'relative',
453
+ },
454
+ video: {
455
+ width: '100%',
456
+ height: '100%',
457
+ },
458
+ center: {
459
+ justifyContent: 'center',
460
+ alignItems: 'center',
461
+ padding: 24,
462
+ },
463
+ loadingText: {
464
+ marginTop: 14,
465
+ color: '#94A3B8',
466
+ fontSize: 14,
467
+ fontWeight: '500',
468
+ },
469
+ errorTitle: {
470
+ color: '#EF4444',
471
+ fontSize: 16,
472
+ fontWeight: '700',
473
+ marginBottom: 6,
474
+ },
475
+ errorText: {
476
+ color: '#F87171',
477
+ fontSize: 13,
478
+ textAlign: 'center',
479
+ lineHeight: 18,
480
+ marginBottom: 16,
481
+ },
482
+ retryButton: {
483
+ backgroundColor: '#7C3AED',
484
+ paddingHorizontal: 20,
485
+ paddingVertical: 10,
486
+ borderRadius: 12,
487
+ },
488
+ retryText: {
489
+ color: '#FFFFFF',
490
+ fontWeight: '600',
491
+ fontSize: 13,
492
+ },
493
+ startButton: {
494
+ backgroundColor: '#7C3AED',
495
+ paddingHorizontal: 24,
496
+ paddingVertical: 14,
497
+ borderRadius: 14,
498
+ },
499
+ startText: {
500
+ color: '#FFFFFF',
501
+ fontWeight: '700',
502
+ fontSize: 15,
503
+ },
504
+ fallbackBox: {
505
+ backgroundColor: '#1E293B',
506
+ borderColor: '#334155',
507
+ borderWidth: 1,
508
+ },
509
+ badge: {
510
+ backgroundColor: 'rgba(124, 58, 237, 0.15)',
511
+ borderColor: '#7C3AED',
512
+ borderWidth: 1,
513
+ paddingHorizontal: 12,
514
+ paddingVertical: 4,
515
+ borderRadius: 20,
516
+ marginBottom: 12,
517
+ },
518
+ badgeText: {
519
+ color: '#A78BFA',
520
+ fontSize: 11,
521
+ fontWeight: '600',
522
+ },
523
+ noticeTitle: {
524
+ color: '#FFFFFF',
525
+ fontWeight: '700',
526
+ fontSize: 17,
527
+ marginBottom: 8,
528
+ },
529
+ noticeBody: {
530
+ color: '#94A3B8',
531
+ fontSize: 13,
532
+ textAlign: 'center',
533
+ lineHeight: 19,
534
+ marginBottom: 16,
535
+ },
536
+ codeSnippet: {
537
+ color: '#C4B5FD',
538
+ backgroundColor: '#0F172A',
539
+ paddingHorizontal: 14,
540
+ paddingVertical: 10,
541
+ borderRadius: 10,
542
+ fontFamily: 'monospace',
543
+ fontSize: 12,
544
+ textAlign: 'center',
545
+ },
546
+ speakerPill: {
547
+ position: 'absolute',
548
+ top: 16,
549
+ alignSelf: 'center',
550
+ backgroundColor: 'rgba(15, 23, 42, 0.85)',
551
+ borderColor: 'rgba(255, 255, 255, 0.1)',
552
+ borderWidth: 1,
553
+ flexDirection: 'row',
554
+ alignItems: 'center',
555
+ paddingHorizontal: 12,
556
+ paddingVertical: 6,
557
+ borderRadius: 20,
558
+ },
559
+ speakerDot: {
560
+ width: 8,
561
+ height: 8,
562
+ borderRadius: 4,
563
+ marginRight: 8,
564
+ },
565
+ dotAvatar: {
566
+ backgroundColor: '#10B981',
567
+ },
568
+ dotUser: {
569
+ backgroundColor: '#3B82F6',
570
+ },
571
+ speakerText: {
572
+ color: '#F8FAFC',
573
+ fontSize: 12,
574
+ fontWeight: '500',
575
+ },
576
+ });
@@ -0,0 +1,19 @@
1
+ import React, { type ReactNode } from 'react';
2
+ import type { FacePeMobileConfig, TokenProvider } from './types';
3
+ interface FacePeContextValue {
4
+ config: FacePeMobileConfig;
5
+ token: string | null;
6
+ loading: boolean;
7
+ error: Error | null;
8
+ refreshToken: (force?: boolean) => Promise<string | null>;
9
+ apiBaseUrl: string;
10
+ }
11
+ export interface FacePeMobileProviderProps {
12
+ children: ReactNode;
13
+ getToken: TokenProvider;
14
+ apiBaseUrl?: string;
15
+ config?: FacePeMobileConfig;
16
+ }
17
+ export declare function FacePeMobileProvider({ children, getToken, apiBaseUrl, config, }: FacePeMobileProviderProps): React.JSX.Element;
18
+ export declare function useFacePeMobileContext(): FacePeContextValue;
19
+ export {};
@@ -0,0 +1,54 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext, useState, useEffect, useRef, useMemo } from 'react';
3
+ const FacePeContext = createContext(null);
4
+ export function FacePeMobileProvider({ children, getToken, apiBaseUrl = 'https://api.facepe.ai/api/v1/sdk', config = {}, }) {
5
+ const [token, setToken] = useState(null);
6
+ const [loading, setLoading] = useState(true);
7
+ const [error, setError] = useState(null);
8
+ const tokenPromiseRef = useRef(null);
9
+ const refreshToken = async (force = false) => {
10
+ if (tokenPromiseRef.current && !force) {
11
+ return tokenPromiseRef.current;
12
+ }
13
+ setLoading(true);
14
+ setError(null);
15
+ const promise = (async () => {
16
+ try {
17
+ const fetched = await getToken({ forceRefresh: force });
18
+ setToken(fetched);
19
+ setLoading(false);
20
+ return fetched;
21
+ }
22
+ catch (err) {
23
+ const normalized = err instanceof Error ? err : new Error(String(err));
24
+ setError(normalized);
25
+ setLoading(false);
26
+ return null;
27
+ }
28
+ finally {
29
+ tokenPromiseRef.current = null;
30
+ }
31
+ })();
32
+ tokenPromiseRef.current = promise;
33
+ return promise;
34
+ };
35
+ useEffect(() => {
36
+ void refreshToken();
37
+ }, []);
38
+ const value = useMemo(() => ({
39
+ config,
40
+ token,
41
+ loading,
42
+ error,
43
+ refreshToken,
44
+ apiBaseUrl: config.apiBaseUrl || apiBaseUrl,
45
+ }), [config, token, loading, error, apiBaseUrl]);
46
+ return _jsx(FacePeContext.Provider, { value: value, children: children });
47
+ }
48
+ export function useFacePeMobileContext() {
49
+ const ctx = useContext(FacePeContext);
50
+ if (!ctx) {
51
+ throw new Error('useFacePeMobileContext must be used within a <FacePeMobileProvider>');
52
+ }
53
+ return ctx;
54
+ }
@@ -0,0 +1,4 @@
1
+ export * from './types';
2
+ export * from './FacePeMobileProvider';
3
+ export * from './FacePeMobileAvatar';
4
+ export * from './useFacePeAvatar';
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export * from './types';
2
+ export * from './FacePeMobileProvider';
3
+ export * from './FacePeMobileAvatar';
4
+ export * from './useFacePeAvatar';
@@ -0,0 +1,95 @@
1
+ export type AvatarGender = 'female' | 'male';
2
+ export type FacePeConnectionStatus = 'idle' | 'connecting' | 'connected' | 'reconnecting' | 'disconnected' | 'error';
3
+ export interface FacePeMobileTheme {
4
+ primaryColor?: string;
5
+ backgroundColor?: string;
6
+ borderRadius?: number;
7
+ textColor?: string;
8
+ }
9
+ export interface TranscriptPayload {
10
+ role: 'user' | 'assistant';
11
+ text: string;
12
+ isFinal?: boolean;
13
+ }
14
+ export interface OrderItem {
15
+ name: string;
16
+ qty: number;
17
+ price?: number;
18
+ }
19
+ export interface FacePeMobileConfig {
20
+ /** FacePe Gateway URL (e.g. https://api.facepe.ai/api/v1/sdk) */
21
+ apiBaseUrl?: string;
22
+ /** Avatar identifier or key */
23
+ avatarKey?: string;
24
+ /** Default language code (e.g., 'en', 'es', 'hi') */
25
+ language?: string;
26
+ /** Theme styling overrides */
27
+ theme?: FacePeMobileTheme;
28
+ /** Auto start streaming when component mounts (default: true) */
29
+ autoStart?: boolean;
30
+ }
31
+ export interface TokenProviderParams {
32
+ forceRefresh?: boolean;
33
+ }
34
+ export type TokenProvider = (params: TokenProviderParams) => Promise<string>;
35
+ export interface FacePeMobileAvatarProps {
36
+ /** Specific avatar identifier or key override */
37
+ avatarKey?: string;
38
+ /** Language to converse in ('en', 'es', 'hi', etc.) */
39
+ language?: string;
40
+ /** Custom container style */
41
+ style?: any;
42
+ /** Custom video style */
43
+ videoStyle?: any;
44
+ /** Video scaling: 'cover' or 'contain' (default: 'cover') */
45
+ objectFit?: 'cover' | 'contain';
46
+ /** Whether to mirror the video stream (default: false) */
47
+ mirror?: boolean;
48
+ /** Auto start streaming when component mounts (default: true) */
49
+ autoStart?: boolean;
50
+ /** Whether microphone should be automatically enabled on stream start (default: true) */
51
+ enableMic?: boolean;
52
+ /** Called when real-time speech transcription arrives */
53
+ onTranscript?: (event: {
54
+ payload: TranscriptPayload;
55
+ }) => void;
56
+ /** Called when avatar starts speaking */
57
+ onSpeakStart?: () => void;
58
+ /** Called when avatar finishes speaking */
59
+ onSpeakEnd?: () => void;
60
+ /** Called when user starts speaking (detected by server VAD) */
61
+ onUserSpeakStart?: () => void;
62
+ /** Called when user finishes speaking */
63
+ onUserSpeakEnd?: () => void;
64
+ /** Called when an order or intent snapshot is emitted */
65
+ onOrderUpdate?: (event: {
66
+ items: OrderItem[];
67
+ }) => void;
68
+ /** Called when customer says checkout / pay */
69
+ onCheckout?: () => void;
70
+ /** Called when stream connection status changes */
71
+ onStatusChange?: (status: FacePeConnectionStatus) => void;
72
+ /** Called when an error occurs */
73
+ onError?: (event: {
74
+ code: string;
75
+ message: string;
76
+ }) => void;
77
+ }
78
+ export interface FacePeMobileAvatarRef {
79
+ /** Start avatar streaming session */
80
+ start: () => Promise<void>;
81
+ /** Stop streaming and close session */
82
+ stop: () => Promise<void>;
83
+ /** Toggle user microphone on/off, returning new enabled state */
84
+ toggleMic: () => Promise<boolean>;
85
+ /** Explicitly set microphone state */
86
+ setMicEnabled: (enabled: boolean) => Promise<void>;
87
+ /** Programmatically make avatar speak a phrase */
88
+ speak: (text: string) => Promise<void>;
89
+ /** Interrupt avatar speech immediately */
90
+ interrupt: () => Promise<void>;
91
+ /** Current connection status */
92
+ readonly status: FacePeConnectionStatus;
93
+ /** Whether microphone is currently enabled */
94
+ readonly isMicEnabled: boolean;
95
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,15 @@
1
+ import type { TranscriptPayload, OrderItem, FacePeConnectionStatus, FacePeMobileAvatarProps } from './types';
2
+ export interface UseFacePeAvatarResult {
3
+ token: string | null;
4
+ loading: boolean;
5
+ error: Error | null;
6
+ status: FacePeConnectionStatus;
7
+ transcripts: TranscriptPayload[];
8
+ isAvatarSpeaking: boolean;
9
+ isUserSpeaking: boolean;
10
+ orderItems: OrderItem[];
11
+ refreshToken: (force?: boolean) => Promise<string | null>;
12
+ clearTranscripts: () => void;
13
+ avatarProps: Pick<FacePeMobileAvatarProps, 'onTranscript' | 'onSpeakStart' | 'onSpeakEnd' | 'onUserSpeakStart' | 'onUserSpeakEnd' | 'onOrderUpdate' | 'onStatusChange'>;
14
+ }
15
+ export declare function useFacePeAvatar(): UseFacePeAvatarResult;
@@ -0,0 +1,64 @@
1
+ import { useState, useCallback, useMemo } from 'react';
2
+ import { useFacePeMobileContext } from './FacePeMobileProvider';
3
+ export function useFacePeAvatar() {
4
+ const { token, loading, error, refreshToken } = useFacePeMobileContext();
5
+ const [status, setStatus] = useState('idle');
6
+ const [transcripts, setTranscripts] = useState([]);
7
+ const [isAvatarSpeaking, setIsAvatarSpeaking] = useState(false);
8
+ const [isUserSpeaking, setIsUserSpeaking] = useState(false);
9
+ const [orderItems, setOrderItems] = useState([]);
10
+ const clearTranscripts = useCallback(() => {
11
+ setTranscripts([]);
12
+ }, []);
13
+ const handleTranscript = useCallback((event) => {
14
+ setTranscripts((prev) => [...prev, event.payload]);
15
+ }, []);
16
+ const handleSpeakStart = useCallback(() => {
17
+ setIsAvatarSpeaking(true);
18
+ }, []);
19
+ const handleSpeakEnd = useCallback(() => {
20
+ setIsAvatarSpeaking(false);
21
+ }, []);
22
+ const handleUserSpeakStart = useCallback(() => {
23
+ setIsUserSpeaking(true);
24
+ }, []);
25
+ const handleUserSpeakEnd = useCallback(() => {
26
+ setIsUserSpeaking(false);
27
+ }, []);
28
+ const handleOrderUpdate = useCallback((event) => {
29
+ setOrderItems(event.items);
30
+ }, []);
31
+ const handleStatusChange = useCallback((newStatus) => {
32
+ setStatus(newStatus);
33
+ }, []);
34
+ const avatarProps = useMemo(() => ({
35
+ onTranscript: handleTranscript,
36
+ onSpeakStart: handleSpeakStart,
37
+ onSpeakEnd: handleSpeakEnd,
38
+ onUserSpeakStart: handleUserSpeakStart,
39
+ onUserSpeakEnd: handleUserSpeakEnd,
40
+ onOrderUpdate: handleOrderUpdate,
41
+ onStatusChange: handleStatusChange,
42
+ }), [
43
+ handleTranscript,
44
+ handleSpeakStart,
45
+ handleSpeakEnd,
46
+ handleUserSpeakStart,
47
+ handleUserSpeakEnd,
48
+ handleOrderUpdate,
49
+ handleStatusChange,
50
+ ]);
51
+ return {
52
+ token,
53
+ loading,
54
+ error,
55
+ status,
56
+ transcripts,
57
+ isAvatarSpeaking,
58
+ isUserSpeaking,
59
+ orderItems,
60
+ refreshToken,
61
+ clearTranscripts,
62
+ avatarProps,
63
+ };
64
+ }
package/package.json CHANGED
@@ -1,6 +1,53 @@
1
1
  {
2
2
  "name": "@facepe/widget-mobile-app",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.1.0",
4
+ "description": "FacePe AI 3D Video Avatar Native Live Streaming SDK for React Native (iOS & Android)",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "files": [
9
+ "dist",
10
+ "README.md"
11
+ ],
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "keywords": [
16
+ "facepe",
17
+ "avatar",
18
+ "ai-avatar",
19
+ "livekit",
20
+ "react-native",
21
+ "ios",
22
+ "android",
23
+ "webrtc",
24
+ "streaming",
25
+ "digital-human"
26
+ ],
27
+ "author": "FacePe AI",
28
+ "license": "MIT",
29
+ "scripts": {
30
+ "lint": "tsc --noEmit",
31
+ "build": "tsc"
32
+ },
33
+ "peerDependencies": {
34
+ "@livekit/react-native": ">=2.0.0",
35
+ "@livekit/react-native-webrtc": ">=124.0.0",
36
+ "react": ">=18.0.0",
37
+ "react-native": ">=0.70.0"
38
+ },
39
+ "peerDependenciesMeta": {
40
+ "@livekit/react-native": {
41
+ "optional": true
42
+ },
43
+ "@livekit/react-native-webrtc": {
44
+ "optional": true
45
+ }
46
+ },
47
+ "devDependencies": {
48
+ "@types/react": "^18.3.3",
49
+ "react": "18.3.1",
50
+ "react-native": "0.74.0",
51
+ "typescript": "^5.4.5"
52
+ }
53
+ }