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.
- package/README.md +197 -125
- package/android/src/main/java/com/rnsrecplay/RecPlayModule.kt +228 -64
- package/index.d.ts +15 -26
- package/index.js +26 -6
- package/ios/RecPlayModule.m +1 -0
- package/ios/RecPlayModule.swift +239 -169
- package/package.json +1 -1
- package/withNativeRecPlay.js +1 -2
|
@@ -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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
227
|
+
object : Runnable {
|
|
183
228
|
override fun run() {
|
|
184
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
200
|
-
reactContext
|
|
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
|
-
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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.
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
-
|
|
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
|
-
|
|
359
|
-
|
|
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
|
-
|
|
372
|
-
|
|
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
|
|
387
|
-
|
|
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 =
|
|
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
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
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<
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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;
|