@siteed/expo-audio-stream 0.1.0 → 0.2.1

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.
@@ -1,162 +1,178 @@
1
+ import debug from "debug";
1
2
  import { EventEmitter } from "expo-modules-core";
2
- import { AudioEventPayload, RecordingOptions } from "./ExpoAudioStream.types";
3
3
 
4
- class ExpoAudioStreamWeb extends EventEmitter {
5
- mediaRecorder: MediaRecorder | null;
6
- audioChunks: Blob[];
7
- isRecording: boolean;
8
- isPaused: boolean;
9
- recordingStartTime: number;
10
- pausedTime: number;
11
- currentDuration: number;
12
- currentSize: number;
13
- currentInterval: number;
14
- lastEmittedSize: number;
15
- streamUuid: string | null;
16
-
17
- constructor() {
18
- const mockNativeModule = {
19
- addListener: (eventName: string) => {
20
- console.log(`Web addListener called for ${eventName}`);
21
- },
22
- removeListeners: (count: number) => {
23
- console.log(`Web removeListeners called, count: ${count}`);
24
- }
25
- };
26
- super(mockNativeModule); // Pass the mock native module to the parent class
27
-
28
-
29
- this.mediaRecorder = null;
30
- this.audioChunks = [];
31
- this.isRecording = false;
32
- this.isPaused = false;
33
- this.recordingStartTime = 0;
34
- this.pausedTime = 0;
35
- this.currentDuration = 0;
36
- this.currentSize = 0;
37
- this.currentInterval = 1000; // Default interval in ms
38
- this.lastEmittedSize = 0;
39
- this.streamUuid = null; // Initialize UUID on first recording start
40
- }
41
-
42
- // Utility to handle user media stream
43
- async getMediaStream() {
44
- try {
45
- return await navigator.mediaDevices.getUserMedia({ audio: true });
46
- } catch (error) {
47
- console.error("Failed to get media stream:", error);
48
- throw error;
49
- }
50
- }
51
-
52
- // Start recording with options
53
- async startRecording(options: RecordingOptions = {}) {
54
- if (this.isRecording) {
55
- throw new Error('Recording is already in progress');
56
- }
57
-
58
- const stream = await this.getMediaStream();
59
- this.mediaRecorder = new MediaRecorder(stream);
60
- this.setupRecordingListeners();
61
- this.mediaRecorder.start(options.interval || this.currentInterval);
62
- this.isRecording = true;
63
- this.recordingStartTime = Date.now();
64
- this.pausedTime = 0;
65
- this.lastEmittedSize = 0;
66
- this.streamUuid = this.generateUUID(); // Generate a UUID for the new recording session
67
- }
68
-
69
- // Setup listeners for the MediaRecorder
70
- setupRecordingListeners() {
71
- if (!this.mediaRecorder) {
72
- throw new Error('No active media recorder');
73
- }
74
- this.mediaRecorder.ondataavailable = (event) => {
75
- this.audioChunks.push(event.data);
76
- this.currentSize += event.data.size; // Update the size of the recording
77
- this.emitAudioEvent(event.data); // Emit the event with the correct payload
78
- this.lastEmittedSize = this.currentSize;
79
- };
80
-
81
- this.mediaRecorder.onstop = () => {
82
- this.isRecording = false;
83
- console.log('Recording stopped', this.audioChunks);
84
- };
85
-
86
- this.mediaRecorder.onpause = () => {
87
- this.isPaused = true;
88
- };
89
-
90
- this.mediaRecorder.onresume = () => {
91
- this.isPaused = false;
92
- this.recordingStartTime += (Date.now() - this.pausedTime); // Adjust start time after resuming
93
- };
94
- }
95
-
96
- emitAudioEvent(data: Blob) {
97
- const fileUri = `${this.streamUuid}.pcm`;
98
- const audioEventPayload: AudioEventPayload = {
99
- fileUri: fileUri,
100
- from: this.lastEmittedSize, // Since this might be continuously streaming, adjust accordingly
101
- deltaSize: data.size,
102
- totalSize: this.currentSize,
103
- buffer: data,
104
- streamUuid: this.streamUuid ?? '', // Generate or manage UUID for stream identification
105
- };
106
-
107
- this.emit('AudioData', audioEventPayload);
108
- }
4
+ import {
5
+ AudioEventPayload,
6
+ AudioStreamResult,
7
+ RecordingOptions,
8
+ } from "./ExpoAudioStream.types";
109
9
 
110
- // Helper method to generate a UUID
111
- generateUUID() {
112
- // Implementation of UUID generation (use a library or custom method)
113
- return 'xxxx-xxxx-xxxx-xxxx'.replace(/[x]/g, (c) => {
114
- const r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
115
- return v.toString(16);
116
- });
117
- }
118
-
119
- // Stop recording
120
- async stopRecording() {
121
- console.debug('Stopping recording', this);
122
- this.mediaRecorder?.stop();
123
- this.isRecording = false;
124
- this.currentDuration = (Date.now() - this.recordingStartTime) / 1000;
125
- return this.currentDuration;
10
+ const log = debug("expo-audio-stream:useAudioRecording");
11
+ class ExpoAudioStreamWeb extends EventEmitter {
12
+ mediaRecorder: MediaRecorder | null;
13
+ audioChunks: Blob[];
14
+ isRecording: boolean;
15
+ isPaused: boolean;
16
+ recordingStartTime: number;
17
+ pausedTime: number;
18
+ currentDuration: number;
19
+ currentSize: number;
20
+ currentInterval: number;
21
+ lastEmittedSize: number;
22
+ streamUuid: string | null;
23
+
24
+ constructor() {
25
+ const mockNativeModule = {
26
+ addListener: (eventName: string) => {
27
+ // Not used on web
28
+ },
29
+ removeListeners: (count: number) => {
30
+ // Not used on web
31
+ },
32
+ };
33
+ super(mockNativeModule); // Pass the mock native module to the parent class
34
+
35
+ this.mediaRecorder = null;
36
+ this.audioChunks = [];
37
+ this.isRecording = false;
38
+ this.isPaused = false;
39
+ this.recordingStartTime = 0;
40
+ this.pausedTime = 0;
41
+ this.currentDuration = 0;
42
+ this.currentSize = 0;
43
+ this.currentInterval = 1000; // Default interval in ms
44
+ this.lastEmittedSize = 0;
45
+ this.streamUuid = null; // Initialize UUID on first recording start
46
+ }
47
+
48
+ // Utility to handle user media stream
49
+ async getMediaStream() {
50
+ try {
51
+ return await navigator.mediaDevices.getUserMedia({ audio: true });
52
+ } catch (error) {
53
+ console.error("Failed to get media stream:", error);
54
+ throw error;
126
55
  }
56
+ }
127
57
 
128
- // Pause recording
129
- async pauseRecording() {
130
- if (!this.mediaRecorder) {
131
- throw new Error('No active media recorder');
132
- }
133
-
134
- if (this.isRecording && !this.isPaused) {
135
- this.mediaRecorder.pause();
136
- this.pausedTime = Date.now();
137
- } else {
138
- throw new Error('Recording is not active or already paused');
139
- }
58
+ // Start recording with options
59
+ async startRecording(options: RecordingOptions = {}) {
60
+ if (this.isRecording) {
61
+ throw new Error("Recording is already in progress");
140
62
  }
141
63
 
142
- // Get current status
143
- status() {
144
- return {
145
- isRecording: this.isRecording,
146
- isPaused: this.isPaused,
147
- duration: Date.now() - this.recordingStartTime,
148
- size: this.currentSize,
149
- interval: this.currentInterval,
150
- };
64
+ const stream = await this.getMediaStream();
65
+ this.mediaRecorder = new MediaRecorder(stream);
66
+ this.setupRecordingListeners();
67
+ this.mediaRecorder.start(options.interval || this.currentInterval);
68
+ this.isRecording = true;
69
+ this.recordingStartTime = Date.now();
70
+ this.pausedTime = 0;
71
+ this.lastEmittedSize = 0;
72
+ this.streamUuid = this.generateUUID(); // Generate a UUID for the new recording session
73
+ const fileUri = `${this.streamUuid}.webm`;
74
+ return fileUri;
75
+ }
76
+
77
+ // Setup listeners for the MediaRecorder
78
+ setupRecordingListeners() {
79
+ if (!this.mediaRecorder) {
80
+ throw new Error("No active media recorder");
151
81
  }
152
-
153
- listAudioFiles() {
154
- // Not applicable on web
82
+ this.mediaRecorder.ondataavailable = (event) => {
83
+ this.audioChunks.push(event.data);
84
+ this.currentSize += event.data.size; // Update the size of the recording
85
+ this.emitAudioEvent(event.data); // Emit the event with the correct payload
86
+ this.lastEmittedSize = this.currentSize;
87
+ };
88
+
89
+ this.mediaRecorder.onstop = () => {
90
+ this.isRecording = false;
91
+ log("Recording stopped", this.audioChunks);
92
+ };
93
+
94
+ this.mediaRecorder.onpause = () => {
95
+ this.isPaused = true;
96
+ };
97
+
98
+ this.mediaRecorder.onresume = () => {
99
+ this.isPaused = false;
100
+ this.recordingStartTime += Date.now() - this.pausedTime; // Adjust start time after resuming
101
+ };
102
+ }
103
+
104
+ emitAudioEvent(data: Blob) {
105
+ const fileUri = `${this.streamUuid}.webm`;
106
+ const audioEventPayload: AudioEventPayload = {
107
+ fileUri,
108
+ mimeType: "audio/webm",
109
+ from: this.lastEmittedSize, // Since this might be continuously streaming, adjust accordingly
110
+ deltaSize: data.size,
111
+ totalSize: this.currentSize,
112
+ buffer: data,
113
+ streamUuid: this.streamUuid ?? "", // Generate or manage UUID for stream identification
114
+ };
115
+
116
+ this.emit("AudioData", audioEventPayload);
117
+ }
118
+
119
+ // Helper method to generate a UUID
120
+ generateUUID() {
121
+ // Implementation of UUID generation (use a library or custom method)
122
+ return "xxxx-xxxx-xxxx-xxxx".replace(/[x]/g, (c) => {
123
+ const r = (Math.random() * 16) | 0,
124
+ v = c === "x" ? r : (r & 0x3) | 0x8;
125
+ return v.toString(16);
126
+ });
127
+ }
128
+
129
+ // Stop recording
130
+ async stopRecording(): Promise<AudioStreamResult | null> {
131
+ this.mediaRecorder?.stop();
132
+ this.isRecording = false;
133
+ this.currentDuration = (Date.now() - this.recordingStartTime) / 1000;
134
+ const result: AudioStreamResult = {
135
+ fileUri: `${this.streamUuid}.webm`,
136
+ duration: this.currentDuration,
137
+ size: this.currentSize,
138
+ mimeType: "audio/webm",
139
+ };
140
+
141
+ return result;
142
+ }
143
+
144
+ // Pause recording
145
+ async pauseRecording() {
146
+ if (!this.mediaRecorder) {
147
+ throw new Error("No active media recorder");
155
148
  }
156
149
 
157
- clearAudioFiles() {
158
- // Not applicable on web
150
+ if (this.isRecording && !this.isPaused) {
151
+ this.mediaRecorder.pause();
152
+ this.pausedTime = Date.now();
153
+ } else {
154
+ throw new Error("Recording is not active or already paused");
159
155
  }
156
+ }
157
+
158
+ // Get current status
159
+ status() {
160
+ return {
161
+ isRecording: this.isRecording,
162
+ isPaused: this.isPaused,
163
+ duration: Date.now() - this.recordingStartTime,
164
+ size: this.currentSize,
165
+ interval: this.currentInterval,
166
+ };
167
+ }
168
+
169
+ listAudioFiles() {
170
+ // Not applicable on web
171
+ }
172
+
173
+ clearAudioFiles() {
174
+ // Not applicable on web
175
+ }
160
176
  }
161
177
 
162
- export default new ExpoAudioStreamWeb();
178
+ export default new ExpoAudioStreamWeb();
package/src/index.ts CHANGED
@@ -1,17 +1,18 @@
1
- import { NativeModulesProxy, EventEmitter, type Subscription } from 'expo-modules-core';
1
+ import {
2
+ EventEmitter,
3
+ NativeModulesProxy,
4
+ type Subscription,
5
+ } from "expo-modules-core";
2
6
 
3
7
  // Import the native module. On web, it will be resolved to ExpoAudioStream.web.ts
4
8
  // and on native platforms to ExpoAudioStream.ts
5
- import ExpoAudioStreamModule from './ExpoAudioStreamModule';
6
- import { AudioEventPayload, RecordingOptions } from './ExpoAudioStream.types';
7
- import { useAudioRecorder } from './useAudioRecording';
9
+ import { AudioEventPayload } from "./ExpoAudioStream.types";
10
+ import ExpoAudioStreamModule from "./ExpoAudioStreamModule";
11
+ import { useAudioRecorder } from "./useAudioRecording";
8
12
 
9
- const emitter = new EventEmitter(ExpoAudioStreamModule ?? NativeModulesProxy.ExpoAudioStream);
10
-
11
- // Function to get the recording duration
12
- export function getRecordingDuration(): Promise<number> {
13
- return ExpoAudioStreamModule.getRecordingDuration();
14
- }
13
+ const emitter = new EventEmitter(
14
+ ExpoAudioStreamModule ?? NativeModulesProxy.ExpoAudioStream,
15
+ );
15
16
 
16
17
  export function listAudioFiles(): Promise<string[]> {
17
18
  return ExpoAudioStreamModule.listAudioFiles();
@@ -21,9 +22,10 @@ export function clearAudioFiles(): Promise<void> {
21
22
  return ExpoAudioStreamModule.clearAudioFiles();
22
23
  }
23
24
 
24
- export function addChangeListener(listener: (event: AudioEventPayload) => void): Subscription {
25
- return emitter.addListener<AudioEventPayload>('AudioData', listener);
25
+ export function addAudioEventListener(
26
+ listener: (event: AudioEventPayload) => void,
27
+ ): Subscription {
28
+ return emitter.addListener<AudioEventPayload>("AudioData", listener);
26
29
  }
27
30
 
28
-
29
31
  export { AudioEventPayload, useAudioRecorder };
@@ -1,141 +1,160 @@
1
- import { NativeModulesProxy, EventEmitter, type Subscription, Platform } from 'expo-modules-core';
2
-
1
+ import { decode as atob } from "base-64";
2
+ import debug from "debug";
3
+ import { Platform } from "expo-modules-core";
3
4
  import { useCallback, useEffect, useState } from "react";
4
- import ExpoAudioStreamModule from './ExpoAudioStreamModule';
5
- import { AudioEventPayload, AudioStreamStatus, RecordingOptions } from "./ExpoAudioStream.types";
6
- import { addChangeListener } from '.';
7
- import * as FileSystem from 'expo-file-system';
8
- import { decode as atob } from 'base-64';
9
5
 
10
- const emitter = new EventEmitter(ExpoAudioStreamModule ?? NativeModulesProxy.ExpoAudioStream);
6
+ import { addAudioEventListener } from ".";
7
+ import {
8
+ AudioStreamResult,
9
+ AudioStreamStatus,
10
+ RecordingOptions,
11
+ } from "./ExpoAudioStream.types";
12
+ import ExpoAudioStreamModule from "./ExpoAudioStreamModule";
13
+
14
+ const log = debug("expo-audio-stream:useAudioRecording");
11
15
 
12
16
  interface UseAudioRecorderState {
13
- startRecording: (_: RecordingOptions) => Promise<void>;
14
- stopRecording: () => Promise<number>;
15
- pauseRecording: () => void;
16
- isRecording: boolean;
17
- isPaused: boolean;
18
- duration: number; // Duration of the recording
19
- size: number; // Size in bytes of the recorded audio
17
+ startRecording: (_: RecordingOptions) => Promise<string | null>;
18
+ stopRecording: () => Promise<AudioStreamResult | null>;
19
+ pauseRecording: () => void;
20
+ isRecording: boolean;
21
+ isPaused: boolean;
22
+ duration: number; // Duration of the recording
23
+ size: number; // Size in bytes of the recorded audio
20
24
  }
21
25
 
22
- export function useAudioRecorder({onAudioStream}: {onAudioStream?: (buffer: Blob) => void}): UseAudioRecorderState {
23
- const [isRecording, setIsRecording] = useState(false);
24
- const [isPaused, setIsPaused] = useState(false);
25
- const [duration, setDuration] = useState(0);
26
- const [size, setSize] = useState(0);
27
-
28
- useEffect( () => {
29
- if(isRecording || isPaused) {
30
- const interval = setInterval(() => {
31
- const status: AudioStreamStatus = ExpoAudioStreamModule.status()
32
- setDuration(status.duration);
33
- setSize(status.size);
34
- }, 1000);
35
- return () => clearInterval(interval);
36
- }
37
-
38
- return () => null;
39
- }, [isRecording, isPaused])
40
-
26
+ export function useAudioRecorder({
27
+ onAudioStream,
28
+ }: {
29
+ onAudioStream?: (buffer: Blob) => void;
30
+ }): UseAudioRecorderState {
31
+ const [isRecording, setIsRecording] = useState(false);
32
+ const [isPaused, setIsPaused] = useState(false);
33
+ const [duration, setDuration] = useState(0);
34
+ const [size, setSize] = useState(0);
41
35
 
42
36
  useEffect(() => {
43
- const subscribe = addChangeListener(async ({fileUri, deltaSize, totalSize, from, streamUuid, encoded, buffer}) => {
44
- console.debug(`Received audio event:`, {fileUri, deltaSize, totalSize, from, streamUuid, encodedLength: encoded?.length})
45
- if(deltaSize > 0) {
46
- // Fetch the audio data from the fileUri
47
- const options = {
48
- encoding: FileSystem.EncodingType.Base64,
49
- position: from,
50
- length: deltaSize,
51
- };
52
-
53
- if(Platform.OS !== 'web') {
54
- // Read the audio file as a base64 string for comparison
55
- try {
56
- const base64Content = await FileSystem.readAsStringAsync(fileUri, options);
57
- const binaryData = atob(base64Content);
58
- const content = new Uint8Array(binaryData.length);
59
- for (let i = 0; i < binaryData.length; i++) {
60
- content[i] = binaryData.charCodeAt(i);
61
- }
62
-
63
- // TODO: get the filetype based on audio setting and encoding
64
- const audioBlob = new Blob([content], { type: 'application/octet-stream' }); // Create a Blob from the byte array
65
- console.debug(`Read audio file (len: ${content.length}) vs ${deltaSize}`)
66
- onAudioStream?.(audioBlob);
67
- } catch (error) {
68
- console.error('Error reading audio file:', error);
69
- }
70
- } else if(buffer) {
71
- onAudioStream?.(buffer);
72
- }
73
- }
74
- });
75
- return () => subscribe.remove();
76
- }, [isRecording, onAudioStream]);
77
-
78
-
79
- const startRecording = useCallback(async (recordingOptions: RecordingOptions) => {
80
- if (!isRecording) {
81
- setIsRecording(true);
82
- setIsPaused(false);
83
- setSize(0);
84
- setDuration(0);
85
- const startTime = Date.now();
37
+ if (isRecording || isPaused) {
38
+ const interval = setInterval(() => {
39
+ const status: AudioStreamStatus = ExpoAudioStreamModule.status();
40
+ setDuration(status.duration);
41
+ setSize(status.size);
42
+ }, 1000);
43
+ return () => clearInterval(interval);
44
+ }
45
+
46
+ return () => null;
47
+ }, [isRecording, isPaused]);
86
48
 
87
- console.log(`module shims`, ExpoAudioStreamModule)
88
- try {
89
- console.log(`start recoding`, recordingOptions)
90
- await ExpoAudioStreamModule.startRecording(recordingOptions);
91
-
92
- } catch (error) {
93
- console.error('Error starting recording:', error);
94
- setIsRecording(false);
95
- }
96
- }
97
- }, [isRecording]);
98
-
99
- const stopRecording = useCallback(async (): Promise<number> => {
100
- if (isRecording) {
101
- setIsRecording(false);
102
- setIsPaused(false);
49
+ useEffect(() => {
50
+ const subscribe = addAudioEventListener(
51
+ async ({
52
+ fileUri,
53
+ deltaSize,
54
+ totalSize,
55
+ from,
56
+ streamUuid,
57
+ encoded,
58
+ mimeType,
59
+ buffer,
60
+ }) => {
61
+ log(`Received audio event:`, {
62
+ fileUri,
63
+ deltaSize,
64
+ totalSize,
65
+ mimeType,
66
+ from,
67
+ streamUuid,
68
+ encodedLength: encoded?.length,
69
+ });
70
+ if (deltaSize > 0) {
71
+ // Coming from native ( ios / android ) otherwise buffer is set
72
+ if (Platform.OS !== "web") {
73
+ // Read the audio file as a base64 string for comparison
103
74
  try {
104
- const recordedDuration = await ExpoAudioStreamModule.stopRecording();
105
- setDuration(recordedDuration);
106
- return recordedDuration;
75
+ // convert encoded string to binary data
76
+ const binaryData = atob(encoded);
77
+ const content = new Uint8Array(binaryData.length);
78
+ for (let i = 0; i < binaryData.length; i++) {
79
+ content[i] = binaryData.charCodeAt(i);
80
+ }
81
+ const audioBlob = new Blob([content], { type: mimeType });
82
+
83
+ // Below code is optional, used to compare encoded data to audio on file system
84
+ // Fetch the audio data from the fileUri
85
+ // const options = {
86
+ // encoding: FileSystem.EncodingType.Base64,
87
+ // position: from,
88
+ // length: deltaSize,
89
+ // };
90
+ // const base64Content = await FileSystem.readAsStringAsync(fileUri, options);
91
+ // const binaryData = atob(base64Content);
92
+ // const content = new Uint8Array(binaryData.length);
93
+ // for (let i = 0; i < binaryData.length; i++) {
94
+ // content[i] = binaryData.charCodeAt(i);
95
+ // }
96
+ // const audioBlob = new Blob([content], { type: 'application/octet-stream' }); // Create a Blob from the byte array
97
+ // console.debug(`Read audio file (len: ${content.length}) vs ${deltaSize}`)
98
+
99
+ onAudioStream?.(audioBlob);
107
100
  } catch (error) {
108
- console.error('Error stopping recording:', error);
109
- return 0;
101
+ console.error("Error reading audio file:", error);
110
102
  }
103
+ } else if (buffer) {
104
+ // Coming from web
105
+ onAudioStream?.(buffer);
106
+ }
111
107
  }
112
- return 0;
113
- }, [isRecording]);
114
-
115
- const pauseRecording = useCallback(() => {
116
- if (isRecording) {
117
- ExpoAudioStreamModule.stopRecording().catch(console.error);
118
- setIsPaused(true);
119
- setIsRecording(false);
120
- }
121
- }, [isRecording]);
122
-
123
- // Cleanup listener on unmount to prevent memory leaks
124
- useEffect(() => {
125
- return () => {
126
- if (isRecording) {
127
- ExpoAudioStreamModule.stopRecording().catch(console.error);
128
- }
129
- };
130
- }, [isRecording]);
108
+ },
109
+ );
110
+ return () => subscribe.remove();
111
+ }, [isRecording, onAudioStream]);
131
112
 
132
- return {
133
- startRecording,
134
- stopRecording,
135
- pauseRecording,
136
- isPaused,
137
- isRecording,
138
- duration,
139
- size
140
- };
141
- }
113
+ const startRecording = useCallback(
114
+ async (recordingOptions: RecordingOptions) => {
115
+ setIsRecording(true);
116
+ setIsPaused(false);
117
+ setSize(0);
118
+ setDuration(0);
119
+ try {
120
+ log(`start recoding`, recordingOptions);
121
+ const fileUrl =
122
+ await ExpoAudioStreamModule.startRecording(recordingOptions);
123
+
124
+ return fileUrl;
125
+ } catch (error) {
126
+ console.error("Error starting recording:", error);
127
+ setIsRecording(false);
128
+ }
129
+ },
130
+ [],
131
+ );
132
+
133
+ const stopRecording = useCallback(async () => {
134
+ setIsRecording(false);
135
+ setIsPaused(false);
136
+ const result: AudioStreamResult =
137
+ await ExpoAudioStreamModule.stopRecording();
138
+ return result;
139
+ }, []);
140
+
141
+ const pauseRecording = useCallback(async () => {
142
+ try {
143
+ await ExpoAudioStreamModule.stopRecording();
144
+ setIsPaused(true);
145
+ setIsRecording(false);
146
+ } catch (error) {
147
+ console.error("Error pausing recording:", error);
148
+ }
149
+ }, []);
150
+
151
+ return {
152
+ startRecording,
153
+ stopRecording,
154
+ pauseRecording,
155
+ isPaused,
156
+ isRecording,
157
+ duration,
158
+ size,
159
+ };
160
+ }