@siteed/expo-audio-stream 0.1.0 → 0.2.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/README.md +80 -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.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 +16 -6
- package/build/ExpoAudioStreamModule.web.js.map +1 -1
- package/build/index.d.ts +1 -2
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -5
- package/build/index.js.map +1 -1
- package/build/useAudioRecording.d.ts +3 -3
- package/build/useAudioRecording.d.ts.map +1 -1
- package/build/useAudioRecording.js +49 -57
- 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/src/ExpoAudioStream.types.ts +12 -3
- package/src/ExpoAudioStreamModule.web.ts +19 -8
- package/src/index.ts +1 -7
- package/src/useAudioRecording.ts +58 -65
- package/yarn-error.log +0 -72
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { RecordingOptions } from "./ExpoAudioStream.types";
|
|
1
|
+
import { AudioStreamResult, RecordingOptions } from "./ExpoAudioStream.types";
|
|
2
2
|
interface UseAudioRecorderState {
|
|
3
|
-
startRecording: (_: RecordingOptions) => Promise<
|
|
4
|
-
stopRecording: () => Promise<
|
|
3
|
+
startRecording: (_: RecordingOptions) => Promise<string | null>;
|
|
4
|
+
stopRecording: () => Promise<AudioStreamResult | null>;
|
|
5
5
|
pauseRecording: () => void;
|
|
6
6
|
isRecording: boolean;
|
|
7
7
|
isPaused: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useAudioRecording.d.ts","sourceRoot":"","sources":["../src/useAudioRecording.ts"],"names":[],"mappings":"AAIA,OAAO,
|
|
1
|
+
{"version":3,"file":"useAudioRecording.d.ts","sourceRoot":"","sources":["../src/useAudioRecording.ts"],"names":[],"mappings":"AAIA,OAAO,EAAqB,iBAAiB,EAAqB,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAUpH,UAAU,qBAAqB;IAC3B,cAAc,EAAE,CAAC,CAAC,EAAE,gBAAgB,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAChE,aAAa,EAAE,MAAM,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAAC;IACvD,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;AAGD,wBAAgB,gBAAgB,CAAC,EAAC,aAAa,EAAC,EAAE;IAAC,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,IAAI,KAAK,IAAI,CAAA;CAAC,GAAG,qBAAqB,CA4GjH"}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { NativeModulesProxy, EventEmitter, Platform } from 'expo-modules-core';
|
|
2
2
|
import { useCallback, useEffect, useState } from "react";
|
|
3
3
|
import ExpoAudioStreamModule from './ExpoAudioStreamModule';
|
|
4
|
-
import {
|
|
5
|
-
import * as FileSystem from 'expo-file-system';
|
|
4
|
+
import { addAudioEventListener } from '.';
|
|
6
5
|
import { decode as atob } from 'base-64';
|
|
6
|
+
import debug from 'debug';
|
|
7
7
|
const emitter = new EventEmitter(ExpoAudioStreamModule ?? NativeModulesProxy.ExpoAudioStream);
|
|
8
|
+
const log = debug("expo-audio-stream:useAudioRecording");
|
|
8
9
|
export function useAudioRecorder({ onAudioStream }) {
|
|
9
10
|
const [isRecording, setIsRecording] = useState(false);
|
|
10
11
|
const [isPaused, setIsPaused] = useState(false);
|
|
@@ -22,27 +23,35 @@ export function useAudioRecorder({ onAudioStream }) {
|
|
|
22
23
|
return () => null;
|
|
23
24
|
}, [isRecording, isPaused]);
|
|
24
25
|
useEffect(() => {
|
|
25
|
-
const subscribe =
|
|
26
|
-
|
|
26
|
+
const subscribe = addAudioEventListener(async ({ fileUri, deltaSize, totalSize, from, streamUuid, encoded, mimeType, buffer }) => {
|
|
27
|
+
log(`Received audio event:`, { fileUri, deltaSize, totalSize, mimeType, from, streamUuid, encodedLength: encoded?.length });
|
|
27
28
|
if (deltaSize > 0) {
|
|
28
|
-
//
|
|
29
|
-
const options = {
|
|
30
|
-
encoding: FileSystem.EncodingType.Base64,
|
|
31
|
-
position: from,
|
|
32
|
-
length: deltaSize,
|
|
33
|
-
};
|
|
29
|
+
// Coming from native ( ios / android ) otherwise buffer is set
|
|
34
30
|
if (Platform.OS !== 'web') {
|
|
35
31
|
// Read the audio file as a base64 string for comparison
|
|
36
32
|
try {
|
|
37
|
-
|
|
38
|
-
const binaryData = atob(
|
|
33
|
+
// convert encoded string to binary data
|
|
34
|
+
const binaryData = atob(encoded);
|
|
39
35
|
const content = new Uint8Array(binaryData.length);
|
|
40
36
|
for (let i = 0; i < binaryData.length; i++) {
|
|
41
37
|
content[i] = binaryData.charCodeAt(i);
|
|
42
38
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
39
|
+
const audioBlob = new Blob([content], { type: mimeType });
|
|
40
|
+
// Below code is optional, used to compare encoded data to audio on file system
|
|
41
|
+
// Fetch the audio data from the fileUri
|
|
42
|
+
// const options = {
|
|
43
|
+
// encoding: FileSystem.EncodingType.Base64,
|
|
44
|
+
// position: from,
|
|
45
|
+
// length: deltaSize,
|
|
46
|
+
// };
|
|
47
|
+
// const base64Content = await FileSystem.readAsStringAsync(fileUri, options);
|
|
48
|
+
// const binaryData = atob(base64Content);
|
|
49
|
+
// const content = new Uint8Array(binaryData.length);
|
|
50
|
+
// for (let i = 0; i < binaryData.length; i++) {
|
|
51
|
+
// content[i] = binaryData.charCodeAt(i);
|
|
52
|
+
// }
|
|
53
|
+
// const audioBlob = new Blob([content], { type: 'application/octet-stream' }); // Create a Blob from the byte array
|
|
54
|
+
// console.debug(`Read audio file (len: ${content.length}) vs ${deltaSize}`)
|
|
46
55
|
onAudioStream?.(audioBlob);
|
|
47
56
|
}
|
|
48
57
|
catch (error) {
|
|
@@ -50,6 +59,7 @@ export function useAudioRecorder({ onAudioStream }) {
|
|
|
50
59
|
}
|
|
51
60
|
}
|
|
52
61
|
else if (buffer) {
|
|
62
|
+
// Coming from web
|
|
53
63
|
onAudioStream?.(buffer);
|
|
54
64
|
}
|
|
55
65
|
}
|
|
@@ -57,54 +67,36 @@ export function useAudioRecorder({ onAudioStream }) {
|
|
|
57
67
|
return () => subscribe.remove();
|
|
58
68
|
}, [isRecording, onAudioStream]);
|
|
59
69
|
const startRecording = useCallback(async (recordingOptions) => {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
}
|
|
70
|
+
setIsRecording(true);
|
|
71
|
+
setIsPaused(false);
|
|
72
|
+
setSize(0);
|
|
73
|
+
setDuration(0);
|
|
74
|
+
try {
|
|
75
|
+
log(`start recoding`, recordingOptions);
|
|
76
|
+
const fileUrl = await ExpoAudioStreamModule.startRecording(recordingOptions);
|
|
77
|
+
return fileUrl;
|
|
75
78
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (isRecording) {
|
|
79
|
+
catch (error) {
|
|
80
|
+
console.error('Error starting recording:', error);
|
|
79
81
|
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
82
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
83
|
+
}, []);
|
|
84
|
+
const stopRecording = useCallback(async () => {
|
|
85
|
+
setIsRecording(false);
|
|
86
|
+
setIsPaused(false);
|
|
87
|
+
const result = await ExpoAudioStreamModule.stopRecording();
|
|
88
|
+
return result;
|
|
89
|
+
}, []);
|
|
90
|
+
const pauseRecording = useCallback(async () => {
|
|
91
|
+
try {
|
|
92
|
+
await ExpoAudioStreamModule.stopRecording();
|
|
96
93
|
setIsPaused(true);
|
|
97
94
|
setIsRecording(false);
|
|
98
95
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (isRecording) {
|
|
104
|
-
ExpoAudioStreamModule.stopRecording().catch(console.error);
|
|
105
|
-
}
|
|
106
|
-
};
|
|
107
|
-
}, [isRecording]);
|
|
96
|
+
catch (error) {
|
|
97
|
+
console.error('Error pausing recording:', error);
|
|
98
|
+
}
|
|
99
|
+
}, []);
|
|
108
100
|
return {
|
|
109
101
|
startRecording,
|
|
110
102
|
stopRecording,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useAudioRecording.js","sourceRoot":"","sources":["../src/useAudioRecording.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,YAAY,EAAqB,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAElG,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AACzD,OAAO,qBAAqB,MAAM,yBAAyB,CAAC;AAE5D,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"useAudioRecording.js","sourceRoot":"","sources":["../src/useAudioRecording.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,YAAY,EAAqB,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAElG,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AACzD,OAAO,qBAAqB,MAAM,yBAAyB,CAAC;AAE5D,OAAO,EAAE,qBAAqB,EAAE,MAAM,GAAG,CAAC;AAE1C,OAAO,EAAE,MAAM,IAAI,IAAI,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,MAAM,OAAO,GAAG,IAAI,YAAY,CAAC,qBAAqB,IAAI,kBAAkB,CAAC,eAAe,CAAC,CAAC;AAE9F,MAAM,GAAG,GAAI,KAAK,CAAC,qCAAqC,CAAC,CAAC;AAa1D,MAAM,UAAU,gBAAgB,CAAC,EAAC,aAAa,EAA2C;IACtF,MAAM,CAAC,WAAW,EAAE,cAAc,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IACtD,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAChD,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;IAC5C,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;IAEpC,SAAS,CAAE,GAAG,EAAE;QACZ,IAAG,WAAW,IAAI,QAAQ,EAAE,CAAC;YACzB,MAAM,QAAQ,GAAG,WAAW,CAAC,GAAG,EAAE;gBAC9B,MAAM,MAAM,GAAsB,qBAAqB,CAAC,MAAM,EAAE,CAAA;gBAChE,WAAW,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;gBAC7B,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;YACzB,CAAC,EAAE,IAAI,CAAC,CAAC;YACT,OAAO,GAAG,EAAE,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;QACzC,CAAC;QAED,OAAO,GAAG,EAAE,CAAC,IAAI,CAAC;IACtB,CAAC,EAAE,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC,CAAA;IAG7B,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,SAAS,GAAG,qBAAqB,CAAC,KAAK,EAAE,EAAC,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAC,EAAE,EAAE;YAC3H,GAAG,CAAC,uBAAuB,EAAE,EAAC,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE,IAAI,EAAE,UAAU,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,EAAC,CAAC,CAAA;YACzH,IAAG,SAAS,GAAG,CAAC,EAAE,CAAC;gBACf,+DAA+D;gBAC7D,IAAG,QAAQ,CAAC,EAAE,KAAK,KAAK,EAAE,CAAC;oBACzB,wDAAwD;oBACxD,IAAI,CAAC;wBACD,wCAAwC;wBACxC,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC;wBACjC,MAAM,OAAO,GAAG,IAAI,UAAU,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;wBAClD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,UAAU,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;4BACzC,OAAO,CAAC,CAAC,CAAC,GAAG,UAAU,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;wBAC1C,CAAC;wBACD,MAAM,SAAS,GAAG,IAAI,IAAI,CAAC,CAAC,OAAO,CAAC,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;wBAE1D,+EAA+E;wBAC/E,wCAAwC;wBACxC,oBAAoB;wBACpB,gDAAgD;wBAChD,sBAAsB;wBACtB,yBAAyB;wBACzB,KAAK;wBACL,8EAA8E;wBAC9E,0CAA0C;wBAC1C,qDAAqD;wBACrD,gDAAgD;wBAChD,yCAAyC;wBACzC,IAAI;wBACJ,oHAAoH;wBACpH,4EAA4E;wBAE5E,aAAa,EAAE,CAAC,SAAS,CAAC,CAAC;oBAC/B,CAAC;oBAAC,OAAO,KAAK,EAAE,CAAC;wBACb,OAAO,CAAC,KAAK,CAAC,2BAA2B,EAAE,KAAK,CAAC,CAAC;oBACtD,CAAC;gBACL,CAAC;qBAAM,IAAG,MAAM,EAAE,CAAC;oBACf,kBAAkB;oBAClB,aAAa,EAAE,CAAC,MAAM,CAAC,CAAC;gBAC5B,CAAC;YACL,CAAC;QACL,CAAC,CAAC,CAAC;QACH,OAAO,GAAG,EAAE,CAAC,SAAS,CAAC,MAAM,EAAE,CAAC;IAClC,CAAC,EAAE,CAAC,WAAW,EAAE,aAAa,CAAC,CAAC,CAAC;IAG/B,MAAM,cAAc,GAAG,WAAW,CAAC,KAAK,EAAE,gBAAkC,EAAE,EAAE;QAC5E,cAAc,CAAC,IAAI,CAAC,CAAC;QACrB,WAAW,CAAC,KAAK,CAAC,CAAC;QACnB,OAAO,CAAC,CAAC,CAAC,CAAC;QACX,WAAW,CAAC,CAAC,CAAC,CAAC;QACf,IAAI,CAAC;YACD,GAAG,CAAC,gBAAgB,EAAE,gBAAgB,CAAC,CAAA;YACvC,MAAM,OAAO,GAAG,MAAM,qBAAqB,CAAC,cAAc,CAAC,gBAAgB,CAAC,CAAC;YAE7E,OAAO,OAAO,CAAC;QACnB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACb,OAAO,CAAC,KAAK,CAAC,2BAA2B,EAAE,KAAK,CAAC,CAAC;YAClD,cAAc,CAAC,KAAK,CAAC,CAAC;QAC1B,CAAC;IACL,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,MAAM,aAAa,GAAG,WAAW,CAAC,KAAK,IAAI,EAAE;QACzC,cAAc,CAAC,KAAK,CAAC,CAAC;QACtB,WAAW,CAAC,KAAK,CAAC,CAAC;QACnB,MAAM,MAAM,GAAsB,MAAM,qBAAqB,CAAC,aAAa,EAAE,CAAC;QAC9E,OAAO,MAAM,CAAC;IAClB,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,MAAM,cAAc,GAAG,WAAW,CAAC,KAAK,IAAI,EAAE;QAC1C,IAAI,CAAC;YACD,MAAM,qBAAqB,CAAC,aAAa,EAAE,CAAA;YAC3C,WAAW,CAAC,IAAI,CAAC,CAAC;YAClB,cAAc,CAAC,KAAK,CAAC,CAAC;QAC1B,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACb,OAAO,CAAC,KAAK,CAAC,0BAA0B,EAAE,KAAK,CAAC,CAAC;QACrD,CAAC;IACL,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,OAAO;QACH,cAAc;QACd,aAAa;QACb,cAAc;QACd,QAAQ;QACR,WAAW;QACX,QAAQ;QACR,IAAI;KACP,CAAC;AACN,CAAC","sourcesContent":["import { NativeModulesProxy, EventEmitter, type Subscription, Platform } from 'expo-modules-core';\n\nimport { useCallback, useEffect, useState } from \"react\";\nimport ExpoAudioStreamModule from './ExpoAudioStreamModule';\nimport { AudioEventPayload, AudioStreamResult, AudioStreamStatus, RecordingOptions } from \"./ExpoAudioStream.types\";\nimport { addAudioEventListener } from '.';\nimport * as FileSystem from 'expo-file-system';\nimport { decode as atob } from 'base-64';\nimport debug from 'debug';\n\nconst emitter = new EventEmitter(ExpoAudioStreamModule ?? NativeModulesProxy.ExpoAudioStream);\n\nconst log = debug(\"expo-audio-stream:useAudioRecording\");\n\ninterface UseAudioRecorderState {\n startRecording: (_: RecordingOptions) => Promise<string | null>;\n stopRecording: () => Promise<AudioStreamResult | null>;\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\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 = addAudioEventListener(async ({fileUri, deltaSize, totalSize, from, streamUuid, encoded, mimeType, buffer}) => {\n log(`Received audio event:`, {fileUri, deltaSize, totalSize, mimeType, from, streamUuid, encodedLength: encoded?.length})\n if(deltaSize > 0) {\n // Coming from native ( ios / android ) otherwise buffer is set\n if(Platform.OS !== 'web') {\n // Read the audio file as a base64 string for comparison\n try {\n // convert encoded string to binary data\n const binaryData = atob(encoded);\n const content = new Uint8Array(binaryData.length);\n for (let i = 0; i < binaryData.length; i++) {\n content[i] = binaryData.charCodeAt(i);\n }\n const audioBlob = new Blob([content], { type: mimeType });\n\n // Below code is optional, used to compare encoded data to audio on file system\n // Fetch the audio data from the fileUri\n // const options = {\n // encoding: FileSystem.EncodingType.Base64,\n // position: from,\n // length: deltaSize,\n // };\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 // 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\n onAudioStream?.(audioBlob);\n } catch (error) {\n console.error('Error reading audio file:', error);\n }\n } else if(buffer) {\n // Coming from web\n onAudioStream?.(buffer);\n }\n }\n });\n return () => subscribe.remove();\n }, [isRecording, onAudioStream]);\n\n\n const startRecording = useCallback(async (recordingOptions: RecordingOptions) => {\n setIsRecording(true);\n setIsPaused(false);\n setSize(0);\n setDuration(0);\n try {\n log(`start recoding`, recordingOptions)\n const fileUrl = await ExpoAudioStreamModule.startRecording(recordingOptions);\n\n return fileUrl;\n } catch (error) {\n console.error('Error starting recording:', error);\n setIsRecording(false);\n }\n }, []);\n\n const stopRecording = useCallback(async () => {\n setIsRecording(false);\n setIsPaused(false);\n const result: AudioStreamResult = await ExpoAudioStreamModule.stopRecording();\n return result;\n }, []);\n\n const pauseRecording = useCallback(async () => {\n try {\n await ExpoAudioStreamModule.stopRecording()\n setIsPaused(true);\n setIsRecording(false);\n } catch (error) {\n console.error('Error pausing recording:', error);\n }\n }, []);\n\n return {\n startRecording,\n stopRecording,\n pauseRecording,\n isPaused,\n isRecording,\n duration,\n size\n };\n}"]}
|
|
@@ -9,15 +9,44 @@ import Foundation
|
|
|
9
9
|
import AVFoundation
|
|
10
10
|
|
|
11
11
|
struct RecordingSettings {
|
|
12
|
-
var sampleRate: Double
|
|
12
|
+
var sampleRate: Double
|
|
13
13
|
var numberOfChannels: Int = 1
|
|
14
14
|
var bitDepth: Int = 16
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
// Helper to convert to little-endian byte array
|
|
18
|
+
extension UInt32 {
|
|
19
|
+
var littleEndianBytes: [UInt8] {
|
|
20
|
+
let value = self.littleEndian
|
|
21
|
+
return [UInt8(value & 0xff), UInt8((value >> 8) & 0xff), UInt8((value >> 16) & 0xff), UInt8((value >> 24) & 0xff)]
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
extension UInt16 {
|
|
26
|
+
var littleEndianBytes: [UInt8] {
|
|
27
|
+
let value = self.littleEndian
|
|
28
|
+
return [UInt8(value & 0xff), UInt8((value >> 8) & 0xff)]
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
struct RecordingResult {
|
|
34
|
+
var fileUri: String
|
|
35
|
+
var mimeType: String
|
|
36
|
+
var duration: Int64
|
|
37
|
+
var size: Int64
|
|
38
|
+
}
|
|
39
|
+
|
|
17
40
|
protocol AudioStreamManagerDelegate: AnyObject {
|
|
18
41
|
func audioStreamManager(_ manager: AudioStreamManager, didReceiveAudioData data: Data, recordingTime: TimeInterval, totalDataSize: Int64)
|
|
19
42
|
}
|
|
20
43
|
|
|
44
|
+
enum AudioStreamError: Error {
|
|
45
|
+
case audioSessionSetupFailed(String)
|
|
46
|
+
case fileCreationFailed(URL)
|
|
47
|
+
case audioProcessingError(String)
|
|
48
|
+
}
|
|
49
|
+
|
|
21
50
|
class AudioStreamManager: NSObject {
|
|
22
51
|
private let audioEngine = AVAudioEngine()
|
|
23
52
|
private var inputNode: AVAudioInputNode {
|
|
@@ -33,7 +62,9 @@ class AudioStreamManager: NSObject {
|
|
|
33
62
|
private var isPaused = false
|
|
34
63
|
private var pausedDuration = 0
|
|
35
64
|
private var fileManager = FileManager.default
|
|
65
|
+
private var recordingSettings: RecordingSettings?
|
|
36
66
|
internal var recordingUUID: UUID?
|
|
67
|
+
internal var mimeType: String = "audio/wav"
|
|
37
68
|
weak var delegate: AudioStreamManagerDelegate? // Define the delegate here
|
|
38
69
|
|
|
39
70
|
override init() {
|
|
@@ -52,85 +83,205 @@ class AudioStreamManager: NSObject {
|
|
|
52
83
|
}
|
|
53
84
|
}
|
|
54
85
|
|
|
86
|
+
@objc func handleAudioSessionInterruption(notification: Notification) {
|
|
87
|
+
guard let info = notification.userInfo,
|
|
88
|
+
let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt,
|
|
89
|
+
let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if type == .began {
|
|
94
|
+
// Pause your audio recording
|
|
95
|
+
} else if type == .ended {
|
|
96
|
+
if let optionsValue = info[AVAudioSessionInterruptionOptionKey] as? UInt {
|
|
97
|
+
let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
|
|
98
|
+
if options.contains(.shouldResume) {
|
|
99
|
+
// Resume your audio recording
|
|
100
|
+
try? AVAudioSession.sharedInstance().setActive(true)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
55
106
|
private func createRecordingFile() -> URL? {
|
|
56
107
|
let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
|
|
57
108
|
recordingUUID = UUID()
|
|
58
|
-
let fileName = "\(recordingUUID!.uuidString).
|
|
109
|
+
let fileName = "\(recordingUUID!.uuidString).wav"
|
|
59
110
|
let fileURL = documentsDirectory.appendingPathComponent(fileName)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
111
|
+
|
|
112
|
+
if fileManager.createFile(atPath: fileURL.path, contents: nil, attributes: nil) {
|
|
113
|
+
do {
|
|
114
|
+
let fileHandle = try FileHandle(forWritingTo: fileURL)
|
|
115
|
+
let wavHeader = createWavHeader(dataSize: 0) // Initially set data size to 0
|
|
116
|
+
fileHandle.write(wavHeader)
|
|
117
|
+
fileHandle.closeFile()
|
|
118
|
+
print("Recording file with header created at:", fileURL.path)
|
|
119
|
+
} catch {
|
|
120
|
+
print("Failed to write WAV header: \(error.localizedDescription)")
|
|
121
|
+
return nil
|
|
122
|
+
}
|
|
123
|
+
}
|
|
63
124
|
return fileURL
|
|
64
125
|
}
|
|
65
126
|
|
|
127
|
+
private func createWavHeader(dataSize: Int) -> Data {
|
|
128
|
+
var header = Data()
|
|
129
|
+
|
|
130
|
+
let sampleRate = UInt32(recordingSettings!.sampleRate)
|
|
131
|
+
let channels = UInt32(recordingSettings!.numberOfChannels)
|
|
132
|
+
let bitDepth = UInt32(recordingSettings!.bitDepth)
|
|
133
|
+
|
|
134
|
+
// Calculate byteRate
|
|
135
|
+
let byteRate = sampleRate * channels * (bitDepth / 8)
|
|
136
|
+
|
|
137
|
+
// "RIFF" chunk descriptor
|
|
138
|
+
header.append(contentsOf: "RIFF".utf8)
|
|
139
|
+
header.append(contentsOf: UInt32(36 + dataSize).littleEndianBytes)
|
|
140
|
+
header.append(contentsOf: "WAVE".utf8)
|
|
141
|
+
|
|
142
|
+
// "fmt " sub-chunk
|
|
143
|
+
header.append(contentsOf: "fmt ".utf8)
|
|
144
|
+
header.append(contentsOf: UInt32(16).littleEndianBytes) // PCM format requires 16 bytes for the fmt sub-chunk
|
|
145
|
+
header.append(contentsOf: UInt16(1).littleEndianBytes) // Audio format 1 for PCM
|
|
146
|
+
header.append(contentsOf: UInt16(channels).littleEndianBytes)
|
|
147
|
+
header.append(contentsOf: sampleRate.littleEndianBytes)
|
|
148
|
+
header.append(contentsOf: byteRate.littleEndianBytes) // byteRate
|
|
149
|
+
header.append(contentsOf: UInt16(channels * (bitDepth / 8)).littleEndianBytes) // blockAlign
|
|
150
|
+
header.append(contentsOf: UInt16(bitDepth).littleEndianBytes) // bits per sample
|
|
151
|
+
|
|
152
|
+
// "data" sub-chunk
|
|
153
|
+
header.append(contentsOf: "data".utf8)
|
|
154
|
+
header.append(contentsOf: UInt32(dataSize).littleEndianBytes) // Sub-chunk data size
|
|
155
|
+
|
|
156
|
+
return header
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
66
160
|
func getStatus() -> [String: Any] {
|
|
67
161
|
let currentTime = Date()
|
|
68
162
|
let totalRecordedTime = startTime != nil ? Int(currentTime.timeIntervalSince(startTime!)) - pausedDuration : 0
|
|
69
163
|
return [
|
|
70
|
-
"duration": totalRecordedTime,
|
|
164
|
+
"duration": totalRecordedTime * 1000,
|
|
71
165
|
"isRecording": isRecording,
|
|
72
166
|
"isPaused": isPaused,
|
|
167
|
+
"mimeType": mimeType,
|
|
73
168
|
"size": totalDataSize,
|
|
74
169
|
"interval": emissionInterval
|
|
75
170
|
]
|
|
76
171
|
}
|
|
77
172
|
|
|
78
|
-
func startRecording(settings: RecordingSettings, intervalMilliseconds: Int) {
|
|
79
|
-
guard !isRecording else {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
173
|
+
func startRecording(settings: RecordingSettings, intervalMilliseconds: Int) -> String? {
|
|
174
|
+
guard !isRecording else {
|
|
175
|
+
print("Debug: Recording is already in progress.")
|
|
176
|
+
return nil
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
emissionInterval = max(100.0, Double(intervalMilliseconds)) / 1000.0
|
|
180
|
+
lastEmissionTime = Date()
|
|
181
|
+
recordingSettings = settings
|
|
83
182
|
|
|
84
|
-
// Configure audio session for the desired sample rate and channel count
|
|
85
183
|
let session = AVAudioSession.sharedInstance()
|
|
86
184
|
do {
|
|
185
|
+
print("Debug: Configuring audio session with sample rate: \(settings.sampleRate) Hz")
|
|
87
186
|
try session.setPreferredSampleRate(settings.sampleRate)
|
|
88
187
|
try session.setPreferredIOBufferDuration(1024 / settings.sampleRate)
|
|
89
188
|
try session.setCategory(.playAndRecord)
|
|
90
189
|
try session.setActive(true)
|
|
190
|
+
print("Debug: Audio session activated successfully.")
|
|
91
191
|
} catch {
|
|
92
|
-
print("Failed to set up audio session: \(error)")
|
|
93
|
-
return
|
|
192
|
+
print("Error: Failed to set up audio session with preferred settings: \(error.localizedDescription)")
|
|
193
|
+
return nil
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
NotificationCenter.default.addObserver(self, selector: #selector(handleAudioSessionInterruption), name: AVAudioSession.interruptionNotification, object: nil)
|
|
197
|
+
|
|
198
|
+
guard let channelLayout = AVAudioChannelLayout(layoutTag: settings.numberOfChannels == 1 ? kAudioChannelLayoutTag_Mono : kAudioChannelLayoutTag_Stereo) else {
|
|
199
|
+
print("Error: Failed to create channel layout.")
|
|
200
|
+
return nil
|
|
94
201
|
}
|
|
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
202
|
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
|
|
203
|
+
|
|
111
204
|
audioEngine.inputNode.installTap(onBus: 0, bufferSize: 1024, format: errorFormat) { [weak self] (buffer, time) in
|
|
112
|
-
guard let self = self, let fileURL = self.recordingFileURL else {
|
|
205
|
+
guard let self = self, let fileURL = self.recordingFileURL else {
|
|
206
|
+
print("Error: File URL or self is nil during buffer processing.")
|
|
207
|
+
return
|
|
208
|
+
}
|
|
113
209
|
self.processAudioBuffer(buffer, fileURL: fileURL)
|
|
114
210
|
}
|
|
115
211
|
|
|
116
212
|
recordingFileURL = createRecordingFile()
|
|
213
|
+
if recordingFileURL == nil {
|
|
214
|
+
print("Error: Failed to create recording file.")
|
|
215
|
+
return nil
|
|
216
|
+
}
|
|
217
|
+
|
|
117
218
|
do {
|
|
118
219
|
startTime = Date()
|
|
119
220
|
try audioEngine.start()
|
|
120
221
|
isRecording = true
|
|
222
|
+
print("Debug: Recording started successfully.")
|
|
223
|
+
return recordingFileURL?.absoluteString
|
|
121
224
|
} catch {
|
|
122
|
-
print("Could not start the audio engine: \(error)")
|
|
225
|
+
print("Error: Could not start the audio engine: \(error.localizedDescription)")
|
|
123
226
|
isRecording = false
|
|
227
|
+
return nil
|
|
124
228
|
}
|
|
125
229
|
}
|
|
230
|
+
|
|
126
231
|
|
|
127
|
-
func stopRecording() {
|
|
232
|
+
func stopRecording() -> RecordingResult? {
|
|
128
233
|
audioEngine.stop()
|
|
129
234
|
audioEngine.inputNode.removeTap(onBus: 0)
|
|
130
235
|
isRecording = false
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
236
|
+
|
|
237
|
+
guard let fileURL = recordingFileURL, let startTime = startTime else {
|
|
238
|
+
print("Recording or file URL is nil.")
|
|
239
|
+
return nil
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
let endTime = Date()
|
|
243
|
+
let duration = Int64(endTime.timeIntervalSince(startTime) * 1000) - Int64(pausedDuration * 1000)
|
|
244
|
+
|
|
245
|
+
// Calculate the total size of audio data written to the file
|
|
246
|
+
let filePath = fileURL.path
|
|
247
|
+
do {
|
|
248
|
+
let fileAttributes = try FileManager.default.attributesOfItem(atPath: filePath)
|
|
249
|
+
let fileSize = fileAttributes[FileAttributeKey.size] as? Int64 ?? 0
|
|
250
|
+
|
|
251
|
+
// Update the WAV header with the correct file size
|
|
252
|
+
updateWavHeader(fileURL: fileURL, totalDataSize: fileSize - 44) // Subtract the header size to get audio data size
|
|
253
|
+
|
|
254
|
+
let result = RecordingResult(fileUri: fileURL.absoluteString, mimeType: mimeType, duration: duration, size: fileSize)
|
|
255
|
+
recordingFileURL = nil // Reset for next recording
|
|
256
|
+
return result
|
|
257
|
+
} catch {
|
|
258
|
+
print("Failed to fetch file attributes: \(error)")
|
|
259
|
+
return nil
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
private func updateWavHeader(fileURL: URL, totalDataSize: Int64) {
|
|
264
|
+
do {
|
|
265
|
+
let fileHandle = try FileHandle(forUpdating: fileURL)
|
|
266
|
+
defer { fileHandle.closeFile() }
|
|
267
|
+
|
|
268
|
+
// Calculate sizes
|
|
269
|
+
let fileSize = totalDataSize + 44 - 8 // Total file size minus 8 bytes for 'RIFF' and size field itself
|
|
270
|
+
let dataSize = totalDataSize // Size of the 'data' sub-chunk
|
|
271
|
+
|
|
272
|
+
// Update RIFF chunk size at offset 4
|
|
273
|
+
fileHandle.seek(toFileOffset: 4)
|
|
274
|
+
let fileSizeBytes = UInt32(fileSize).littleEndianBytes
|
|
275
|
+
fileHandle.write(Data(fileSizeBytes))
|
|
276
|
+
|
|
277
|
+
// Update data chunk size at offset 40
|
|
278
|
+
fileHandle.seek(toFileOffset: 40)
|
|
279
|
+
let dataSizeBytes = UInt32(dataSize).littleEndianBytes
|
|
280
|
+
fileHandle.write(Data(dataSizeBytes))
|
|
281
|
+
|
|
282
|
+
} catch let error {
|
|
283
|
+
print("Error updating WAV header: \(error)")
|
|
284
|
+
}
|
|
134
285
|
}
|
|
135
286
|
|
|
136
287
|
private func processAudioBuffer(_ buffer: AVAudioPCMBuffer, fileURL: URL) {
|
|
@@ -146,18 +297,19 @@ class AudioStreamManager: NSObject {
|
|
|
146
297
|
}
|
|
147
298
|
let data = Data(bytes: bufferData, count: Int(audioData.mDataByteSize))
|
|
148
299
|
|
|
300
|
+
print("Writing data size: \(data.count) bytes") // Debug: Check the size of data being written
|
|
149
301
|
fileHandle.seekToEndOfFile()
|
|
150
302
|
fileHandle.write(data)
|
|
151
303
|
fileHandle.closeFile()
|
|
152
304
|
|
|
153
305
|
totalDataSize += Int64(data.count)
|
|
154
|
-
|
|
306
|
+
print("Total data size written: \(totalDataSize) bytes") // Debug: Check total data written
|
|
307
|
+
|
|
155
308
|
let currentTime = Date()
|
|
156
309
|
if let lastEmissionTime = lastEmissionTime, currentTime.timeIntervalSince(lastEmissionTime) >= emissionInterval {
|
|
157
310
|
if let startTime = startTime {
|
|
158
311
|
let recordingTime = currentTime.timeIntervalSince(startTime)
|
|
159
312
|
print("Emitting data: Recording time \(recordingTime) seconds, Data size \(totalDataSize) bytes")
|
|
160
|
-
print("delegate", self.delegate)
|
|
161
313
|
self.delegate?.audioStreamManager(self, didReceiveAudioData: data, recordingTime: recordingTime, totalDataSize: totalDataSize)
|
|
162
314
|
self.lastEmissionTime = currentTime // Update last emission time
|
|
163
315
|
self.lastEmittedSize = totalDataSize
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import ExpoModulesCore
|
|
2
|
+
import AVFoundation
|
|
2
3
|
|
|
3
4
|
let audioDataEvent: String = "AudioData"
|
|
4
5
|
|
|
5
6
|
public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
|
|
6
7
|
private var streamManager = AudioStreamManager()
|
|
7
|
-
|
|
8
|
+
|
|
8
9
|
public func definition() -> ModuleDefinition {
|
|
9
10
|
Name("ExpoAudioStream")
|
|
10
11
|
|
|
@@ -15,7 +16,7 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
|
|
|
15
16
|
print("Setting streamManager delegate")
|
|
16
17
|
streamManager.delegate = self
|
|
17
18
|
}
|
|
18
|
-
|
|
19
|
+
|
|
19
20
|
AsyncFunction("startRecording") { (options: [String: Any], promise: Promise) in
|
|
20
21
|
self.checkMicrophonePermission { granted in
|
|
21
22
|
guard granted else {
|
|
@@ -24,15 +25,15 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
|
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
// Extract settings from provided options, using default values if necessary
|
|
27
|
-
let sampleRate = options["sampleRate"] as? Double ??
|
|
28
|
-
let numberOfChannels = options["channelConfig"] as? Int ?? 1
|
|
29
|
-
let bitDepth = options["audioFormat"] as? Int ?? 16
|
|
28
|
+
let sampleRate = options["sampleRate"] as? Double ?? 16000.0 // it fails if not 48000, why?
|
|
29
|
+
let numberOfChannels = options["channelConfig"] as? Int ?? 1 // Mono channel configuration
|
|
30
|
+
let bitDepth = options["audioFormat"] as? Int ?? 16 // 16bits
|
|
30
31
|
let interval = options["interval"] as? Int ?? 1000
|
|
31
32
|
|
|
32
33
|
let settings = RecordingSettings(sampleRate: sampleRate, numberOfChannels: numberOfChannels, bitDepth: bitDepth)
|
|
33
|
-
self.streamManager.startRecording(settings: settings, intervalMilliseconds: interval)
|
|
34
|
+
let url = self.streamManager.startRecording(settings: settings, intervalMilliseconds: interval)
|
|
34
35
|
|
|
35
|
-
promise.resolve(
|
|
36
|
+
promise.resolve(url)
|
|
36
37
|
}
|
|
37
38
|
}
|
|
38
39
|
|
|
@@ -41,8 +42,17 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
|
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
AsyncFunction("stopRecording") { (promise: Promise) in
|
|
44
|
-
self.streamManager.stopRecording()
|
|
45
|
-
|
|
45
|
+
if let recordingResult = self.streamManager.stopRecording() {
|
|
46
|
+
// Convert RecordingResult to a dictionary
|
|
47
|
+
let resultDict: [String: Any] = [
|
|
48
|
+
"fileUri": recordingResult.fileUri,
|
|
49
|
+
"duration": recordingResult.duration,
|
|
50
|
+
"size": recordingResult.size
|
|
51
|
+
]
|
|
52
|
+
promise.resolve(resultDict)
|
|
53
|
+
} else {
|
|
54
|
+
promise.reject("ERROR", "Failed to stop recording or no recording in progress.")
|
|
55
|
+
}
|
|
46
56
|
}
|
|
47
57
|
|
|
48
58
|
AsyncFunction("listAudioFiles") { (promise: Promise) in
|
|
@@ -56,7 +66,6 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
|
|
|
56
66
|
}
|
|
57
67
|
|
|
58
68
|
func audioStreamManager(_ manager: AudioStreamManager, didReceiveAudioData data: Data, recordingTime: TimeInterval, totalDataSize: Int64) {
|
|
59
|
-
print("audioStreamManager debug sending data")
|
|
60
69
|
guard let fileURL = manager.recordingFileURL else { return }
|
|
61
70
|
let encodedData = data.base64EncodedString()
|
|
62
71
|
|
|
@@ -71,13 +80,13 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
|
|
|
71
80
|
"encoded": encodedData,
|
|
72
81
|
"deltaSize": deltaSize,
|
|
73
82
|
"totalSize": fileSize,
|
|
83
|
+
"mimeType": manager.mimeType,
|
|
74
84
|
"streamUuid": manager.recordingUUID?.uuidString ?? UUID().uuidString
|
|
75
85
|
]
|
|
76
86
|
|
|
77
87
|
// Update the last emitted size for the next calculation
|
|
78
88
|
manager.lastEmittedSize += Int64(deltaSize)
|
|
79
89
|
|
|
80
|
-
print("Sending audio data", eventBody)
|
|
81
90
|
// Emit the event to JavaScript
|
|
82
91
|
sendEvent(audioDataEvent, eventBody)
|
|
83
92
|
}
|
|
@@ -122,7 +131,7 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
|
|
|
122
131
|
|
|
123
132
|
do {
|
|
124
133
|
let files = try FileManager.default.contentsOfDirectory(at: documentDirectory, includingPropertiesForKeys: nil)
|
|
125
|
-
let audioFiles = files.filter { $0.pathExtension == "
|
|
134
|
+
let audioFiles = files.filter { $0.pathExtension == "wav" }.map { $0.lastPathComponent }
|
|
126
135
|
return audioFiles
|
|
127
136
|
} catch {
|
|
128
137
|
print("Error listing audio files:", error.localizedDescription)
|