Skip to content

Commit c8cb1b3

Browse files
Merge pull request #8 from SimformSolutionsPvtLtd/feature/UNT-T19176
feat(UNT-T19176): work on native android for bridging waveform methods
2 parents 2242ccc + e5a443d commit c8cb1b3

File tree

17 files changed

+1185
-110
lines changed

17 files changed

+1185
-110
lines changed

android/build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,7 @@ dependencies {
9292
implementation "com.facebook.react:react-native:+"
9393
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
9494

95+
96+
// Android Audio Playback Dependency
97+
implementation "com.google.android.exoplayer:exoplayer:2.17.1"
9598
}
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
package com.audiowaveform
2+
3+
import android.net.Uri
4+
import android.os.CountDownTimer
5+
import com.facebook.react.bridge.Arguments
6+
import com.facebook.react.bridge.Promise
7+
import com.facebook.react.bridge.ReactApplicationContext
8+
import com.facebook.react.bridge.WritableMap
9+
import com.facebook.react.common.JavascriptException
10+
import com.facebook.react.modules.core.DeviceEventManagerModule
11+
import com.google.android.exoplayer2.ExoPlayer
12+
import com.google.android.exoplayer2.MediaItem
13+
import com.google.android.exoplayer2.Player
14+
15+
class AudioPlayer(
16+
context: ReactApplicationContext,
17+
playerKey: String,
18+
) {
19+
private val appContext = context
20+
private lateinit var player: ExoPlayer
21+
private var playerListener: Player.Listener? = null
22+
private var isPlayerPrepared: Boolean = false
23+
private var finishMode = FinishMode.Stop
24+
private val key = playerKey
25+
private var updateFrequency = UpdateFrequency.Low
26+
private lateinit var audioPlaybackListener: CountDownTimer
27+
28+
fun preparePlayer(path: String?, volume: Int?, frequency: UpdateFrequency, promise: Promise) {
29+
if (path != null) {
30+
updateFrequency = frequency
31+
val uri = Uri.parse(path)
32+
val mediaItem = MediaItem.fromUri(uri)
33+
player = ExoPlayer.Builder(appContext).build()
34+
player.addMediaItem(mediaItem)
35+
36+
player.prepare()
37+
playerListener = object : Player.Listener {
38+
39+
@Deprecated("Deprecated in Java")
40+
override fun onPlayerStateChanged(isReady: Boolean, state: Int) {
41+
if (!isPlayerPrepared) {
42+
if (state == Player.STATE_READY) {
43+
player.volume = (volume ?: 1).toFloat()
44+
isPlayerPrepared = true
45+
val duration = player.duration
46+
promise.resolve(duration.toString())
47+
}
48+
}
49+
if (state == Player.STATE_ENDED) {
50+
val args: MutableMap<String, Any?> = HashMap()
51+
when (finishMode) {
52+
FinishMode.Loop -> {
53+
player.seekTo(0)
54+
player.play()
55+
args[Constants.finishType] = 0
56+
}
57+
FinishMode.Pause -> {
58+
player.seekTo(0)
59+
player.playWhenReady = false
60+
stopListening()
61+
args[Constants.finishType] = 1
62+
}
63+
else -> {
64+
player.stop()
65+
player.release()
66+
stopListening()
67+
args[Constants.finishType] = 2
68+
}
69+
}
70+
args[Constants.playerKey] = key
71+
}
72+
}
73+
}
74+
player.addListener(playerListener!!)
75+
} else {
76+
promise.reject("preparePlayer Error", "path to audio file or unique key can't be null")
77+
}
78+
}
79+
80+
fun seekToPosition(progress: Long?, promise: Promise) {
81+
if (progress != null) {
82+
player.seekTo(progress)
83+
promise.resolve(true)
84+
} else {
85+
promise.resolve(false)
86+
}
87+
}
88+
89+
fun getDuration(durationType: DurationType, promise: Promise) {
90+
if (durationType == DurationType.Current) {
91+
val duration = player.currentPosition
92+
promise.resolve(duration.toString())
93+
} else {
94+
val duration = player.duration
95+
promise.resolve(duration.toString())
96+
}
97+
}
98+
99+
fun start(finishMode: Int?, promise: Promise) {
100+
try {
101+
if (finishMode != null && finishMode == 0) {
102+
this.finishMode = FinishMode.Loop
103+
} else if (finishMode != null && finishMode == 1) {
104+
this.finishMode = FinishMode.Pause
105+
} else {
106+
this.finishMode = FinishMode.Stop
107+
}
108+
player.playWhenReady = true
109+
player.play()
110+
promise.resolve(true)
111+
startListening(promise)
112+
} catch (e: Exception) {
113+
promise.reject("Can not start the player", e.toString())
114+
}
115+
}
116+
117+
fun stop(promise: Promise) {
118+
stopListening()
119+
if (playerListener != null) {
120+
player.removeListener(playerListener!!)
121+
}
122+
isPlayerPrepared = false
123+
player.stop()
124+
player.release()
125+
promise.resolve(true)
126+
}
127+
128+
fun pause(promise: Promise) {
129+
try {
130+
stopListening()
131+
player.pause()
132+
promise.resolve(true)
133+
} catch (e: Exception) {
134+
promise.reject("Failed to pause the player", e.toString())
135+
}
136+
137+
}
138+
139+
fun setVolume(volume: Float?, promise: Promise) {
140+
try {
141+
if (volume != null) {
142+
player.volume = volume
143+
promise.resolve(true)
144+
} else {
145+
promise.resolve(false)
146+
}
147+
} catch (e: Exception) {
148+
promise.resolve(false)
149+
}
150+
}
151+
152+
private fun startListening(promise: Promise) {
153+
try {
154+
audioPlaybackListener = object : CountDownTimer(player.duration, 10) {
155+
override fun onTick(millisUntilFinished: Long) {
156+
val currentPosition = player.currentPosition.toString()
157+
val args: WritableMap = Arguments.createMap()
158+
args.putString(Constants.currentDuration, currentPosition)
159+
args.putString(Constants.playerKey, key)
160+
appContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)?.emit("onCurrentDuration", args)
161+
}
162+
override fun onFinish() {}
163+
}.start()
164+
} catch(err: JavascriptException) {
165+
promise.reject("startListening Error", err)
166+
}
167+
}
168+
169+
private fun stopListening() {
170+
if (::audioPlaybackListener.isInitialized) {
171+
audioPlaybackListener.cancel()
172+
}
173+
}
174+
}
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
package com.audiowaveform
2+
3+
import android.Manifest
4+
import android.app.Activity
5+
import android.content.pm.PackageManager
6+
import android.media.MediaMetadataRetriever
7+
import android.media.MediaRecorder
8+
import android.os.Build
9+
import android.util.Log
10+
import androidx.annotation.RequiresApi
11+
import androidx.core.app.ActivityCompat
12+
import com.facebook.react.bridge.Arguments
13+
import com.facebook.react.bridge.Promise
14+
import java.io.IOException
15+
import java.lang.IllegalStateException
16+
import kotlin.math.log10
17+
18+
private const val RECORD_AUDIO_REQUEST_CODE = 1001
19+
20+
class AudioRecorder {
21+
private var permissions = arrayOf(Manifest.permission.RECORD_AUDIO)
22+
private var useLegacyNormalization = false
23+
24+
private fun isPermissionGranted(activity: Activity?): Int? {
25+
return activity?.let { ActivityCompat.checkSelfPermission(it, permissions[0]) }
26+
}
27+
28+
fun checkPermission(activity: Activity?, promise: Promise): String {
29+
val permissionResponse = isPermissionGranted(activity)
30+
if (permissionResponse === PackageManager.PERMISSION_GRANTED) {
31+
promise.resolve("granted")
32+
return "granted"
33+
} else {
34+
promise.resolve("denied")
35+
return "denied"
36+
}
37+
}
38+
39+
fun getPermission(activity: Activity?, promise: Promise): String {
40+
val permissionResponse = isPermissionGranted(activity)
41+
if (permissionResponse === PackageManager.PERMISSION_GRANTED) {
42+
promise.resolve("granted");
43+
return "granted"
44+
} else {
45+
activity?.let {
46+
ActivityCompat.requestPermissions(
47+
it, permissions,
48+
RECORD_AUDIO_REQUEST_CODE
49+
)
50+
}
51+
return "denied"
52+
}
53+
}
54+
55+
fun getDecibel(recorder: MediaRecorder?): Double? {
56+
if (useLegacyNormalization) {
57+
val db = 20 * log10((recorder?.maxAmplitude?.toDouble() ?: (0.0 / 32768.0)))
58+
if (db == Double.NEGATIVE_INFINITY) {
59+
Log.e(Constants.LOG_TAG, "Microphone might be turned off")
60+
} else {
61+
return db
62+
}
63+
return db;
64+
} else {
65+
return recorder?.maxAmplitude?.toDouble() ?: 0.0
66+
}
67+
}
68+
69+
fun initRecorder(
70+
path: String,
71+
recorder: MediaRecorder?,
72+
encoder: Int,
73+
outputFormat: Int,
74+
sampleRate: Int,
75+
bitRate: Int?,
76+
promise: Promise
77+
) {
78+
recorder?.apply {
79+
setAudioSource(MediaRecorder.AudioSource.MIC)
80+
setOutputFormat(getOutputFormat(outputFormat))
81+
setAudioEncoder(getEncoder(encoder))
82+
setAudioSamplingRate(sampleRate)
83+
if (bitRate != null) {
84+
setAudioEncodingBitRate(bitRate)
85+
}
86+
setOutputFile(path)
87+
try {
88+
prepare()
89+
promise.resolve(true)
90+
} catch (e: IOException) {
91+
Log.e(Constants.LOG_TAG, "Failed to stop initialize recorder")
92+
}
93+
}
94+
}
95+
96+
fun stopRecording(recorder: MediaRecorder?, path: String, promise: Promise) {
97+
try {
98+
recorder?.apply {
99+
stop()
100+
reset()
101+
release()
102+
}
103+
val tempArrayForCommunication : MutableList<String> = mutableListOf()
104+
val duration = getDuration(path)
105+
tempArrayForCommunication.add(path)
106+
tempArrayForCommunication.add(duration)
107+
promise.resolve(Arguments.fromList(tempArrayForCommunication))
108+
} catch (e: IllegalStateException) {
109+
Log.e(Constants.LOG_TAG, "Failed to stop recording")
110+
}
111+
}
112+
113+
private fun getDuration(path: String): String {
114+
val mediaMetadataRetriever = MediaMetadataRetriever()
115+
try {
116+
mediaMetadataRetriever.setDataSource(path)
117+
val duration = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
118+
return duration ?: "-1"
119+
} catch (e: Exception) {
120+
Log.e(Constants.LOG_TAG, "Failed to get recording duration")
121+
} finally {
122+
mediaMetadataRetriever.release()
123+
}
124+
return "-1"
125+
}
126+
127+
fun startRecorder(recorder: MediaRecorder?, useLegacy: Boolean, promise: Promise) {
128+
try {
129+
useLegacyNormalization = useLegacy
130+
recorder?.start()
131+
promise.resolve(true)
132+
} catch (e: IllegalStateException) {
133+
Log.e(Constants.LOG_TAG, "Failed to start recording")
134+
}
135+
}
136+
137+
@RequiresApi(Build.VERSION_CODES.N)
138+
fun pauseRecording(recorder: MediaRecorder?, promise: Promise) {
139+
try {
140+
recorder?.pause()
141+
promise.resolve(false)
142+
} catch (e: IllegalStateException) {
143+
Log.e(Constants.LOG_TAG, "Failed to pause recording")
144+
}
145+
}
146+
147+
@RequiresApi(Build.VERSION_CODES.N)
148+
fun resumeRecording(recorder: MediaRecorder?, promise: Promise) {
149+
try {
150+
recorder?.resume()
151+
promise.resolve(true)
152+
} catch (e: IllegalStateException) {
153+
Log.e(Constants.LOG_TAG, "Failed to resume recording")
154+
}
155+
}
156+
157+
private fun getEncoder(encoder: Int): Int {
158+
return when (encoder) {
159+
Constants.acc -> MediaRecorder.AudioEncoder.AAC
160+
Constants.aac_eld -> MediaRecorder.AudioEncoder.AAC_ELD
161+
Constants.he_aac -> MediaRecorder.AudioEncoder.HE_AAC
162+
Constants.amr_nb -> MediaRecorder.AudioEncoder.AMR_NB
163+
Constants.amr_wb -> MediaRecorder.AudioEncoder.AMR_WB
164+
Constants.opus -> {
165+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
166+
MediaRecorder.AudioEncoder.OPUS
167+
} else {
168+
Log.e(Constants.LOG_TAG, "Minimum android Q is required, Setting Acc encoder.")
169+
MediaRecorder.AudioEncoder.AAC
170+
}
171+
}
172+
Constants.vorbis -> MediaRecorder.AudioEncoder.VORBIS
173+
else -> MediaRecorder.AudioEncoder.AAC
174+
}
175+
}
176+
177+
private fun getOutputFormat(format: Int): Int {
178+
return when (format) {
179+
Constants.mpeg4 -> MediaRecorder.OutputFormat.MPEG_4
180+
Constants.three_gpp -> MediaRecorder.OutputFormat.THREE_GPP
181+
Constants.ogg -> {
182+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
183+
MediaRecorder.OutputFormat.OGG
184+
} else {
185+
Log.e(Constants.LOG_TAG, "Minimum android Q is required, Setting Acc encoder.")
186+
MediaRecorder.OutputFormat.MPEG_4
187+
}
188+
}
189+
Constants.amr_wb -> MediaRecorder.OutputFormat.AMR_WB
190+
Constants.amr_nb -> MediaRecorder.OutputFormat.AMR_NB
191+
Constants.webm -> MediaRecorder.OutputFormat.WEBM
192+
Constants.mpeg_2_ts -> {
193+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
194+
MediaRecorder.OutputFormat.MPEG_2_TS
195+
} else {
196+
Log.e(Constants.LOG_TAG, "Minimum android Q is required, Setting MPEG_4 output format.")
197+
MediaRecorder.OutputFormat.MPEG_4
198+
}
199+
}
200+
Constants.aac_adts -> MediaRecorder.OutputFormat.AAC_ADTS
201+
else -> MediaRecorder.OutputFormat.MPEG_4
202+
}
203+
}
204+
}

0 commit comments

Comments
 (0)