@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@idealyst/audio",
3
- "version": "1.2.107",
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
+ };
@@ -1,3 +1,4 @@
1
1
  export { useAudio } from './useAudio';
2
2
  export { useRecorder } from './useRecorder';
3
3
  export { usePlayer } from './usePlayer';
4
+ export { useBackgroundRecorder } from './useBackgroundRecorder';
@@ -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
+ }
@@ -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
  }