@qafka/react-native 2.3.1 → 2.3.3
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/android/src/main/AndroidManifest.xml +2 -0
- package/dist/hooks/useVoiceChat.js +7 -1
- package/dist/native/QafkaAudio.d.ts +7 -0
- package/dist/native/QafkaAudio.js +20 -0
- package/dist/native/ensure-record-permission.d.ts +20 -0
- package/dist/native/ensure-record-permission.js +11 -0
- package/dist/services/RealtimeService.js +11 -0
- package/package.json +1 -1
|
@@ -358,8 +358,14 @@ function useVoiceChat({ apiUrl, apiKey, userContext, contextDescription, onToolS
|
|
|
358
358
|
}
|
|
359
359
|
}, []);
|
|
360
360
|
const connect = (0, react_1.useCallback)(async () => {
|
|
361
|
-
if (serviceRef.current?.isConnected)
|
|
361
|
+
if (serviceRef.current?.isConnected) {
|
|
362
|
+
// Re-entering the voice page after a swipe to chat (which calls pauseMic
|
|
363
|
+
// and stops native capture). The socket is still open, so the fresh-
|
|
364
|
+
// connect path below would no-op and leave the mic stopped — resume
|
|
365
|
+
// capture so the user can be heard again.
|
|
366
|
+
await serviceRef.current.resumeMic();
|
|
362
367
|
return;
|
|
368
|
+
}
|
|
363
369
|
setState('connecting');
|
|
364
370
|
const service = new RealtimeService_1.RealtimeService(apiUrl, apiKey);
|
|
365
371
|
if (getSessionToken)
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
export declare const QafkaAudio: {
|
|
2
|
+
/**
|
|
3
|
+
* Ensure microphone access before capturing. iOS prompts natively inside
|
|
4
|
+
* startCapture; on Android the RECORD_AUDIO runtime permission is requested
|
|
5
|
+
* here (the platform never auto-prompts), otherwise capture starts denied and
|
|
6
|
+
* the voice stream is silent. Resolves to whether mic access is available.
|
|
7
|
+
*/
|
|
8
|
+
ensureRecordPermission: () => Promise<boolean>;
|
|
2
9
|
startCapture: () => Promise<boolean>;
|
|
3
10
|
stopCapture: () => Promise<boolean>;
|
|
4
11
|
playAudioChunk: (base64Data: string) => Promise<boolean>;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.QafkaAudio = void 0;
|
|
4
4
|
const react_native_1 = require("react-native");
|
|
5
|
+
const ensure_record_permission_1 = require("./ensure-record-permission");
|
|
5
6
|
const { QafkaAudio: NativeQafkaAudio } = react_native_1.NativeModules;
|
|
6
7
|
const emitter = NativeQafkaAudio ? new react_native_1.NativeEventEmitter(NativeQafkaAudio) : null;
|
|
7
8
|
const MISSING_MODULE_MESSAGE = `QafkaAudio native module is not linked. ` +
|
|
@@ -15,6 +16,25 @@ const requireModule = () => {
|
|
|
15
16
|
return NativeQafkaAudio;
|
|
16
17
|
};
|
|
17
18
|
exports.QafkaAudio = {
|
|
19
|
+
/**
|
|
20
|
+
* Ensure microphone access before capturing. iOS prompts natively inside
|
|
21
|
+
* startCapture; on Android the RECORD_AUDIO runtime permission is requested
|
|
22
|
+
* here (the platform never auto-prompts), otherwise capture starts denied and
|
|
23
|
+
* the voice stream is silent. Resolves to whether mic access is available.
|
|
24
|
+
*/
|
|
25
|
+
ensureRecordPermission: () => (0, ensure_record_permission_1.ensureRecordPermission)({
|
|
26
|
+
os: react_native_1.Platform.OS,
|
|
27
|
+
check: () => react_native_1.PermissionsAndroid.check(react_native_1.PermissionsAndroid.PERMISSIONS.RECORD_AUDIO),
|
|
28
|
+
request: async () => {
|
|
29
|
+
const result = await react_native_1.PermissionsAndroid.request(react_native_1.PermissionsAndroid.PERMISSIONS.RECORD_AUDIO, {
|
|
30
|
+
title: 'Microphone permission',
|
|
31
|
+
message: 'Microphone access is required for voice conversations.',
|
|
32
|
+
buttonPositive: 'OK',
|
|
33
|
+
buttonNegative: 'Cancel',
|
|
34
|
+
});
|
|
35
|
+
return result === react_native_1.PermissionsAndroid.RESULTS.GRANTED;
|
|
36
|
+
},
|
|
37
|
+
}),
|
|
18
38
|
startCapture: () => requireModule().startCapture(),
|
|
19
39
|
stopCapture: () => requireModule().stopCapture(),
|
|
20
40
|
playAudioChunk: (base64Data) => requireModule().playAudioChunk(base64Data),
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve microphone permission before voice capture starts.
|
|
3
|
+
*
|
|
4
|
+
* iOS requests the permission as part of starting native capture, so this is a
|
|
5
|
+
* no-op there. On Android the platform never auto-prompts for the runtime
|
|
6
|
+
* RECORD_AUDIO permission — it must be requested explicitly, otherwise capture
|
|
7
|
+
* starts denied and the voice stream stays silent.
|
|
8
|
+
*
|
|
9
|
+
* Kept pure and dependency-injected so it can be unit-tested without the
|
|
10
|
+
* react-native runtime; {@link QafkaAudio} wires the real platform calls in.
|
|
11
|
+
*/
|
|
12
|
+
export interface RecordPermissionDeps {
|
|
13
|
+
/** Platform.OS */
|
|
14
|
+
os: string;
|
|
15
|
+
/** Resolve whether RECORD_AUDIO is already granted. */
|
|
16
|
+
check: () => Promise<boolean>;
|
|
17
|
+
/** Prompt the user for RECORD_AUDIO; resolve whether it was granted. */
|
|
18
|
+
request: () => Promise<boolean>;
|
|
19
|
+
}
|
|
20
|
+
export declare function ensureRecordPermission(deps: RecordPermissionDeps): Promise<boolean>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ensureRecordPermission = ensureRecordPermission;
|
|
4
|
+
async function ensureRecordPermission(deps) {
|
|
5
|
+
// iOS handles the prompt natively inside startCapture.
|
|
6
|
+
if (deps.os !== 'android')
|
|
7
|
+
return true;
|
|
8
|
+
if (await deps.check())
|
|
9
|
+
return true;
|
|
10
|
+
return deps.request();
|
|
11
|
+
}
|
|
@@ -129,6 +129,17 @@ class RealtimeService {
|
|
|
129
129
|
}
|
|
130
130
|
async startAudioPipeline() {
|
|
131
131
|
try {
|
|
132
|
+
// Android needs the RECORD_AUDIO runtime permission requested explicitly
|
|
133
|
+
// before capture (iOS prompts natively inside startCapture). Without this
|
|
134
|
+
// capture starts denied and the voice stream is silent.
|
|
135
|
+
const micGranted = await QafkaAudio_1.QafkaAudio.ensureRecordPermission();
|
|
136
|
+
if (!micGranted) {
|
|
137
|
+
this.eventHandler?.({
|
|
138
|
+
type: 'error',
|
|
139
|
+
message: 'Microphone permission denied',
|
|
140
|
+
});
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
132
143
|
// Attach listener BEFORE startCapture so the native tap
|
|
133
144
|
// doesn't drop early buffers (hasListeners/listenerCount gate).
|
|
134
145
|
this.attachAudioListener();
|
package/package.json
CHANGED