rns-recplay 2.0.3 → 3.0.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.
@@ -14,12 +14,10 @@ import android.os.Build
14
14
  import android.os.Handler
15
15
  import android.os.Looper
16
16
  import android.util.Log
17
- import android.view.KeyEvent
18
17
  import androidx.core.content.ContextCompat
19
18
  import com.facebook.react.bridge.*
20
19
  import com.facebook.react.modules.core.DeviceEventManagerModule
21
20
  import com.facebook.react.modules.core.PermissionAwareActivity
22
- import com.facebook.react.modules.core.PermissionListener
23
21
  import com.google.android.exoplayer2.C
24
22
  import com.google.android.exoplayer2.ExoPlayer
25
23
  import com.google.android.exoplayer2.MediaItem
@@ -27,6 +25,7 @@ import com.google.android.exoplayer2.Player
27
25
  import com.google.android.exoplayer2.audio.AudioAttributes
28
26
  import java.io.File
29
27
  import android.media.AudioAttributes as AndroidAudioAttributes
28
+ import kotlin.math.log10
30
29
 
31
30
  class RecPlayModule(
32
31
  private val reactContext: ReactApplicationContext,
@@ -35,8 +34,12 @@ class RecPlayModule(
35
34
 
36
35
  private var recorder: MediaRecorder? = null
37
36
  private var player: ExoPlayer? = null
37
+
38
38
  private var isPaused = false
39
- private var secondsElapsed = 0
39
+ private var secondsElapsed = 0.0
40
+ private var subSecondTicks = 0
41
+ private var recordingStartMs = 0L
42
+ private var accumulatedRecordingMs = 0L
40
43
 
41
44
  private var audioFocusRequest: AudioFocusRequest? = null
42
45
  private var audioFocusChangeListener: AudioManager.OnAudioFocusChangeListener? = null
@@ -51,6 +54,42 @@ class RecPlayModule(
51
54
 
52
55
  override fun getName(): String = "RecPlayModule"
53
56
 
57
+ private fun getRecordingDurationSeconds(): Double {
58
+ val activeSegmentMs = if (!isPaused && recordingStartMs > 0L) {
59
+ System.currentTimeMillis() - recordingStartMs
60
+ } else {
61
+ 0L
62
+ }
63
+ return (accumulatedRecordingMs + activeSegmentMs) / 1000.0
64
+ }
65
+
66
+ private fun resolveRecorderAudioSource(useBT: Boolean): Int =
67
+ when {
68
+ useBT && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> MediaRecorder.AudioSource.VOICE_COMMUNICATION
69
+ isCommunicationActive() -> MediaRecorder.AudioSource.VOICE_COMMUNICATION
70
+ else -> MediaRecorder.AudioSource.MIC
71
+ }
72
+
73
+
74
+ private fun stopPlaybackInternal() {
75
+ playbackHandler.removeCallbacks(playbackRunnable)
76
+ player?.stop()
77
+ player?.release()
78
+ player = null
79
+ abandonAudioFocus()
80
+ unregisterNoisyReceiver()
81
+ }
82
+
83
+ private fun resolvePlaybackUri(uriString: String): Uri {
84
+ val trimmed = uriString.trim()
85
+ return if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) {
86
+ Uri.parse(trimmed)
87
+ } else {
88
+ val literalPath = trimmed.removePrefix("file://")
89
+ Uri.fromFile(File(literalPath))
90
+ }
91
+ }
92
+
54
93
  // --- NEW: WebRTC Detection Helper ---
55
94
  private fun isCommunicationActive(): Boolean {
56
95
  val audioManager = reactContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
@@ -164,40 +203,74 @@ class RecPlayModule(
164
203
  }
165
204
 
166
205
  private val playbackRunnable =
167
- object : Runnable {
168
- override fun run() {
169
- player?.let {
170
- if (it.isPlaying) {
171
- val params = Arguments.createMap()
172
- params.putDouble("currentPosition", it.currentPosition.toDouble() / 1000.0)
173
- params.putDouble("duration", it.duration.toDouble() / 1000.0)
174
- sendEvent("onPlaybackProgress", params)
175
- }
176
- playbackHandler.postDelayed(this, 500)
206
+ object : Runnable {
207
+ override fun run() {
208
+ val currentPlayer = player ?: run {
209
+ // No player — stop rescheduling, mirrors iOS progress observer guard
210
+ playbackHandler.removeCallbacks(this)
211
+ return
212
+ }
213
+ if (currentPlayer.isPlaying) {
214
+ val rawDuration = currentPlayer.duration
215
+ if (rawDuration != C.TIME_UNSET && rawDuration >= 0) {
216
+ val params = Arguments.createMap()
217
+ params.putDouble("currentPosition", currentPlayer.currentPosition.toDouble() / 1000.0)
218
+ params.putDouble("duration", rawDuration.toDouble() / 1000.0)
219
+ sendEvent("onPlaybackProgress", params)
177
220
  }
178
221
  }
222
+ playbackHandler.postDelayed(this, 50)
179
223
  }
224
+ }
180
225
 
181
226
  private val timerRunnable =
182
- object : Runnable {
227
+ object : Runnable {
183
228
  override fun run() {
184
- if (recorder == null) return
229
+ val currentRecorder = recorder ?: run {
230
+ // Mirror iOS: invalidate self when recorder is gone, don't reschedule
231
+ handler.removeCallbacks(this)
232
+ return
233
+ }
185
234
  if (!isPaused) {
186
- val params = Arguments.createMap()
187
- params.putInt("seconds", secondsElapsed)
188
- sendEvent("onTimerUpdate", params)
189
- secondsElapsed++
235
+ // 1. Volume Logic
236
+ val maxAmp = currentRecorder.maxAmplitude
237
+ val db = if (maxAmp > 0) 20 * log10(maxAmp.toDouble() / 32767.0) else -160.0
238
+ val normalizedVolume = ((db + 60) / 60).coerceIn(0.0, 1.0)
239
+
240
+ sendEvent("onVolumeUpdate", Arguments.createMap().apply {
241
+ putDouble("value", db)
242
+ putDouble("normalized", normalizedVolume)
243
+ })
244
+
245
+ // 2. Timer Logic (Executes every 10 ticks = 1 second)
246
+ subSecondTicks++
247
+ if (subSecondTicks >= 10) {
248
+ secondsElapsed = getRecordingDurationSeconds()
249
+ sendEvent("onTimerUpdate", Arguments.createMap().apply {
250
+ putInt("seconds", secondsElapsed.toInt())
251
+ })
252
+ subSecondTicks = 0
253
+ }
190
254
  }
191
- handler.postDelayed(this, 1000)
255
+ handler.postDelayed(this, 100) // Change 1000 to 100
192
256
  }
193
- }
257
+ }
258
+
259
+ private fun stopTimer() {
260
+ handler.removeCallbacks(timerRunnable)
261
+ subSecondTicks = 0
262
+ }
194
263
 
195
264
  private fun sendEvent(
196
265
  eventName: String,
197
266
  params: WritableMap?,
198
267
  ) {
199
- if (reactContext.hasActiveCatalystInstance()) {
200
- reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java).emit(eventName, params)
268
+ try {
269
+ reactContext
270
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
271
+ .emit(eventName, params)
272
+ } catch (e: Exception) {
273
+ Log.w(TAG, "sendEvent '$eventName' dropped — JS not ready: ${e.message}")
201
274
  }
202
275
  }
203
276
 
@@ -212,10 +285,7 @@ class RecPlayModule(
212
285
  handler.post {
213
286
  try {
214
287
  if (shouldStopPrevious || player != null) {
215
- playbackHandler.removeCallbacks(playbackRunnable)
216
- player?.stop()
217
- player?.release()
218
- player = null
288
+ stopPlaybackInternal()
219
289
  }
220
290
 
221
291
  // If call is active, force ducking instead of exclusive focus
@@ -234,26 +304,52 @@ class RecPlayModule(
234
304
  addListener(
235
305
  object : Player.Listener {
236
306
  override fun onPlaybackStateChanged(state: Int) {
237
- if (state == Player.STATE_ENDED) {
238
- playbackHandler.removeCallbacks(playbackRunnable)
239
- abandonAudioFocus()
240
- unregisterNoisyReceiver()
241
- sendEvent("onPlaybackFinished", Arguments.createMap().apply { putBoolean("finished", true) })
307
+ when (state) {
308
+ Player.STATE_BUFFERING -> {
309
+ sendEvent("onPlaybackStatus", Arguments.createMap().apply { putString("status", "BUFFERING") })
310
+ }
311
+ Player.STATE_READY -> {
312
+ if (playWhenReady) {
313
+ sendEvent("onPlaybackStatus", Arguments.createMap().apply { putString("status", "PLAYING") })
314
+ } else {
315
+ sendEvent("onPlaybackStatus", Arguments.createMap().apply { putString("status", "PAUSED") })
316
+ }
317
+ }
318
+ Player.STATE_ENDED -> {
319
+ playbackHandler.removeCallbacks(playbackRunnable)
320
+ abandonAudioFocus()
321
+ unregisterNoisyReceiver()
322
+ sendEvent("onPlaybackFinished", Arguments.createMap().apply { putBoolean("finished", true) })
323
+ sendEvent("onPlaybackStatus", Arguments.createMap().apply { putString("status", "ENDED") })
324
+ }
242
325
  }
243
326
  }
244
327
 
245
328
  override fun onIsPlayingChanged(isPlaying: Boolean) {
246
- sendEvent(
247
- "onPlaybackStatus",
248
- Arguments.createMap().apply { putString("status", if (isPlaying) "PLAYING" else "PAUSED") },
249
- )
329
+ if (!isPlaying && player?.playbackState == Player.STATE_READY) {
330
+ // Only send PAUSED when the player is ready but not playing (user-driven pause).
331
+ // Do NOT send here during STATE_BUFFERING that's onPlaybackStateChanged's job.
332
+ sendEvent("onPlaybackStatus", Arguments.createMap().apply { putString("status", "PAUSED") })
333
+ }
250
334
  if (isPlaying) {
251
335
  playbackHandler.post(playbackRunnable)
252
336
  registerNoisyReceiver()
253
337
  } else {
338
+ playbackHandler.removeCallbacks(playbackRunnable)
254
339
  unregisterNoisyReceiver()
255
340
  }
256
341
  }
342
+
343
+ override fun onPlayerError(error: com.google.android.exoplayer2.PlaybackException) {
344
+ Log.e(TAG, "playAudio failed for $uriString", error)
345
+ playbackHandler.removeCallbacks(playbackRunnable)
346
+ abandonAudioFocus()
347
+ unregisterNoisyReceiver()
348
+ sendEvent("onPlaybackStatus", Arguments.createMap().apply {
349
+ putString("status", "ERROR")
350
+ putString("message", error.message ?: "Playback failed")
351
+ })
352
+ }
257
353
  },
258
354
  )
259
355
  }
@@ -266,11 +362,14 @@ class RecPlayModule(
266
362
  .build()
267
363
  player?.setAudioAttributes(audioAttributes, false)
268
364
 
269
- player?.setMediaItem(MediaItem.fromUri(Uri.parse(uriString)))
365
+ val resolvedUri = resolvePlaybackUri(uriString)
366
+ Log.d(TAG, "playAudio resolved uri=$resolvedUri from input=$uriString")
367
+ player?.setMediaItem(MediaItem.fromUri(resolvedUri))
270
368
  player?.prepare()
271
369
  player?.repeatMode = if (loop) Player.REPEAT_MODE_ONE else Player.REPEAT_MODE_OFF
272
370
  player?.playWhenReady = true
273
371
  } catch (e: Exception) {
372
+ Log.e(TAG, "playAudio exception for $uriString", e)
274
373
  sendEvent("onPlaybackStatus", Arguments.createMap().apply { putString("status", "ERROR") })
275
374
  }
276
375
  }
@@ -282,11 +381,12 @@ class RecPlayModule(
282
381
  shouldStopPlayback: Boolean,
283
382
  duck: Boolean,
284
383
  mixWithOthers: Boolean,
384
+ useBT: Boolean,
285
385
  promise: Promise,
286
386
  ) {
287
387
  handler.post {
288
388
  try {
289
- if (shouldStopPlayback) stopPlayback(null)
389
+ if (shouldStopPlayback) stopPlaybackInternal()
290
390
 
291
391
  val focusType: Int? =
292
392
  when {
@@ -304,9 +404,7 @@ class RecPlayModule(
304
404
 
305
405
  recorder =
306
406
  (if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) MediaRecorder(reactContext) else MediaRecorder()).apply {
307
- // Use VOICE_COMMUNICATION if in a call to share mic with WebRTC
308
- val source = if (isCommunicationActive()) MediaRecorder.AudioSource.VOICE_COMMUNICATION else MediaRecorder.AudioSource.MIC
309
- setAudioSource(source)
407
+ setAudioSource(resolveRecorderAudioSource(useBT))
310
408
  setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
311
409
  setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
312
410
  setAudioEncodingBitRate(128000)
@@ -316,9 +414,14 @@ class RecPlayModule(
316
414
  start()
317
415
  }
318
416
 
319
- secondsElapsed = 0
417
+ secondsElapsed = 0.0
418
+ accumulatedRecordingMs = 0L
419
+ recordingStartMs = System.currentTimeMillis()
420
+ subSecondTicks = 0 // Reset sub-ticks
320
421
  isPaused = false
422
+ handler.removeCallbacks(timerRunnable)
321
423
  handler.post(timerRunnable)
424
+
322
425
  promise.resolve(name)
323
426
  } catch (e: Exception) {
324
427
  recorder?.release()
@@ -330,36 +433,54 @@ class RecPlayModule(
330
433
 
331
434
  @ReactMethod
332
435
  fun stopRecording(promise: Promise) {
333
- handler.removeCallbacks(timerRunnable)
334
- try {
335
- recorder?.stop()
336
- recorder?.release()
337
- recorder = null
338
- abandonAudioFocus()
339
- promise.resolve("file://$currentFilePath")
340
- } catch (e: Exception) {
341
- promise.reject("STOP_ERROR", e.message)
436
+ handler.post {
437
+ try {
438
+ val finalDuration = getRecordingDurationSeconds()
439
+ recorder?.apply {
440
+ stop()
441
+ release()
442
+ }
443
+
444
+ val uri = "file://$currentFilePath"
445
+
446
+ val result = Arguments.createMap().apply {
447
+ putString("uri", uri)
448
+ putDouble("duration", finalDuration)
449
+ }
450
+
451
+ recorder = null
452
+ currentFilePath = null // ← mirrors iOS clearing audioRecorder which drops the URL
453
+ secondsElapsed = finalDuration
454
+ accumulatedRecordingMs = 0L
455
+ recordingStartMs = 0L
456
+ isPaused = false
457
+ stopTimer()
458
+ abandonAudioFocus()
459
+ promise.resolve(result)
460
+ } catch (e: Exception) {
461
+ promise.reject("STOP_ERROR", e.message)
462
+ }
342
463
  }
343
464
  }
344
465
 
345
466
  @ReactMethod
346
467
  fun stopPlayback(promise: Promise?) {
347
468
  handler.post {
348
- playbackHandler.removeCallbacks(playbackRunnable)
349
- player?.stop()
350
- player?.release()
351
- player = null
352
- abandonAudioFocus()
353
- unregisterNoisyReceiver()
469
+ stopPlaybackInternal()
354
470
  promise?.resolve(true)
355
471
  }
356
472
  }
357
473
 
358
- @ReactMethod
359
- fun pauseRecording(promise: Promise) {
474
+ @ReactMethod
475
+ fun pauseRecording(promise: Promise) {
476
+ handler.post {
360
477
  try {
361
478
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
362
479
  recorder?.pause()
480
+ if (!isPaused && recordingStartMs > 0L) {
481
+ accumulatedRecordingMs += System.currentTimeMillis() - recordingStartMs
482
+ recordingStartMs = 0L
483
+ }
363
484
  isPaused = true
364
485
  }
365
486
  promise.resolve(true)
@@ -367,12 +488,17 @@ class RecPlayModule(
367
488
  promise.reject("PAUSE_ERROR", e.message)
368
489
  }
369
490
  }
491
+ }
370
492
 
371
- @ReactMethod
372
- fun resumeRecording(promise: Promise) {
493
+ @ReactMethod
494
+ fun resumeRecording(promise: Promise) {
495
+ handler.post {
373
496
  try {
374
497
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
375
498
  recorder?.resume()
499
+ if (isPaused) {
500
+ recordingStartMs = System.currentTimeMillis()
501
+ }
376
502
  isPaused = false
377
503
  }
378
504
  promise.resolve(true)
@@ -380,16 +506,48 @@ class RecPlayModule(
380
506
  promise.reject("RESUME_ERROR", e.message)
381
507
  }
382
508
  }
509
+ }
510
+
511
+ @ReactMethod
512
+ fun seekTo(seconds: Double) {
513
+ handler.post {
514
+ player?.seekTo((seconds * 1000).toLong().coerceAtLeast(0L))
515
+ }
516
+ }
517
+
518
+ @ReactMethod
519
+ fun togglePlayback() {
520
+ handler.post {
521
+ player?.let {
522
+ if (it.isPlaying) {
523
+ it.pause()
524
+ } else {
525
+ it.play()
526
+ }
527
+ }
528
+ }
529
+ }
383
530
 
384
531
  @ReactMethod
385
532
  fun checkPermission(promise: Promise) {
386
- val granted = ContextCompat.checkSelfPermission(reactContext, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED
387
- promise.resolve(if (granted) "granted" else "denied")
533
+ val isGranted = ContextCompat.checkSelfPermission(reactContext, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED
534
+ if (isGranted) {
535
+ promise.resolve("granted")
536
+ return
537
+ }
538
+ // shouldShowRequestPermissionRationale returns false AFTER a denial with "Don't ask again"
539
+ // — that's the Android equivalent of iOS's "blocked" state
540
+ val activity = reactApplicationContext.currentActivity
541
+ val isBlocked = activity != null &&
542
+ !androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.RECORD_AUDIO)
543
+ promise.resolve(if (isBlocked) "blocked" else "denied")
388
544
  }
389
545
 
390
546
  @ReactMethod
391
547
  fun requestPermission(promise: Promise) {
392
- val activity = currentActivity as? PermissionAwareActivity ?: return promise.reject("ERROR", "No Activity")
548
+ val activity =
549
+ reactApplicationContext.currentActivity as? PermissionAwareActivity
550
+ ?: return promise.reject("ERROR", "No Activity")
393
551
  activity.requestPermissions(arrayOf(Manifest.permission.RECORD_AUDIO), 101) { requestCode, _, grantResults ->
394
552
  if (requestCode ==
395
553
  101
@@ -408,6 +566,12 @@ class RecPlayModule(
408
566
  }
409
567
  }
410
568
 
569
+ @ReactMethod
570
+ fun addListener(eventName: String) { /* required by NativeEventEmitter */ }
571
+
572
+ @ReactMethod
573
+ fun removeListeners(count: Int) { /* required by NativeEventEmitter */ }
574
+
411
575
  override fun onCatalystInstanceDestroy() {
412
576
  handler.removeCallbacks(timerRunnable)
413
577
  playbackHandler.removeCallbacks(playbackRunnable)
package/index.d.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  export type PermissionStatus = "granted" | "denied" | "blocked" | "unavailable";
2
2
  export type PlaybackStatus = "BUFFERING" | "PLAYING" | "PAUSED" | "ENDED" | "IDLE";
3
-
3
+ export type RecordingResult = {
4
+ uri: string;
5
+ duration: number; // Duration in seconds (e.g., 5.4)
6
+ };
4
7
  /**
5
8
  * Options for playback audio
6
9
  */
@@ -38,8 +41,15 @@ export type RecordingOptions = {
38
41
  duck?: boolean;
39
42
  /** Mix recording with other apps? Default: true */
40
43
  mixWithOthers?: boolean;
44
+ /** useBT recording via bluetooth mic or not. Default: false */
45
+ useBT?: boolean;
41
46
  /** Callback fired every second with elapsed seconds */
42
47
  onSecondsUpdate?: (seconds: number) => void;
48
+ /** * Callback fired every 100ms with volume data
49
+ * @param db Raw decibel value (approx. -60 to 0)
50
+ * @param normalized Normalized value (0.0 to 1.0) for UI bars
51
+ */
52
+ onVolumeUpdate?: (db: number, normalized: number) => void;
43
53
  };
44
54
 
45
55
  /**
@@ -51,11 +61,9 @@ declare const Recplay: {
51
61
  * Start recording audio
52
62
  * @example
53
63
  * const fileName = await Recplay.startRecording({
54
- * fileName: "myrec",
55
- * shouldStopPlayback: true,
56
- * duck: false,
57
- * mixWithOthers: false,
58
- * onSecondsUpdate: (s) => console.log("Recording seconds:", s)
64
+ * fileName: "myrec",
65
+ * onSecondsUpdate: (s) => console.log("Seconds:", s),
66
+ * onVolumeUpdate: (db, vol) => console.log("Volume:", vol) // 0.0 - 1.0
59
67
  * });
60
68
  */
61
69
  startRecording: (options?: RecordingOptions) => Promise<string>;
@@ -66,7 +74,7 @@ declare const Recplay: {
66
74
  * const recordedFile = await Recplay.stopRecording();
67
75
  * console.log("Recorded file path:", recordedFile);
68
76
  */
69
- stopRecording: () => Promise<string>;
77
+ stopRecording: () => Promise<RecordingResult>;
70
78
 
71
79
  /**
72
80
  * Pause current recording
@@ -93,7 +101,6 @@ declare const Recplay: {
93
101
  * Check microphone permission
94
102
  * @example
95
103
  * const status = await Recplay.checkPermission();
96
- * console.log(status); // "granted" | "denied" | "blocked" | "unavailable"
97
104
  */
98
105
  checkPermission: () => Promise<PermissionStatus>;
99
106
 
@@ -101,39 +108,21 @@ declare const Recplay: {
101
108
  * Request microphone permission
102
109
  * @example
103
110
  * const status = await Recplay.requestPermission();
104
- * console.log(status); // "granted" | "denied" | "blocked" | "unavailable"
105
111
  */
106
112
  requestPermission: () => Promise<PermissionStatus>;
107
113
 
108
114
  /**
109
115
  * Play audio with options object
110
- * @example
111
- * Recplay.playAudio({
112
- * uri: "file.mp3",
113
- * shouldStopPrevious: true,
114
- * loop: false,
115
- * mixWithOthers: false,
116
- * duck: true,
117
- * callbacks: {
118
- * onStatus: (s) => console.log("Status:", s),
119
- * onProgress: (cur, dur) => console.log("Progress:", cur, "/", dur),
120
- * onPlaybackFinished: () => console.log("Playback finished")
121
- * }
122
- * });
123
116
  */
124
117
  playAudio: (options: PlaybackOptions) => void;
125
118
 
126
119
  /**
127
120
  * Stop current playback
128
- * @example
129
- * await Recplay.stopPlayback();
130
121
  */
131
122
  stopPlayback: () => Promise<boolean>;
132
123
 
133
124
  /**
134
125
  * Toggle playback pause/resume
135
- * @example
136
- * Recplay.togglePlayback();
137
126
  */
138
127
  togglePlayback: () => void;
139
128
  };
package/index.js CHANGED
@@ -4,6 +4,7 @@ const { RecPlayModule } = NativeModules;
4
4
  const eventEmitter = new NativeEventEmitter(RecPlayModule);
5
5
 
6
6
  let internalSub = null;
7
+ let internalVolumeSub = null; // New: listener for 100ms volume updates
7
8
  let internalPlaySub = null;
8
9
  let internalStatusSub = null;
9
10
  let internalProgressSub = null;
@@ -16,22 +17,42 @@ const Recplay = {
16
17
  shouldStopPlayback = true,
17
18
  duck = true,
18
19
  mixWithOthers = true,
20
+ useBT = false,
19
21
  onSecondsUpdate = null,
22
+ onVolumeUpdate = null, // New callback for UI visualizers
20
23
  } = {}) => {
21
- if (internalSub) internalSub.remove();
24
+ // Clean up any existing recording subscriptions
25
+ [internalSub, internalVolumeSub].forEach(s => s?.remove());
26
+
22
27
  if (onSecondsUpdate) {
23
28
  internalSub = eventEmitter.addListener('onTimerUpdate', (data) => {
24
29
  onSecondsUpdate(data.seconds);
25
30
  });
26
31
  }
27
- return RecPlayModule.startRecording(fileName, shouldStopPlayback, duck, mixWithOthers);
32
+
33
+ if (onVolumeUpdate) {
34
+ internalVolumeSub = eventEmitter.addListener('onVolumeUpdate', (data) => {
35
+ // data contains { value: dB, normalized: 0.0-1.0 }
36
+ onVolumeUpdate(data.value, data.normalized);
37
+ });
38
+ }
39
+
40
+ return RecPlayModule.startRecording(fileName, shouldStopPlayback, duck, mixWithOthers, useBT);
28
41
  },
29
42
 
30
43
 
31
44
  /** Stop recording */
32
45
  stopRecording: async () => {
33
- if (internalSub) { internalSub.remove(); internalSub = null; }
34
- return RecPlayModule.stopRecording();
46
+ // 1. Cleanup JS listeners
47
+ [internalSub, internalVolumeSub].forEach(s => s?.remove());
48
+ internalSub = null;
49
+ internalVolumeSub = null;
50
+
51
+ // 2. Get the object { uri, duration } from Native
52
+ const result = await RecPlayModule.stopRecording();
53
+
54
+ // Result is now { uri: string, duration: number }
55
+ return result;
35
56
  },
36
57
 
37
58
  /** Pause recording */
@@ -70,7 +91,6 @@ const Recplay = {
70
91
  );
71
92
  }
72
93
 
73
- // Map named params to native positional params
74
94
  RecPlayModule.playAudio(uri, shouldStopPrevious, loop, mixWithOthers, duck);
75
95
  },
76
96
 
@@ -84,4 +104,4 @@ const Recplay = {
84
104
  togglePlayback: () => RecPlayModule.togglePlayback(),
85
105
  };
86
106
 
87
- export default Recplay;
107
+ export default Recplay;
@@ -24,6 +24,7 @@ RCT_EXTERN_METHOD(startRecording
24
24
  : (BOOL)duck
25
25
  mixWithOthers
26
26
  : (BOOL)mixWithOthers
27
+ useBT:(BOOL)useBT
27
28
  resolver
28
29
  : (RCTPromiseResolveBlock)resolve
29
30
  rejecter