@lumavox/react-native 0.16.0 → 0.17.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 +15 -5
- package/app.plugin.js +5 -1
- package/dist/audio.native.d.ts +73 -9
- package/dist/audio.native.js +180 -20
- package/dist/device-audio.d.ts +8 -4
- package/dist/session.d.ts +40 -2
- package/dist/session.js +116 -20
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Voice conversations with Lumavox agents in a React Native app, on iOS,
|
|
4
4
|
Android and the web — **one install**: the client (`@lumavox/client`), the
|
|
5
5
|
hooks (`@lumavox/react`), and the audio they lack, a player and a recorder
|
|
6
|
-
for **push-to-talk**, on
|
|
6
|
+
for **push-to-talk**, **half-duplex** and **hands-free**, on
|
|
7
7
|
[`react-native-audio-api`](https://github.com/software-mansion/react-native-audio-api)
|
|
8
8
|
on a phone and Web Audio in a browser.
|
|
9
9
|
|
|
@@ -54,10 +54,20 @@ function Talk({ agentId }: { agentId: string }) {
|
|
|
54
54
|
- A call, Siri or another app pauses the reply and throws away a recording
|
|
55
55
|
in progress — `interrupted`, `onAudioInterrupted`, `onAudioResumed`; so
|
|
56
56
|
does leaving the screen: no background audio in this release.
|
|
57
|
-
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
- **Half-duplex on every phone**: `agent.listen({ halfDuplex: true })` —
|
|
58
|
+
tap once and talk for the whole conversation; the microphone stays open,
|
|
59
|
+
but nothing is heard while the agent speaks, so it never hears itself
|
|
60
|
+
and needs no echo cancellation. It cannot be talked over: give a stop
|
|
61
|
+
button (`agent.cancel()`).
|
|
62
|
+
- **Hands-free** (`agent.listen()`, talking over the agent to stop it):
|
|
63
|
+
on the web where the browser cancels echo, and on a phone **in early
|
|
64
|
+
access**, on one build of the audio library, exactly:
|
|
65
|
+
`npm install react-native-audio-api@1.0.0-nightly-6fd4c68-20261102`,
|
|
66
|
+
then build the app again. While it listens the phone's sound is a
|
|
67
|
+
call's — the echo taken out, the agent a little quieter — and Bluetooth
|
|
68
|
+
earbuds' microphone is used. On 0.13 it is refused (`audio.handsFree()`
|
|
69
|
+
says why): offer half-duplex, or `listen({ assumeNoEcho: true })` with
|
|
70
|
+
headphones on. Background audio comes with the library's 1.0.
|
|
61
71
|
- **A clock for a lip-synced face or subtitles**, on a phone and the web:
|
|
62
72
|
`audio.onScheduled(({ pcm, sample_rate, chunk, startsAt }) => …)` tells
|
|
63
73
|
every frame as it is queued, before it plays; draw against
|
package/app.plugin.js
CHANGED
|
@@ -68,7 +68,10 @@ function audioApiOptions(options = {}) {
|
|
|
68
68
|
androidFSStopWithTask: true,
|
|
69
69
|
// The library's own defaults: FFmpeg decodes compressed files, which a
|
|
70
70
|
// conversation's raw PCM never needs — `true` makes the app smaller.
|
|
71
|
+
// 1.0 turned the option round, FFmpeg off unless `enableFFmpeg`: each
|
|
72
|
+
// build reads its own and ignores the other's, so both say the same.
|
|
71
73
|
disableFFmpeg: options.disableFFmpeg ?? false,
|
|
74
|
+
enableFFmpeg: !(options.disableFFmpeg ?? false),
|
|
72
75
|
disableStaticExternalLibs: options.disableStaticExternalLibs ?? false,
|
|
73
76
|
};
|
|
74
77
|
}
|
|
@@ -90,7 +93,8 @@ function withLumavox(config, options, library) {
|
|
|
90
93
|
}
|
|
91
94
|
|
|
92
95
|
function loadLibrary() {
|
|
93
|
-
|
|
96
|
+
// With its extension: 1.0's `exports` names `./app.plugin.js` alone.
|
|
97
|
+
const plugin = require(`${LIBRARY}/app.plugin.js`);
|
|
94
98
|
return plugin.default ?? plugin;
|
|
95
99
|
}
|
|
96
100
|
|
package/dist/audio.native.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* A phone's player and recorder, on `react-native-audio-api` 0.13.6
|
|
3
|
-
*
|
|
2
|
+
* A phone's player and recorder, on `react-native-audio-api` — 0.13.6, or
|
|
3
|
+
* the one 1.0 nightly hands-free runs on (`session.ts`, `PINNED_NIGHTLY`):
|
|
4
|
+
* what Metro picks for `./audio` on iOS and Android (`index.ts`). With
|
|
4
5
|
* `session.ts`, the only file that touches the library.
|
|
5
6
|
*
|
|
6
7
|
* - **The reply plays through one `AudioBufferQueueSourceNode`**, started
|
|
@@ -25,7 +26,8 @@
|
|
|
25
26
|
* - **What is heard is the clock less the app's `outputLatencyMs`**:
|
|
26
27
|
* 0.13.6 says nothing of the device's latency — its source nodes'
|
|
27
28
|
* `getLatency()` is their time-stretcher's — so the app, which can know
|
|
28
|
-
* its earbuds are on, says it.
|
|
29
|
+
* its earbuds are on, says it. Unset on the nightly, the context's own
|
|
30
|
+
* (`outputLatency` and `baseLatency`, 1.0's).
|
|
29
31
|
* - **The microphone records at 16 kHz**, mono, 100 ms a buffer — less to
|
|
30
32
|
* send than 24 kHz, and a rate the socket takes. `onAudioReady` hands the
|
|
31
33
|
* samples over JSI as floats, at the rate the device chose, which need
|
|
@@ -36,22 +38,60 @@
|
|
|
36
38
|
* thrown away (the hook discards the recording on the socket). The audio
|
|
37
39
|
* comes back on its own when the system says it may, else on `resume()`
|
|
38
40
|
* or the next press.
|
|
39
|
-
* - **
|
|
40
|
-
*
|
|
41
|
-
*
|
|
41
|
+
* - **Half-duplex on any build** (`listen`'s `halfDuplex`): the recorder
|
|
42
|
+
* started once for the whole conversation, which suits the library —
|
|
43
|
+
* on iOS, 0.13.6's recorder shares the player's one engine, and each
|
|
44
|
+
* start stops and starts it and each stop rebuilds it, a gap in the
|
|
45
|
+
* voice; the loudspeaker holds (`defaultToSpeaker`), and Android's
|
|
46
|
+
* recorder and player are two streams that open side by side.
|
|
47
|
+
* - **Hands-free on the pinned nightly alone** (D17, D18; `handsFree`):
|
|
48
|
+
* 0.13.6 cancels no echo, so a microphone left open hears the agent
|
|
49
|
+
* (D12, D16) — refused there, `assumeNoEcho` with headphones on. On the
|
|
50
|
+
* nightly a capture that asks for echo cancellation (`listen` does, on
|
|
51
|
+
* `handsFree`'s yes) is iOS's voice processing in `videoChat`, and
|
|
52
|
+
* Android's voice-communication input with the reply on the call's path
|
|
53
|
+
* (`androidOutputProfile`), in its communication mode
|
|
54
|
+
* (`session.configure`): its canceller hears only that path. The reply
|
|
55
|
+
* moves to it — a new context — once nothing is playing, never mid-word,
|
|
56
|
+
* and back once the capture ends. **A recorder that will not start that
|
|
57
|
+
* way fails the capture**, and `listen` answers why: the app offers
|
|
58
|
+
* half-duplex (D16, D19). Whether an Android phone's canceller works is
|
|
59
|
+
* not asked: the platform says it only to native code (left-overs #177).
|
|
60
|
+
* - **`duck`**: the reply goes through a gain, lowered while it may be
|
|
61
|
+
* talked over — barge-in's first step.
|
|
42
62
|
*/
|
|
43
63
|
import { type AgentAudio, type AudioDevice, type ScheduledAudio } from "@lumavox/client";
|
|
44
64
|
import type { AudioOptions, CreateAudio, DeviceAudio, Output } from "./device-audio.js";
|
|
45
65
|
/** Hz the microphone records at unless a rate the socket takes is asked. */
|
|
46
66
|
export declare const RECORD_RATE = 16000;
|
|
47
|
-
/** Why `handsFree` refuses on
|
|
48
|
-
export declare const PHONE_ECHO = "Hands-free needs echo cancellation, which comes with react-native-audio-api 1.0. Use push-to-talk, or assumeNoEcho with headphones on.";
|
|
67
|
+
/** Why `handsFree` refuses on 0.13. */
|
|
68
|
+
export declare const PHONE_ECHO = "Hands-free needs echo cancellation, which comes with react-native-audio-api 1.0. Use half-duplex or push-to-talk, or assumeNoEcho with headphones on.";
|
|
69
|
+
/** Why it refuses on a 1.0 build that is not the one it was tested on. */
|
|
70
|
+
export declare const untestedBuild: (version: string) => string;
|
|
49
71
|
export declare class PhoneAudio implements DeviceAudio {
|
|
50
72
|
interrupted: boolean;
|
|
51
73
|
/** On a phone the system chooses the speaker. */
|
|
52
74
|
readonly canChooseOutput = false;
|
|
53
75
|
private context;
|
|
54
76
|
private queue;
|
|
77
|
+
/** Between the queue and the speaker: the reply lowered while it may be talked over. */
|
|
78
|
+
private gain;
|
|
79
|
+
private volume;
|
|
80
|
+
/** The context's output on Android: the call's path while hands-free cancels echo. */
|
|
81
|
+
private profile;
|
|
82
|
+
/** A capture is cancelling echo: the session is hands-free's. */
|
|
83
|
+
private cancelling;
|
|
84
|
+
private unroute;
|
|
85
|
+
private output;
|
|
86
|
+
private disposed;
|
|
87
|
+
/** The call's route last asked on Android (`session.route`). */
|
|
88
|
+
private routed;
|
|
89
|
+
/**
|
|
90
|
+
* Until when an interruption is the session made again — Android's focus
|
|
91
|
+
* given up and taken back, told a moment later on its own thread — and
|
|
92
|
+
* not another app's.
|
|
93
|
+
*/
|
|
94
|
+
private settling;
|
|
55
95
|
/** The ids of the buffers enqueued and not yet played. */
|
|
56
96
|
private readonly playing;
|
|
57
97
|
/** Where the last buffer enqueued ends, on the context's clock. */
|
|
@@ -67,6 +107,7 @@ export declare class PhoneAudio implements DeviceAudio {
|
|
|
67
107
|
private readonly unwatch;
|
|
68
108
|
/** Seconds. */
|
|
69
109
|
private readonly leadIn;
|
|
110
|
+
/** The app's, or `null`: the context's own where it says one. */
|
|
70
111
|
private readonly latency;
|
|
71
112
|
constructor(options?: AudioOptions);
|
|
72
113
|
prime(): Promise<void>;
|
|
@@ -84,20 +125,43 @@ export declare class PhoneAudio implements DeviceAudio {
|
|
|
84
125
|
setInput(): void;
|
|
85
126
|
setProcessing(): void;
|
|
86
127
|
setOutputDevice(): Promise<boolean>;
|
|
87
|
-
/**
|
|
128
|
+
/**
|
|
129
|
+
* At 16 kHz, or at the rate asked when the socket takes it; cancelling
|
|
130
|
+
* echo when hands-free asks and the library can.
|
|
131
|
+
*/
|
|
88
132
|
startCapture(onPcm: (pcm: ArrayBuffer) => void, options?: {
|
|
89
133
|
rate?: number;
|
|
134
|
+
echoCancellation?: boolean;
|
|
90
135
|
}): Promise<{
|
|
91
136
|
rate: number;
|
|
92
137
|
}>;
|
|
138
|
+
/** The recorder, open: its echo cancelled when `cancel`. */
|
|
139
|
+
private record;
|
|
93
140
|
stopCapture(): Promise<void>;
|
|
94
141
|
resume(): Promise<void>;
|
|
95
142
|
setOutput(output: Output): void;
|
|
143
|
+
/** Hands-free's first step on sound over the reply: lowered, until the words say who it was. */
|
|
144
|
+
duck(on: boolean): void;
|
|
96
145
|
handsFree(): ReturnType<DeviceAudio["handsFree"]>;
|
|
97
146
|
onInterruption(fn: (interrupted: boolean) => void): () => void;
|
|
98
147
|
dispose(): void;
|
|
99
148
|
/** Behind what is queued; the playhead moves to its end. */
|
|
100
149
|
private enqueue;
|
|
150
|
+
/**
|
|
151
|
+
* The session hands-free's, Android's reply on the call's path and the
|
|
152
|
+
* call's route: before the recorder starts, so the canceller hears the
|
|
153
|
+
* first reply too.
|
|
154
|
+
*/
|
|
155
|
+
private enterHandsFree;
|
|
156
|
+
/** Back to push-to-talk's session; the reply leaves the call's path once it has played out. */
|
|
157
|
+
private leaveHandsFree;
|
|
158
|
+
/**
|
|
159
|
+
* The session for hands-free or not — disposed, the mode given back and
|
|
160
|
+
* nothing taken again for a screen gone — its focus's comings and goings
|
|
161
|
+
* no interruption.
|
|
162
|
+
*/
|
|
163
|
+
private reconfigure;
|
|
164
|
+
private reroute;
|
|
101
165
|
/** The queue for a reply at `rate`: this one, or a new context's. */
|
|
102
166
|
private player;
|
|
103
167
|
/**
|
package/dist/audio.native.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* A phone's player and recorder, on `react-native-audio-api` 0.13.6
|
|
3
|
-
*
|
|
2
|
+
* A phone's player and recorder, on `react-native-audio-api` — 0.13.6, or
|
|
3
|
+
* the one 1.0 nightly hands-free runs on (`session.ts`, `PINNED_NIGHTLY`):
|
|
4
|
+
* what Metro picks for `./audio` on iOS and Android (`index.ts`). With
|
|
4
5
|
* `session.ts`, the only file that touches the library.
|
|
5
6
|
*
|
|
6
7
|
* - **The reply plays through one `AudioBufferQueueSourceNode`**, started
|
|
@@ -25,7 +26,8 @@
|
|
|
25
26
|
* - **What is heard is the clock less the app's `outputLatencyMs`**:
|
|
26
27
|
* 0.13.6 says nothing of the device's latency — its source nodes'
|
|
27
28
|
* `getLatency()` is their time-stretcher's — so the app, which can know
|
|
28
|
-
* its earbuds are on, says it.
|
|
29
|
+
* its earbuds are on, says it. Unset on the nightly, the context's own
|
|
30
|
+
* (`outputLatency` and `baseLatency`, 1.0's).
|
|
29
31
|
* - **The microphone records at 16 kHz**, mono, 100 ms a buffer — less to
|
|
30
32
|
* send than 24 kHz, and a rate the socket takes. `onAudioReady` hands the
|
|
31
33
|
* samples over JSI as floats, at the rate the device chose, which need
|
|
@@ -36,17 +38,41 @@
|
|
|
36
38
|
* thrown away (the hook discards the recording on the socket). The audio
|
|
37
39
|
* comes back on its own when the system says it may, else on `resume()`
|
|
38
40
|
* or the next press.
|
|
39
|
-
* - **
|
|
40
|
-
*
|
|
41
|
-
*
|
|
41
|
+
* - **Half-duplex on any build** (`listen`'s `halfDuplex`): the recorder
|
|
42
|
+
* started once for the whole conversation, which suits the library —
|
|
43
|
+
* on iOS, 0.13.6's recorder shares the player's one engine, and each
|
|
44
|
+
* start stops and starts it and each stop rebuilds it, a gap in the
|
|
45
|
+
* voice; the loudspeaker holds (`defaultToSpeaker`), and Android's
|
|
46
|
+
* recorder and player are two streams that open side by side.
|
|
47
|
+
* - **Hands-free on the pinned nightly alone** (D17, D18; `handsFree`):
|
|
48
|
+
* 0.13.6 cancels no echo, so a microphone left open hears the agent
|
|
49
|
+
* (D12, D16) — refused there, `assumeNoEcho` with headphones on. On the
|
|
50
|
+
* nightly a capture that asks for echo cancellation (`listen` does, on
|
|
51
|
+
* `handsFree`'s yes) is iOS's voice processing in `videoChat`, and
|
|
52
|
+
* Android's voice-communication input with the reply on the call's path
|
|
53
|
+
* (`androidOutputProfile`), in its communication mode
|
|
54
|
+
* (`session.configure`): its canceller hears only that path. The reply
|
|
55
|
+
* moves to it — a new context — once nothing is playing, never mid-word,
|
|
56
|
+
* and back once the capture ends. **A recorder that will not start that
|
|
57
|
+
* way fails the capture**, and `listen` answers why: the app offers
|
|
58
|
+
* half-duplex (D16, D19). Whether an Android phone's canceller works is
|
|
59
|
+
* not asked: the platform says it only to native code (left-overs #177).
|
|
60
|
+
* - **`duck`**: the reply goes through a gain, lowered while it may be
|
|
61
|
+
* talked over — barge-in's first step.
|
|
42
62
|
*/
|
|
43
63
|
import { DEFAULT_SAMPLE_RATE, floatToPcm16, pcm16ToFloat, RECORDING_RATES, resample, } from "@lumavox/client";
|
|
44
64
|
import { AudioContext, AudioRecorder, } from "react-native-audio-api";
|
|
45
65
|
import * as session from "./session.js";
|
|
46
66
|
/** Hz the microphone records at unless a rate the socket takes is asked. */
|
|
47
67
|
export const RECORD_RATE = 16000;
|
|
48
|
-
/** Why `handsFree` refuses on
|
|
49
|
-
export const PHONE_ECHO = "Hands-free needs echo cancellation, which comes with react-native-audio-api 1.0. Use push-to-talk, or assumeNoEcho with headphones on.";
|
|
68
|
+
/** Why `handsFree` refuses on 0.13. */
|
|
69
|
+
export const PHONE_ECHO = "Hands-free needs echo cancellation, which comes with react-native-audio-api 1.0. Use half-duplex or push-to-talk, or assumeNoEcho with headphones on.";
|
|
70
|
+
/** Why it refuses on a 1.0 build that is not the one it was tested on. */
|
|
71
|
+
export const untestedBuild = (version) => `Hands-free runs on react-native-audio-api ${session.PINNED_NIGHTLY}, the build it was tested on; this app has ${version}. Use half-duplex or push-to-talk.`;
|
|
72
|
+
/** After the session is made again, how long a focus change is still its own: Android tells it on another thread, a moment later. */
|
|
73
|
+
const SETTLE_MS = 500;
|
|
74
|
+
/** The reply's loudness while the person may be talking over it (`duck`): about -10 dB, as the web's. */
|
|
75
|
+
const DUCKED = 0.3;
|
|
50
76
|
/** Seconds of microphone per buffer delivered: the recorder's own latency. */
|
|
51
77
|
const RECORD_SECONDS = 0.1;
|
|
52
78
|
export class PhoneAudio {
|
|
@@ -56,6 +82,23 @@ export class PhoneAudio {
|
|
|
56
82
|
this.canChooseOutput = false;
|
|
57
83
|
this.context = null;
|
|
58
84
|
this.queue = null;
|
|
85
|
+
/** Between the queue and the speaker: the reply lowered while it may be talked over. */
|
|
86
|
+
this.gain = null;
|
|
87
|
+
this.volume = 1;
|
|
88
|
+
/** The context's output on Android: the call's path while hands-free cancels echo. */
|
|
89
|
+
this.profile = "media";
|
|
90
|
+
/** A capture is cancelling echo: the session is hands-free's. */
|
|
91
|
+
this.cancelling = false;
|
|
92
|
+
this.unroute = null;
|
|
93
|
+
this.disposed = false;
|
|
94
|
+
/** The call's route last asked on Android (`session.route`). */
|
|
95
|
+
this.routed = null;
|
|
96
|
+
/**
|
|
97
|
+
* Until when an interruption is the session made again — Android's focus
|
|
98
|
+
* given up and taken back, told a moment later on its own thread — and
|
|
99
|
+
* not another app's.
|
|
100
|
+
*/
|
|
101
|
+
this.settling = 0;
|
|
59
102
|
/** The ids of the buffers enqueued and not yet played. */
|
|
60
103
|
this.playing = new Set();
|
|
61
104
|
/** Where the last buffer enqueued ends, on the context's clock. */
|
|
@@ -69,9 +112,17 @@ export class PhoneAudio {
|
|
|
69
112
|
this.captured = new Set();
|
|
70
113
|
this.interruptions = new Set();
|
|
71
114
|
this.leadIn = (options.leadInMs ?? 0) / 1000;
|
|
72
|
-
this.latency =
|
|
73
|
-
|
|
115
|
+
this.latency =
|
|
116
|
+
options.outputLatencyMs === undefined
|
|
117
|
+
? null
|
|
118
|
+
: options.outputLatencyMs / 1000;
|
|
119
|
+
this.output = options.output ?? "speaker";
|
|
120
|
+
session.configure(this.output).catch(() => {
|
|
121
|
+
// swallowed: the library's defaults stand until `prime()` or a press sets the session again.
|
|
122
|
+
});
|
|
74
123
|
this.unwatch = session.watch(({ began, shouldResume }) => {
|
|
124
|
+
if (Date.now() < this.settling)
|
|
125
|
+
return;
|
|
75
126
|
if (began)
|
|
76
127
|
this.interrupt();
|
|
77
128
|
else if (shouldResume && this.interrupted) {
|
|
@@ -138,7 +189,10 @@ export class PhoneAudio {
|
|
|
138
189
|
return this.context?.currentTime ?? 0;
|
|
139
190
|
}
|
|
140
191
|
heardTime() {
|
|
141
|
-
|
|
192
|
+
const context = this.context;
|
|
193
|
+
const latency = this.latency ??
|
|
194
|
+
(context?.outputLatency ?? 0) + (context?.baseLatency ?? 0);
|
|
195
|
+
return Math.max(0, this.now() - latency);
|
|
142
196
|
}
|
|
143
197
|
onScheduled(fn) {
|
|
144
198
|
return add(this.scheduled, fn);
|
|
@@ -161,7 +215,10 @@ export class PhoneAudio {
|
|
|
161
215
|
async setOutputDevice() {
|
|
162
216
|
return false;
|
|
163
217
|
}
|
|
164
|
-
/**
|
|
218
|
+
/**
|
|
219
|
+
* At 16 kHz, or at the rate asked when the socket takes it; cancelling
|
|
220
|
+
* echo when hands-free asks and the library can.
|
|
221
|
+
*/
|
|
165
222
|
async startCapture(onPcm, options) {
|
|
166
223
|
// A press is the person going on: the audio is taken back first.
|
|
167
224
|
if (this.interrupted)
|
|
@@ -176,7 +233,22 @@ export class PhoneAudio {
|
|
|
176
233
|
const rate = asked && RECORDING_RATES.includes(asked)
|
|
177
234
|
? asked
|
|
178
235
|
: RECORD_RATE;
|
|
179
|
-
const
|
|
236
|
+
const cancel = options?.echoCancellation === true && session.cancelsEcho();
|
|
237
|
+
try {
|
|
238
|
+
if (cancel)
|
|
239
|
+
await this.enterHandsFree();
|
|
240
|
+
return await this.record(onPcm, rate, cancel);
|
|
241
|
+
}
|
|
242
|
+
catch (failure) {
|
|
243
|
+
await this.leaveHandsFree();
|
|
244
|
+
throw failure;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/** The recorder, open: its echo cancelled when `cancel`. */
|
|
248
|
+
async record(onPcm, rate, cancel) {
|
|
249
|
+
const recorder = new AudioRecorder(cancel
|
|
250
|
+
? { iosVoiceProcessing: true, androidInputPreset: "voiceCommunication" }
|
|
251
|
+
: undefined);
|
|
180
252
|
const mine = { recorder, open: true };
|
|
181
253
|
const ready = recorder.onAudioReady({
|
|
182
254
|
sampleRate: rate,
|
|
@@ -211,8 +283,10 @@ export class PhoneAudio {
|
|
|
211
283
|
async stopCapture() {
|
|
212
284
|
const mine = this.capture;
|
|
213
285
|
this.capture = null;
|
|
214
|
-
if (!mine)
|
|
286
|
+
if (!mine) {
|
|
287
|
+
await this.leaveHandsFree();
|
|
215
288
|
return;
|
|
289
|
+
}
|
|
216
290
|
try {
|
|
217
291
|
// What the recorder hands over until it has stopped is the end of
|
|
218
292
|
// the last word: delivered before this settles.
|
|
@@ -221,6 +295,8 @@ export class PhoneAudio {
|
|
|
221
295
|
finally {
|
|
222
296
|
mine.open = false;
|
|
223
297
|
mine.recorder.clearOnAudioReady();
|
|
298
|
+
// Once the recorder has let go: Android's session is made again.
|
|
299
|
+
await this.leaveHandsFree();
|
|
224
300
|
}
|
|
225
301
|
}
|
|
226
302
|
async resume() {
|
|
@@ -232,43 +308,126 @@ export class PhoneAudio {
|
|
|
232
308
|
this.tell(this.interruptions, false);
|
|
233
309
|
}
|
|
234
310
|
setOutput(output) {
|
|
235
|
-
|
|
311
|
+
this.output = output;
|
|
312
|
+
session.configure(output, this.cancelling).catch(() => {
|
|
313
|
+
// swallowed: the session stays as it was — the route the system chose.
|
|
314
|
+
});
|
|
315
|
+
if (this.cancelling)
|
|
316
|
+
this.reroute();
|
|
317
|
+
}
|
|
318
|
+
/** Hands-free's first step on sound over the reply: lowered, until the words say who it was. */
|
|
319
|
+
duck(on) {
|
|
320
|
+
this.volume = on ? DUCKED : 1;
|
|
321
|
+
if (this.gain)
|
|
322
|
+
this.gain.gain.value = this.volume;
|
|
236
323
|
}
|
|
237
324
|
async handsFree() {
|
|
238
|
-
|
|
325
|
+
if (session.cancelsEcho())
|
|
326
|
+
return { ok: true };
|
|
327
|
+
const version = session.libraryVersion();
|
|
328
|
+
return {
|
|
329
|
+
ok: false,
|
|
330
|
+
reason: version.startsWith("0.") ? PHONE_ECHO : untestedBuild(version),
|
|
331
|
+
};
|
|
239
332
|
}
|
|
240
333
|
onInterruption(fn) {
|
|
241
334
|
return add(this.interruptions, fn);
|
|
242
335
|
}
|
|
243
336
|
dispose() {
|
|
337
|
+
this.disposed = true;
|
|
244
338
|
this.stop();
|
|
339
|
+
// Hands-free's session undone with the recorder.
|
|
245
340
|
void this.stopCapture();
|
|
246
341
|
this.unwatch();
|
|
247
342
|
void this.context?.close();
|
|
248
343
|
this.context = null;
|
|
249
344
|
this.queue = null;
|
|
345
|
+
this.gain = null;
|
|
250
346
|
}
|
|
251
347
|
/** Behind what is queued; the playhead moves to its end. */
|
|
252
348
|
enqueue(buffer) {
|
|
253
349
|
this.playing.add(this.queue.enqueueBuffer(buffer));
|
|
254
350
|
this.playhead += buffer.duration;
|
|
255
351
|
}
|
|
352
|
+
/**
|
|
353
|
+
* The session hands-free's, Android's reply on the call's path and the
|
|
354
|
+
* call's route: before the recorder starts, so the canceller hears the
|
|
355
|
+
* first reply too.
|
|
356
|
+
*/
|
|
357
|
+
async enterHandsFree() {
|
|
358
|
+
this.cancelling = true;
|
|
359
|
+
this.routed = null;
|
|
360
|
+
await this.reconfigure(true);
|
|
361
|
+
this.unroute ?? (this.unroute = session.watchRoutes(() => this.reroute()));
|
|
362
|
+
this.reroute();
|
|
363
|
+
if (this.context && this.playing.size === 0) {
|
|
364
|
+
this.player(this.context.sampleRate);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
/** Back to push-to-talk's session; the reply leaves the call's path once it has played out. */
|
|
368
|
+
async leaveHandsFree() {
|
|
369
|
+
if (!this.cancelling)
|
|
370
|
+
return;
|
|
371
|
+
this.cancelling = false;
|
|
372
|
+
this.unroute?.();
|
|
373
|
+
this.unroute = null;
|
|
374
|
+
await this.reconfigure(false);
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* The session for hands-free or not — disposed, the mode given back and
|
|
378
|
+
* nothing taken again for a screen gone — its focus's comings and goings
|
|
379
|
+
* no interruption.
|
|
380
|
+
*/
|
|
381
|
+
async reconfigure(handsFree) {
|
|
382
|
+
this.settling = Number.POSITIVE_INFINITY;
|
|
383
|
+
try {
|
|
384
|
+
await session.configure(this.output, handsFree, !this.disposed);
|
|
385
|
+
}
|
|
386
|
+
finally {
|
|
387
|
+
this.settling = Date.now() + SETTLE_MS;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
reroute() {
|
|
391
|
+
session
|
|
392
|
+
.route(this.output, this.routed)
|
|
393
|
+
.then((device) => {
|
|
394
|
+
this.routed = device;
|
|
395
|
+
})
|
|
396
|
+
.catch(() => {
|
|
397
|
+
// swallowed: the system's route stands — the loudspeaker, or the earpiece in a call.
|
|
398
|
+
});
|
|
399
|
+
}
|
|
256
400
|
/** The queue for a reply at `rate`: this one, or a new context's. */
|
|
257
401
|
player(rate) {
|
|
258
|
-
|
|
402
|
+
const profile = this.cancelling && session.android() ? "voiceCommunication" : "media";
|
|
403
|
+
if (this.queue &&
|
|
404
|
+
this.context?.sampleRate === rate &&
|
|
405
|
+
// Onto the call's path or off it, never mid-reply.
|
|
406
|
+
(this.profile === profile || this.playing.size > 0)) {
|
|
259
407
|
return this.queue;
|
|
408
|
+
}
|
|
260
409
|
this.stop();
|
|
261
410
|
void this.context?.close();
|
|
262
|
-
const context = new AudioContext(
|
|
411
|
+
const context = new AudioContext(profile === "media"
|
|
412
|
+
? { sampleRate: rate }
|
|
413
|
+
: { sampleRate: rate, androidOutputProfile: profile });
|
|
414
|
+
this.profile = profile;
|
|
263
415
|
const queue = context.createBufferQueueSource();
|
|
264
|
-
|
|
265
|
-
|
|
416
|
+
const gain = context.createGain();
|
|
417
|
+
gain.gain.value = this.volume;
|
|
418
|
+
queue.connect(gain);
|
|
419
|
+
gain.connect(context.destination);
|
|
420
|
+
const ended = ({ bufferId }) => {
|
|
266
421
|
if (this.queue !== queue)
|
|
267
422
|
return;
|
|
268
423
|
if (this.playing.delete(bufferId) && this.playing.size === 0) {
|
|
269
424
|
this.tell(this.drained);
|
|
270
425
|
}
|
|
271
426
|
};
|
|
427
|
+
// 0.13's name, and 1.0's: each takes the other's as a plain property,
|
|
428
|
+
// set for nothing — and with the wrong one alone no reply ever ends.
|
|
429
|
+
queue.onBufferEnded = ended;
|
|
430
|
+
queue.onbufferended = ended;
|
|
272
431
|
// Never `start()`: 0.13.6's wrapper defaults `offset` to -1 and then
|
|
273
432
|
// throws on its own default (`offset must be a finite non-negative
|
|
274
433
|
// number: -1`), which crashed the first reply on Android's emulator.
|
|
@@ -279,6 +438,7 @@ export class PhoneAudio {
|
|
|
279
438
|
this.quiet(context);
|
|
280
439
|
this.context = context;
|
|
281
440
|
this.queue = queue;
|
|
441
|
+
this.gain = gain;
|
|
282
442
|
return queue;
|
|
283
443
|
}
|
|
284
444
|
/**
|
package/dist/device-audio.d.ts
CHANGED
|
@@ -26,9 +26,9 @@ export interface AudioOptions {
|
|
|
26
26
|
leadInMs?: number;
|
|
27
27
|
/**
|
|
28
28
|
* Milliseconds from the phone's player to the ear, which `heardTime()`
|
|
29
|
-
* takes off the clock: `react-native-audio-api`
|
|
30
|
-
* `0` unless set — Bluetooth earbuds add about 150
|
|
31
|
-
* its own.
|
|
29
|
+
* takes off the clock — and half-duplex waits out: `react-native-audio-api`
|
|
30
|
+
* 0.13 cannot say it, so `0` unless set — Bluetooth earbuds add about 150
|
|
31
|
+
* to 250. A browser says its own.
|
|
32
32
|
*/
|
|
33
33
|
outputLatencyMs?: number;
|
|
34
34
|
/** The microphone, by its `inputs()` id, in a browser; a phone's system chooses. */
|
|
@@ -57,7 +57,9 @@ export interface DeviceAudio extends AgentAudio, PlaybackClock, DeviceChoices {
|
|
|
57
57
|
/**
|
|
58
58
|
* Whether hands-free may run (`useAgent`'s `listen`): on the web where
|
|
59
59
|
* the browser cancels echo; on a phone not until `react-native-audio-api`
|
|
60
|
-
* 1.0 does — with headphones on, an app passes `assumeNoEcho`.
|
|
60
|
+
* 1.0 does — with headphones on, an app passes `assumeNoEcho`. Half-duplex
|
|
61
|
+
* (`listen({ halfDuplex: true })`) needs no echo cancelled, and runs
|
|
62
|
+
* everywhere.
|
|
61
63
|
*/
|
|
62
64
|
handsFree(): Promise<{
|
|
63
65
|
ok: true;
|
|
@@ -65,6 +67,8 @@ export interface DeviceAudio extends AgentAudio, PlaybackClock, DeviceChoices {
|
|
|
65
67
|
ok: false;
|
|
66
68
|
reason: string;
|
|
67
69
|
}>;
|
|
70
|
+
/** Hands-free's first step: the reply quieter while it may be talked over, or as before. */
|
|
71
|
+
duck(on: boolean): void;
|
|
68
72
|
}
|
|
69
73
|
/** `createAudio`'s signature, which each platform's file implements. */
|
|
70
74
|
export type CreateAudio = (options?: AudioOptions) => DeviceAudio;
|
package/dist/session.d.ts
CHANGED
|
@@ -1,6 +1,44 @@
|
|
|
1
|
+
import { type CommunicationDevice } from "react-native-audio-api";
|
|
1
2
|
import type { Output } from "./device-audio.js";
|
|
2
|
-
/**
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* **The one `react-native-audio-api` build hands-free runs on before 1.0**
|
|
5
|
+
* (D17, D18): 1.0 is the first with echo cancellation — iOS's voice
|
|
6
|
+
* processing, Android's voice-communication audio — and is not released
|
|
7
|
+
* (no date, 2026-11-02), so one nightly, read in its source and tested,
|
|
8
|
+
* never "the latest": nightlies change under an app, and its iOS voice
|
|
9
|
+
* processing was weeks old. This one is commit 6fd4c68, built unchanged on
|
|
10
|
+
* 2026-10-31, 11-01 and 11-02 — the build npm's `audio-api-nightly` tag
|
|
11
|
+
* named on 2026-11-02. **Moved only for a fix we need, after a test on a
|
|
12
|
+
* real iPhone and Android; 1.0 replaces it.** Every other build, 0.13
|
|
13
|
+
* included, refuses hands-free and runs push-to-talk and half-duplex.
|
|
14
|
+
* `package.json`'s peer range names it too (`test/package.test.ts`).
|
|
15
|
+
*/
|
|
16
|
+
export declare const PINNED_NIGHTLY = "1.0.0-nightly-6fd4c68-20261102";
|
|
17
|
+
/** The library's version, as installed. */
|
|
18
|
+
export declare const libraryVersion: () => string;
|
|
19
|
+
/** Whether this is the build hands-free runs on: echo can be cancelled. */
|
|
20
|
+
export declare const cancelsEcho: () => boolean;
|
|
21
|
+
/** Android, where the reply must be on the call's path for its echo to be cancelled. */
|
|
22
|
+
export declare const android: () => boolean;
|
|
23
|
+
/**
|
|
24
|
+
* The audio session's category, mode and options for `output`, for
|
|
25
|
+
* hands-free or not. Settles at once but for Android's session on 1.0,
|
|
26
|
+
* which takes a mode only as it is made active, so is made again.
|
|
27
|
+
*/
|
|
28
|
+
export declare function configure(output: Output, handsFree?: boolean,
|
|
29
|
+
/** `false` when the adapter is gone: the mode given back, the session left inactive. */
|
|
30
|
+
active?: boolean): Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* Hands-free on Android: the call's voice to Bluetooth earbuds when they
|
|
33
|
+
* are connected — and their microphone with it (D8) — else to `output`.
|
|
34
|
+
* Asked again when a device comes or goes, never twice the same: the
|
|
35
|
+
* library's own Bluetooth link sets the device too, and its change is told
|
|
36
|
+
* as a route's. `asked`, the last asked; answers what is now. Nothing on
|
|
37
|
+
* iOS, whose route follows `allowBluetoothHFP` by itself, nor on 0.13.
|
|
38
|
+
*/
|
|
39
|
+
export declare function route(output: Output, asked: CommunicationDevice | null): Promise<CommunicationDevice | null>;
|
|
40
|
+
/** Every change of the devices — earbuds connected, taken out — until the answer is called. */
|
|
41
|
+
export declare function watchRoutes(fn: () => void): () => void;
|
|
4
42
|
/** Make the session active: before the first sound, and after an interruption. */
|
|
5
43
|
export declare function activate(): Promise<void>;
|
|
6
44
|
/** Whether the app may record: asked only when not granted yet. */
|
package/dist/session.js
CHANGED
|
@@ -1,24 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The phone's audio session: how the app's audio sits beside the system's
|
|
3
3
|
* — the speaker, the earbuds, calls and other apps. With `audio.native.ts`
|
|
4
|
-
* the only file that touches `react-native-audio-api`,
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* the only file that touches `react-native-audio-api`, and **the one that
|
|
5
|
+
* knows which of the library it has**: 0.13, or the one 1.0 nightly
|
|
6
|
+
* hands-free runs on (`PINNED_NIGHTLY`), whose session API is renamed
|
|
7
|
+
* (`setSystemOptions`) — told apart by what the library has, at run time,
|
|
8
|
+
* so one build of this package runs on both.
|
|
7
9
|
*
|
|
8
10
|
* - **Play and record at once, from the loudspeaker** (`playAndRecord`,
|
|
9
11
|
* `defaultToSpeaker`): without it iOS plays a recording app's audio from
|
|
10
12
|
* the earpiece. Headphones and Bluetooth take over when connected; the
|
|
11
13
|
* earpiece is the app's choice (`Output`). Android keeps the loudspeaker
|
|
12
|
-
*
|
|
13
|
-
* earpiece.
|
|
14
|
+
* but in hands-free: the library sets no communication mode otherwise,
|
|
15
|
+
* and so chooses no earpiece.
|
|
14
16
|
* - **The earbuds' microphone on iPhones** (`allowBluetoothHFP`): their
|
|
15
17
|
* hands-free profile, at call quality, for the whole conversation — not
|
|
16
|
-
* switched per press, since every switch moves the route and
|
|
17
|
-
* engine can die on a Bluetooth route change (its #1209
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
18
|
+
* switched per press, since every switch moves the route and the
|
|
19
|
+
* library's engine can die on a Bluetooth route change (its #1209).
|
|
20
|
+
* - **`default` mode, not `voiceChat`**, for push-to-talk and half-duplex:
|
|
21
|
+
* neither needs echo cancelled — the reply stops while the person speaks,
|
|
22
|
+
* or nothing is heard while it plays — and `voiceChat` plays at the call
|
|
23
|
+
* volume, quieter on the loudspeaker.
|
|
24
|
+
* - **Hands-free, on the pinned nightly** (`handsFree`): iOS's
|
|
25
|
+
* `videoChat` — set before the recorder's voice processing, which would
|
|
26
|
+
* otherwise move the session to `voiceChat` and the earpiece — and
|
|
27
|
+
* Android's communication mode, its voice to the loudspeaker (or the
|
|
28
|
+
* earpiece asked), to Bluetooth earbuds when they are connected: the
|
|
29
|
+
* canceller hears only audio on the call's path, and the earbuds'
|
|
30
|
+
* microphone comes with it (D8).
|
|
22
31
|
* - **Interruptions are watched** (`observeAudioInterruptions`): a call,
|
|
23
32
|
* Siri or another app on iOS, a loss of audio focus on Android — which
|
|
24
33
|
* watching *asks for*, so other apps' audio stops while a conversation is
|
|
@@ -26,17 +35,104 @@
|
|
|
26
35
|
* audio in this release (the plugin turns the library's off), so the
|
|
27
36
|
* conversation pauses with the screen and goes on when it is back.
|
|
28
37
|
*/
|
|
29
|
-
import { AppState } from "react-native";
|
|
38
|
+
import { AppState, Platform } from "react-native";
|
|
30
39
|
import { AudioManager } from "react-native-audio-api";
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
import library from "react-native-audio-api/package.json";
|
|
41
|
+
/**
|
|
42
|
+
* **The one `react-native-audio-api` build hands-free runs on before 1.0**
|
|
43
|
+
* (D17, D18): 1.0 is the first with echo cancellation — iOS's voice
|
|
44
|
+
* processing, Android's voice-communication audio — and is not released
|
|
45
|
+
* (no date, 2026-11-02), so one nightly, read in its source and tested,
|
|
46
|
+
* never "the latest": nightlies change under an app, and its iOS voice
|
|
47
|
+
* processing was weeks old. This one is commit 6fd4c68, built unchanged on
|
|
48
|
+
* 2026-10-31, 11-01 and 11-02 — the build npm's `audio-api-nightly` tag
|
|
49
|
+
* named on 2026-11-02. **Moved only for a fix we need, after a test on a
|
|
50
|
+
* real iPhone and Android; 1.0 replaces it.** Every other build, 0.13
|
|
51
|
+
* included, refuses hands-free and runs push-to-talk and half-duplex.
|
|
52
|
+
* `package.json`'s peer range names it too (`test/package.test.ts`).
|
|
53
|
+
*/
|
|
54
|
+
export const PINNED_NIGHTLY = "1.0.0-nightly-6fd4c68-20261102";
|
|
55
|
+
/** The library's version, as installed. */
|
|
56
|
+
export const libraryVersion = () => library.version;
|
|
57
|
+
/** 1.0's session API: `setSystemOptions` for 0.13's `setAudioSessionOptions`. */
|
|
58
|
+
const isOne = () => typeof AudioManager.setSystemOptions === "function";
|
|
59
|
+
/** Whether this is the build hands-free runs on: echo can be cancelled. */
|
|
60
|
+
export const cancelsEcho = () => isOne() && library.version === PINNED_NIGHTLY;
|
|
61
|
+
/** Android, where the reply must be on the call's path for its echo to be cancelled. */
|
|
62
|
+
export const android = () => Platform.OS === "android";
|
|
63
|
+
/**
|
|
64
|
+
* The audio session's category, mode and options for `output`, for
|
|
65
|
+
* hands-free or not. Settles at once but for Android's session on 1.0,
|
|
66
|
+
* which takes a mode only as it is made active, so is made again.
|
|
67
|
+
*/
|
|
68
|
+
export async function configure(output, handsFree = false,
|
|
69
|
+
/** `false` when the adapter is gone: the mode given back, the session left inactive. */
|
|
70
|
+
active = true) {
|
|
71
|
+
const options = output === "speaker"
|
|
72
|
+
? ["defaultToSpeaker", "allowBluetoothHFP"]
|
|
73
|
+
: ["allowBluetoothHFP"];
|
|
74
|
+
// Voice processing takes a mode that is no call's to `voiceChat` itself,
|
|
75
|
+
// and the library then sets its own again on every activation: a chat
|
|
76
|
+
// mode set first — `videoChat`, the loudspeaker's, or `voiceChat`, the
|
|
77
|
+
// earpiece's. Never without it: the call volume.
|
|
78
|
+
const mode = !handsFree
|
|
79
|
+
? "default"
|
|
80
|
+
: output === "speaker"
|
|
81
|
+
? "videoChat"
|
|
82
|
+
: "voiceChat";
|
|
83
|
+
if (!isOne()) {
|
|
84
|
+
AudioManager.setAudioSessionOptions?.({
|
|
85
|
+
iosCategory: "playAndRecord",
|
|
86
|
+
iosMode: mode,
|
|
87
|
+
iosOptions: options,
|
|
88
|
+
});
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
// Both groups every time: one left out is set to its defaults.
|
|
92
|
+
AudioManager.setSystemOptions?.({
|
|
93
|
+
ios: { category: "playAndRecord", mode, options },
|
|
94
|
+
android: handsFree
|
|
95
|
+
? { mode: "inCommunication", communicationDevice: output }
|
|
96
|
+
: {},
|
|
39
97
|
});
|
|
98
|
+
if (android() && communicating !== handsFree) {
|
|
99
|
+
// Its communication mode is set, and the mode before it given back,
|
|
100
|
+
// only as the session is made active and inactive — which gives up the
|
|
101
|
+
// audio focus and takes it again: told as an interruption, which the
|
|
102
|
+
// adapter takes for none (`audio.native.ts`, `settling`).
|
|
103
|
+
communicating = handsFree;
|
|
104
|
+
await AudioManager.setAudioSessionActivity(false);
|
|
105
|
+
if (active)
|
|
106
|
+
await AudioManager.setAudioSessionActivity(true);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/** Android's communication mode is on: hands-free's session was made. */
|
|
110
|
+
let communicating = false;
|
|
111
|
+
/**
|
|
112
|
+
* Hands-free on Android: the call's voice to Bluetooth earbuds when they
|
|
113
|
+
* are connected — and their microphone with it (D8) — else to `output`.
|
|
114
|
+
* Asked again when a device comes or goes, never twice the same: the
|
|
115
|
+
* library's own Bluetooth link sets the device too, and its change is told
|
|
116
|
+
* as a route's. `asked`, the last asked; answers what is now. Nothing on
|
|
117
|
+
* iOS, whose route follows `allowBluetoothHFP` by itself, nor on 0.13.
|
|
118
|
+
*/
|
|
119
|
+
export async function route(output, asked) {
|
|
120
|
+
if (!android() || !isOne() || !AudioManager.getDevicesInfo)
|
|
121
|
+
return asked;
|
|
122
|
+
const { availableOutputs } = await AudioManager.getDevicesInfo();
|
|
123
|
+
// Earbuds with a microphone, as the library names Android's device
|
|
124
|
+
// types: a speaker on Bluetooth alone (A2DP) has none, and with
|
|
125
|
+
// `systemDefault` the call would go to the earpiece.
|
|
126
|
+
const earbuds = availableOutputs.some((device) => /^Bluetooth (SCO|LE Headset)$/.test(device.category));
|
|
127
|
+
const device = earbuds ? "systemDefault" : output;
|
|
128
|
+
if (device !== asked)
|
|
129
|
+
await AudioManager.setCommunicationDevice?.(device);
|
|
130
|
+
return device;
|
|
131
|
+
}
|
|
132
|
+
/** Every change of the devices — earbuds connected, taken out — until the answer is called. */
|
|
133
|
+
export function watchRoutes(fn) {
|
|
134
|
+
const subscription = AudioManager.addSystemEventListener("routeChange", () => fn());
|
|
135
|
+
return () => subscription.remove();
|
|
40
136
|
}
|
|
41
137
|
/** Make the session active: before the first sound, and after an interruption. */
|
|
42
138
|
export function activate() {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lumavox/react-native",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Voice conversations with Lumavox agents in a React Native app, on iOS, Android and the web: the client, the React hooks, and push-to-talk audio on react-native-audio-api.",
|
|
3
|
+
"version": "0.17.0",
|
|
4
|
+
"description": "Voice conversations with Lumavox agents in a React Native app, on iOS, Android and the web: the client, the React hooks, and push-to-talk, half-duplex and hands-free audio on react-native-audio-api.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"files": [
|
|
7
7
|
"dist",
|
|
@@ -30,13 +30,13 @@
|
|
|
30
30
|
"typecheck": "tsc --noEmit && tsc --noEmit -p test"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@lumavox/client": "0.
|
|
34
|
-
"@lumavox/react": "0.
|
|
33
|
+
"@lumavox/client": "0.17.0",
|
|
34
|
+
"@lumavox/react": "0.17.0"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
37
|
"react": ">=18",
|
|
38
38
|
"react-native": ">=0.76",
|
|
39
|
-
"react-native-audio-api": ">=0.13.6 <0.14"
|
|
39
|
+
"react-native-audio-api": ">=0.13.6 <0.14 || 1.0.0-nightly-6fd4c68-20261102"
|
|
40
40
|
},
|
|
41
41
|
"peerDependenciesMeta": {
|
|
42
42
|
"react-native": {
|