Skip to content

Commit 51c880c

Browse files
authored
feat: rotating file writer (#913)
1 parent 4cc65e2 commit 51c880c

34 files changed

Lines changed: 714 additions & 161 deletions

apps/common-app/src/demos/Record/Record.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ const Record: FC = () => {
241241
}, [onPauseRecording, onResumeRecording]);
242242

243243
useEffect(() => {
244-
Recorder.enableFileOutput();
244+
Recorder.enableFileOutput({ rotateIntervalBytes: 1024 * 1024 });
245245

246246
return () => {
247247
Recorder.disableFileOutput();

packages/audiodocs/docs/inputs/audio-recorder.mdx

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ const MyRecorder: React.FC = () => {
145145
return;
146146
}
147147

148-
console.log('Recording started to file:', result.path);
148+
console.log('Recording started');
149149
setIsRecording(true);
150150
};
151151

@@ -387,11 +387,7 @@ export default MyRecorder;
387387
fileNameOverride: `my_audio_${mySessionId}`
388388
});
389389

390-
if (result.status === 'success') {
391-
const openedFilePath = result.path;
392-
} else if (result.status === 'error') {
393-
console.error(result.message);
394-
}
390+
console.log(result.status);
395391
```
396392
</td>
397393
</tr>
@@ -412,7 +408,7 @@ export default MyRecorder;
412408
const result = audioRecorder.stop();
413409

414410
if (result.status === 'success') {
415-
const { path, duration, size } = result;
411+
const { paths, duration, size } = result;
416412
} else if (result.status === 'error') {
417413
console.error(result.message);
418414
}
@@ -722,6 +718,7 @@ interface OnAudioReadyEventType {
722718
```tsx
723719
interface AudioRecorderFileOptions {
724720
channelCount?: number;
721+
rotateIntervalBytes?: number;
725722

726723
format?: FileFormat;
727724
preset?: FilePresetType;
@@ -734,6 +731,7 @@ interface AudioRecorderFileOptions {
734731
```
735732

736733
- `channelCount` - The desired channel count in the resulting file. not all file formats supports all possible channel counts.
734+
- `rotateIntervalBytes` - The threshold size (in bytes) at which the recorder will start writing to a new file. If set to 0 (default), file output rotation is disabled. When active, new files are named with the original prefix appended with a timestamp.
737735
- `format` - The desired extension and file format of the recorder file. Check: [FileFormat](#fileformat) below.
738736
- `preset` - The desired recorder file properties, you can use either one of built-in properties or tweak low-level parameters yourself. Check [FilePresetType](#filepresettype) for more details.
739737
- `directory` - Either `FileDirectory.Cache` or `FileDirectory.Document` (default: `FileDirectory.Cache`). Determines the system directory that the file will be saved to.
@@ -760,13 +758,13 @@ enum FileFormat {
760758

761759
```tsx
762760
interface FileInfo {
763-
path: string;
761+
paths: string[];
764762
size: number;
765763
duration: number;
766764
}
767765
```
768766

769-
- `path` - The path to the recorded audio file.
767+
- `paths` - Paths to the recorded audio files. When file rotation is disabled it has only one entry, otherwise list of paths to recorder files is returned.
770768
- `size` - The file size (in MB).
771769
- `duration` - The recording duration (in seconds).
772770

packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AndroidAudioRecorder.cpp

Lines changed: 102 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#include <android/log.h>
12
#include <audioapi/android/core/AndroidAudioRecorder.h>
23
#include <audioapi/android/core/utils/AndroidFileWriterBackend.h>
34
#include <audioapi/android/core/utils/AndroidRecorderCallback.h>
@@ -6,6 +7,7 @@
67
#include <audioapi/android/core/utils/ffmpegBackend/FFmpegFileWriter.h>
78
#endif // RN_AUDIO_API_FFMPEG_DISABLED
89

10+
#include <audioapi/android/core/utils/AndroidRotatingFileWriter.h>
911
#include <audioapi/android/core/utils/miniaudioBackend/MiniAudioFileWriter.h>
1012
#include <audioapi/core/sources/RecorderAdapterNode.h>
1113
#include <audioapi/core/utils/Constants.h>
@@ -19,6 +21,7 @@
1921
#include <string>
2022
#include <unordered_map>
2123
#include <utility>
24+
#include <vector>
2225

2326
namespace audioapi {
2427

@@ -102,37 +105,34 @@ Result<NoneType, std::string> AndroidAudioRecorder::openAudioStream() {
102105
/// RN side requires their "file://" prefix, but sometimes it returned raw path.
103106
/// Most likely this was due to alpha version mistakes, but in case of problems leaving this here. (ㆆ _ ㆆ)
104107
/// @returns On success, returns the file URI where the recording is being saved (if file output is enabled).
105-
Result<std::string, std::string> AndroidAudioRecorder::start(const std::string &fileNameOverride) {
108+
Result<NoneType, std::string> AndroidAudioRecorder::start(const std::string &fileNameOverride) {
106109
std::scoped_lock startLock(callbackMutex_, fileWriterMutex_, adapterNodeMutex_);
107110

108111
if (!isIdle()) {
109-
return Result<std::string, std::string>::Err("Recorder is already recording");
112+
return Result<NoneType, std::string>::Err("Recorder is already recording");
110113
}
111114

112115
auto streamResult = openAudioStream();
113116

114117
if (!streamResult.is_ok()) {
115-
return Result<std::string, std::string>::Err(streamResult.unwrap_err());
118+
return Result<NoneType, std::string>::Err(streamResult.unwrap_err());
116119
}
117120

118121
if (mStream_ == nullptr) {
119-
return Result<std::string, std::string>::Err("Audio stream is not initialized.");
122+
return Result<NoneType, std::string>::Err("Audio stream is not initialized.");
120123
}
121124

122125
if (usesFileOutput()) {
123-
auto fileResult = std::static_pointer_cast<AndroidFileWriterBackend>(fileWriter_)
124-
->openFile(
125-
streamSampleRate_,
126-
streamChannelCount_,
127-
streamMaxBufferSizeInFrames_,
128-
fileNameOverride);
129-
130-
if (!fileResult.is_ok()) {
131-
return Result<std::string, std::string>::Err(
132-
"Failed to open file for writing: " + fileResult.unwrap_err());
126+
recordingSegmentPaths_.clear();
127+
auto writerResult = setupFileWriter(fileProperties_, fileNameOverride);
128+
if (!writerResult.is_ok()) {
129+
return writerResult;
133130
}
134-
135-
filePath_ = fileResult.unwrap();
131+
__android_log_print(
132+
ANDROID_LOG_INFO,
133+
"AndroidAudioRecorder",
134+
"File created successfully at path: %s",
135+
filePath_.c_str());
136136
}
137137

138138
if (usesCallback()) {
@@ -149,32 +149,32 @@ Result<std::string, std::string> AndroidAudioRecorder::start(const std::string &
149149
auto result = mStream_->requestStart();
150150

151151
if (result != oboe::Result::OK) {
152-
return Result<std::string, std::string>::Err(
152+
return Result<NoneType, std::string>::Err(
153153
"Failed to start stream: " + std::string(oboe::convertToText(result)));
154154
}
155155

156156
state_.store(RecorderState::Recording, std::memory_order_release);
157-
return Result<std::string, std::string>::Ok(std::format("file://{}", filePath_));
157+
return Result<NoneType, std::string>::Ok(None);
158158
}
159159

160160
/// @brief Stops the audio stream and finalizes any output (file writing, callback, adapter node).
161161
/// This method should be called from the JS thread only.
162162
/// @returns On success, returns the file URI, size in MB and duration in seconds of the recorded file (if file output is enabled).
163163
/// NOTE: due to the file access nature on Android, the size might sometimes be zeroed (really long files).
164-
Result<std::tuple<std::string, double, double>, std::string> AndroidAudioRecorder::stop() {
164+
Result<std::tuple<std::vector<std::string>, double, double>, std::string>
165+
AndroidAudioRecorder::stop() {
165166
std::scoped_lock stopLock(callbackMutex_, fileWriterMutex_, adapterNodeMutex_);
166167

167-
std::string filePath = std::format("file://{}", filePath_);
168168
double outputFileSize = 0.0;
169169
double outputDuration = 0.0;
170170

171171
if (isIdle()) {
172-
return Result<std::tuple<std::string, double, double>, std::string>::Err(
172+
return Result<std::tuple<std::vector<std::string>, double, double>, std::string>::Err(
173173
"Recorder is not in recording state.");
174174
}
175175

176176
if (mStream_ == nullptr) {
177-
return Result<std::tuple<std::string, double, double>, std::string>::Err(
177+
return Result<std::tuple<std::vector<std::string>, double, double>, std::string>::Err(
178178
"Audio stream is not initialized.");
179179
}
180180

@@ -185,7 +185,7 @@ Result<std::tuple<std::string, double, double>, std::string> AndroidAudioRecorde
185185
auto fileResult = fileWriter_->closeFile();
186186

187187
if (!fileResult.is_ok()) {
188-
return Result<std::tuple<std::string, double, double>, std::string>::Err(
188+
return Result<std::tuple<std::vector<std::string>, double, double>, std::string>::Err(
189189
"Failed to close file: " + fileResult.unwrap_err());
190190
}
191191

@@ -201,9 +201,20 @@ Result<std::tuple<std::string, double, double>, std::string> AndroidAudioRecorde
201201
adapterNode_->adapterCleanup();
202202
}
203203

204+
std::vector<std::string> outputPaths;
205+
for (const auto &raw : recordingSegmentPaths_) {
206+
if (!raw.empty()) {
207+
outputPaths.push_back(std::format("file://{}", raw));
208+
}
209+
}
210+
if (usesFileOutput() && outputPaths.empty() && !filePath_.empty()) {
211+
outputPaths.push_back(std::format("file://{}", filePath_));
212+
}
213+
214+
recordingSegmentPaths_.clear();
204215
filePath_ = "";
205-
return Result<std::tuple<std::string, double, double>, std::string>::Ok(
206-
{filePath, outputFileSize, outputDuration});
216+
return Result<std::tuple<std::vector<std::string>, double, double>, std::string>::Ok(
217+
std::make_tuple(std::move(outputPaths), outputFileSize, outputDuration));
207218
}
208219

209220
/// @brief Enables file output for the recorder with the specified properties.
@@ -213,37 +224,85 @@ Result<std::tuple<std::string, double, double>, std::string> AndroidAudioRecorde
213224
/// This method should be called from the JS thread only.
214225
/// @param properties Properties defining the audio file format and encoding options.
215226
/// @returns On success, returns the file URI where the recording is being saved, otherwise returns an error message.
216-
Result<std::string, std::string> AndroidAudioRecorder::enableFileOutput(
227+
Result<NoneType, std::string> AndroidAudioRecorder::enableFileOutput(
217228
std::shared_ptr<AudioFileProperties> properties) {
218229
std::scoped_lock fileWriterLock(fileWriterMutex_);
230+
fileProperties_ = properties;
219231

220-
if (properties->format == AudioFileProperties::Format::WAV) {
221-
fileWriter_ = std::make_shared<MiniAudioFileWriter>(audioEventHandlerRegistry_, properties);
222-
} else {
232+
if (!isIdle()) {
233+
auto writerResult = setupFileWriter(properties);
234+
if (!writerResult.is_ok()) {
235+
return writerResult;
236+
}
237+
}
238+
239+
fileOutputEnabled_.store(true, std::memory_order_release);
240+
return Result<NoneType, std::string>::Ok(None);
241+
}
242+
243+
std::shared_ptr<AudioFileWriter> AndroidAudioRecorder::createFileWriter(
244+
const std::shared_ptr<AudioFileProperties> &props) {
245+
if (props->format == AudioFileProperties::Format::WAV) {
246+
return std::make_shared<MiniAudioFileWriter>(
247+
audioEventHandlerRegistry_,
248+
props,
249+
streamSampleRate_,
250+
streamChannelCount_,
251+
streamMaxBufferSizeInFrames_);
252+
}
223253
#if !RN_AUDIO_API_FFMPEG_DISABLED
224-
fileWriter_ = std::make_shared<android::ffmpeg::FFmpegAudioFileWriter>(
225-
audioEventHandlerRegistry_, properties);
254+
return std::make_shared<android::ffmpeg::FFmpegAudioFileWriter>(
255+
audioEventHandlerRegistry_,
256+
props,
257+
streamSampleRate_,
258+
streamChannelCount_,
259+
streamMaxBufferSizeInFrames_);
226260
#else
261+
return nullptr;
262+
#endif
263+
}
264+
265+
Result<NoneType, std::string> AndroidAudioRecorder::setupFileWriter(
266+
const std::shared_ptr<AudioFileProperties> &properties,
267+
const std::string &fileNameOverride) {
268+
#if RN_AUDIO_API_FFMPEG_DISABLED
269+
if (properties->format != AudioFileProperties::Format::WAV) {
227270
return Result<std::string, std::string>::Err(
228271
"FFmpeg backend is disabled. Cannot create file writer for the requested format. Use WAV format instead.");
272+
}
229273
#endif
274+
275+
if (properties->rotateIntervalBytes > 0) {
276+
fileWriter_ = std::make_shared<AndroidRotatingFileWriter>(
277+
audioEventHandlerRegistry_,
278+
properties,
279+
properties->rotateIntervalBytes,
280+
[this](const std::shared_ptr<AudioFileProperties> &p) { return createFileWriter(p); },
281+
[this](const std::string &path) {
282+
if (!path.empty()) {
283+
recordingSegmentPaths_.push_back(path);
284+
}
285+
});
286+
} else {
287+
fileWriter_ = createFileWriter(properties);
230288
}
231289

232-
if (!isIdle()) {
233-
auto fileResult =
234-
std::static_pointer_cast<AndroidFileWriterBackend>(fileWriter_)
235-
->openFile(streamSampleRate_, streamChannelCount_, streamMaxBufferSizeInFrames_, "");
290+
fileWriter_->setOnErrorCallback(errorCallbackId_.load(std::memory_order_acquire));
236291

237-
if (!fileResult.is_ok()) {
238-
return Result<std::string, std::string>::Err(
239-
"Failed to open file for writing: " + fileResult.unwrap_err());
240-
}
292+
auto backend = std::static_pointer_cast<AndroidFileWriterBackend>(fileWriter_);
293+
auto fileResult = backend->openFile(
294+
streamSampleRate_, streamChannelCount_, streamMaxBufferSizeInFrames_, fileNameOverride);
241295

242-
filePath_ = fileResult.unwrap();
296+
if (!fileResult.is_ok()) {
297+
return Result<NoneType, std::string>::Err(
298+
"Failed to open file for writing: " + fileResult.unwrap_err());
243299
}
244300

245-
fileOutputEnabled_.store(true, std::memory_order_release);
246-
return Result<std::string, std::string>::Ok(filePath_);
301+
filePath_ = fileResult.unwrap();
302+
if (properties->rotateIntervalBytes == 0) {
303+
recordingSegmentPaths_.push_back(filePath_);
304+
}
305+
return Result<NoneType, std::string>::Ok(None);
247306
}
248307

249308
/// @brief Disables file output for the recorder.
@@ -360,8 +419,7 @@ oboe::DataCallbackResult AndroidAudioRecorder::onAudioReady(
360419

361420
if (usesFileOutput()) {
362421
if (auto fileWriterLock = Locker::tryLock(fileWriterMutex_)) {
363-
std::static_pointer_cast<AndroidFileWriterBackend>(fileWriter_)
364-
->writeAudioData(audioData, numFrames);
422+
fileWriter_->writeAudioData(audioData, numFrames);
365423
}
366424
}
367425

packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/AndroidAudioRecorder.h

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include <oboe/Oboe.h>
99
#include <memory>
1010
#include <string>
11+
#include <vector>
1112

1213
namespace audioapi {
1314

@@ -23,10 +24,10 @@ class AndroidAudioRecorder : public oboe::AudioStreamCallback, public AudioRecor
2324
~AndroidAudioRecorder() override;
2425
void cleanup();
2526

26-
Result<std::string, std::string> start(const std::string &fileNameOverride) override;
27-
Result<std::tuple<std::string, double, double>, std::string> stop() override;
27+
Result<NoneType, std::string> start(const std::string &fileNameOverride) override;
28+
Result<std::tuple<std::vector<std::string>, double, double>, std::string> stop() override;
2829

29-
Result<std::string, std::string> enableFileOutput(
30+
Result<NoneType, std::string> enableFileOutput(
3031
std::shared_ptr<AudioFileProperties> properties) override;
3132
void disableFileOutput() override;
3233

@@ -60,7 +61,13 @@ class AndroidAudioRecorder : public oboe::AudioStreamCallback, public AudioRecor
6061
facebook::jni::global_ref<NativeAudioRecorder> nativeAudioRecorder_;
6162

6263
std::shared_ptr<oboe::AudioStream> mStream_;
64+
std::vector<std::string> recordingSegmentPaths_;
6365
Result<NoneType, std::string> openAudioStream();
66+
std::shared_ptr<AudioFileWriter> createFileWriter(
67+
const std::shared_ptr<AudioFileProperties> &props);
68+
Result<NoneType, std::string> setupFileWriter(
69+
const std::shared_ptr<AudioFileProperties> &properties,
70+
const std::string &fileNameOverride = "");
6471
};
6572

6673
} // namespace audioapi

packages/react-native-audio-api/android/src/main/cpp/audioapi/android/core/utils/AndroidFileWriterBackend.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#include <audioapi/android/core/utils/AndroidFileWriterBackend.h>
2+
23
#include <memory>
3-
#include <utility>
44

55
namespace audioapi {
66
AndroidFileWriterBackend::AndroidFileWriterBackend(
@@ -18,6 +18,6 @@ AndroidFileWriterBackend::AndroidFileWriterBackend(
1818
}
1919

2020
void AndroidFileWriterBackend::writeAudioData(void *data, int numFrames) {
21-
offloader_->getSender()->send({data, numFrames});
21+
offloader_->getSender()->send({.data = data, .numFrames = numFrames});
2222
}
2323
} // namespace audioapi

0 commit comments

Comments
 (0)