react-native-audio-api 0.13.4 → 0.13.6

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.
Files changed (65) hide show
  1. package/android/src/main/cpp/audioapi/android/OnLoad.cpp +5 -1
  2. package/android/src/main/cpp/audioapi/android/core/AndroidAudioRecorder.cpp +26 -17
  3. package/android/src/main/cpp/audioapi/android/system/NativeRecorderControl.cpp +41 -0
  4. package/android/src/main/cpp/audioapi/android/system/NativeRecorderControl.h +23 -0
  5. package/android/src/main/java/com/swmansion/audioapi/system/AudioEvent.kt +1 -0
  6. package/android/src/main/java/com/swmansion/audioapi/system/CentralizedForegroundService.kt +136 -30
  7. package/android/src/main/java/com/swmansion/audioapi/system/ForegroundServiceManager.kt +47 -12
  8. package/android/src/main/java/com/swmansion/audioapi/system/MediaSessionManager.kt +23 -0
  9. package/android/src/main/java/com/swmansion/audioapi/system/NativeRecorderControl.kt +39 -0
  10. package/android/src/main/java/com/swmansion/audioapi/system/RecorderState.kt +17 -0
  11. package/android/src/main/java/com/swmansion/audioapi/system/notification/NotificationRegistry.kt +48 -20
  12. package/android/src/main/java/com/swmansion/audioapi/system/notification/RecordingNotification.kt +221 -227
  13. package/android/src/main/java/com/swmansion/audioapi/system/notification/RecordingNotificationReceiver.kt +122 -9
  14. package/android/src/main/java/com/swmansion/audioapi/system/notification/state/RecordingNotificationState.kt +17 -11
  15. package/common/cpp/audioapi/AudioAPIModuleInstaller.h +44 -0
  16. package/common/cpp/audioapi/HostObjects/inputs/AudioRecorderHostObject.cpp +18 -11
  17. package/common/cpp/audioapi/HostObjects/inputs/AudioRecorderHostObject.h +1 -0
  18. package/common/cpp/audioapi/HostObjects/utils/JsEnumParser.cpp +2 -0
  19. package/common/cpp/audioapi/core/inputs/ActiveRecorderHandle.cpp +117 -0
  20. package/common/cpp/audioapi/core/inputs/ActiveRecorderHandle.h +85 -0
  21. package/common/cpp/audioapi/core/inputs/AudioRecorder.cpp +8 -0
  22. package/common/cpp/audioapi/core/inputs/AudioRecorder.h +3 -1
  23. package/common/cpp/audioapi/core/inputs/RecorderState.h +13 -0
  24. package/common/cpp/audioapi/core/sources/RecorderAdapterNode.h +0 -1
  25. package/common/cpp/audioapi/core/utils/AudioRecorderCallback.cpp +4 -4
  26. package/common/cpp/audioapi/core/utils/AudioRecorderCallback.h +2 -4
  27. package/common/cpp/audioapi/events/AudioEvent.h +1 -0
  28. package/ios/audioapi/ios/AudioAPIModule.mm +30 -23
  29. package/ios/audioapi/ios/core/IOSAudioRecorder.mm +27 -29
  30. package/ios/audioapi/ios/core/NativeAudioRecorder.h +1 -1
  31. package/ios/audioapi/ios/core/NativeAudioRecorder.m +9 -7
  32. package/ios/audioapi/ios/core/utils/FileOptions.mm +18 -5
  33. package/ios/audioapi/ios/core/utils/IOSFileWriter.h +5 -0
  34. package/ios/audioapi/ios/core/utils/IOSFileWriter.mm +50 -20
  35. package/ios/audioapi/ios/core/utils/IOSRotatingFileWriter.mm +2 -7
  36. package/ios/audioapi/ios/system/SystemNotificationManager.mm +4 -1
  37. package/lib/commonjs/core/AudioRecorder.js +12 -4
  38. package/lib/commonjs/core/AudioRecorder.js.map +1 -1
  39. package/lib/commonjs/mock/index.js +11 -1
  40. package/lib/commonjs/mock/index.js.map +1 -1
  41. package/lib/commonjs/plugin/withAudioAPI.js +4 -2
  42. package/lib/commonjs/plugin/withAudioAPI.js.map +1 -1
  43. package/lib/module/core/AudioRecorder.js +12 -4
  44. package/lib/module/core/AudioRecorder.js.map +1 -1
  45. package/lib/module/mock/index.js +11 -1
  46. package/lib/module/mock/index.js.map +1 -1
  47. package/lib/module/plugin/withAudioAPI.js +4 -2
  48. package/lib/module/plugin/withAudioAPI.js.map +1 -1
  49. package/lib/typescript/core/AudioRecorder.d.ts +2 -0
  50. package/lib/typescript/core/AudioRecorder.d.ts.map +1 -1
  51. package/lib/typescript/mock/index.d.ts +3 -0
  52. package/lib/typescript/mock/index.d.ts.map +1 -1
  53. package/lib/typescript/plugin/withAudioAPI.d.ts +1 -0
  54. package/lib/typescript/plugin/withAudioAPI.d.ts.map +1 -1
  55. package/lib/typescript/system/notification/types.d.ts +32 -2
  56. package/lib/typescript/system/notification/types.d.ts.map +1 -1
  57. package/package.json +1 -1
  58. package/src/AudioAPIModule/globals.d.ts +5 -0
  59. package/src/core/AudioRecorder.ts +17 -4
  60. package/src/mock/index.ts +14 -1
  61. package/src/plugin/withAudioAPI.ts +4 -2
  62. package/src/system/notification/types.ts +32 -2
  63. package/android/src/main/res/layout/btn_round_ripple.xml +0 -9
  64. package/android/src/main/res/layout/notification_collapsed.xml +0 -45
  65. package/android/src/main/res/layout/notification_expanded.xml +0 -44
