@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.
- package/README.md +75 -6
- package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +129 -49
- package/build/ExpoAudioStream.types.d.ts +8 -3
- package/build/ExpoAudioStream.types.d.ts.map +1 -1
- package/build/ExpoAudioStream.types.js +0 -1
- package/build/ExpoAudioStream.types.js.map +1 -1
- package/build/ExpoAudioStreamModule.js +2 -2
- package/build/ExpoAudioStreamModule.js.map +1 -1
- package/build/ExpoAudioStreamModule.web.d.ts +3 -3
- package/build/ExpoAudioStreamModule.web.d.ts.map +1 -1
- package/build/ExpoAudioStreamModule.web.js +27 -17
- package/build/ExpoAudioStreamModule.web.js.map +1 -1
- package/build/index.d.ts +4 -5
- package/build/index.d.ts.map +1 -1
- package/build/index.js +5 -11
- package/build/index.js.map +1 -1
- package/build/useAudioRecording.d.ts +4 -4
- package/build/useAudioRecording.d.ts.map +1 -1
- package/build/useAudioRecording.js +64 -65
- package/build/useAudioRecording.js.map +1 -1
- package/ios/AudioStreamManager.swift +189 -37
- package/ios/ExpoAudioStreamModule.swift +21 -12
- package/package.json +9 -3
- package/plugin/build/index.d.ts +1 -1
- package/plugin/build/index.js +4 -4
- package/plugin/src/index.ts +14 -8
- package/src/ExpoAudioStream.types.ts +20 -11
- package/src/ExpoAudioStreamModule.ts +2 -2
- package/src/ExpoAudioStreamModule.web.ts +165 -149
- package/src/index.ts +15 -13
- package/src/useAudioRecording.ts +146 -127
- package/yarn-error.log +0 -72
|
@@ -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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
154
|
-
|
|
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
|
-
|
|
158
|
-
|
|
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 {
|
|
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
|
|
6
|
-
import
|
|
7
|
-
import { useAudioRecorder } from
|
|
9
|
+
import { AudioEventPayload } from "./ExpoAudioStream.types";
|
|
10
|
+
import ExpoAudioStreamModule from "./ExpoAudioStreamModule";
|
|
11
|
+
import { useAudioRecorder } from "./useAudioRecording";
|
|
8
12
|
|
|
9
|
-
const emitter = new EventEmitter(
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
25
|
-
|
|
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 };
|
package/src/useAudioRecording.ts
CHANGED
|
@@ -1,141 +1,160 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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({
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
+
}
|