@revrag-ai/embed-react-native 1.0.5 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commonjs/Event/onwid.js +70 -0
- package/dist/commonjs/NativeOnwid.js +5 -0
- package/dist/commonjs/button.json +1 -0
- package/dist/commonjs/component/OnwidButton.js +507 -0
- package/dist/commonjs/component/audiowave.js +153 -0
- package/dist/commonjs/component/voice.js +127 -0
- package/dist/commonjs/hooks/initialize.js +96 -0
- package/dist/commonjs/hooks/initialize.types.js +2 -0
- package/{lib/module → dist/commonjs}/hooks/initializelivekit.js +7 -4
- package/dist/commonjs/hooks/voiceAgent.js +353 -0
- package/dist/commonjs/hooks/voiceAgent.types.js +4 -0
- package/dist/commonjs/index.d.js +22 -0
- package/dist/commonjs/index.js +34 -0
- package/dist/commonjs/onwidApi/api.js +185 -0
- package/dist/commonjs/onwidApi/api.types.js +2 -0
- package/dist/commonjs/package.json +1 -0
- package/dist/commonjs/store.key.js +38 -0
- package/dist/commonjs/style/onwidButton.style.js +243 -0
- package/dist/commonjs/utils/reanimatedHelpers.js +94 -0
- package/dist/commonjs/utils/utils.js +2 -0
- package/dist/module/Event/onwid.js +70 -0
- package/dist/module/NativeOnwid.js +5 -0
- package/dist/module/button.json +1 -0
- package/dist/module/component/OnwidButton.js +507 -0
- package/dist/module/component/audiowave.js +153 -0
- package/dist/module/component/voice.js +127 -0
- package/dist/module/hooks/initialize.js +96 -0
- package/dist/module/hooks/initialize.types.js +2 -0
- package/dist/module/hooks/initializelivekit.js +17 -0
- package/dist/module/hooks/voiceAgent.js +353 -0
- package/dist/module/hooks/voiceAgent.types.js +4 -0
- package/dist/module/index.d.js +22 -0
- package/dist/module/index.js +34 -0
- package/dist/module/onwidApi/api.js +185 -0
- package/dist/module/onwidApi/api.types.js +2 -0
- package/dist/module/store.key.js +38 -0
- package/dist/module/style/onwidButton.style.js +243 -0
- package/dist/module/utils/reanimatedHelpers.js +94 -0
- package/dist/module/utils/utils.js +2 -0
- package/{lib → dist}/typescript/Event/onwid.d.ts +1 -0
- package/{lib → dist}/typescript/NativeOnwid.d.ts +1 -0
- package/{lib → dist}/typescript/component/OnwidButton.d.ts +1 -0
- package/{lib → dist}/typescript/component/audiowave.d.ts +1 -0
- package/{lib → dist}/typescript/component/voice.d.ts +1 -0
- package/{lib → dist}/typescript/hooks/initialize.d.ts +1 -0
- package/{lib → dist}/typescript/hooks/initialize.types.d.ts +1 -0
- package/{lib → dist}/typescript/hooks/initializelivekit.d.ts +1 -0
- package/{lib → dist}/typescript/hooks/voiceAgent.d.ts +1 -0
- package/{lib → dist}/typescript/hooks/voiceAgent.types.d.ts +1 -0
- package/{lib → dist}/typescript/index.d.ts +3 -3
- package/{lib → dist}/typescript/onwidApi/api.d.ts +1 -0
- package/{lib → dist}/typescript/onwidApi/api.types.d.ts +1 -0
- package/{lib → dist}/typescript/store.key.d.ts +1 -0
- package/{lib → dist}/typescript/style/onwidButton.style.d.ts +1 -0
- package/{lib → dist}/typescript/utils/reanimatedHelpers.d.ts +1 -0
- package/dist/typescript/utils/utils.d.ts +1 -0
- package/package.json +29 -21
- package/lib/index.d.ts +0 -77
- package/lib/module/Event/onwid.js +0 -74
- package/lib/module/NativeOnwid.js +0 -4
- package/lib/module/component/OnwidButton.js +0 -366
- package/lib/module/component/audiowave.js +0 -137
- package/lib/module/component/voice.js +0 -103
- package/lib/module/hooks/initialize.js +0 -92
- package/lib/module/hooks/initialize.types.js +0 -2
- package/lib/module/hooks/voiceAgent.js +0 -334
- package/lib/module/hooks/voiceAgent.types.js +0 -2
- package/lib/module/index.js +0 -61
- package/lib/module/onwidApi/api.js +0 -184
- package/lib/module/onwidApi/api.types.js +0 -2
- package/lib/module/store.key.js +0 -47
- package/lib/module/style/onwidButton.style.js +0 -230
- package/lib/module/utils/reanimatedHelpers.js +0 -87
- package/lib/module/utils/utils.js +0 -1
- package/lib/typescript/utils/utils.d.ts +0 -0
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.WaveformVisualizer = void 0;
|
|
4
|
-
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
-
const react_1 = require("react");
|
|
6
|
-
const react_native_1 = require("react-native");
|
|
7
|
-
// React Native compatible waveform simulator
|
|
8
|
-
const useReactNativeAudioWaveform = (roomRef) => {
|
|
9
|
-
const [isAudioActive, setIsAudioActive] = (0, react_1.useState)(false);
|
|
10
|
-
const intervalRef = (0, react_1.useRef)(null);
|
|
11
|
-
const [currentHeights, setCurrentHeights] = (0, react_1.useState)(Array(10).fill(0));
|
|
12
|
-
// Create animated values for each bar
|
|
13
|
-
const barCount = 10;
|
|
14
|
-
const animatedBars = (0, react_1.useRef)(Array(barCount)
|
|
15
|
-
.fill(0)
|
|
16
|
-
.map(() => new react_native_1.Animated.Value(0))).current;
|
|
17
|
-
(0, react_1.useEffect)(() => {
|
|
18
|
-
// Check if there's an active room connection AND if agent is talking
|
|
19
|
-
const checkAudioActivity = () => {
|
|
20
|
-
const room = roomRef.current;
|
|
21
|
-
if ((room === null || room === void 0 ? void 0 : room.state) !== 'connected') {
|
|
22
|
-
setIsAudioActive(false);
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
// Check if any remote participant is currently speaking
|
|
26
|
-
let isAgentSpeaking = false;
|
|
27
|
-
// Loop through all remote participants
|
|
28
|
-
room.remoteParticipants.forEach((participant) => {
|
|
29
|
-
const audioTrackPublications = Array.from(participant.getTrackPublications().values());
|
|
30
|
-
const remoteAudioTrack = audioTrackPublications.find((pub) => { var _a; return ((_a = pub.track) === null || _a === void 0 ? void 0 : _a.kind) === 'audio'; });
|
|
31
|
-
// Check if this participant has audio track, is not muted, and is actively speaking
|
|
32
|
-
if ((remoteAudioTrack === null || remoteAudioTrack === void 0 ? void 0 : remoteAudioTrack.track) && !(remoteAudioTrack === null || remoteAudioTrack === void 0 ? void 0 : remoteAudioTrack.isMuted)) {
|
|
33
|
-
// Check audio level to detect actual speech (optional but more accurate)
|
|
34
|
-
const audioLevel = participant.audioLevel || 0;
|
|
35
|
-
if (audioLevel > 0.05) {
|
|
36
|
-
// Threshold for detecting speech
|
|
37
|
-
isAgentSpeaking = true;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
setIsAudioActive(isAgentSpeaking);
|
|
42
|
-
};
|
|
43
|
-
// Initial check
|
|
44
|
-
checkAudioActivity();
|
|
45
|
-
// Set up periodic checking for room state changes
|
|
46
|
-
intervalRef.current = setInterval(checkAudioActivity, 500);
|
|
47
|
-
// Clean up on unmount
|
|
48
|
-
return () => {
|
|
49
|
-
if (intervalRef.current) {
|
|
50
|
-
clearInterval(intervalRef.current);
|
|
51
|
-
intervalRef.current = null;
|
|
52
|
-
}
|
|
53
|
-
setIsAudioActive(false);
|
|
54
|
-
};
|
|
55
|
-
}, [roomRef]);
|
|
56
|
-
// Continuous smooth animation
|
|
57
|
-
(0, react_1.useEffect)(() => {
|
|
58
|
-
const animateWaveform = () => {
|
|
59
|
-
// Generate smooth waveform data - stop animation completely when not active
|
|
60
|
-
const targetHeights = isAudioActive
|
|
61
|
-
? Array(barCount)
|
|
62
|
-
.fill(0)
|
|
63
|
-
.map((_, index) => {
|
|
64
|
-
const timeOffset = Date.now() / 800 + index * 0.3;
|
|
65
|
-
const baseHeight = 0.5;
|
|
66
|
-
const amplitude = 0.5;
|
|
67
|
-
const height = baseHeight + amplitude * Math.abs(Math.sin(timeOffset));
|
|
68
|
-
return Math.max(0.1, Math.min(1.0, height));
|
|
69
|
-
})
|
|
70
|
-
: Array(barCount).fill(0); // Completely freeze animation when mic is muted
|
|
71
|
-
// Update current heights for conditional logic
|
|
72
|
-
setCurrentHeights(targetHeights);
|
|
73
|
-
const animations = animatedBars.map((animatedValue, index) => {
|
|
74
|
-
const targetHeight = targetHeights[index] || 0;
|
|
75
|
-
return react_native_1.Animated.timing(animatedValue, {
|
|
76
|
-
toValue: targetHeight,
|
|
77
|
-
duration: isAudioActive ? 400 : 600, // Slower fade out when going inactive
|
|
78
|
-
useNativeDriver: false,
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
react_native_1.Animated.parallel(animations).start();
|
|
82
|
-
};
|
|
83
|
-
// Start animation immediately and repeat
|
|
84
|
-
animateWaveform();
|
|
85
|
-
const animationInterval = setInterval(animateWaveform, 300);
|
|
86
|
-
return () => {
|
|
87
|
-
clearInterval(animationInterval);
|
|
88
|
-
};
|
|
89
|
-
}, [isAudioActive, animatedBars]);
|
|
90
|
-
return {
|
|
91
|
-
animatedBars,
|
|
92
|
-
currentHeights,
|
|
93
|
-
isActive: isAudioActive,
|
|
94
|
-
};
|
|
95
|
-
};
|
|
96
|
-
const WaveformVisualizer = ({ roomRef }) => {
|
|
97
|
-
const { animatedBars, currentHeights, isActive } = useReactNativeAudioWaveform(roomRef);
|
|
98
|
-
console.log('animatedBars', animatedBars);
|
|
99
|
-
return ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: {
|
|
100
|
-
flexDirection: 'row',
|
|
101
|
-
alignItems: 'center',
|
|
102
|
-
height: '100%',
|
|
103
|
-
alignSelf: 'center',
|
|
104
|
-
// width: '100%',
|
|
105
|
-
// flex: 1,
|
|
106
|
-
justifyContent: 'center',
|
|
107
|
-
// position: 'absolute',
|
|
108
|
-
zIndex: 1000,
|
|
109
|
-
}, children: animatedBars.map((animatedHeight, idx) => {
|
|
110
|
-
// Use the tracked height values instead of trying to access animated value directly
|
|
111
|
-
const currentHeightValue = currentHeights[idx] || 0.1;
|
|
112
|
-
// Apply conditional logic based on height
|
|
113
|
-
let conditionalValue;
|
|
114
|
-
if (currentHeightValue > 0.7) {
|
|
115
|
-
conditionalValue = 1;
|
|
116
|
-
}
|
|
117
|
-
else if (currentHeightValue >= 0.4 && currentHeightValue <= 0.5) {
|
|
118
|
-
conditionalValue = 5;
|
|
119
|
-
}
|
|
120
|
-
else {
|
|
121
|
-
conditionalValue = 1;
|
|
122
|
-
}
|
|
123
|
-
// You can use conditionalValue for width, color, or other properties
|
|
124
|
-
return ((0, jsx_runtime_1.jsx)(react_native_1.Animated.View, { style: {
|
|
125
|
-
width: conditionalValue === 10 ? 4 : 4,
|
|
126
|
-
borderRadius: 100, // Example: wider bars for value 5
|
|
127
|
-
height: animatedHeight.interpolate({
|
|
128
|
-
inputRange: [0, 1],
|
|
129
|
-
outputRange: [0, 25],
|
|
130
|
-
}),
|
|
131
|
-
alignSelf: 'center',
|
|
132
|
-
backgroundColor: idx <= 1 || idx >= 8 ? 'rgba(255, 255, 255, 0.5)' : 'white',
|
|
133
|
-
margin: 1.5,
|
|
134
|
-
} }, idx));
|
|
135
|
-
}) }));
|
|
136
|
-
};
|
|
137
|
-
exports.WaveformVisualizer = WaveformVisualizer;
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
4
|
-
const react_native_1 = require("@livekit/react-native");
|
|
5
|
-
const react_1 = require("react");
|
|
6
|
-
// Track audio session state globally to prevent multiple starts/stops
|
|
7
|
-
let audioSessionActive = false;
|
|
8
|
-
let audioSessionStarting = false;
|
|
9
|
-
let audioSessionStopping = false;
|
|
10
|
-
// Track connection to prevent unmounting while connected
|
|
11
|
-
let activeConnectionToken = null;
|
|
12
|
-
const Voice = (props) => {
|
|
13
|
-
const { url, token, onDisconnected, onConnected, roomRef } = props;
|
|
14
|
-
const [audioSessionStarted, setAudioSessionStarted] = (0, react_1.useState)(audioSessionActive);
|
|
15
|
-
const mountedRef = (0, react_1.useRef)(true);
|
|
16
|
-
const connectedRef = (0, react_1.useRef)(false);
|
|
17
|
-
// Start audio session safely - prevent multiple simultaneous starts
|
|
18
|
-
const startAudioSession = async () => {
|
|
19
|
-
if (audioSessionActive || audioSessionStarting) {
|
|
20
|
-
console.log('Audio session already active or starting, skipping');
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
try {
|
|
24
|
-
audioSessionStarting = true;
|
|
25
|
-
console.log('Starting audio session');
|
|
26
|
-
await react_native_1.AudioSession.startAudioSession();
|
|
27
|
-
audioSessionActive = true;
|
|
28
|
-
if (mountedRef.current) {
|
|
29
|
-
setAudioSessionStarted(true);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
catch (err) {
|
|
33
|
-
console.error('Failed to start audio session:', err);
|
|
34
|
-
}
|
|
35
|
-
finally {
|
|
36
|
-
audioSessionStarting = false;
|
|
37
|
-
}
|
|
38
|
-
};
|
|
39
|
-
// Stop audio session safely - prevent multiple simultaneous stops
|
|
40
|
-
const stopAudioSession = async () => {
|
|
41
|
-
if (!audioSessionActive || audioSessionStopping) {
|
|
42
|
-
console.log('Audio session already inactive or stopping, skipping');
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
try {
|
|
46
|
-
audioSessionStopping = true;
|
|
47
|
-
console.log('Stopping audio session');
|
|
48
|
-
await react_native_1.AudioSession.stopAudioSession();
|
|
49
|
-
audioSessionActive = false;
|
|
50
|
-
}
|
|
51
|
-
catch (err) {
|
|
52
|
-
console.error('Failed to stop audio session:', err);
|
|
53
|
-
}
|
|
54
|
-
finally {
|
|
55
|
-
audioSessionStopping = false;
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
// Setup audio session
|
|
59
|
-
(0, react_1.useEffect)(() => {
|
|
60
|
-
mountedRef.current = true;
|
|
61
|
-
startAudioSession();
|
|
62
|
-
return () => {
|
|
63
|
-
var _a;
|
|
64
|
-
mountedRef.current = false;
|
|
65
|
-
// IMPORTANT: Don't stop the audio session on unmount if there might be an active call
|
|
66
|
-
// This prevents audio session start/stop loops when components remount
|
|
67
|
-
if (((_a = roomRef.current) === null || _a === void 0 ? void 0 : _a.state) !== 'connected' && !connectedRef.current) {
|
|
68
|
-
stopAudioSession();
|
|
69
|
-
}
|
|
70
|
-
else {
|
|
71
|
-
console.log('Skipping audio session stop because room is still connected');
|
|
72
|
-
}
|
|
73
|
-
};
|
|
74
|
-
}, []);
|
|
75
|
-
// Track connection state changes to avoid unmounting while connected
|
|
76
|
-
(0, react_1.useEffect)(() => {
|
|
77
|
-
if (token) {
|
|
78
|
-
activeConnectionToken = token;
|
|
79
|
-
}
|
|
80
|
-
return () => {
|
|
81
|
-
// Only clear token when unmounting with this specific token
|
|
82
|
-
if (activeConnectionToken === token) {
|
|
83
|
-
activeConnectionToken = null;
|
|
84
|
-
}
|
|
85
|
-
};
|
|
86
|
-
}, [token]);
|
|
87
|
-
// Only render LiveKitRoom when audio session is ready
|
|
88
|
-
if (!audioSessionStarted) {
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
91
|
-
// IMPORTANT: Never return empty fragment when connected!
|
|
92
|
-
// Instead, always render the LiveKitRoom component to maintain the connection
|
|
93
|
-
return ((0, jsx_runtime_1.jsx)(react_native_1.LiveKitRoom, { serverUrl: url, token: token, screen: false, audio: true, onConnected: () => {
|
|
94
|
-
console.log('LiveKitRoom connected');
|
|
95
|
-
connectedRef.current = true;
|
|
96
|
-
onConnected('connected');
|
|
97
|
-
}, room: roomRef.current || undefined, onDisconnected: () => {
|
|
98
|
-
console.log('LiveKitRoom disconnected');
|
|
99
|
-
connectedRef.current = false;
|
|
100
|
-
onDisconnected('disconnected');
|
|
101
|
-
} }));
|
|
102
|
-
};
|
|
103
|
-
exports.default = Voice;
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.useInitialize = useInitialize;
|
|
7
|
-
/**
|
|
8
|
-
* Custom hook for initializing the OnWid SDK
|
|
9
|
-
*
|
|
10
|
-
* Required Parameters:
|
|
11
|
-
* - apiKey: string - Unique identifier for the user
|
|
12
|
-
* - deviceId: string - Unique identifier for the device
|
|
13
|
-
* - metadata: object - Additional device/user information
|
|
14
|
-
*
|
|
15
|
-
* The initialization process:
|
|
16
|
-
* 1. Validates required input parameters
|
|
17
|
-
* 2. Stores API key securely in keychain
|
|
18
|
-
* 3. Registers the device with provided details
|
|
19
|
-
*/
|
|
20
|
-
const store_key_1 = require("../store.key");
|
|
21
|
-
const api_1 = require("../onwidApi/api");
|
|
22
|
-
const initializelivekit_1 = __importDefault(require("./initializelivekit"));
|
|
23
|
-
const react_native_1 = require("react-native");
|
|
24
|
-
function useInitialize({ apiKey, onwidUrl, metadata, }) {
|
|
25
|
-
const checkPermissions = async () => {
|
|
26
|
-
try {
|
|
27
|
-
// Check for required permissions on Android
|
|
28
|
-
if (react_native_1.Platform.OS === 'android') {
|
|
29
|
-
const recordAudioPermission = react_native_1.PermissionsAndroid.PERMISSIONS.RECORD_AUDIO;
|
|
30
|
-
if (!recordAudioPermission) {
|
|
31
|
-
throw new Error('RECORD_AUDIO permission not available');
|
|
32
|
-
}
|
|
33
|
-
const permissions = [recordAudioPermission];
|
|
34
|
-
const results = await Promise.all(permissions.map((permission) => react_native_1.PermissionsAndroid.request(permission)));
|
|
35
|
-
const allGranted = results.every((result) => result === react_native_1.PermissionsAndroid.RESULTS.GRANTED);
|
|
36
|
-
if (!allGranted) {
|
|
37
|
-
throw new Error('Required permissions not granted');
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
catch (err) {
|
|
42
|
-
const errorMessage = err instanceof Error ? err.message : 'Failed to check permissions';
|
|
43
|
-
throw new Error(errorMessage);
|
|
44
|
-
}
|
|
45
|
-
};
|
|
46
|
-
/**
|
|
47
|
-
* Validates required initialization parameters
|
|
48
|
-
* @throws Error if any required parameter is missing or invalid
|
|
49
|
-
*/
|
|
50
|
-
const validateInputs = () => {
|
|
51
|
-
if (!apiKey || typeof apiKey !== 'string') {
|
|
52
|
-
throw new Error('apiKey is required and must be a string');
|
|
53
|
-
}
|
|
54
|
-
if (!onwidUrl || typeof onwidUrl !== 'string') {
|
|
55
|
-
throw new Error('onwidUrl is required and must be a string');
|
|
56
|
-
}
|
|
57
|
-
if (metadata && typeof metadata === 'object' && !metadata.config) {
|
|
58
|
-
throw new Error('metadata must contain a config object');
|
|
59
|
-
}
|
|
60
|
-
};
|
|
61
|
-
const initialize = async () => {
|
|
62
|
-
try {
|
|
63
|
-
await checkPermissions();
|
|
64
|
-
(0, initializelivekit_1.default)();
|
|
65
|
-
// Validate required parameters before proceeding
|
|
66
|
-
validateInputs();
|
|
67
|
-
// Store API key in keychain
|
|
68
|
-
await (0, store_key_1.setAgentData)({
|
|
69
|
-
apiKey,
|
|
70
|
-
onwidUrl,
|
|
71
|
-
});
|
|
72
|
-
// Get the APIService instance and initialize it
|
|
73
|
-
const apiService = api_1.APIService.getInstance();
|
|
74
|
-
await apiService.initialize();
|
|
75
|
-
console.log('registerOnInitialize');
|
|
76
|
-
// Register new device with provided details
|
|
77
|
-
const registerResponse = await apiService.registerOnInitialize();
|
|
78
|
-
if (!registerResponse.success) {
|
|
79
|
-
throw new Error(registerResponse.error || 'Device registration failed');
|
|
80
|
-
}
|
|
81
|
-
if (registerResponse.data) {
|
|
82
|
-
// todo: store config
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
catch (err) {
|
|
86
|
-
const errorMessage = err instanceof Error ? err.message : 'Initialization failed';
|
|
87
|
-
throw new Error(errorMessage);
|
|
88
|
-
}
|
|
89
|
-
};
|
|
90
|
-
// Initialize immediately when hook is called
|
|
91
|
-
initialize().catch(console.error);
|
|
92
|
-
}
|
|
@@ -1,334 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.useVoiceAgent = void 0;
|
|
4
|
-
const livekit_client_1 = require("livekit-client");
|
|
5
|
-
const react_1 = require("react");
|
|
6
|
-
const onwid_1 = require("../Event/onwid");
|
|
7
|
-
const api_1 = require("../onwidApi/api");
|
|
8
|
-
const store_key_1 = require("../store.key");
|
|
9
|
-
// Create a singleton instance of Room that persists across hook instances
|
|
10
|
-
// This ensures we don't create multiple Room instances that could conflict
|
|
11
|
-
let roomInstance = null;
|
|
12
|
-
let isConnecting = false;
|
|
13
|
-
let isDisconnecting = false;
|
|
14
|
-
let hasListeners = false;
|
|
15
|
-
let connectionAttempts = 0;
|
|
16
|
-
let stableConnectionTimerId = null;
|
|
17
|
-
const MAX_CONNECTION_ATTEMPTS = 3;
|
|
18
|
-
const STABLE_CONNECTION_TIMEOUT = 5000; // 5 seconds
|
|
19
|
-
// Create getters for the room instance
|
|
20
|
-
const getRoomInstance = () => {
|
|
21
|
-
if (!roomInstance) {
|
|
22
|
-
console.log('Creating new Room instance');
|
|
23
|
-
// Configure the room with the right options at creation time
|
|
24
|
-
roomInstance = new livekit_client_1.Room({
|
|
25
|
-
adaptiveStream: true,
|
|
26
|
-
dynacast: true,
|
|
27
|
-
// Using the most stable configuration for React Native
|
|
28
|
-
publishDefaults: {
|
|
29
|
-
simulcast: false, // Disable simulcast to reduce SDP complexity
|
|
30
|
-
},
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
return roomInstance;
|
|
34
|
-
};
|
|
35
|
-
// Reset connection attempt counter
|
|
36
|
-
const resetConnectionAttempts = () => {
|
|
37
|
-
connectionAttempts = 0;
|
|
38
|
-
// Clear any pending stable connection timer
|
|
39
|
-
if (stableConnectionTimerId) {
|
|
40
|
-
clearTimeout(stableConnectionTimerId);
|
|
41
|
-
stableConnectionTimerId = null;
|
|
42
|
-
}
|
|
43
|
-
};
|
|
44
|
-
// Cleanup function to reset the room state
|
|
45
|
-
const resetRoomState = () => {
|
|
46
|
-
if (roomInstance) {
|
|
47
|
-
try {
|
|
48
|
-
console.log('Resetting room state');
|
|
49
|
-
console.log('isDisconnecting', isDisconnecting);
|
|
50
|
-
// Only disconnect if currently connected
|
|
51
|
-
if (roomInstance.state !== livekit_client_1.ConnectionState.Disconnected &&
|
|
52
|
-
!isDisconnecting) {
|
|
53
|
-
isDisconnecting = true;
|
|
54
|
-
roomInstance.disconnect().finally(() => {
|
|
55
|
-
isDisconnecting = false;
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
// Don't destroy the room instance, just reset its state
|
|
59
|
-
hasListeners = false;
|
|
60
|
-
resetConnectionAttempts();
|
|
61
|
-
}
|
|
62
|
-
catch (e) {
|
|
63
|
-
console.error('Error resetting room state:', e);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
const useVoiceAgent = () => {
|
|
68
|
-
const [isLoading, setIsLoading] = (0, react_1.useState)(false);
|
|
69
|
-
const [error, setError] = (0, react_1.useState)(null);
|
|
70
|
-
const [tokenDetails, setTokenDetails] = (0, react_1.useState)(null);
|
|
71
|
-
const [isMicMuted, setIsMicMuted] = (0, react_1.useState)(false);
|
|
72
|
-
const [connectionState, setConnectionState] = (0, react_1.useState)(() => {
|
|
73
|
-
// Initialize with the current room state if it exists
|
|
74
|
-
const room = getRoomInstance();
|
|
75
|
-
return room ? room.state : livekit_client_1.ConnectionState.Disconnected;
|
|
76
|
-
});
|
|
77
|
-
const [stableConnection, setStableConnection] = (0, react_1.useState)(false);
|
|
78
|
-
console.log('ConnectionState_connected', connectionState
|
|
79
|
-
// tokenDetails?.token
|
|
80
|
-
);
|
|
81
|
-
// Setup event listeners for the room
|
|
82
|
-
const setupRoomListeners = () => {
|
|
83
|
-
if (hasListeners || !roomInstance)
|
|
84
|
-
return;
|
|
85
|
-
console.log('Setting up room listeners');
|
|
86
|
-
const handleConnectionChange = (state) => {
|
|
87
|
-
console.log('Connection state changed:', state);
|
|
88
|
-
// Use a function to ensure we're setting with the latest state
|
|
89
|
-
setConnectionState(() => state);
|
|
90
|
-
// Handle connection state changes
|
|
91
|
-
if (state === livekit_client_1.ConnectionState.Connected) {
|
|
92
|
-
// Reset connection attempts when connected successfully
|
|
93
|
-
resetConnectionAttempts();
|
|
94
|
-
// Set up a timer to mark the connection as stable after a few seconds
|
|
95
|
-
// This prevents immediate disconnections from being treated as stable
|
|
96
|
-
if (stableConnectionTimerId) {
|
|
97
|
-
clearTimeout(stableConnectionTimerId);
|
|
98
|
-
}
|
|
99
|
-
stableConnectionTimerId = setTimeout(() => {
|
|
100
|
-
console.log('Connection marked as stable');
|
|
101
|
-
setStableConnection(true);
|
|
102
|
-
}, STABLE_CONNECTION_TIMEOUT);
|
|
103
|
-
}
|
|
104
|
-
else if (state === livekit_client_1.ConnectionState.Disconnected) {
|
|
105
|
-
// Mark connection as unstable
|
|
106
|
-
setStableConnection(false);
|
|
107
|
-
// Clear any pending stable connection timer
|
|
108
|
-
if (stableConnectionTimerId) {
|
|
109
|
-
clearTimeout(stableConnectionTimerId);
|
|
110
|
-
stableConnectionTimerId = null;
|
|
111
|
-
}
|
|
112
|
-
// If disconnected unexpectedly and we have token details, don't automatically reconnect
|
|
113
|
-
// This prevents connection loops
|
|
114
|
-
if (tokenDetails && !isDisconnecting) {
|
|
115
|
-
console.log('Room disconnected unexpectedly - not auto-reconnecting');
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
};
|
|
119
|
-
const handleTrackSubscribed = (track) => {
|
|
120
|
-
if (track.kind === 'audio') {
|
|
121
|
-
track.setVolume(1.0);
|
|
122
|
-
}
|
|
123
|
-
};
|
|
124
|
-
// Handle participant disconnected - this could be helpful to detect server-side kicks
|
|
125
|
-
roomInstance.on('participantDisconnected', () => {
|
|
126
|
-
console.log('Participant disconnected from room');
|
|
127
|
-
});
|
|
128
|
-
roomInstance.on('connectionStateChanged', handleConnectionChange);
|
|
129
|
-
roomInstance.on('trackSubscribed', handleTrackSubscribed);
|
|
130
|
-
// Listen for SDP negotiation errors to handle them better
|
|
131
|
-
roomInstance.on('mediaDevicesError', (e) => {
|
|
132
|
-
console.log('Media devices error:', e.message);
|
|
133
|
-
});
|
|
134
|
-
hasListeners = true;
|
|
135
|
-
};
|
|
136
|
-
// Initialize room and listeners
|
|
137
|
-
(0, react_1.useEffect)(() => {
|
|
138
|
-
const room = getRoomInstance();
|
|
139
|
-
setupRoomListeners();
|
|
140
|
-
// Sync local state with room state
|
|
141
|
-
setConnectionState(room.state);
|
|
142
|
-
return () => {
|
|
143
|
-
// Do NOT disconnect or destroy the room on component unmount
|
|
144
|
-
// This ensures the singleton persists across component lifecycle
|
|
145
|
-
// But we do want to update local state if the component is unmounted
|
|
146
|
-
if (!tokenDetails) {
|
|
147
|
-
setTokenDetails(null);
|
|
148
|
-
}
|
|
149
|
-
};
|
|
150
|
-
}, []);
|
|
151
|
-
// Connect to LiveKit when token is set
|
|
152
|
-
(0, react_1.useEffect)(() => {
|
|
153
|
-
const room = getRoomInstance();
|
|
154
|
-
if (!tokenDetails || isConnecting)
|
|
155
|
-
return;
|
|
156
|
-
// Always sync our state with the room's current state
|
|
157
|
-
setConnectionState(room.state);
|
|
158
|
-
const connectToRoom = async () => {
|
|
159
|
-
// Prevent multiple connection attempts
|
|
160
|
-
if (isConnecting)
|
|
161
|
-
return;
|
|
162
|
-
// Limit connection attempts to prevent infinite loops
|
|
163
|
-
if (connectionAttempts >= MAX_CONNECTION_ATTEMPTS) {
|
|
164
|
-
console.log(`Maximum connection attempts (${MAX_CONNECTION_ATTEMPTS}) reached. Not trying again.`);
|
|
165
|
-
setError(`Failed to connect after ${MAX_CONNECTION_ATTEMPTS} attempts`);
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
connectionAttempts++;
|
|
169
|
-
try {
|
|
170
|
-
isConnecting = true;
|
|
171
|
-
console.log(`Connecting to LiveKit room... (attempt ${connectionAttempts})`);
|
|
172
|
-
// Only attempt to connect if we're disconnected
|
|
173
|
-
if (room.state === livekit_client_1.ConnectionState.Disconnected) {
|
|
174
|
-
// Update state before connection attempt
|
|
175
|
-
setConnectionState(livekit_client_1.ConnectionState.Connecting);
|
|
176
|
-
await room.connect(tokenDetails.server_url, tokenDetails.token, {
|
|
177
|
-
autoSubscribe: true, // Ensure we subscribe to tracks automatically
|
|
178
|
-
});
|
|
179
|
-
// Explicitly set to connected if connection was successful
|
|
180
|
-
setConnectionState(room.state);
|
|
181
|
-
console.log('Connected to LiveKit room');
|
|
182
|
-
}
|
|
183
|
-
else if (room.state === livekit_client_1.ConnectionState.Connected) {
|
|
184
|
-
console.log('Room is already connected');
|
|
185
|
-
// Ensure our state matches
|
|
186
|
-
setConnectionState(livekit_client_1.ConnectionState.Connected);
|
|
187
|
-
}
|
|
188
|
-
else {
|
|
189
|
-
console.log('Room is in transition state:', room.state);
|
|
190
|
-
// Sync our state with the room's current state
|
|
191
|
-
setConnectionState(room.state);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
catch (err) {
|
|
195
|
-
const message = err instanceof Error ? err.message : 'Failed to connect to room';
|
|
196
|
-
setError(message);
|
|
197
|
-
console.error('Connection error:', message);
|
|
198
|
-
// Sync state to disconnected if we failed to connect
|
|
199
|
-
setConnectionState(room.state);
|
|
200
|
-
// Don't keep retrying if we hit an SDP error
|
|
201
|
-
if (message.includes('SDP') || message.includes('sdp')) {
|
|
202
|
-
console.log('SDP error detected, will not retry automatically');
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
finally {
|
|
206
|
-
isConnecting = false;
|
|
207
|
-
}
|
|
208
|
-
};
|
|
209
|
-
connectToRoom();
|
|
210
|
-
// No cleanup here - we don't want to disconnect when token changes or component unmounts
|
|
211
|
-
}, [tokenDetails]);
|
|
212
|
-
// Log connection status periodically for debugging
|
|
213
|
-
(0, react_1.useEffect)(() => {
|
|
214
|
-
const debugInterval = setInterval(() => {
|
|
215
|
-
if (roomInstance) {
|
|
216
|
-
const state = roomInstance.state;
|
|
217
|
-
const participantCount = roomInstance.numParticipants;
|
|
218
|
-
console.log(`[DEBUG] Room state: ${state}, Participants: ${participantCount}, Stable: ${stableConnection}`);
|
|
219
|
-
}
|
|
220
|
-
}, 10000); // Every 10 seconds
|
|
221
|
-
return () => clearInterval(debugInterval);
|
|
222
|
-
}, [stableConnection]);
|
|
223
|
-
// Generate token and connect
|
|
224
|
-
const generateVoiceToken = async () => {
|
|
225
|
-
console.log('generateVoiceToken');
|
|
226
|
-
// Don't generate a new token if we're already connecting/connected
|
|
227
|
-
if (isConnecting || isLoading) {
|
|
228
|
-
console.log('Already connecting or loading, skipping token generation');
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
// Reset connection attempts when starting fresh
|
|
232
|
-
resetConnectionAttempts();
|
|
233
|
-
setStableConnection(false);
|
|
234
|
-
const userData = await (0, store_key_1.getAgentData)(onwid_1.EventKeys.USER_DATA);
|
|
235
|
-
setIsLoading(true);
|
|
236
|
-
setError(null);
|
|
237
|
-
console.log('userData', userData);
|
|
238
|
-
try {
|
|
239
|
-
const apiService = api_1.APIService.getInstance();
|
|
240
|
-
const response = await apiService.getTokenDetails({
|
|
241
|
-
app_user_id: userData === null || userData === void 0 ? void 0 : userData.app_user_id,
|
|
242
|
-
call_type: 'EMBEDDED',
|
|
243
|
-
});
|
|
244
|
-
if (!response.data)
|
|
245
|
-
throw new Error('No voice token found');
|
|
246
|
-
// Only set token details if we're not already connected
|
|
247
|
-
const room = getRoomInstance();
|
|
248
|
-
if (room.state !== livekit_client_1.ConnectionState.Connected) {
|
|
249
|
-
setTokenDetails(response.data);
|
|
250
|
-
}
|
|
251
|
-
else {
|
|
252
|
-
console.log('Room already connected, skipping token update');
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
catch (err) {
|
|
256
|
-
const message = err instanceof Error ? err.message : 'Failed to generate voice token';
|
|
257
|
-
setError(message);
|
|
258
|
-
throw new Error(message);
|
|
259
|
-
}
|
|
260
|
-
finally {
|
|
261
|
-
setIsLoading(false);
|
|
262
|
-
}
|
|
263
|
-
};
|
|
264
|
-
// End call
|
|
265
|
-
const endCall = async () => {
|
|
266
|
-
if (isDisconnecting) {
|
|
267
|
-
console.log('Already disconnecting, skipping');
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
|
-
try {
|
|
271
|
-
console.log('Ending call');
|
|
272
|
-
isDisconnecting = true;
|
|
273
|
-
setTokenDetails(null);
|
|
274
|
-
setIsMicMuted(false);
|
|
275
|
-
resetConnectionAttempts();
|
|
276
|
-
setStableConnection(false);
|
|
277
|
-
const room = getRoomInstance();
|
|
278
|
-
if (room.state !== livekit_client_1.ConnectionState.Disconnected) {
|
|
279
|
-
// Update state before disconnection
|
|
280
|
-
setConnectionState(livekit_client_1.ConnectionState.Connecting);
|
|
281
|
-
await room.disconnect();
|
|
282
|
-
// Update state after disconnection
|
|
283
|
-
setConnectionState(livekit_client_1.ConnectionState.Disconnected);
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
catch (err) {
|
|
287
|
-
setError(err instanceof Error ? err.message : 'Failed to end call');
|
|
288
|
-
// Make sure state is correctly reflected even if there's an error
|
|
289
|
-
const room = getRoomInstance();
|
|
290
|
-
setConnectionState(room.state);
|
|
291
|
-
}
|
|
292
|
-
finally {
|
|
293
|
-
isDisconnecting = false;
|
|
294
|
-
}
|
|
295
|
-
};
|
|
296
|
-
// Mute microphone
|
|
297
|
-
const muteMic = () => {
|
|
298
|
-
const room = getRoomInstance();
|
|
299
|
-
if (room.localParticipant) {
|
|
300
|
-
room.localParticipant.setMicrophoneEnabled(false);
|
|
301
|
-
setIsMicMuted(true);
|
|
302
|
-
console.log('Microphone muted');
|
|
303
|
-
}
|
|
304
|
-
};
|
|
305
|
-
// Unmute microphone
|
|
306
|
-
const unmuteMic = () => {
|
|
307
|
-
const room = getRoomInstance();
|
|
308
|
-
if (room.localParticipant) {
|
|
309
|
-
room.localParticipant.setMicrophoneEnabled(true);
|
|
310
|
-
setIsMicMuted(false);
|
|
311
|
-
console.log('Microphone unmuted');
|
|
312
|
-
}
|
|
313
|
-
};
|
|
314
|
-
// Clean up everything (use only when the app is shutting down)
|
|
315
|
-
const cleanup = () => {
|
|
316
|
-
endCall();
|
|
317
|
-
resetRoomState();
|
|
318
|
-
};
|
|
319
|
-
return {
|
|
320
|
-
initializeVoiceAgent: generateVoiceToken,
|
|
321
|
-
endCall,
|
|
322
|
-
muteMic,
|
|
323
|
-
unmuteMic,
|
|
324
|
-
isMicMuted,
|
|
325
|
-
connectionState,
|
|
326
|
-
room: getRoomInstance(),
|
|
327
|
-
tokenDetails,
|
|
328
|
-
isLoading,
|
|
329
|
-
error,
|
|
330
|
-
roomRef: { current: getRoomInstance() },
|
|
331
|
-
cleanup,
|
|
332
|
-
};
|
|
333
|
-
};
|
|
334
|
-
exports.useVoiceAgent = useVoiceAgent;
|