@@ -6,30 +6,143 @@ import android.content.Intent
6
6
  import android.util.Log
7
7
  import com.swmansion.audioapi.AudioAPIModule
8
8
  import com.swmansion.audioapi.system.AudioEvent
9
+ import com.swmansion.audioapi.system.MediaSessionManager
10
+ import com.swmansion.audioapi.system.NativeRecorderControl
11
+ import com.swmansion.audioapi.system.RecorderState
12
+ import java.util.concurrent.Executors
9
13
 
10
14
  class RecordingNotificationReceiver(
11
15
  private val module: AudioAPIModule,
12
16
  ) : BroadcastReceiver() {
13
17
  companion object {
14
- const val NOTIFICATION_RECORDING_STOPPED = "com.swmansion.audioapi.NOTIFICATION_RECORDING_STOPPED"
15
- const val NOTIFICATION_RECORDING_RESUMED = "com.swmansion.audioapi.NOTIFICATION_RECORDING_RESUMED"
18
+ const val ACTION_PAUSE = "com.swmansion.audioapi.RECORDING_NOTIFICATION_PAUSE"
19
+ const val ACTION_RESUME = "com.swmansion.audioapi.RECORDING_NOTIFICATION_RESUME"
20
+ const val ACTION_STOP = "com.swmansion.audioapi.RECORDING_NOTIFICATION_STOP"
21
+ const val ACTION_DISMISSED = "com.swmansion.audioapi.RECORDING_NOTIFICATION_DISMISSED"
22
+
23
+ /** Boolean extra of [ACTION_DISMISSED]: whether the swipe ends the recording or the notification comes back. */
24
+ const val EXTRA_DISMISS_STOPS_RECORDING = "com.swmansion.audioapi.DISMISS_STOPS_RECORDING"
25
+
16
26
  private const val TAG = "RecordingNotificationReceiver"
27
+
28
+ private val controlExecutor = Executors.newSingleThreadExecutor()
17
29
  }
18
30
 
19
31
  override fun onReceive(
20
32
  context: Context?,
21
33
  intent: Intent?,
22
34
  ) {
23
- when (intent?.action) {
24
- NOTIFICATION_RECORDING_STOPPED -> {
25
- Log.d(TAG, "Recording stopped via notification")
26
- module.invokeHandlerWithEventNameAndEventBody(AudioEvent.RECORDING_NOTIFICATION_PAUSE.ordinal, mapOf())
35
+ if (intent == null) {
36
+ return
37
+ }
38
+ when (intent.action) {
39
+ ACTION_PAUSE -> {
40
+ applyToRecorder(
41
+ action = NativeRecorderControl::pause,
42
+ intendedState = RecorderState.PAUSED,
43
+ event = AudioEvent.RECORDING_NOTIFICATION_PAUSE,
44
+ )
45
+ }
46
+
47
+ ACTION_RESUME -> {
48
+ applyToRecorder(
49
+ action = NativeRecorderControl::resume,
50
+ intendedState = RecorderState.RECORDING,
51
+ event = AudioEvent.RECORDING_NOTIFICATION_RESUME,
52
+ )
53
+ }
54
+
55
+ ACTION_STOP -> {
56
+ stopRecording()
57
+ }
58
+
59
+ ACTION_DISMISSED -> {
60
+ if (intent.getBooleanExtra(EXTRA_DISMISS_STOPS_RECORDING, false)) {
61
+ stopRecording()
62
+ } else {
63
+ restoreWhileRecording()
64
+ }
65
+ }
66
+ }
67
+ }
68
+
69
+ private fun stopRecording() =
70
+ applyToRecorder(
71
+ action = NativeRecorderControl::stop,
72
+ intendedState = RecorderState.IDLE,
73
+ event = AudioEvent.RECORDING_NOTIFICATION_STOP,
74
+ )
75
+
76
+ /**
77
+ * Handles a swipe of a pinned notification. Since Android 14 the system lets the user
78
+ * swipe away an ongoing notification even when it belongs to a foreground service, and
79
+ * `setOngoing(true)` no longer prevents that. It can be set to rearming, even though
80
+ * the user cancels it, it shows again
81
+ */
82
+ private fun restoreWhileRecording() {
83
+ val pendingResult = goAsync()
84
+ controlExecutor.execute {
85
+ try {
86
+ when (NativeRecorderControl.currentState()) {
87
+ RecorderState.RECORDING -> MediaSessionManager.setRecordingNotificationPaused(false)
88
+ RecorderState.PAUSED -> MediaSessionManager.setRecordingNotificationPaused(true)
89
+ RecorderState.IDLE -> Unit
90
+ }
91
+ } catch (e: LinkageError) {
92
+ Log.e(TAG, "Native library unavailable, cannot restore the recording notification: ${e.message}", e)
93
+ } catch (e: Exception) {
94
+ Log.e(TAG, "Error while restoring the dismissed recording notification: ${e.message}", e)
95
+ } finally {
96
+ pendingResult.finish()
27
97
  }
98
+ }
99
+ }
28
100
 
29
- NOTIFICATION_RECORDING_RESUMED -> {
30
- Log.d(TAG, "Recording resumed via notification")
31
- module.invokeHandlerWithEventNameAndEventBody(AudioEvent.RECORDING_NOTIFICATION_RESUME.ordinal, mapOf())
101
+ /**
102
+ * Every action acts on the recorder natively so the notification keeps working after
103
+ * the app task was removed, when no JS listener is reachable.
104
+ *
105
+ * Runs on an executor because [onReceive] is called on the main thread and the native
106
+ * calls take the recorder's locks
107
+ */
108
+ private fun applyToRecorder(
109
+ action: () -> RecorderState,
110
+ intendedState: RecorderState,
111
+ event: AudioEvent,
112
+ ) {
113
+ val pendingResult = goAsync()
114
+ controlExecutor.execute {
115
+ try {
116
+ val state = action()
117
+ renderNotification(state)
118
+ if (state == intendedState) {
119
+ dispatchEventToJs(event)
120
+ }
121
+ } catch (e: LinkageError) {
122
+ Log.e(TAG, "Native library unavailable, cannot handle ${event.name}: ${e.message}", e)
123
+ } catch (e: Exception) {
124
+ Log.e(TAG, "Error while handling ${event.name} on the recorder: ${e.message}", e)
125
+ } finally {
126
+ pendingResult.finish()
32
127
  }
33
128
  }
34
129
  }
130
+
131
+ private fun renderNotification(state: RecorderState) {
132
+ when (state) {
133
+ RecorderState.RECORDING -> MediaSessionManager.setRecordingNotificationPaused(false)
134
+ RecorderState.PAUSED -> MediaSessionManager.setRecordingNotificationPaused(true)
135
+ RecorderState.IDLE -> MediaSessionManager.hideRecordingNotification()
136
+ }
137
+ }
138
+
139
+ /** Syncing a live JS runtime is best-effort — in the task-removed scenario the JNI
140
+ * dispatch can throw, and that must not undo the native work that already completed. */
141
+ private fun dispatchEventToJs(event: AudioEvent) {
142
+ try {
143
+ module.invokeHandlerWithEventNameAndEventBody(event.ordinal, mapOf())
144
+ } catch (e: Exception) {
145
+ Log.e(TAG, "Recording notification action completed natively, but notifying JS failed: ${e.message}", e)
146
+ }
147
+ }
35
148
  }
@@ -1,24 +1,30 @@
1
1
  package com.swmansion.audioapi.system.notification.state
2
2
 
3
- import android.content.Intent
4
- import androidx.core.app.NotificationCompat
5
3
  import com.facebook.react.bridge.ReadableMap
6
4
  import com.swmansion.audioapi.system.notification.RecordingNotificationReceiver
7
5
 
8
- data class RecordingNotificationState(
9
- var builder: NotificationCompat.Builder? = null,
6
+ /**
7
+ * Options are sticky: a `show()` call keeps every value the previous call set unless the
8
+ * new options override it.
9
+ */
10
+ class RecordingNotificationState(
10
11
  var receiver: RecordingNotificationReceiver? = null,
11
- var initialized: Boolean,
12
- var pauseIntent: Intent? = null,
13
- var resumeIntent: Intent? = null,
12
+ var initialized: Boolean = false,
13
+ /** Last options map handed to `show()`, so an unchanged one can skip re-parsing. */
14
+ var cachedRNOptions: ReadableMap? = null,
14
15
  var title: String? = null,
15
16
  var contentText: String? = null,
16
17
  var paused: Boolean = false,
17
18
  var smallIconResourceName: String? = null,
18
19
  var largeIconResourceName: String? = null,
19
- var pauseIconResourceName: String? = null,
20
- var resumeIconResourceName: String? = null,
21
20
  var backgroundColor: Int? = null,
22
- var cachedRNOptions: ReadableMap? = null,
23
- var darkTheme: Boolean,
21
+ var showStopAction: Boolean = false,
22
+ var dismissible: Boolean = false,
23
+ var pauseActionTitle: String? = null,
24
+ var resumeActionTitle: String? = null,
25
+ var stopActionTitle: String? = null,
26
+ var deepLinkUri: String? = null,
27
+ var usesChronometer: Boolean = false,
28
+ var startedAtMs: Long? = null,
29
+ var pausedAtMs: Long? = null,
24
30
  )
@@ -8,6 +8,7 @@
8
8
  #include <audioapi/HostObjects/utils/AudioFileUtilsHostObject.h>
9
9
  #include <audioapi/core/AudioContext.h>
10
10
  #include <audioapi/core/OfflineAudioContext.h>
11
+ #include <audioapi/core/inputs/ActiveRecorderHandle.h>
11
12
  #include <audioapi/core/inputs/AudioRecorder.h>
12
13
  #include <audioapi/jsi/JsiPromise.h>
13
14
  #include <audioapi/utils/AudioBuffer.hpp>
@@ -18,6 +19,7 @@
18
19
  #include <audioapi/core/utils/worklets/SafeIncludes.h>
19
20
 
20
21
  #include <memory>
22
+ #include <utility>
21
23
  namespace audioapi {
22
24
 
23
25
  using namespace facebook;
@@ -38,6 +40,8 @@ class AudioAPIModuleInstaller {
38
40
  auto createAudioBuffer = getCreateAudioBufferFunction(jsiRuntime);
39
41
  auto createAudioDecoder = getCreateAudioDecoderFunction(jsiRuntime, jsCallInvoker);
40
42
  auto createAudioFileUtils = getCreateAudioFileUtilsFunction(jsiRuntime, jsCallInvoker);
43
+ auto isRecordingOngoing = getIsRecordingOngoingFunction(jsiRuntime);
44
+ auto consumeLastRecordingResult = getConsumeLastRecordingResultFunction(jsiRuntime);
41
45
 
42
46
  jsiRuntime->global().setProperty(*jsiRuntime, "createAudioContext", createAudioContext);
43
47
  jsiRuntime->global().setProperty(*jsiRuntime, "createAudioRecorder", createAudioRecorder);
@@ -46,6 +50,9 @@ class AudioAPIModuleInstaller {
46
50
  jsiRuntime->global().setProperty(*jsiRuntime, "createAudioBuffer", createAudioBuffer);
47
51
  jsiRuntime->global().setProperty(*jsiRuntime, "createAudioDecoder", createAudioDecoder);
48
52
  jsiRuntime->global().setProperty(*jsiRuntime, "createAudioFileUtils", createAudioFileUtils);
53
+ jsiRuntime->global().setProperty(*jsiRuntime, "isRecordingOngoing", isRecordingOngoing);
54
+ jsiRuntime->global().setProperty(
55
+ *jsiRuntime, "consumeLastRecordingResult", consumeLastRecordingResult);
49
56
 
50
57
  auto audioEventHandlerRegistryHostObject =
51
58
  std::make_shared<AudioEventHandlerRegistryHostObject>(audioEventHandlerRegistry);
@@ -150,6 +157,43 @@ class AudioAPIModuleInstaller {
150
157
  });
151
158
  }
152
159
 
160
+ static jsi::Function getIsRecordingOngoingFunction(jsi::Runtime *jsiRuntime) {
161
+ return jsi::Function::createFromHostFunction(
162
+ *jsiRuntime,
163
+ jsi::PropNameID::forAscii(*jsiRuntime, "isRecordingOngoing"),
164
+ 0,
165
+ [](jsi::Runtime &runtime, const jsi::Value &thisValue, const jsi::Value *args, size_t count)
166
+ -> jsi::Value {
167
+ return jsi::Value(ActiveRecorderHandle::global().isRecordingOngoing());
168
+ });
169
+ }
170
+
171
+ static jsi::Function getConsumeLastRecordingResultFunction(jsi::Runtime *jsiRuntime) {
172
+ return jsi::Function::createFromHostFunction(
173
+ *jsiRuntime,
174
+ jsi::PropNameID::forAscii(*jsiRuntime, "consumeLastRecordingResult"),
175
+ 0,
176
+ [](jsi::Runtime &runtime, const jsi::Value &thisValue, const jsi::Value *args, size_t count)
177
+ -> jsi::Value {
178
+ auto result = ActiveRecorderHandle::global().consumeLastRecordingResult();
179
+ if (!result.has_value()) {
180
+ return jsi::Value::null();
181
+ }
182
+
183
+ auto jsResult = jsi::Object(runtime);
184
+ auto pathsArray = jsi::Array(runtime, result->paths.size());
185
+ for (size_t i = 0; i < result->paths.size(); ++i) {
186
+ pathsArray.setValueAtIndex(
187
+ runtime, i, jsi::String::createFromUtf8(runtime, result->paths[i]));
188
+ }
189
+ jsResult.setProperty(runtime, "paths", pathsArray);
190
+ jsResult.setProperty(runtime, "size", result->size);
191
+ jsResult.setProperty(runtime, "duration", result->duration);
192
+
193
+ return jsi::Value(std::move(jsResult));
194
+ });
195
+ }
196
+
153
197
  static jsi::Function getCreateAudioDecoderFunction(
154
198
  jsi::Runtime *jsiRuntime,
155
199
  const std::shared_ptr<react::CallInvoker> &jsCallInvoker) {
@@ -2,12 +2,14 @@
2
2
 
3
3
  #include <audioapi/HostObjects/sources/AudioBufferHostObject.h>
4
4
  #include <audioapi/HostObjects/sources/RecorderAdapterNodeHostObject.h>
5
+ #include <audioapi/core/inputs/ActiveRecorderHandle.h>
5
6
  #include <audioapi/core/inputs/AudioRecorder.h>
6
7
  #include <audioapi/events/AudioEventHandlerRegistry.h>
7
8
  #include <audioapi/jsi/JsiPromise.h>
8
9
  #include <audioapi/jsi/JsiUtils.h>
9
10
  #include <audioapi/utils/AudioBuffer.hpp>
10
11
  #include <audioapi/utils/AudioFileProperties.h>
12
+ #include <audioapi/utils/Result.hpp>
11
13
  #ifdef ANDROID
12
14
  #include <audioapi/android/core/AndroidAudioRecorder.h>
13
15
  #else
@@ -49,13 +51,17 @@ AudioRecorderHostObject::AudioRecorderHostObject(
49
51
  JSI_EXPORT_FUNCTION(AudioRecorderHostObject, getCurrentDuration));
50
52
  }
51
53
 
54
+ AudioRecorderHostObject::~AudioRecorderHostObject() {
55
+ ActiveRecorderHandle::global().stopAndReturnInfo(audioRecorder_);
56
+ }
57
+
52
58
  JSI_HOST_FUNCTION_IMPL(AudioRecorderHostObject, start) {
53
59
  auto fileNameOverride = jsiutils::argToString(runtime, args, count, 0, "");
54
60
  auto audioRecorder = audioRecorder_;
55
61
 
56
62
  return promiseVendor_->createAsyncPromise(
57
63
  [audioRecorder, fileNameOverride = std::move(fileNameOverride)]() -> PromiseResolver {
58
- auto result = audioRecorder->start(fileNameOverride);
64
+ auto result = ActiveRecorderHandle::global().tryStart(audioRecorder, fileNameOverride);
59
65
 
60
66
  return [result = std::move(result)](
61
67
  jsi::Runtime &runtime) -> std::variant<jsi::Value, std::string> {
@@ -80,10 +86,11 @@ JSI_HOST_FUNCTION_IMPL(AudioRecorderHostObject, stop) {
80
86
  auto audioRecorder = audioRecorder_;
81
87
 
82
88
  return promiseVendor_->createAsyncPromise([audioRecorder]() -> PromiseResolver {
83
- auto result = audioRecorder->stop();
89
+ auto result = ActiveRecorderHandle::global().stopAndReturnInfo(audioRecorder);
90
+
91
+ using returnValue = std::variant<jsi::Value, std::string>;
84
92
 
85
- return [result =
86
- std::move(result)](jsi::Runtime &runtime) -> std::variant<jsi::Value, std::string> {
93
+ return [result = std::move(result)](jsi::Runtime &runtime) -> returnValue {
87
94
  auto jsResult = jsi::Object(runtime);
88
95
 
89
96
  jsResult.setProperty(
@@ -92,15 +99,15 @@ JSI_HOST_FUNCTION_IMPL(AudioRecorderHostObject, stop) {
92
99
  jsi::String::createFromUtf8(runtime, result.is_ok() ? "success" : "error"));
93
100
 
94
101
  if (result.is_ok()) {
95
- auto info = result.unwrap();
96
- const auto &paths = std::get<0>(info);
97
- auto pathsArray = jsi::Array(runtime, paths.size());
98
- for (size_t i = 0; i < paths.size(); ++i) {
99
- pathsArray.setValueAtIndex(runtime, i, jsi::String::createFromUtf8(runtime, paths[i]));
102
+ const auto &info = result.unwrap();
103
+ auto pathsArray = jsi::Array(runtime, info.paths.size());
104
+ for (size_t i = 0; i < info.paths.size(); ++i) {
105
+ pathsArray.setValueAtIndex(
106
+ runtime, i, jsi::String::createFromUtf8(runtime, info.paths[i]));
100
107
  }
101
108
  jsResult.setProperty(runtime, "paths", pathsArray);
102
- jsResult.setProperty(runtime, "size", std::get<1>(info));
103
- jsResult.setProperty(runtime, "duration", std::get<2>(info));
109
+ jsResult.setProperty(runtime, "size", info.size);
110
+ jsResult.setProperty(runtime, "duration", info.duration);
104
111
  } else {
105
112
  jsResult.setProperty(
106
113
  runtime, "message", jsi::String::createFromUtf8(runtime, result.unwrap_err()));
@@ -18,6 +18,7 @@ class AudioRecorderHostObject : public JsiHostObject {
18
18
  const std::shared_ptr<AudioEventHandlerRegistry> &audioEventHandlerRegistry,
19
19
  jsi::Runtime *runtime,
20
20
  const std::shared_ptr<react::CallInvoker> &callInvoker);
21
+ ~AudioRecorderHostObject() override;
21
22
 
22
23
  JSI_HOST_FUNCTION_DECL(start);
23
24
  JSI_HOST_FUNCTION_DECL(stop);
@@ -150,6 +150,8 @@ AudioEvent audioEventFromString(const std::string &event) {
150
150
  return AudioEvent::RECORDER_ERROR;
151
151
  if (event == "bufferingStateChanged")
152
152
  return AudioEvent::BUFFERING_STATE_CHANGE;
153
+ if (event == "recordingNotificationStop")
154
+ return AudioEvent::RECORDING_NOTIFICATION_STOP;
153
155
 
154
156
  throw std::invalid_argument("Unknown audio event: " + event);
155
157
  }
@@ -0,0 +1,117 @@
1
+ #include <audioapi/core/inputs/ActiveRecorderHandle.h>
2
+
3
+ #include <audioapi/core/inputs/AudioRecorder.h>
4
+
5
+ #include <memory>
6
+ #include <string>
7
+ #include <tuple>
8
+ #include <utility>
9
+
10
+ namespace audioapi {
11
+
12
+ ActiveRecorderHandle &ActiveRecorderHandle::global() {
13
+ static ActiveRecorderHandle handle;
14
+ return handle;
15
+ }
16
+
17
+ Result<NoneType, std::string> ActiveRecorderHandle::tryStart(
18
+ const std::shared_ptr<AudioRecorder> &recorder,
19
+ const std::string &fileNameOverride) {
20
+ if (!recorder) {
21
+ return Result<NoneType, std::string>::Err("Cannot start a null recorder");
22
+ }
23
+
24
+ std::scoped_lock lock(mutex_);
25
+
26
+ auto prevRecorder = recorder_.lock();
27
+ if (prevRecorder && prevRecorder != recorder && !prevRecorder->isIdle()) {
28
+ return Result<NoneType, std::string>::Err("Another recording is already in progress");
29
+ }
30
+
31
+ auto result = recorder->start(fileNameOverride);
32
+ if (result.is_ok()) {
33
+ recorder_ = recorder;
34
+ }
35
+ return result;
36
+ }
37
+
38
+ void ActiveRecorderHandle::clearRecorder() {
39
+ std::scoped_lock lock(mutex_);
40
+ recorder_.reset();
41
+ }
42
+
43
+ RecorderState ActiveRecorderHandle::currentState() const {
44
+ std::scoped_lock lock(mutex_);
45
+ const std::shared_ptr<AudioRecorder> recorder = recorder_.lock();
46
+ return stateOf(recorder);
47
+ }
48
+
49
+ bool ActiveRecorderHandle::isRecordingOngoing() const {
50
+ return currentState() != RecorderState::Idle;
51
+ }
52
+
53
+ RecorderState ActiveRecorderHandle::pause() {
54
+ std::scoped_lock lock(mutex_);
55
+ const std::shared_ptr<AudioRecorder> recorder = recorder_.lock();
56
+ if (recorder && recorder->isRecording()) {
57
+ recorder->pause();
58
+ }
59
+ return stateOf(recorder);
60
+ }
61
+
62
+ RecorderState ActiveRecorderHandle::resume() {
63
+ std::scoped_lock lock(mutex_);
64
+ const std::shared_ptr<AudioRecorder> recorder = recorder_.lock();
65
+ if (recorder && recorder->isPaused()) {
66
+ recorder->resume();
67
+ }
68
+ return stateOf(recorder);
69
+ }
70
+
71
+ Result<FileInfo, std::string> ActiveRecorderHandle::stopAndReturnInfo() {
72
+ std::scoped_lock lock(mutex_);
73
+ const std::shared_ptr<AudioRecorder> recorder = recorder_.lock();
74
+ if (!recorder) {
75
+ return Result<FileInfo, std::string>::Err("Recorder is not in recording state.");
76
+ }
77
+
78
+ auto result = recorder->stop();
79
+ if (!result.is_ok()) {
80
+ return Result<FileInfo, std::string>::Err(result.unwrap_err());
81
+ }
82
+
83
+ auto [paths, size, duration] = result.unwrap();
84
+ FileInfo recording{.paths = std::move(paths), .size = size, .duration = duration};
85
+ if (!recording.paths.empty()) {
86
+ lastResult_ = recording;
87
+ }
88
+ clearRecorder();
89
+ return Result<FileInfo, std::string>::Ok(std::move(recording));
90
+ }
91
+
92
+ Result<FileInfo, std::string> ActiveRecorderHandle::stopAndReturnInfo(
93
+ const std::shared_ptr<AudioRecorder> &expected) {
94
+ std::scoped_lock lock(mutex_);
95
+ if (recorder_.lock() != expected) {
96
+ // if we do not store provided recorder, it meands that it cannot be started, so it is in idle state
97
+ return Result<FileInfo, std::string>::Err("Recorder is not in recording state.");
98
+ }
99
+ return stopAndReturnInfo();
100
+ }
101
+
102
+ RecorderState ActiveRecorderHandle::stopAndReturnState() {
103
+ std::scoped_lock lock(mutex_);
104
+ stopAndReturnInfo();
105
+ return currentState();
106
+ }
107
+
108
+ std::optional<FileInfo> ActiveRecorderHandle::consumeLastRecordingResult() {
109
+ std::scoped_lock lock(mutex_);
110
+ return std::exchange(lastResult_, std::nullopt);
111
+ }
112
+
113
+ RecorderState ActiveRecorderHandle::stateOf(const std::shared_ptr<AudioRecorder> &recorder) {
114
+ return recorder ? recorder->getState() : RecorderState::Idle;
115
+ }
116
+
117
+ } // namespace audioapi
@@ -0,0 +1,85 @@
1
+ #pragma once
2
+
3
+ #include <audioapi/core/inputs/RecorderState.h>
4
+ #include <audioapi/utils/Macros.h>
5
+ #include <audioapi/utils/Result.hpp>
6
+ #include <memory>
7
+ #include <mutex>
8
+ #include <optional>
9
+ #include <string>
10
+ #include <vector>
11
+
12
+ namespace audioapi {
13
+
14
+ class AudioRecorder;
15
+
16
+ struct FileInfo {
17
+ std::vector<std::string> paths;
18
+ double size;
19
+ double duration;
20
+ };
21
+
22
+ /// @brief The process-wide view of the recording session that is currently active.
23
+ ///
24
+ /// The JavaScript host object owns the AudioRecorder. Native code still needs
25
+ /// to reach that recorder when js is dead. This handle publishes a non-owning
26
+ /// reference to the recorder so those actions can reach it.
27
+ /// Only one recording session is published at a time.
28
+ class ActiveRecorderHandle {
29
+ public:
30
+ static ActiveRecorderHandle &global();
31
+ DELETE_COPY_AND_MOVE(ActiveRecorderHandle);
32
+ ~ActiveRecorderHandle() = default;
33
+
34
+ /// @brief Starts @p recorder and stores it in the slot unless another non-idle
35
+ /// recorder already occupies it. The slot is assigned only if start succeeds.
36
+ Result<NoneType, std::string> tryStart(
37
+ const std::shared_ptr<AudioRecorder> &recorder,
38
+ const std::string &fileNameOverride);
39
+
40
+ /// @brief The state of the recorder in the slot, or Idle when the slot is empty.
41
+ [[nodiscard]] RecorderState currentState() const;
42
+
43
+ /// @brief True while a recording session is active; a paused recording counts as
44
+ /// ongoing because it still owns an open output file.
45
+ [[nodiscard]] bool isRecordingOngoing() const;
46
+
47
+ /// @brief Pauses an actively recording session; a no-op in any other state.
48
+ RecorderState pause();
49
+
50
+ /// @brief Resumes a paused session; a no-op in any other state.
51
+ RecorderState resume();
52
+
53
+ /// @brief Stops the occupant and returns AudioRecorder::stop()'s Result,
54
+ /// including the original error. On success with non-empty paths, stashes a
55
+ /// copy for consumeLastRecordingResult() and clears the slot. Blocks until
56
+ /// the output file is finalized. Because of this, don't call on a UI thread.
57
+ Result<FileInfo, std::string> stopAndReturnInfo();
58
+
59
+ /// @brief stopAndReturnInfo() if the slot still holds @p expected; otherwise
60
+ /// returns an error and does not stop the occupant.
61
+ Result<FileInfo, std::string> stopAndReturnInfo(const std::shared_ptr<AudioRecorder> &expected);
62
+
63
+ /// @brief Stops the occupant (same as stopAndReturnInfo) and returns the
64
+ /// resulting slot state. The stop Result is discarded; file info is still stashed.
65
+ RecorderState stopAndReturnState();
66
+
67
+ /// @brief Consume-once: returns the file info stashed by stopAndReturnInfo()
68
+ /// and clears it, or std::nullopt when nothing is stashed.
69
+ std::optional<FileInfo> consumeLastRecordingResult();
70
+
71
+ private:
72
+ ActiveRecorderHandle() = default;
73
+ friend struct ActiveRecorderHandleTestPeer;
74
+
75
+ static RecorderState stateOf(const std::shared_ptr<AudioRecorder> &recorder);
76
+
77
+ /// @brief Drops the slot without stopping the recorder.
78
+ void clearRecorder();
79
+
80
+ mutable std::recursive_mutex mutex_;
81
+ std::weak_ptr<AudioRecorder> recorder_;
82
+ std::optional<FileInfo> lastResult_;
83
+ };
84
+
85
+ } // namespace audioapi
@@ -51,6 +51,14 @@ double AudioRecorder::getCurrentDuration() const {
51
51
  return duration;
52
52
  }
53
53
 
54
+ RecorderState AudioRecorder::getState() const {
55
+ if (isIdle()) {
56
+ return RecorderState::Idle;
57
+ }
58
+
59
+ return isPaused() ? RecorderState::Paused : RecorderState::Recording;
60
+ }
61
+
54
62
  bool AudioRecorder::usesCallback() const {
55
63
  return wantsCallback() && callbackOutputConfigured_.load(std::memory_order_acquire);
56
64
  }
@@ -1,5 +1,6 @@
1
1
  #pragma once
2
2
 
3
+ #include <audioapi/core/inputs/RecorderState.h>
3
4
  #include <audioapi/events/AudioEventHandlerRegistry.h>
4
5
  #include <audioapi/events/EventCaller.hpp>
5
6
  #include <audioapi/utils/AudioBuffer.hpp>
@@ -29,7 +30,6 @@ class AudioRecorder {
29
30
  public:
30
31
  using StopResult = Result<std::tuple<std::vector<std::string>, double, double>, std::string>;
31
32
 
32
- enum class RecorderState : uint8_t { Idle = 0, Recording, Paused };
33
33
  explicit AudioRecorder(
34
34
  const std::shared_ptr<AudioEventHandlerRegistry> &audioEventHandlerRegistry)
35
35
  : audioEventHandlerRegistry_(audioEventHandlerRegistry),
@@ -86,6 +86,8 @@ class AudioRecorder {
86
86
  /// both platforms.
87
87
  bool isIdle() const;
88
88
 
89
+ RecorderState getState() const;
90
+
89
91
  protected:
90
92
  bool wantsCallback() const;
91
93
  bool wantsFileOutput() const;
@@ -0,0 +1,13 @@
1
+ #pragma once
2
+
3
+ #include <cstdint>
4
+
5
+ namespace audioapi {
6
+
7
+ /// @brief Lifecycle state of an AudioRecorder.
8
+ ///
9
+ /// Mirrored by Kotlin's com.swmansion.audioapi.system.RecorderState, which maps the
10
+ /// values by ordinal — keep the two declarations in the same order.
11
+ enum class RecorderState : uint8_t { Idle = 0, Recording, Paused };
12
+
13
+ } // namespace audioapi
@@ -57,7 +57,6 @@ class RecorderAdapterNode : public AudioNode {
57
57
  AudioBuffer overflowBuffer_;
58
58
  size_t overflowSize_ = 0;
59
59
 
60
- std::atomic<bool> isInitialized_{false};
61
60
  /// Incremented around each processNode() call; adapterCleanup waits for quiescence.
62
61
  std::atomic<uint32_t> currentProcesses_{0};
63
62
  };
@@ -76,14 +76,14 @@ void AudioRecorderCallback::invokeCallback(
76
76
  framesEmitted_ += numFrames;
77
77
  }
78
78
 
79
- void AudioRecorderCallback::assignOnErrorCallbackId(uint64_t callbackId) {
80
- errorEvent_.assignCallbackId(callbackId);
81
- }
82
-
83
79
  /// @brief Invokes the error callback with the provided message.
84
80
  /// @param message The error message to be sent to the callback.
85
81
  void AudioRecorderCallback::invokeOnErrorCallback(const std::string &message) {
86
82
  errorEvent_.dispatch(StringPayload{.name = "message", .reason = message});
87
83
  }
88
84
 
85
+ void AudioRecorderCallback::assignOnErrorCallbackId(uint64_t callbackId) {
86
+ errorEvent_.assignCallbackId(callbackId);
87
+ }
88
+
89
89
  } // namespace audioapi
@@ -4,6 +4,7 @@
4
4
  #include <audioapi/utils/AudioArray.hpp>
5
5
  #include <audioapi/utils/AudioBuffer.hpp>
6
6
  #include <audioapi/utils/CircularArray.hpp>
7
+ #include <audioapi/utils/Macros.h>
7
8
  #include <audioapi/utils/Result.hpp>
8
9
  #include <audioapi/utils/SpscChannel.hpp>
9
10
  #include <audioapi/utils/TaskOffloader.hpp>
@@ -25,10 +26,7 @@ class AudioRecorderCallback {
25
26
  size_t bufferLength,
26
27
  int channelCount,
27
28
  uint64_t callbackId);
28
- AudioRecorderCallback(const AudioRecorderCallback &) = delete;
29
- AudioRecorderCallback(AudioRecorderCallback &&) = delete;
30
- AudioRecorderCallback &operator=(const AudioRecorderCallback &) = delete;
31
- AudioRecorderCallback &operator=(AudioRecorderCallback &&) = delete;
29
+ DELETE_COPY_AND_MOVE(AudioRecorderCallback);
32
30
  virtual ~AudioRecorderCallback();
33
31
 
34
32
  virtual void cleanup() = 0;