@idealyst/audio 1.2.107 → 1.2.108
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/package.json +1 -1
- package/src/background/BackgroundRecorder.native.ts +324 -0
- package/src/background/BackgroundRecorder.web.ts +127 -0
- package/src/background/index.native.ts +1 -0
- package/src/background/index.ts +1 -0
- package/src/constants.ts +22 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useBackgroundRecorder.ts +194 -0
- package/src/index.native.ts +17 -1
- package/src/index.ts +17 -1
- package/src/index.web.ts +17 -1
- package/src/types.ts +161 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@idealyst/audio",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.108",
|
|
4
4
|
"description": "Unified cross-platform audio for React and React Native - recording, playback, and PCM streaming",
|
|
5
5
|
"documentation": "https://github.com/IdealystIO/idealyst-framework/tree/main/packages/audio#readme",
|
|
6
6
|
"readme": "README.md",
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native Background Recorder
|
|
3
|
+
*
|
|
4
|
+
* Wraps an IRecorder with React Native AppState awareness to manage
|
|
5
|
+
* background recording lifecycle. The underlying react-native-audio-api
|
|
6
|
+
* handles the actual background execution (foreground service on Android,
|
|
7
|
+
* background audio mode on iOS).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { AppState, type AppStateStatus as RNAppStateStatus } from 'react-native';
|
|
11
|
+
import type {
|
|
12
|
+
IBackgroundRecorder,
|
|
13
|
+
IRecorder,
|
|
14
|
+
IAudioContext,
|
|
15
|
+
BackgroundRecorderStatus,
|
|
16
|
+
BackgroundRecorderConfig,
|
|
17
|
+
BackgroundLifecycleCallback,
|
|
18
|
+
BackgroundLifecycleInfo,
|
|
19
|
+
BackgroundStatusCallback,
|
|
20
|
+
RecorderDataCallback,
|
|
21
|
+
RecorderLevelCallback,
|
|
22
|
+
RecorderStatus,
|
|
23
|
+
AudioConfig,
|
|
24
|
+
PermissionStatus,
|
|
25
|
+
AppStateStatus,
|
|
26
|
+
} from '../types';
|
|
27
|
+
import { DEFAULT_BACKGROUND_RECORDER_STATUS, SESSION_PRESETS } from '../constants';
|
|
28
|
+
import { getAudioSessionManager } from '../session/index.native';
|
|
29
|
+
import { createRecorder } from '../recording/index.native';
|
|
30
|
+
|
|
31
|
+
export class NativeBackgroundRecorder implements IBackgroundRecorder {
|
|
32
|
+
readonly recorder: IRecorder;
|
|
33
|
+
|
|
34
|
+
private sessionManager = getAudioSessionManager();
|
|
35
|
+
private config: BackgroundRecorderConfig;
|
|
36
|
+
|
|
37
|
+
private _status: BackgroundRecorderStatus = { ...DEFAULT_BACKGROUND_RECORDER_STATUS };
|
|
38
|
+
private appStateSubscription: { remove: () => void } | null = null;
|
|
39
|
+
private interruptionUnsubscribe: (() => void) | null = null;
|
|
40
|
+
private recorderStateUnsubscribe: (() => void) | null = null;
|
|
41
|
+
private maxDurationTimer: ReturnType<typeof setTimeout> | null = null;
|
|
42
|
+
private backgroundStartTime: number | null = null;
|
|
43
|
+
|
|
44
|
+
private lifecycleCallbacks = new Set<BackgroundLifecycleCallback>();
|
|
45
|
+
private statusCallbacks = new Set<BackgroundStatusCallback>();
|
|
46
|
+
|
|
47
|
+
constructor(audioContext: IAudioContext, config: BackgroundRecorderConfig = {}) {
|
|
48
|
+
this.config = config;
|
|
49
|
+
|
|
50
|
+
// Create the underlying recorder
|
|
51
|
+
this.recorder = createRecorder(audioContext);
|
|
52
|
+
|
|
53
|
+
// Subscribe to inner recorder state changes to keep status in sync
|
|
54
|
+
this.recorderStateUnsubscribe = this.recorder.onStateChange(
|
|
55
|
+
(innerStatus: RecorderStatus) => {
|
|
56
|
+
this.updateStatus({
|
|
57
|
+
...innerStatus,
|
|
58
|
+
// Preserve background-specific fields
|
|
59
|
+
appState: this._status.appState,
|
|
60
|
+
isInBackground: this._status.isInBackground,
|
|
61
|
+
wasInterrupted: this._status.wasInterrupted,
|
|
62
|
+
backgroundSince: this._status.backgroundSince,
|
|
63
|
+
backgroundDuration: this._status.backgroundDuration,
|
|
64
|
+
});
|
|
65
|
+
},
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
// Listen to AppState changes
|
|
69
|
+
this.appStateSubscription = AppState.addEventListener('change', this.handleAppStateChange);
|
|
70
|
+
|
|
71
|
+
// Listen to audio session interruptions (phone calls, Siri, etc.)
|
|
72
|
+
this.interruptionUnsubscribe = this.sessionManager.onInterruption((interruption) => {
|
|
73
|
+
if (interruption.type === 'began') {
|
|
74
|
+
this.updateStatus({ wasInterrupted: true });
|
|
75
|
+
this.fireLifecycle({
|
|
76
|
+
event: 'interrupted',
|
|
77
|
+
timestamp: Date.now(),
|
|
78
|
+
});
|
|
79
|
+
} else if (interruption.type === 'ended') {
|
|
80
|
+
this.fireLifecycle({
|
|
81
|
+
event: 'interruptionEnded',
|
|
82
|
+
timestamp: Date.now(),
|
|
83
|
+
shouldResume: interruption.shouldResume,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
get status(): BackgroundRecorderStatus {
|
|
90
|
+
return { ...this._status };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// -- Proxied recording control --
|
|
94
|
+
|
|
95
|
+
async start(configOverride?: Partial<AudioConfig>): Promise<void> {
|
|
96
|
+
// Configure audio session for background if needed
|
|
97
|
+
if (this.config.autoConfigureSession !== false) {
|
|
98
|
+
const sessionConfig = this.config.session ?? SESSION_PRESETS.backgroundRecord;
|
|
99
|
+
await this.sessionManager.configure(sessionConfig);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Reset background state for new recording session
|
|
103
|
+
this.updateStatus({
|
|
104
|
+
wasInterrupted: false,
|
|
105
|
+
backgroundDuration: 0,
|
|
106
|
+
backgroundSince: null,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
await this.recorder.start(configOverride ?? this.config.audio);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async stop(): Promise<void> {
|
|
113
|
+
this.clearMaxDurationTimer();
|
|
114
|
+
const wasInBackground = this._status.isInBackground;
|
|
115
|
+
|
|
116
|
+
await this.recorder.stop();
|
|
117
|
+
|
|
118
|
+
if (wasInBackground) {
|
|
119
|
+
this.fireLifecycle({
|
|
120
|
+
event: 'stopped',
|
|
121
|
+
timestamp: Date.now(),
|
|
122
|
+
backgroundDuration: this._status.backgroundDuration,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async pause(): Promise<void> {
|
|
128
|
+
await this.recorder.pause();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async resume(): Promise<void> {
|
|
132
|
+
await this.recorder.resume();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// -- Proxied permission --
|
|
136
|
+
|
|
137
|
+
async checkPermission(): Promise<PermissionStatus> {
|
|
138
|
+
return this.recorder.checkPermission();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async requestPermission(): Promise<PermissionStatus> {
|
|
142
|
+
return this.recorder.requestPermission();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// -- Proxied data streaming --
|
|
146
|
+
|
|
147
|
+
onData(callback: RecorderDataCallback): () => void {
|
|
148
|
+
return this.recorder.onData(callback);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
onLevel(callback: RecorderLevelCallback, intervalMs?: number): () => void {
|
|
152
|
+
return this.recorder.onLevel(callback, intervalMs);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// -- Background-specific --
|
|
156
|
+
|
|
157
|
+
onLifecycle(callback: BackgroundLifecycleCallback): () => void {
|
|
158
|
+
this.lifecycleCallbacks.add(callback);
|
|
159
|
+
return () => {
|
|
160
|
+
this.lifecycleCallbacks.delete(callback);
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
onStatusChange(callback: BackgroundStatusCallback): () => void {
|
|
165
|
+
this.statusCallbacks.add(callback);
|
|
166
|
+
return () => {
|
|
167
|
+
this.statusCallbacks.delete(callback);
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
resetPeakLevel(): void {
|
|
172
|
+
this.recorder.resetPeakLevel();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
dispose(): void {
|
|
176
|
+
this.clearMaxDurationTimer();
|
|
177
|
+
|
|
178
|
+
if (this.appStateSubscription) {
|
|
179
|
+
this.appStateSubscription.remove();
|
|
180
|
+
this.appStateSubscription = null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (this.interruptionUnsubscribe) {
|
|
184
|
+
this.interruptionUnsubscribe();
|
|
185
|
+
this.interruptionUnsubscribe = null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (this.recorderStateUnsubscribe) {
|
|
189
|
+
this.recorderStateUnsubscribe();
|
|
190
|
+
this.recorderStateUnsubscribe = null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
this.lifecycleCallbacks.clear();
|
|
194
|
+
this.statusCallbacks.clear();
|
|
195
|
+
this.recorder.dispose();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// -- Private --
|
|
199
|
+
|
|
200
|
+
private handleAppStateChange = (nextAppState: RNAppStateStatus): void => {
|
|
201
|
+
const mappedState = this.mapAppState(nextAppState);
|
|
202
|
+
const previousState = this._status.appState;
|
|
203
|
+
const isRecording = this._status.isRecording;
|
|
204
|
+
|
|
205
|
+
this.updateStatus({ appState: mappedState });
|
|
206
|
+
|
|
207
|
+
// Transition: foreground -> background
|
|
208
|
+
if (
|
|
209
|
+
previousState === 'active' &&
|
|
210
|
+
(mappedState === 'background' || mappedState === 'inactive')
|
|
211
|
+
) {
|
|
212
|
+
this.backgroundStartTime = Date.now();
|
|
213
|
+
this.updateStatus({
|
|
214
|
+
isInBackground: true,
|
|
215
|
+
backgroundSince: this.backgroundStartTime,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
if (isRecording) {
|
|
219
|
+
this.fireLifecycle({
|
|
220
|
+
event: 'backgrounded',
|
|
221
|
+
timestamp: Date.now(),
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
this.startMaxDurationTimer();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Transition: background -> foreground
|
|
229
|
+
if (
|
|
230
|
+
(previousState === 'background' || previousState === 'inactive') &&
|
|
231
|
+
mappedState === 'active'
|
|
232
|
+
) {
|
|
233
|
+
const bgDuration = this.backgroundStartTime
|
|
234
|
+
? Date.now() - this.backgroundStartTime
|
|
235
|
+
: 0;
|
|
236
|
+
|
|
237
|
+
this.clearMaxDurationTimer();
|
|
238
|
+
|
|
239
|
+
this.updateStatus({
|
|
240
|
+
isInBackground: false,
|
|
241
|
+
backgroundSince: null,
|
|
242
|
+
backgroundDuration: this._status.backgroundDuration + bgDuration,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
this.backgroundStartTime = null;
|
|
246
|
+
|
|
247
|
+
if (isRecording) {
|
|
248
|
+
this.fireLifecycle({
|
|
249
|
+
event: 'foregrounded',
|
|
250
|
+
timestamp: Date.now(),
|
|
251
|
+
backgroundDuration: bgDuration,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
private mapAppState(rnState: RNAppStateStatus): AppStateStatus {
|
|
258
|
+
switch (rnState) {
|
|
259
|
+
case 'active':
|
|
260
|
+
return 'active';
|
|
261
|
+
case 'background':
|
|
262
|
+
return 'background';
|
|
263
|
+
case 'inactive':
|
|
264
|
+
return 'inactive';
|
|
265
|
+
case 'unknown':
|
|
266
|
+
return 'unknown';
|
|
267
|
+
case 'extension':
|
|
268
|
+
return 'extension';
|
|
269
|
+
default:
|
|
270
|
+
return 'unknown';
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private startMaxDurationTimer(): void {
|
|
275
|
+
this.clearMaxDurationTimer();
|
|
276
|
+
|
|
277
|
+
const maxDuration = this.config.maxBackgroundDuration;
|
|
278
|
+
if (!maxDuration || maxDuration <= 0) return;
|
|
279
|
+
|
|
280
|
+
this.maxDurationTimer = setTimeout(() => {
|
|
281
|
+
this.fireLifecycle({
|
|
282
|
+
event: 'maxDurationReached',
|
|
283
|
+
timestamp: Date.now(),
|
|
284
|
+
backgroundDuration: maxDuration,
|
|
285
|
+
});
|
|
286
|
+
this.stop().catch(console.error);
|
|
287
|
+
}, maxDuration);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private clearMaxDurationTimer(): void {
|
|
291
|
+
if (this.maxDurationTimer) {
|
|
292
|
+
clearTimeout(this.maxDurationTimer);
|
|
293
|
+
this.maxDurationTimer = null;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
private fireLifecycle(info: BackgroundLifecycleInfo): void {
|
|
298
|
+
this.lifecycleCallbacks.forEach((cb) => {
|
|
299
|
+
try {
|
|
300
|
+
cb(info);
|
|
301
|
+
} catch (e) {
|
|
302
|
+
console.error('[BackgroundRecorder] Error in lifecycle callback:', e);
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
private updateStatus(partial: Partial<BackgroundRecorderStatus>): void {
|
|
308
|
+
this._status = { ...this._status, ...partial };
|
|
309
|
+
this.statusCallbacks.forEach((cb) => {
|
|
310
|
+
try {
|
|
311
|
+
cb(this._status);
|
|
312
|
+
} catch (e) {
|
|
313
|
+
console.error('[BackgroundRecorder] Error in status callback:', e);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function createBackgroundRecorder(
|
|
320
|
+
audioContext: IAudioContext,
|
|
321
|
+
config?: BackgroundRecorderConfig,
|
|
322
|
+
): IBackgroundRecorder {
|
|
323
|
+
return new NativeBackgroundRecorder(audioContext, config);
|
|
324
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web Background Recorder (graceful degradation)
|
|
3
|
+
*
|
|
4
|
+
* On web, there is no "background" concept for apps.
|
|
5
|
+
* This wraps the web recorder and provides the same interface,
|
|
6
|
+
* but background lifecycle events never fire.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
IBackgroundRecorder,
|
|
11
|
+
IAudioContext,
|
|
12
|
+
BackgroundRecorderStatus,
|
|
13
|
+
BackgroundRecorderConfig,
|
|
14
|
+
BackgroundLifecycleCallback,
|
|
15
|
+
BackgroundStatusCallback,
|
|
16
|
+
RecorderDataCallback,
|
|
17
|
+
RecorderLevelCallback,
|
|
18
|
+
RecorderStatus,
|
|
19
|
+
AudioConfig,
|
|
20
|
+
PermissionStatus,
|
|
21
|
+
} from '../types';
|
|
22
|
+
import { DEFAULT_BACKGROUND_RECORDER_STATUS } from '../constants';
|
|
23
|
+
import { createRecorder } from '../recording';
|
|
24
|
+
|
|
25
|
+
export class WebBackgroundRecorder implements IBackgroundRecorder {
|
|
26
|
+
readonly recorder;
|
|
27
|
+
|
|
28
|
+
private _status: BackgroundRecorderStatus = { ...DEFAULT_BACKGROUND_RECORDER_STATUS };
|
|
29
|
+
private statusCallbacks = new Set<BackgroundStatusCallback>();
|
|
30
|
+
private recorderStateUnsubscribe: (() => void) | null = null;
|
|
31
|
+
|
|
32
|
+
constructor(audioContext: IAudioContext, _config?: BackgroundRecorderConfig) {
|
|
33
|
+
this.recorder = createRecorder(audioContext);
|
|
34
|
+
|
|
35
|
+
this.recorderStateUnsubscribe = this.recorder.onStateChange(
|
|
36
|
+
(innerStatus: RecorderStatus) => {
|
|
37
|
+
this.updateStatus({
|
|
38
|
+
...innerStatus,
|
|
39
|
+
appState: 'active',
|
|
40
|
+
isInBackground: false,
|
|
41
|
+
wasInterrupted: false,
|
|
42
|
+
backgroundSince: null,
|
|
43
|
+
backgroundDuration: 0,
|
|
44
|
+
});
|
|
45
|
+
},
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
get status(): BackgroundRecorderStatus {
|
|
50
|
+
return { ...this._status };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async start(config?: Partial<AudioConfig>): Promise<void> {
|
|
54
|
+
await this.recorder.start(config);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async stop(): Promise<void> {
|
|
58
|
+
await this.recorder.stop();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async pause(): Promise<void> {
|
|
62
|
+
await this.recorder.pause();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async resume(): Promise<void> {
|
|
66
|
+
await this.recorder.resume();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async checkPermission(): Promise<PermissionStatus> {
|
|
70
|
+
return this.recorder.checkPermission();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async requestPermission(): Promise<PermissionStatus> {
|
|
74
|
+
return this.recorder.requestPermission();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
onData(callback: RecorderDataCallback): () => void {
|
|
78
|
+
return this.recorder.onData(callback);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
onLevel(callback: RecorderLevelCallback, intervalMs?: number): () => void {
|
|
82
|
+
return this.recorder.onLevel(callback, intervalMs);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
onLifecycle(_callback: BackgroundLifecycleCallback): () => void {
|
|
86
|
+
// No-op on web — background events never fire
|
|
87
|
+
return () => {};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
onStatusChange(callback: BackgroundStatusCallback): () => void {
|
|
91
|
+
this.statusCallbacks.add(callback);
|
|
92
|
+
return () => {
|
|
93
|
+
this.statusCallbacks.delete(callback);
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
resetPeakLevel(): void {
|
|
98
|
+
this.recorder.resetPeakLevel();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
dispose(): void {
|
|
102
|
+
if (this.recorderStateUnsubscribe) {
|
|
103
|
+
this.recorderStateUnsubscribe();
|
|
104
|
+
this.recorderStateUnsubscribe = null;
|
|
105
|
+
}
|
|
106
|
+
this.statusCallbacks.clear();
|
|
107
|
+
this.recorder.dispose();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private updateStatus(partial: Partial<BackgroundRecorderStatus>): void {
|
|
111
|
+
this._status = { ...this._status, ...partial };
|
|
112
|
+
this.statusCallbacks.forEach((cb) => {
|
|
113
|
+
try {
|
|
114
|
+
cb(this._status);
|
|
115
|
+
} catch (e) {
|
|
116
|
+
console.error('[BackgroundRecorder] Error in status callback:', e);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function createBackgroundRecorder(
|
|
123
|
+
audioContext: IAudioContext,
|
|
124
|
+
config?: BackgroundRecorderConfig,
|
|
125
|
+
): IBackgroundRecorder {
|
|
126
|
+
return new WebBackgroundRecorder(audioContext, config);
|
|
127
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { NativeBackgroundRecorder, createBackgroundRecorder } from './BackgroundRecorder.native';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { WebBackgroundRecorder, createBackgroundRecorder } from './BackgroundRecorder.web';
|
package/src/constants.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
RecorderStatus,
|
|
5
5
|
PlayerStatus,
|
|
6
6
|
AudioSessionState,
|
|
7
|
+
BackgroundRecorderStatus,
|
|
7
8
|
AudioProfiles,
|
|
8
9
|
SessionPresets,
|
|
9
10
|
} from './types';
|
|
@@ -105,6 +106,14 @@ export const SESSION_PRESETS: SessionPresets = {
|
|
|
105
106
|
categoryOptions: ['defaultToSpeaker', 'allowBluetooth', 'allowBluetoothA2DP', 'mixWithOthers'],
|
|
106
107
|
active: true,
|
|
107
108
|
},
|
|
109
|
+
|
|
110
|
+
/** For background audio recording (STT, voice memos, transcription) */
|
|
111
|
+
backgroundRecord: {
|
|
112
|
+
category: 'playAndRecord',
|
|
113
|
+
mode: 'spokenAudio',
|
|
114
|
+
categoryOptions: ['defaultToSpeaker', 'allowBluetooth', 'allowBluetoothA2DP', 'mixWithOthers'],
|
|
115
|
+
active: true,
|
|
116
|
+
},
|
|
108
117
|
};
|
|
109
118
|
|
|
110
119
|
// ============================================
|
|
@@ -159,3 +168,16 @@ export const BIT_DEPTH_MAX_VALUES = {
|
|
|
159
168
|
16: 32768,
|
|
160
169
|
32: 1.0,
|
|
161
170
|
} as const;
|
|
171
|
+
|
|
172
|
+
// ============================================
|
|
173
|
+
// BACKGROUND RECORDER DEFAULTS
|
|
174
|
+
// ============================================
|
|
175
|
+
|
|
176
|
+
export const DEFAULT_BACKGROUND_RECORDER_STATUS: BackgroundRecorderStatus = {
|
|
177
|
+
...DEFAULT_RECORDER_STATUS,
|
|
178
|
+
appState: 'active',
|
|
179
|
+
isInBackground: false,
|
|
180
|
+
wasInterrupted: false,
|
|
181
|
+
backgroundSince: null,
|
|
182
|
+
backgroundDuration: 0,
|
|
183
|
+
};
|
package/src/hooks/index.ts
CHANGED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useBackgroundRecorder Hook
|
|
3
|
+
*
|
|
4
|
+
* Provides recording functionality with PCM data streaming and
|
|
5
|
+
* background lifecycle awareness. On native, detects app state
|
|
6
|
+
* transitions and fires lifecycle callbacks. On web, works
|
|
7
|
+
* identically to useRecorder (background events never fire).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
11
|
+
import type {
|
|
12
|
+
UseBackgroundRecorderOptions,
|
|
13
|
+
UseBackgroundRecorderResult,
|
|
14
|
+
BackgroundRecorderStatus,
|
|
15
|
+
PermissionStatus,
|
|
16
|
+
AudioConfig,
|
|
17
|
+
RecorderDataCallback,
|
|
18
|
+
BackgroundLifecycleCallback,
|
|
19
|
+
IBackgroundRecorder,
|
|
20
|
+
} from '../types';
|
|
21
|
+
import {
|
|
22
|
+
DEFAULT_BACKGROUND_RECORDER_STATUS,
|
|
23
|
+
DEFAULT_LEVEL_UPDATE_INTERVAL,
|
|
24
|
+
} from '../constants';
|
|
25
|
+
import { getAudioContext } from '../context';
|
|
26
|
+
import { createBackgroundRecorder } from '../background';
|
|
27
|
+
|
|
28
|
+
export function useBackgroundRecorder(
|
|
29
|
+
options: UseBackgroundRecorderOptions = {},
|
|
30
|
+
): UseBackgroundRecorderResult {
|
|
31
|
+
const {
|
|
32
|
+
config,
|
|
33
|
+
session,
|
|
34
|
+
autoRequestPermission = false,
|
|
35
|
+
levelUpdateInterval = DEFAULT_LEVEL_UPDATE_INTERVAL,
|
|
36
|
+
maxBackgroundDuration,
|
|
37
|
+
autoConfigureSession = true,
|
|
38
|
+
onLifecycleEvent,
|
|
39
|
+
} = options;
|
|
40
|
+
|
|
41
|
+
const [status, setStatus] = useState<BackgroundRecorderStatus>(
|
|
42
|
+
DEFAULT_BACKGROUND_RECORDER_STATUS,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const audioContextRef = useRef(getAudioContext());
|
|
46
|
+
const bgRecorderRef = useRef<IBackgroundRecorder | null>(null);
|
|
47
|
+
const mountedRef = useRef(true);
|
|
48
|
+
const lifecycleCallbackRef = useRef<BackgroundLifecycleCallback | undefined>(onLifecycleEvent);
|
|
49
|
+
|
|
50
|
+
// Keep lifecycle callback ref up to date
|
|
51
|
+
lifecycleCallbackRef.current = onLifecycleEvent;
|
|
52
|
+
|
|
53
|
+
// Initialize background recorder lazily
|
|
54
|
+
const getBgRecorder = useCallback(() => {
|
|
55
|
+
if (!bgRecorderRef.current) {
|
|
56
|
+
bgRecorderRef.current = createBackgroundRecorder(audioContextRef.current, {
|
|
57
|
+
audio: config,
|
|
58
|
+
session,
|
|
59
|
+
maxBackgroundDuration,
|
|
60
|
+
autoConfigureSession,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return bgRecorderRef.current;
|
|
64
|
+
}, [config, session, maxBackgroundDuration, autoConfigureSession]);
|
|
65
|
+
|
|
66
|
+
// Check permission
|
|
67
|
+
const checkPermission = useCallback(async (): Promise<PermissionStatus> => {
|
|
68
|
+
return getBgRecorder().checkPermission();
|
|
69
|
+
}, [getBgRecorder]);
|
|
70
|
+
|
|
71
|
+
// Request permission
|
|
72
|
+
const requestPermission = useCallback(async (): Promise<PermissionStatus> => {
|
|
73
|
+
return getBgRecorder().requestPermission();
|
|
74
|
+
}, [getBgRecorder]);
|
|
75
|
+
|
|
76
|
+
// Start recording
|
|
77
|
+
const start = useCallback(async (startConfig?: Partial<AudioConfig>) => {
|
|
78
|
+
const bgRecorder = getBgRecorder();
|
|
79
|
+
const audioContext = audioContextRef.current;
|
|
80
|
+
|
|
81
|
+
// Ensure audio context is initialized
|
|
82
|
+
if (!audioContext.isInitialized) {
|
|
83
|
+
await audioContext.initialize();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
await bgRecorder.start(startConfig ?? config);
|
|
87
|
+
}, [getBgRecorder, config]);
|
|
88
|
+
|
|
89
|
+
// Stop recording
|
|
90
|
+
const stop = useCallback(async () => {
|
|
91
|
+
await getBgRecorder().stop();
|
|
92
|
+
}, [getBgRecorder]);
|
|
93
|
+
|
|
94
|
+
// Pause recording
|
|
95
|
+
const pause = useCallback(async () => {
|
|
96
|
+
await getBgRecorder().pause();
|
|
97
|
+
}, [getBgRecorder]);
|
|
98
|
+
|
|
99
|
+
// Resume recording
|
|
100
|
+
const resume = useCallback(async () => {
|
|
101
|
+
await getBgRecorder().resume();
|
|
102
|
+
}, [getBgRecorder]);
|
|
103
|
+
|
|
104
|
+
// Subscribe to data
|
|
105
|
+
const subscribeToData = useCallback((callback: RecorderDataCallback): (() => void) => {
|
|
106
|
+
return getBgRecorder().onData(callback);
|
|
107
|
+
}, [getBgRecorder]);
|
|
108
|
+
|
|
109
|
+
// Reset peak level
|
|
110
|
+
const resetPeakLevel = useCallback(() => {
|
|
111
|
+
if (bgRecorderRef.current) {
|
|
112
|
+
bgRecorderRef.current.resetPeakLevel();
|
|
113
|
+
}
|
|
114
|
+
}, []);
|
|
115
|
+
|
|
116
|
+
// Setup on mount
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
mountedRef.current = true;
|
|
119
|
+
|
|
120
|
+
const bgRecorder = getBgRecorder();
|
|
121
|
+
|
|
122
|
+
// Subscribe to background status changes
|
|
123
|
+
const unsubStatus = bgRecorder.onStatusChange((newStatus) => {
|
|
124
|
+
if (mountedRef.current) {
|
|
125
|
+
setStatus(newStatus);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Subscribe to level updates
|
|
130
|
+
const unsubLevel = bgRecorder.onLevel((level) => {
|
|
131
|
+
if (mountedRef.current) {
|
|
132
|
+
setStatus((prev) => ({ ...prev, level }));
|
|
133
|
+
}
|
|
134
|
+
}, levelUpdateInterval);
|
|
135
|
+
|
|
136
|
+
// Subscribe to lifecycle events and forward to callback
|
|
137
|
+
const unsubLifecycle = bgRecorder.onLifecycle((info) => {
|
|
138
|
+
if (mountedRef.current && lifecycleCallbackRef.current) {
|
|
139
|
+
lifecycleCallbackRef.current(info);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Auto request permission if enabled
|
|
144
|
+
if (autoRequestPermission) {
|
|
145
|
+
requestPermission().catch(console.error);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return () => {
|
|
149
|
+
mountedRef.current = false;
|
|
150
|
+
unsubStatus();
|
|
151
|
+
unsubLevel();
|
|
152
|
+
unsubLifecycle();
|
|
153
|
+
|
|
154
|
+
// Dispose background recorder on unmount
|
|
155
|
+
if (bgRecorderRef.current) {
|
|
156
|
+
bgRecorderRef.current.dispose();
|
|
157
|
+
bgRecorderRef.current = null;
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
}, [getBgRecorder, autoRequestPermission, requestPermission, levelUpdateInterval]);
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
// Standard recorder state
|
|
164
|
+
status,
|
|
165
|
+
isRecording: status.isRecording,
|
|
166
|
+
isPaused: status.isPaused,
|
|
167
|
+
permission: status.permission,
|
|
168
|
+
duration: status.duration,
|
|
169
|
+
level: status.level,
|
|
170
|
+
error: status.error ?? null,
|
|
171
|
+
|
|
172
|
+
// Background-specific state
|
|
173
|
+
isInBackground: status.isInBackground,
|
|
174
|
+
wasInterrupted: status.wasInterrupted,
|
|
175
|
+
backgroundDuration: status.backgroundDuration,
|
|
176
|
+
appState: status.appState,
|
|
177
|
+
|
|
178
|
+
// Actions
|
|
179
|
+
start,
|
|
180
|
+
stop,
|
|
181
|
+
pause,
|
|
182
|
+
resume,
|
|
183
|
+
|
|
184
|
+
// Permissions
|
|
185
|
+
checkPermission,
|
|
186
|
+
requestPermission,
|
|
187
|
+
|
|
188
|
+
// Data subscription
|
|
189
|
+
subscribeToData,
|
|
190
|
+
|
|
191
|
+
// Utilities
|
|
192
|
+
resetPeakLevel,
|
|
193
|
+
};
|
|
194
|
+
}
|
package/src/index.native.ts
CHANGED
|
@@ -63,6 +63,18 @@ export type {
|
|
|
63
63
|
// Presets
|
|
64
64
|
AudioProfiles,
|
|
65
65
|
SessionPresets,
|
|
66
|
+
|
|
67
|
+
// Background recorder
|
|
68
|
+
AppStateStatus,
|
|
69
|
+
BackgroundRecorderStatus,
|
|
70
|
+
BackgroundLifecycleEvent,
|
|
71
|
+
BackgroundLifecycleInfo,
|
|
72
|
+
BackgroundLifecycleCallback,
|
|
73
|
+
BackgroundStatusCallback,
|
|
74
|
+
BackgroundRecorderConfig,
|
|
75
|
+
IBackgroundRecorder,
|
|
76
|
+
UseBackgroundRecorderOptions,
|
|
77
|
+
UseBackgroundRecorderResult,
|
|
66
78
|
} from './types';
|
|
67
79
|
|
|
68
80
|
// Constants
|
|
@@ -81,6 +93,7 @@ export {
|
|
|
81
93
|
DEFAULT_SESSION_STATE,
|
|
82
94
|
DEFAULT_LEVEL_UPDATE_INTERVAL,
|
|
83
95
|
DEFAULT_POSITION_UPDATE_INTERVAL,
|
|
96
|
+
DEFAULT_BACKGROUND_RECORDER_STATUS,
|
|
84
97
|
} from './constants';
|
|
85
98
|
|
|
86
99
|
// Context (native)
|
|
@@ -95,8 +108,11 @@ export { createRecorder, NativeRecorder } from './recording/index.native';
|
|
|
95
108
|
// Playback (native)
|
|
96
109
|
export { createPlayer, NativePlayer } from './playback/index.native';
|
|
97
110
|
|
|
111
|
+
// Background Recording (native)
|
|
112
|
+
export { createBackgroundRecorder, NativeBackgroundRecorder } from './background/index.native';
|
|
113
|
+
|
|
98
114
|
// Hooks
|
|
99
|
-
export { useAudio, useRecorder, usePlayer } from './hooks';
|
|
115
|
+
export { useAudio, useRecorder, usePlayer, useBackgroundRecorder } from './hooks';
|
|
100
116
|
|
|
101
117
|
// Utilities
|
|
102
118
|
export {
|
package/src/index.ts
CHANGED
|
@@ -63,6 +63,18 @@ export type {
|
|
|
63
63
|
// Presets
|
|
64
64
|
AudioProfiles,
|
|
65
65
|
SessionPresets,
|
|
66
|
+
|
|
67
|
+
// Background recorder
|
|
68
|
+
AppStateStatus,
|
|
69
|
+
BackgroundRecorderStatus,
|
|
70
|
+
BackgroundLifecycleEvent,
|
|
71
|
+
BackgroundLifecycleInfo,
|
|
72
|
+
BackgroundLifecycleCallback,
|
|
73
|
+
BackgroundStatusCallback,
|
|
74
|
+
BackgroundRecorderConfig,
|
|
75
|
+
IBackgroundRecorder,
|
|
76
|
+
UseBackgroundRecorderOptions,
|
|
77
|
+
UseBackgroundRecorderResult,
|
|
66
78
|
} from './types';
|
|
67
79
|
|
|
68
80
|
// Constants
|
|
@@ -81,6 +93,7 @@ export {
|
|
|
81
93
|
DEFAULT_SESSION_STATE,
|
|
82
94
|
DEFAULT_LEVEL_UPDATE_INTERVAL,
|
|
83
95
|
DEFAULT_POSITION_UPDATE_INTERVAL,
|
|
96
|
+
DEFAULT_BACKGROUND_RECORDER_STATUS,
|
|
84
97
|
} from './constants';
|
|
85
98
|
|
|
86
99
|
// Context
|
|
@@ -95,8 +108,11 @@ export { createRecorder, WebRecorder } from './recording';
|
|
|
95
108
|
// Playback
|
|
96
109
|
export { createPlayer, WebPlayer } from './playback';
|
|
97
110
|
|
|
111
|
+
// Background Recording (web)
|
|
112
|
+
export { createBackgroundRecorder, WebBackgroundRecorder } from './background';
|
|
113
|
+
|
|
98
114
|
// Hooks
|
|
99
|
-
export { useAudio, useRecorder, usePlayer } from './hooks';
|
|
115
|
+
export { useAudio, useRecorder, usePlayer, useBackgroundRecorder } from './hooks';
|
|
100
116
|
|
|
101
117
|
// Utilities
|
|
102
118
|
export {
|
package/src/index.web.ts
CHANGED
|
@@ -63,6 +63,18 @@ export type {
|
|
|
63
63
|
// Presets
|
|
64
64
|
AudioProfiles,
|
|
65
65
|
SessionPresets,
|
|
66
|
+
|
|
67
|
+
// Background recorder
|
|
68
|
+
AppStateStatus,
|
|
69
|
+
BackgroundRecorderStatus,
|
|
70
|
+
BackgroundLifecycleEvent,
|
|
71
|
+
BackgroundLifecycleInfo,
|
|
72
|
+
BackgroundLifecycleCallback,
|
|
73
|
+
BackgroundStatusCallback,
|
|
74
|
+
BackgroundRecorderConfig,
|
|
75
|
+
IBackgroundRecorder,
|
|
76
|
+
UseBackgroundRecorderOptions,
|
|
77
|
+
UseBackgroundRecorderResult,
|
|
66
78
|
} from './types';
|
|
67
79
|
|
|
68
80
|
// Constants
|
|
@@ -81,6 +93,7 @@ export {
|
|
|
81
93
|
DEFAULT_SESSION_STATE,
|
|
82
94
|
DEFAULT_LEVEL_UPDATE_INTERVAL,
|
|
83
95
|
DEFAULT_POSITION_UPDATE_INTERVAL,
|
|
96
|
+
DEFAULT_BACKGROUND_RECORDER_STATUS,
|
|
84
97
|
} from './constants';
|
|
85
98
|
|
|
86
99
|
// Context
|
|
@@ -95,8 +108,11 @@ export { createRecorder, WebRecorder } from './recording';
|
|
|
95
108
|
// Playback
|
|
96
109
|
export { createPlayer, WebPlayer } from './playback';
|
|
97
110
|
|
|
111
|
+
// Background Recording (web)
|
|
112
|
+
export { createBackgroundRecorder, WebBackgroundRecorder } from './background';
|
|
113
|
+
|
|
98
114
|
// Hooks
|
|
99
|
-
export { useAudio, useRecorder, usePlayer } from './hooks';
|
|
115
|
+
export { useAudio, useRecorder, usePlayer, useBackgroundRecorder } from './hooks';
|
|
100
116
|
|
|
101
117
|
// Utilities
|
|
102
118
|
export {
|
package/src/types.ts
CHANGED
|
@@ -324,6 +324,9 @@ export type AudioErrorCode =
|
|
|
324
324
|
| 'BUFFER_UNDERRUN'
|
|
325
325
|
// Recording errors
|
|
326
326
|
| 'RECORDING_ERROR'
|
|
327
|
+
// Background errors
|
|
328
|
+
| 'BACKGROUND_NOT_SUPPORTED'
|
|
329
|
+
| 'BACKGROUND_MAX_DURATION'
|
|
327
330
|
// General errors
|
|
328
331
|
| 'INITIALIZATION_FAILED'
|
|
329
332
|
| 'INVALID_STATE'
|
|
@@ -469,4 +472,162 @@ export interface SessionPresets {
|
|
|
469
472
|
voiceChat: AudioSessionConfig;
|
|
470
473
|
ambient: AudioSessionConfig;
|
|
471
474
|
default: AudioSessionConfig;
|
|
475
|
+
backgroundRecord: AudioSessionConfig;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// ============================================
|
|
479
|
+
// BACKGROUND RECORDER TYPES
|
|
480
|
+
// ============================================
|
|
481
|
+
|
|
482
|
+
export type AppStateStatus = 'active' | 'background' | 'inactive' | 'unknown' | 'extension';
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Extended recorder status with background lifecycle state.
|
|
486
|
+
*/
|
|
487
|
+
export interface BackgroundRecorderStatus extends RecorderStatus {
|
|
488
|
+
/** Current app state */
|
|
489
|
+
appState: AppStateStatus;
|
|
490
|
+
|
|
491
|
+
/** Whether the recorder is currently operating in background */
|
|
492
|
+
isInBackground: boolean;
|
|
493
|
+
|
|
494
|
+
/** Whether the recording was interrupted by the OS (phone call, Siri, etc.) */
|
|
495
|
+
wasInterrupted: boolean;
|
|
496
|
+
|
|
497
|
+
/** Timestamp when the app last entered background, or null */
|
|
498
|
+
backgroundSince: number | null;
|
|
499
|
+
|
|
500
|
+
/** Total time spent recording in background (ms) */
|
|
501
|
+
backgroundDuration: number;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/** Lifecycle event types fired by the background recorder */
|
|
505
|
+
export type BackgroundLifecycleEvent =
|
|
506
|
+
| 'backgrounded'
|
|
507
|
+
| 'foregrounded'
|
|
508
|
+
| 'interrupted'
|
|
509
|
+
| 'interruptionEnded'
|
|
510
|
+
| 'maxDurationReached'
|
|
511
|
+
| 'stopped';
|
|
512
|
+
|
|
513
|
+
export interface BackgroundLifecycleInfo {
|
|
514
|
+
event: BackgroundLifecycleEvent;
|
|
515
|
+
timestamp: number;
|
|
516
|
+
/** Duration spent in background when foregrounded (ms) */
|
|
517
|
+
backgroundDuration?: number;
|
|
518
|
+
/** Whether the OS suggested resuming after interruption */
|
|
519
|
+
shouldResume?: boolean;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
export type BackgroundLifecycleCallback = (info: BackgroundLifecycleInfo) => void;
|
|
523
|
+
export type BackgroundStatusCallback = (status: BackgroundRecorderStatus) => void;
|
|
524
|
+
|
|
525
|
+
export interface BackgroundRecorderConfig {
|
|
526
|
+
/** Audio configuration for recording */
|
|
527
|
+
audio?: Partial<AudioConfig>;
|
|
528
|
+
|
|
529
|
+
/** Audio session configuration for background recording.
|
|
530
|
+
* Defaults to SESSION_PRESETS.backgroundRecord */
|
|
531
|
+
session?: Partial<AudioSessionConfig>;
|
|
532
|
+
|
|
533
|
+
/** Maximum duration to record in background (ms). undefined = no limit */
|
|
534
|
+
maxBackgroundDuration?: number;
|
|
535
|
+
|
|
536
|
+
/** Whether to automatically configure the audio session for background.
|
|
537
|
+
* Default: true */
|
|
538
|
+
autoConfigureSession?: boolean;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Background-aware recorder interface.
|
|
543
|
+
* Composes an IRecorder with AppState lifecycle management.
|
|
544
|
+
*/
|
|
545
|
+
export interface IBackgroundRecorder {
|
|
546
|
+
/** The wrapped recorder instance */
|
|
547
|
+
readonly recorder: IRecorder;
|
|
548
|
+
|
|
549
|
+
/** Background-aware status */
|
|
550
|
+
readonly status: BackgroundRecorderStatus;
|
|
551
|
+
|
|
552
|
+
// Recording control (proxied to inner recorder)
|
|
553
|
+
start(config?: Partial<AudioConfig>): Promise<void>;
|
|
554
|
+
stop(): Promise<void>;
|
|
555
|
+
pause(): Promise<void>;
|
|
556
|
+
resume(): Promise<void>;
|
|
557
|
+
|
|
558
|
+
// Permission (proxied)
|
|
559
|
+
checkPermission(): Promise<PermissionStatus>;
|
|
560
|
+
requestPermission(): Promise<PermissionStatus>;
|
|
561
|
+
|
|
562
|
+
// Data streaming (proxied)
|
|
563
|
+
onData(callback: RecorderDataCallback): () => void;
|
|
564
|
+
onLevel(callback: RecorderLevelCallback, intervalMs?: number): () => void;
|
|
565
|
+
|
|
566
|
+
// Background-specific
|
|
567
|
+
onLifecycle(callback: BackgroundLifecycleCallback): () => void;
|
|
568
|
+
onStatusChange(callback: BackgroundStatusCallback): () => void;
|
|
569
|
+
|
|
570
|
+
// Cleanup
|
|
571
|
+
resetPeakLevel(): void;
|
|
572
|
+
dispose(): void;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// ============================================
|
|
576
|
+
// BACKGROUND RECORDER HOOK TYPES
|
|
577
|
+
// ============================================
|
|
578
|
+
|
|
579
|
+
export interface UseBackgroundRecorderOptions {
|
|
580
|
+
/** Audio configuration */
|
|
581
|
+
config?: Partial<AudioConfig>;
|
|
582
|
+
|
|
583
|
+
/** Audio session configuration for background recording */
|
|
584
|
+
session?: Partial<AudioSessionConfig>;
|
|
585
|
+
|
|
586
|
+
/** Auto request permission on mount */
|
|
587
|
+
autoRequestPermission?: boolean;
|
|
588
|
+
|
|
589
|
+
/** Level update interval in ms. Default: 100 */
|
|
590
|
+
levelUpdateInterval?: number;
|
|
591
|
+
|
|
592
|
+
/** Maximum background recording duration (ms). undefined = no limit */
|
|
593
|
+
maxBackgroundDuration?: number;
|
|
594
|
+
|
|
595
|
+
/** Whether to auto-configure audio session. Default: true */
|
|
596
|
+
autoConfigureSession?: boolean;
|
|
597
|
+
|
|
598
|
+
/** Called on background lifecycle events */
|
|
599
|
+
onLifecycleEvent?: BackgroundLifecycleCallback;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
export interface UseBackgroundRecorderResult {
|
|
603
|
+
// Standard recorder state
|
|
604
|
+
status: BackgroundRecorderStatus;
|
|
605
|
+
isRecording: boolean;
|
|
606
|
+
isPaused: boolean;
|
|
607
|
+
permission: PermissionStatus;
|
|
608
|
+
duration: number;
|
|
609
|
+
level: AudioLevel;
|
|
610
|
+
error: AudioError | null;
|
|
611
|
+
|
|
612
|
+
// Background-specific state
|
|
613
|
+
isInBackground: boolean;
|
|
614
|
+
wasInterrupted: boolean;
|
|
615
|
+
backgroundDuration: number;
|
|
616
|
+
appState: AppStateStatus;
|
|
617
|
+
|
|
618
|
+
// Actions
|
|
619
|
+
start: (config?: Partial<AudioConfig>) => Promise<void>;
|
|
620
|
+
stop: () => Promise<void>;
|
|
621
|
+
pause: () => Promise<void>;
|
|
622
|
+
resume: () => Promise<void>;
|
|
623
|
+
|
|
624
|
+
// Permissions
|
|
625
|
+
checkPermission: () => Promise<PermissionStatus>;
|
|
626
|
+
requestPermission: () => Promise<PermissionStatus>;
|
|
627
|
+
|
|
628
|
+
// Data subscription
|
|
629
|
+
subscribeToData: (callback: RecorderDataCallback) => () => void;
|
|
630
|
+
|
|
631
|
+
// Utilities
|
|
632
|
+
resetPeakLevel: () => void;
|
|
472
633
|
}
|