@siteed/expo-audio-stream 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +2 -0
- package/README.md +85 -0
- package/android/.gradle/8.1.1/checksums/checksums.lock +0 -0
- package/android/.gradle/8.1.1/dependencies-accessors/dependencies-accessors.lock +0 -0
- package/android/.gradle/8.1.1/dependencies-accessors/gc.properties +0 -0
- package/android/.gradle/8.1.1/fileChanges/last-build.bin +0 -0
- package/android/.gradle/8.1.1/fileHashes/fileHashes.lock +0 -0
- package/android/.gradle/8.1.1/gc.properties +0 -0
- package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
- package/android/.gradle/buildOutputCleanup/cache.properties +2 -0
- package/android/.gradle/vcs-1/gc.properties +0 -0
- package/android/build.gradle +92 -0
- package/android/src/main/AndroidManifest.xml +4 -0
- package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +318 -0
- package/app.plugin.js +1 -0
- package/build/ExpoAudioStream.types.d.ts +23 -0
- package/build/ExpoAudioStream.types.d.ts.map +1 -0
- package/build/ExpoAudioStream.types.js +3 -0
- package/build/ExpoAudioStream.types.js.map +1 -0
- package/build/ExpoAudioStreamModule.d.ts +3 -0
- package/build/ExpoAudioStreamModule.d.ts.map +1 -0
- package/build/ExpoAudioStreamModule.js +5 -0
- package/build/ExpoAudioStreamModule.js.map +1 -0
- package/build/ExpoAudioStreamModule.web.d.ts +35 -0
- package/build/ExpoAudioStreamModule.web.d.ts.map +1 -0
- package/build/ExpoAudioStreamModule.web.js +143 -0
- package/build/ExpoAudioStreamModule.web.js.map +1 -0
- package/build/index.d.ts +9 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +21 -0
- package/build/index.js.map +1 -0
- package/build/useAudioRecording.d.ts +15 -0
- package/build/useAudioRecording.d.ts.map +1 -0
- package/build/useAudioRecording.js +118 -0
- package/build/useAudioRecording.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/AudioStreamManager.swift +168 -0
- package/ios/ExpoAudioStream.podspec +27 -0
- package/ios/ExpoAudioStreamModule.swift +133 -0
- package/package.json +61 -0
- package/plugin/build/index.d.ts +5 -0
- package/plugin/build/index.js +17 -0
- package/plugin/src/index.ts +27 -0
- package/plugin/tsconfig.json +9 -0
- package/release-it.js +18 -0
- package/src/ExpoAudioStream.types.ts +25 -0
- package/src/ExpoAudioStreamModule.ts +5 -0
- package/src/ExpoAudioStreamModule.web.ts +162 -0
- package/src/index.ts +29 -0
- package/src/useAudioRecording.ts +141 -0
- package/tsconfig.json +9 -0
- package/yarn-error.log +72 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { EventEmitter } from "expo-modules-core";
|
|
2
|
+
class ExpoAudioStreamWeb extends EventEmitter {
|
|
3
|
+
mediaRecorder;
|
|
4
|
+
audioChunks;
|
|
5
|
+
isRecording;
|
|
6
|
+
isPaused;
|
|
7
|
+
recordingStartTime;
|
|
8
|
+
pausedTime;
|
|
9
|
+
currentDuration;
|
|
10
|
+
currentSize;
|
|
11
|
+
currentInterval;
|
|
12
|
+
lastEmittedSize;
|
|
13
|
+
streamUuid;
|
|
14
|
+
constructor() {
|
|
15
|
+
const mockNativeModule = {
|
|
16
|
+
addListener: (eventName) => {
|
|
17
|
+
console.log(`Web addListener called for ${eventName}`);
|
|
18
|
+
},
|
|
19
|
+
removeListeners: (count) => {
|
|
20
|
+
console.log(`Web removeListeners called, count: ${count}`);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
super(mockNativeModule); // Pass the mock native module to the parent class
|
|
24
|
+
this.mediaRecorder = null;
|
|
25
|
+
this.audioChunks = [];
|
|
26
|
+
this.isRecording = false;
|
|
27
|
+
this.isPaused = false;
|
|
28
|
+
this.recordingStartTime = 0;
|
|
29
|
+
this.pausedTime = 0;
|
|
30
|
+
this.currentDuration = 0;
|
|
31
|
+
this.currentSize = 0;
|
|
32
|
+
this.currentInterval = 1000; // Default interval in ms
|
|
33
|
+
this.lastEmittedSize = 0;
|
|
34
|
+
this.streamUuid = null; // Initialize UUID on first recording start
|
|
35
|
+
}
|
|
36
|
+
// Utility to handle user media stream
|
|
37
|
+
async getMediaStream() {
|
|
38
|
+
try {
|
|
39
|
+
return await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
console.error("Failed to get media stream:", error);
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Start recording with options
|
|
47
|
+
async startRecording(options = {}) {
|
|
48
|
+
if (this.isRecording) {
|
|
49
|
+
throw new Error('Recording is already in progress');
|
|
50
|
+
}
|
|
51
|
+
const stream = await this.getMediaStream();
|
|
52
|
+
this.mediaRecorder = new MediaRecorder(stream);
|
|
53
|
+
this.setupRecordingListeners();
|
|
54
|
+
this.mediaRecorder.start(options.interval || this.currentInterval);
|
|
55
|
+
this.isRecording = true;
|
|
56
|
+
this.recordingStartTime = Date.now();
|
|
57
|
+
this.pausedTime = 0;
|
|
58
|
+
this.lastEmittedSize = 0;
|
|
59
|
+
this.streamUuid = this.generateUUID(); // Generate a UUID for the new recording session
|
|
60
|
+
}
|
|
61
|
+
// Setup listeners for the MediaRecorder
|
|
62
|
+
setupRecordingListeners() {
|
|
63
|
+
if (!this.mediaRecorder) {
|
|
64
|
+
throw new Error('No active media recorder');
|
|
65
|
+
}
|
|
66
|
+
this.mediaRecorder.ondataavailable = (event) => {
|
|
67
|
+
this.audioChunks.push(event.data);
|
|
68
|
+
this.currentSize += event.data.size; // Update the size of the recording
|
|
69
|
+
this.emitAudioEvent(event.data); // Emit the event with the correct payload
|
|
70
|
+
this.lastEmittedSize = this.currentSize;
|
|
71
|
+
};
|
|
72
|
+
this.mediaRecorder.onstop = () => {
|
|
73
|
+
this.isRecording = false;
|
|
74
|
+
console.log('Recording stopped', this.audioChunks);
|
|
75
|
+
};
|
|
76
|
+
this.mediaRecorder.onpause = () => {
|
|
77
|
+
this.isPaused = true;
|
|
78
|
+
};
|
|
79
|
+
this.mediaRecorder.onresume = () => {
|
|
80
|
+
this.isPaused = false;
|
|
81
|
+
this.recordingStartTime += (Date.now() - this.pausedTime); // Adjust start time after resuming
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
emitAudioEvent(data) {
|
|
85
|
+
const fileUri = `${this.streamUuid}.pcm`;
|
|
86
|
+
const audioEventPayload = {
|
|
87
|
+
fileUri: fileUri,
|
|
88
|
+
from: this.lastEmittedSize, // Since this might be continuously streaming, adjust accordingly
|
|
89
|
+
deltaSize: data.size,
|
|
90
|
+
totalSize: this.currentSize,
|
|
91
|
+
buffer: data,
|
|
92
|
+
streamUuid: this.streamUuid ?? '', // Generate or manage UUID for stream identification
|
|
93
|
+
};
|
|
94
|
+
this.emit('AudioData', audioEventPayload);
|
|
95
|
+
}
|
|
96
|
+
// Helper method to generate a UUID
|
|
97
|
+
generateUUID() {
|
|
98
|
+
// Implementation of UUID generation (use a library or custom method)
|
|
99
|
+
return 'xxxx-xxxx-xxxx-xxxx'.replace(/[x]/g, (c) => {
|
|
100
|
+
const r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
|
|
101
|
+
return v.toString(16);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
// Stop recording
|
|
105
|
+
async stopRecording() {
|
|
106
|
+
console.debug('Stopping recording', this);
|
|
107
|
+
this.mediaRecorder?.stop();
|
|
108
|
+
this.isRecording = false;
|
|
109
|
+
this.currentDuration = (Date.now() - this.recordingStartTime) / 1000;
|
|
110
|
+
return this.currentDuration;
|
|
111
|
+
}
|
|
112
|
+
// Pause recording
|
|
113
|
+
async pauseRecording() {
|
|
114
|
+
if (!this.mediaRecorder) {
|
|
115
|
+
throw new Error('No active media recorder');
|
|
116
|
+
}
|
|
117
|
+
if (this.isRecording && !this.isPaused) {
|
|
118
|
+
this.mediaRecorder.pause();
|
|
119
|
+
this.pausedTime = Date.now();
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
throw new Error('Recording is not active or already paused');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// Get current status
|
|
126
|
+
status() {
|
|
127
|
+
return {
|
|
128
|
+
isRecording: this.isRecording,
|
|
129
|
+
isPaused: this.isPaused,
|
|
130
|
+
duration: Date.now() - this.recordingStartTime,
|
|
131
|
+
size: this.currentSize,
|
|
132
|
+
interval: this.currentInterval,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
listAudioFiles() {
|
|
136
|
+
// Not applicable on web
|
|
137
|
+
}
|
|
138
|
+
clearAudioFiles() {
|
|
139
|
+
// Not applicable on web
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
export default new ExpoAudioStreamWeb();
|
|
143
|
+
//# sourceMappingURL=ExpoAudioStreamModule.web.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExpoAudioStreamModule.web.js","sourceRoot":"","sources":["../src/ExpoAudioStreamModule.web.ts"],"names":[],"mappings":"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","sourcesContent":["import { EventEmitter } from \"expo-modules-core\";\nimport { AudioEventPayload, RecordingOptions } from \"./ExpoAudioStream.types\";\n\nclass ExpoAudioStreamWeb extends EventEmitter {\n mediaRecorder: MediaRecorder | null;\n audioChunks: Blob[];\n isRecording: boolean;\n isPaused: boolean;\n recordingStartTime: number;\n pausedTime: number;\n currentDuration: number;\n currentSize: number;\n currentInterval: number;\n lastEmittedSize: number;\n streamUuid: string | null;\n\n constructor() {\n const mockNativeModule = {\n addListener: (eventName: string) => {\n console.log(`Web addListener called for ${eventName}`);\n },\n removeListeners: (count: number) => {\n console.log(`Web removeListeners called, count: ${count}`);\n }\n };\n super(mockNativeModule); // Pass the mock native module to the parent class\n\n\n this.mediaRecorder = null;\n this.audioChunks = [];\n this.isRecording = false;\n this.isPaused = false;\n this.recordingStartTime = 0;\n this.pausedTime = 0;\n this.currentDuration = 0;\n this.currentSize = 0;\n this.currentInterval = 1000; // Default interval in ms\n this.lastEmittedSize = 0;\n this.streamUuid = null; // Initialize UUID on first recording start\n }\n\n // Utility to handle user media stream\n async getMediaStream() {\n try {\n return await navigator.mediaDevices.getUserMedia({ audio: true });\n } catch (error) {\n console.error(\"Failed to get media stream:\", error);\n throw error;\n }\n }\n\n // Start recording with options\n async startRecording(options: RecordingOptions = {}) {\n if (this.isRecording) {\n throw new Error('Recording is already in progress');\n }\n\n const stream = await this.getMediaStream();\n this.mediaRecorder = new MediaRecorder(stream);\n this.setupRecordingListeners();\n this.mediaRecorder.start(options.interval || this.currentInterval);\n this.isRecording = true;\n this.recordingStartTime = Date.now();\n this.pausedTime = 0;\n this.lastEmittedSize = 0;\n this.streamUuid = this.generateUUID(); // Generate a UUID for the new recording session\n }\n\n // Setup listeners for the MediaRecorder\n setupRecordingListeners() {\n if (!this.mediaRecorder) {\n throw new Error('No active media recorder');\n }\n this.mediaRecorder.ondataavailable = (event) => {\n this.audioChunks.push(event.data);\n this.currentSize += event.data.size; // Update the size of the recording\n this.emitAudioEvent(event.data); // Emit the event with the correct payload\n this.lastEmittedSize = this.currentSize;\n };\n\n this.mediaRecorder.onstop = () => {\n this.isRecording = false;\n console.log('Recording stopped', this.audioChunks);\n };\n\n this.mediaRecorder.onpause = () => {\n this.isPaused = true;\n };\n\n this.mediaRecorder.onresume = () => {\n this.isPaused = false;\n this.recordingStartTime += (Date.now() - this.pausedTime); // Adjust start time after resuming\n };\n }\n\n emitAudioEvent(data: Blob) {\n const fileUri = `${this.streamUuid}.pcm`;\n const audioEventPayload: AudioEventPayload = {\n fileUri: fileUri,\n from: this.lastEmittedSize, // Since this might be continuously streaming, adjust accordingly\n deltaSize: data.size,\n totalSize: this.currentSize,\n buffer: data,\n streamUuid: this.streamUuid ?? '', // Generate or manage UUID for stream identification\n };\n\n this.emit('AudioData', audioEventPayload);\n }\n\n // Helper method to generate a UUID\n generateUUID() {\n // Implementation of UUID generation (use a library or custom method)\n return 'xxxx-xxxx-xxxx-xxxx'.replace(/[x]/g, (c) => {\n const r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);\n return v.toString(16);\n });\n }\n\n // Stop recording\n async stopRecording() {\n console.debug('Stopping recording', this);\n this.mediaRecorder?.stop();\n this.isRecording = false;\n this.currentDuration = (Date.now() - this.recordingStartTime) / 1000;\n return this.currentDuration;\n }\n\n // Pause recording\n async pauseRecording() {\n if (!this.mediaRecorder) {\n throw new Error('No active media recorder');\n }\n\n if (this.isRecording && !this.isPaused) {\n this.mediaRecorder.pause();\n this.pausedTime = Date.now();\n } else {\n throw new Error('Recording is not active or already paused');\n }\n }\n\n // Get current status\n status() {\n return {\n isRecording: this.isRecording,\n isPaused: this.isPaused,\n duration: Date.now() - this.recordingStartTime,\n size: this.currentSize,\n interval: this.currentInterval,\n };\n }\n\n listAudioFiles() {\n // Not applicable on web\n }\n\n clearAudioFiles() {\n // Not applicable on web\n }\n}\n\nexport default new ExpoAudioStreamWeb();"]}
|
package/build/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type Subscription } from 'expo-modules-core';
|
|
2
|
+
import { AudioEventPayload } from './ExpoAudioStream.types';
|
|
3
|
+
import { useAudioRecorder } from './useAudioRecording';
|
|
4
|
+
export declare function getRecordingDuration(): Promise<number>;
|
|
5
|
+
export declare function listAudioFiles(): Promise<string[]>;
|
|
6
|
+
export declare function clearAudioFiles(): Promise<void>;
|
|
7
|
+
export declare function addChangeListener(listener: (event: AudioEventPayload) => void): Subscription;
|
|
8
|
+
export { AudioEventPayload, useAudioRecorder };
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoC,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAKxF,OAAO,EAAE,iBAAiB,EAAoB,MAAM,yBAAyB,CAAC;AAC9E,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAKvD,wBAAgB,oBAAoB,IAAI,OAAO,CAAC,MAAM,CAAC,CAEtD;AAED,wBAAgB,cAAc,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAElD;AAED,wBAAgB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAE/C;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,GAAG,YAAY,CAE5F;AAGD,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,CAAC"}
|
package/build/index.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { NativeModulesProxy, EventEmitter } from 'expo-modules-core';
|
|
2
|
+
// Import the native module. On web, it will be resolved to ExpoAudioStream.web.ts
|
|
3
|
+
// and on native platforms to ExpoAudioStream.ts
|
|
4
|
+
import ExpoAudioStreamModule from './ExpoAudioStreamModule';
|
|
5
|
+
import { useAudioRecorder } from './useAudioRecording';
|
|
6
|
+
const emitter = new EventEmitter(ExpoAudioStreamModule ?? NativeModulesProxy.ExpoAudioStream);
|
|
7
|
+
// Function to get the recording duration
|
|
8
|
+
export function getRecordingDuration() {
|
|
9
|
+
return ExpoAudioStreamModule.getRecordingDuration();
|
|
10
|
+
}
|
|
11
|
+
export function listAudioFiles() {
|
|
12
|
+
return ExpoAudioStreamModule.listAudioFiles();
|
|
13
|
+
}
|
|
14
|
+
export function clearAudioFiles() {
|
|
15
|
+
return ExpoAudioStreamModule.clearAudioFiles();
|
|
16
|
+
}
|
|
17
|
+
export function addChangeListener(listener) {
|
|
18
|
+
return emitter.addListener('AudioData', listener);
|
|
19
|
+
}
|
|
20
|
+
export { useAudioRecorder };
|
|
21
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,YAAY,EAAqB,MAAM,mBAAmB,CAAC;AAExF,kFAAkF;AAClF,gDAAgD;AAChD,OAAO,qBAAqB,MAAM,yBAAyB,CAAC;AAE5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAEvD,MAAM,OAAO,GAAG,IAAI,YAAY,CAAC,qBAAqB,IAAI,kBAAkB,CAAC,eAAe,CAAC,CAAC;AAE9F,yCAAyC;AACzC,MAAM,UAAU,oBAAoB;IAClC,OAAO,qBAAqB,CAAC,oBAAoB,EAAE,CAAC;AACtD,CAAC;AAED,MAAM,UAAU,cAAc;IAC5B,OAAO,qBAAqB,CAAC,cAAc,EAAE,CAAC;AAChD,CAAC;AAED,MAAM,UAAU,eAAe;IAC7B,OAAO,qBAAqB,CAAC,eAAe,EAAE,CAAC;AACjD,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,QAA4C;IAC5E,OAAO,OAAO,CAAC,WAAW,CAAoB,WAAW,EAAE,QAAQ,CAAC,CAAC;AACvE,CAAC;AAGD,OAAO,EAAqB,gBAAgB,EAAE,CAAC","sourcesContent":["import { NativeModulesProxy, EventEmitter, type Subscription } from 'expo-modules-core';\n\n// Import the native module. On web, it will be resolved to ExpoAudioStream.web.ts\n// and on native platforms to ExpoAudioStream.ts\nimport ExpoAudioStreamModule from './ExpoAudioStreamModule';\nimport { AudioEventPayload, RecordingOptions } from './ExpoAudioStream.types';\nimport { useAudioRecorder } from './useAudioRecording';\n\nconst emitter = new EventEmitter(ExpoAudioStreamModule ?? NativeModulesProxy.ExpoAudioStream);\n\n// Function to get the recording duration\nexport function getRecordingDuration(): Promise<number> {\n return ExpoAudioStreamModule.getRecordingDuration();\n}\n\nexport function listAudioFiles(): Promise<string[]> {\n return ExpoAudioStreamModule.listAudioFiles();\n}\n\nexport function clearAudioFiles(): Promise<void> {\n return ExpoAudioStreamModule.clearAudioFiles();\n}\n\nexport function addChangeListener(listener: (event: AudioEventPayload) => void): Subscription {\n return emitter.addListener<AudioEventPayload>('AudioData', listener);\n}\n\n\nexport { AudioEventPayload, useAudioRecorder };\n"]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { RecordingOptions } from "./ExpoAudioStream.types";
|
|
2
|
+
interface UseAudioRecorderState {
|
|
3
|
+
startRecording: (_: RecordingOptions) => Promise<void>;
|
|
4
|
+
stopRecording: () => Promise<number>;
|
|
5
|
+
pauseRecording: () => void;
|
|
6
|
+
isRecording: boolean;
|
|
7
|
+
isPaused: boolean;
|
|
8
|
+
duration: number;
|
|
9
|
+
size: number;
|
|
10
|
+
}
|
|
11
|
+
export declare function useAudioRecorder({ onAudioStream }: {
|
|
12
|
+
onAudioStream?: (buffer: Blob) => void;
|
|
13
|
+
}): UseAudioRecorderState;
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=useAudioRecording.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useAudioRecording.d.ts","sourceRoot":"","sources":["../src/useAudioRecording.ts"],"names":[],"mappings":"AAIA,OAAO,EAAwC,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAOjG,UAAU,qBAAqB;IAC3B,cAAc,EAAE,CAAC,CAAC,EAAE,gBAAgB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACvD,aAAa,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IACrC,cAAc,EAAE,MAAM,IAAI,CAAC;IAC3B,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,gBAAgB,CAAC,EAAC,aAAa,EAAC,EAAE;IAAC,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,IAAI,KAAK,IAAI,CAAA;CAAC,GAAG,qBAAqB,CAuHjH"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { NativeModulesProxy, EventEmitter, Platform } from 'expo-modules-core';
|
|
2
|
+
import { useCallback, useEffect, useState } from "react";
|
|
3
|
+
import ExpoAudioStreamModule from './ExpoAudioStreamModule';
|
|
4
|
+
import { addChangeListener } from '.';
|
|
5
|
+
import * as FileSystem from 'expo-file-system';
|
|
6
|
+
import { decode as atob } from 'base-64';
|
|
7
|
+
const emitter = new EventEmitter(ExpoAudioStreamModule ?? NativeModulesProxy.ExpoAudioStream);
|
|
8
|
+
export function useAudioRecorder({ onAudioStream }) {
|
|
9
|
+
const [isRecording, setIsRecording] = useState(false);
|
|
10
|
+
const [isPaused, setIsPaused] = useState(false);
|
|
11
|
+
const [duration, setDuration] = useState(0);
|
|
12
|
+
const [size, setSize] = useState(0);
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (isRecording || isPaused) {
|
|
15
|
+
const interval = setInterval(() => {
|
|
16
|
+
const status = ExpoAudioStreamModule.status();
|
|
17
|
+
setDuration(status.duration);
|
|
18
|
+
setSize(status.size);
|
|
19
|
+
}, 1000);
|
|
20
|
+
return () => clearInterval(interval);
|
|
21
|
+
}
|
|
22
|
+
return () => null;
|
|
23
|
+
}, [isRecording, isPaused]);
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
const subscribe = addChangeListener(async ({ fileUri, deltaSize, totalSize, from, streamUuid, encoded, buffer }) => {
|
|
26
|
+
console.debug(`Received audio event:`, { fileUri, deltaSize, totalSize, from, streamUuid, encodedLength: encoded?.length });
|
|
27
|
+
if (deltaSize > 0) {
|
|
28
|
+
// Fetch the audio data from the fileUri
|
|
29
|
+
const options = {
|
|
30
|
+
encoding: FileSystem.EncodingType.Base64,
|
|
31
|
+
position: from,
|
|
32
|
+
length: deltaSize,
|
|
33
|
+
};
|
|
34
|
+
if (Platform.OS !== 'web') {
|
|
35
|
+
// Read the audio file as a base64 string for comparison
|
|
36
|
+
try {
|
|
37
|
+
const base64Content = await FileSystem.readAsStringAsync(fileUri, options);
|
|
38
|
+
const binaryData = atob(base64Content);
|
|
39
|
+
const content = new Uint8Array(binaryData.length);
|
|
40
|
+
for (let i = 0; i < binaryData.length; i++) {
|
|
41
|
+
content[i] = binaryData.charCodeAt(i);
|
|
42
|
+
}
|
|
43
|
+
// TODO: get the filetype based on audio setting and encoding
|
|
44
|
+
const audioBlob = new Blob([content], { type: 'application/octet-stream' }); // Create a Blob from the byte array
|
|
45
|
+
console.debug(`Read audio file (len: ${content.length}) vs ${deltaSize}`);
|
|
46
|
+
onAudioStream?.(audioBlob);
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
console.error('Error reading audio file:', error);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else if (buffer) {
|
|
53
|
+
onAudioStream?.(buffer);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
return () => subscribe.remove();
|
|
58
|
+
}, [isRecording, onAudioStream]);
|
|
59
|
+
const startRecording = useCallback(async (recordingOptions) => {
|
|
60
|
+
if (!isRecording) {
|
|
61
|
+
setIsRecording(true);
|
|
62
|
+
setIsPaused(false);
|
|
63
|
+
setSize(0);
|
|
64
|
+
setDuration(0);
|
|
65
|
+
const startTime = Date.now();
|
|
66
|
+
console.log(`module shims`, ExpoAudioStreamModule);
|
|
67
|
+
try {
|
|
68
|
+
console.log(`start recoding`, recordingOptions);
|
|
69
|
+
await ExpoAudioStreamModule.startRecording(recordingOptions);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
console.error('Error starting recording:', error);
|
|
73
|
+
setIsRecording(false);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}, [isRecording]);
|
|
77
|
+
const stopRecording = useCallback(async () => {
|
|
78
|
+
if (isRecording) {
|
|
79
|
+
setIsRecording(false);
|
|
80
|
+
setIsPaused(false);
|
|
81
|
+
try {
|
|
82
|
+
const recordedDuration = await ExpoAudioStreamModule.stopRecording();
|
|
83
|
+
setDuration(recordedDuration);
|
|
84
|
+
return recordedDuration;
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
console.error('Error stopping recording:', error);
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return 0;
|
|
92
|
+
}, [isRecording]);
|
|
93
|
+
const pauseRecording = useCallback(() => {
|
|
94
|
+
if (isRecording) {
|
|
95
|
+
ExpoAudioStreamModule.stopRecording().catch(console.error);
|
|
96
|
+
setIsPaused(true);
|
|
97
|
+
setIsRecording(false);
|
|
98
|
+
}
|
|
99
|
+
}, [isRecording]);
|
|
100
|
+
// Cleanup listener on unmount to prevent memory leaks
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
return () => {
|
|
103
|
+
if (isRecording) {
|
|
104
|
+
ExpoAudioStreamModule.stopRecording().catch(console.error);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
}, [isRecording]);
|
|
108
|
+
return {
|
|
109
|
+
startRecording,
|
|
110
|
+
stopRecording,
|
|
111
|
+
pauseRecording,
|
|
112
|
+
isPaused,
|
|
113
|
+
isRecording,
|
|
114
|
+
duration,
|
|
115
|
+
size
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=useAudioRecording.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useAudioRecording.js","sourceRoot":"","sources":["../src/useAudioRecording.ts"],"names":[],"mappings":"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","sourcesContent":["import { NativeModulesProxy, EventEmitter, type Subscription, Platform } from 'expo-modules-core';\n\nimport { useCallback, useEffect, useState } from \"react\";\nimport ExpoAudioStreamModule from './ExpoAudioStreamModule';\nimport { AudioEventPayload, AudioStreamStatus, RecordingOptions } from \"./ExpoAudioStream.types\";\nimport { addChangeListener } from '.';\nimport * as FileSystem from 'expo-file-system';\nimport { decode as atob } from 'base-64';\n\nconst emitter = new EventEmitter(ExpoAudioStreamModule ?? NativeModulesProxy.ExpoAudioStream);\n\ninterface UseAudioRecorderState {\n startRecording: (_: RecordingOptions) => Promise<void>;\n stopRecording: () => Promise<number>;\n pauseRecording: () => void;\n isRecording: boolean;\n isPaused: boolean;\n duration: number; // Duration of the recording\n size: number; // Size in bytes of the recorded audio\n}\n\nexport function useAudioRecorder({onAudioStream}: {onAudioStream?: (buffer: Blob) => void}): UseAudioRecorderState {\n const [isRecording, setIsRecording] = useState(false);\n const [isPaused, setIsPaused] = useState(false);\n const [duration, setDuration] = useState(0);\n const [size, setSize] = useState(0);\n\n useEffect( () => {\n if(isRecording || isPaused) {\n const interval = setInterval(() => {\n const status: AudioStreamStatus = ExpoAudioStreamModule.status()\n setDuration(status.duration);\n setSize(status.size);\n }, 1000);\n return () => clearInterval(interval);\n }\n\n return () => null;\n }, [isRecording, isPaused])\n\n\n useEffect(() => {\n const subscribe = addChangeListener(async ({fileUri, deltaSize, totalSize, from, streamUuid, encoded, buffer}) => {\n console.debug(`Received audio event:`, {fileUri, deltaSize, totalSize, from, streamUuid, encodedLength: encoded?.length})\n if(deltaSize > 0) {\n // Fetch the audio data from the fileUri\n const options = {\n encoding: FileSystem.EncodingType.Base64,\n position: from,\n length: deltaSize,\n };\n\n if(Platform.OS !== 'web') {\n // Read the audio file as a base64 string for comparison\n try {\n const base64Content = await FileSystem.readAsStringAsync(fileUri, options);\n const binaryData = atob(base64Content);\n const content = new Uint8Array(binaryData.length);\n for (let i = 0; i < binaryData.length; i++) {\n content[i] = binaryData.charCodeAt(i);\n }\n\n // TODO: get the filetype based on audio setting and encoding\n const audioBlob = new Blob([content], { type: 'application/octet-stream' }); // Create a Blob from the byte array\n console.debug(`Read audio file (len: ${content.length}) vs ${deltaSize}`)\n onAudioStream?.(audioBlob);\n } catch (error) {\n console.error('Error reading audio file:', error);\n }\n } else if(buffer) {\n onAudioStream?.(buffer);\n }\n }\n });\n return () => subscribe.remove();\n }, [isRecording, onAudioStream]);\n\n\n const startRecording = useCallback(async (recordingOptions: RecordingOptions) => {\n if (!isRecording) {\n setIsRecording(true);\n setIsPaused(false);\n setSize(0);\n setDuration(0);\n const startTime = Date.now();\n\n console.log(`module shims`, ExpoAudioStreamModule)\n try {\n console.log(`start recoding`, recordingOptions)\n await ExpoAudioStreamModule.startRecording(recordingOptions);\n\n } catch (error) {\n console.error('Error starting recording:', error);\n setIsRecording(false);\n }\n }\n }, [isRecording]);\n\n const stopRecording = useCallback(async (): Promise<number> => {\n if (isRecording) {\n setIsRecording(false);\n setIsPaused(false);\n try {\n const recordedDuration = await ExpoAudioStreamModule.stopRecording();\n setDuration(recordedDuration);\n return recordedDuration;\n } catch (error) {\n console.error('Error stopping recording:', error);\n return 0;\n }\n }\n return 0;\n }, [isRecording]);\n\n const pauseRecording = useCallback(() => {\n if (isRecording) {\n ExpoAudioStreamModule.stopRecording().catch(console.error);\n setIsPaused(true);\n setIsRecording(false);\n }\n }, [isRecording]);\n\n // Cleanup listener on unmount to prevent memory leaks\n useEffect(() => {\n return () => {\n if (isRecording) {\n ExpoAudioStreamModule.stopRecording().catch(console.error);\n }\n };\n }, [isRecording]);\n\n return {\n startRecording,\n stopRecording,\n pauseRecording,\n isPaused,\n isRecording,\n duration,\n size\n };\n}"]}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
//
|
|
2
|
+
// AudioStreamManager.swift
|
|
3
|
+
// ExpoAudioStream
|
|
4
|
+
//
|
|
5
|
+
// Created by Arthur Breton on 21/4/2024.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import Foundation
|
|
9
|
+
import AVFoundation
|
|
10
|
+
|
|
11
|
+
struct RecordingSettings {
|
|
12
|
+
var sampleRate: Double = 48000.0
|
|
13
|
+
var numberOfChannels: Int = 1
|
|
14
|
+
var bitDepth: Int = 16
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
protocol AudioStreamManagerDelegate: AnyObject {
|
|
18
|
+
func audioStreamManager(_ manager: AudioStreamManager, didReceiveAudioData data: Data, recordingTime: TimeInterval, totalDataSize: Int64)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
class AudioStreamManager: NSObject {
|
|
22
|
+
private let audioEngine = AVAudioEngine()
|
|
23
|
+
private var inputNode: AVAudioInputNode {
|
|
24
|
+
return audioEngine.inputNode
|
|
25
|
+
}
|
|
26
|
+
internal var recordingFileURL: URL?
|
|
27
|
+
private var startTime: Date?
|
|
28
|
+
internal var lastEmissionTime: Date?
|
|
29
|
+
internal var lastEmittedSize: Int64 = 0
|
|
30
|
+
private var emissionInterval: TimeInterval = 1.0 // Default to 1 second
|
|
31
|
+
private var totalDataSize: Int64 = 0
|
|
32
|
+
private var isRecording = false
|
|
33
|
+
private var isPaused = false
|
|
34
|
+
private var pausedDuration = 0
|
|
35
|
+
private var fileManager = FileManager.default
|
|
36
|
+
internal var recordingUUID: UUID?
|
|
37
|
+
weak var delegate: AudioStreamManagerDelegate? // Define the delegate here
|
|
38
|
+
|
|
39
|
+
override init() {
|
|
40
|
+
super.init()
|
|
41
|
+
configureAudioSession()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private func configureAudioSession() {
|
|
45
|
+
let session = AVAudioSession.sharedInstance()
|
|
46
|
+
do {
|
|
47
|
+
try session.setCategory(.playAndRecord, mode: .default)
|
|
48
|
+
try session.setActive(true)
|
|
49
|
+
print("Audio session configured successfully.")
|
|
50
|
+
} catch {
|
|
51
|
+
print("Failed to set up audio session: \(error.localizedDescription)")
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private func createRecordingFile() -> URL? {
|
|
56
|
+
let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
|
|
57
|
+
recordingUUID = UUID()
|
|
58
|
+
let fileName = "\(recordingUUID!.uuidString).pcm"
|
|
59
|
+
let fileURL = documentsDirectory.appendingPathComponent(fileName)
|
|
60
|
+
fileManager.createFile(atPath: fileURL.path, contents: nil, attributes: nil)
|
|
61
|
+
print("Recording file created at:", fileURL.path)
|
|
62
|
+
|
|
63
|
+
return fileURL
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
func getStatus() -> [String: Any] {
|
|
67
|
+
let currentTime = Date()
|
|
68
|
+
let totalRecordedTime = startTime != nil ? Int(currentTime.timeIntervalSince(startTime!)) - pausedDuration : 0
|
|
69
|
+
return [
|
|
70
|
+
"duration": totalRecordedTime,
|
|
71
|
+
"isRecording": isRecording,
|
|
72
|
+
"isPaused": isPaused,
|
|
73
|
+
"size": totalDataSize,
|
|
74
|
+
"interval": emissionInterval
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
func startRecording(settings: RecordingSettings, intervalMilliseconds: Int) {
|
|
79
|
+
guard !isRecording else { return }
|
|
80
|
+
|
|
81
|
+
emissionInterval = max(100.0, Double(intervalMilliseconds)) / 1000.0 // Convert ms to seconds, ensure minimum of 100 ms
|
|
82
|
+
lastEmissionTime = Date() // Reset last emission time
|
|
83
|
+
|
|
84
|
+
// Configure audio session for the desired sample rate and channel count
|
|
85
|
+
let session = AVAudioSession.sharedInstance()
|
|
86
|
+
do {
|
|
87
|
+
try session.setPreferredSampleRate(settings.sampleRate)
|
|
88
|
+
try session.setPreferredIOBufferDuration(1024 / settings.sampleRate)
|
|
89
|
+
try session.setCategory(.playAndRecord)
|
|
90
|
+
try session.setActive(true)
|
|
91
|
+
} catch {
|
|
92
|
+
print("Failed to set up audio session: \(error)")
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Create an audio format with specified or default settings
|
|
97
|
+
let channelLayout = AVAudioChannelLayout(layoutTag: settings.numberOfChannels == 1 ? kAudioChannelLayoutTag_Mono : kAudioChannelLayoutTag_Stereo) ?? AVAudioChannelLayout(layoutTag: kAudioChannelLayoutTag_Stereo)!
|
|
98
|
+
let errorFormat = AVAudioFormat(standardFormatWithSampleRate: settings.sampleRate, channelLayout: channelLayout)
|
|
99
|
+
|
|
100
|
+
// Create an audio format with default settings
|
|
101
|
+
let format = audioEngine.inputNode.inputFormat(forBus: 0)
|
|
102
|
+
|
|
103
|
+
// Debugging statements
|
|
104
|
+
print("Desired Sample Rate:", settings.sampleRate)
|
|
105
|
+
print("Channel Layout:", channelLayout.description)
|
|
106
|
+
print("Created Audio Format Sample Rate: \(format.sampleRate) channelLayout: \(format.channelLayout) channelCount: \(format.channelCount)")
|
|
107
|
+
print("Error Audio Format Sample Rate: \(errorFormat.sampleRate) channel Layout: \(errorFormat.channelLayout) channelCount: \(errorFormat.channelCount)")
|
|
108
|
+
print("Hardware Format Sample Rate:", audioEngine.inputNode.inputFormat(forBus: 0).sampleRate)
|
|
109
|
+
|
|
110
|
+
// Install tap on the input node and handle audio buffer
|
|
111
|
+
audioEngine.inputNode.installTap(onBus: 0, bufferSize: 1024, format: errorFormat) { [weak self] (buffer, time) in
|
|
112
|
+
guard let self = self, let fileURL = self.recordingFileURL else { return }
|
|
113
|
+
self.processAudioBuffer(buffer, fileURL: fileURL)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
recordingFileURL = createRecordingFile()
|
|
117
|
+
do {
|
|
118
|
+
startTime = Date()
|
|
119
|
+
try audioEngine.start()
|
|
120
|
+
isRecording = true
|
|
121
|
+
} catch {
|
|
122
|
+
print("Could not start the audio engine: \(error)")
|
|
123
|
+
isRecording = false
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
func stopRecording() {
|
|
128
|
+
audioEngine.stop()
|
|
129
|
+
audioEngine.inputNode.removeTap(onBus: 0)
|
|
130
|
+
isRecording = false
|
|
131
|
+
recordingFileURL = nil // Optionally reset or handle the finalization of the file
|
|
132
|
+
print("Recording stopped.")
|
|
133
|
+
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private func processAudioBuffer(_ buffer: AVAudioPCMBuffer, fileURL: URL) {
|
|
137
|
+
guard let fileHandle = try? FileHandle(forWritingTo: fileURL) else {
|
|
138
|
+
print("Failed to open file handle for URL: \(fileURL)")
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let audioData = buffer.audioBufferList.pointee.mBuffers
|
|
143
|
+
guard let bufferData = audioData.mData else {
|
|
144
|
+
print("Buffer data is nil.")
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
let data = Data(bytes: bufferData, count: Int(audioData.mDataByteSize))
|
|
148
|
+
|
|
149
|
+
fileHandle.seekToEndOfFile()
|
|
150
|
+
fileHandle.write(data)
|
|
151
|
+
fileHandle.closeFile()
|
|
152
|
+
|
|
153
|
+
totalDataSize += Int64(data.count)
|
|
154
|
+
|
|
155
|
+
let currentTime = Date()
|
|
156
|
+
if let lastEmissionTime = lastEmissionTime, currentTime.timeIntervalSince(lastEmissionTime) >= emissionInterval {
|
|
157
|
+
if let startTime = startTime {
|
|
158
|
+
let recordingTime = currentTime.timeIntervalSince(startTime)
|
|
159
|
+
print("Emitting data: Recording time \(recordingTime) seconds, Data size \(totalDataSize) bytes")
|
|
160
|
+
print("delegate", self.delegate)
|
|
161
|
+
self.delegate?.audioStreamManager(self, didReceiveAudioData: data, recordingTime: recordingTime, totalDataSize: totalDataSize)
|
|
162
|
+
self.lastEmissionTime = currentTime // Update last emission time
|
|
163
|
+
self.lastEmittedSize = totalDataSize
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = 'ExpoAudioStream'
|
|
7
|
+
s.version = package['version']
|
|
8
|
+
s.summary = package['description']
|
|
9
|
+
s.description = package['description']
|
|
10
|
+
s.license = package['license']
|
|
11
|
+
s.author = package['author']
|
|
12
|
+
s.homepage = package['homepage']
|
|
13
|
+
s.platforms = { :ios => '13.4', :tvos => '13.4' }
|
|
14
|
+
s.swift_version = '5.4'
|
|
15
|
+
s.source = { git: 'https://github.com/deeeed/expo-audio-stream' }
|
|
16
|
+
s.static_framework = true
|
|
17
|
+
|
|
18
|
+
s.dependency 'ExpoModulesCore'
|
|
19
|
+
|
|
20
|
+
# Swift/Objective-C compatibility
|
|
21
|
+
s.pod_target_xcconfig = {
|
|
22
|
+
'DEFINES_MODULE' => 'YES',
|
|
23
|
+
'SWIFT_COMPILATION_MODE' => 'wholemodule'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
s.source_files = "**/*.{h,m,swift}"
|
|
27
|
+
end
|