Skip to content

Commit 4d023e2

Browse files
authored
Merge branch 'master' into master
2 parents 9f93885 + 1986e6f commit 4d023e2

File tree

15 files changed

+811
-992
lines changed

15 files changed

+811
-992
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
55

66
## Unreleased
7+
### Added
8+
- Add inline base64 audio URL support
9+
- Add ability to generate metering events while recording, which measure sound input levels
710

11+
### Fixed
12+
- Android: Fixed a compatibility issue on Android where on some Android models (e.g. HUAWEI) a -38 error is generated
13+
- iOS: Fixed duration not being provided until getCurrentTime is called
814

915
## [2.0.3] - 2020-03-23
1016
### Added

ExampleApp/package-lock.json

Lines changed: 467 additions & 946 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ExampleApp/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
"react-native": "0.59.10"
1515
},
1616
"devDependencies": {
17-
"@babel/core": "^7.9.0",
18-
"@babel/runtime": "^7.9.2",
17+
"@babel/core": "^7.10.5",
18+
"@babel/runtime": "^7.10.5",
1919
"metro-react-native-babel-preset": "^0.54.1"
2020
}
2121
}

android/src/main/java/com/reactnativecommunity/rctaudiotoolkit/AudioPlayerModule.java

Lines changed: 42 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ public class AudioPlayerModule extends ReactContextBaseJavaModule implements Med
4141
Map<Integer, Boolean> playerAutoDestroy = new HashMap<>();
4242
Map<Integer, Boolean> playerContinueInBackground = new HashMap<>();
4343
Map<Integer, Callback> playerSeekCallback = new HashMap<>();
44+
Map<Integer, Float> playerSpeed = new HashMap<>();
4445

4546
boolean looping = false;
4647
private ReactApplicationContext context;
@@ -165,7 +166,7 @@ private Uri uriFromPath(String path) {
165166
if (file.exists()) {
166167
return Uri.fromFile(file);
167168
}
168-
169+
169170
// Try finding file in Android "raw" resources
170171
if (path.lastIndexOf('.') != -1) {
171172
fileNameWithoutExt = path.substring(0, path.lastIndexOf('.'));
@@ -174,7 +175,7 @@ private Uri uriFromPath(String path) {
174175
}
175176

176177
int resId = this.context.getResources().getIdentifier(fileNameWithoutExt,
177-
"raw", this.context.getPackageName());
178+
"raw", this.context.getPackageName());
178179
if (resId != 0) {
179180
return Uri.parse("android.resource://" + this.context.getPackageName() + "/" + resId);
180181
}
@@ -193,6 +194,7 @@ public void destroy(Integer playerId, Callback callback) {
193194
this.playerAutoDestroy.remove(playerId);
194195
this.playerContinueInBackground.remove(playerId);
195196
this.playerSeekCallback.remove(playerId);
197+
this.playerSpeed.remove(playerId);
196198

197199
WritableMap data = new WritableNativeMap();
198200
data.putString("message", "Destroyed player");
@@ -251,8 +253,6 @@ public void prepare(Integer playerId, String path, ReadableMap options, final Ca
251253
destroy(playerId);
252254
this.lastPlayerId = playerId;
253255

254-
Uri uri = uriFromPath(path);
255-
256256
//MediaPlayer player = MediaPlayer.create(this.context, uri, null, attributes);
257257
MediaPlayer player = new MediaPlayer();
258258

@@ -264,13 +264,23 @@ public void prepare(Integer playerId, String path, ReadableMap options, final Ca
264264
265265
player.setAudioAttributes(attributes);
266266
*/
267-
268-
try {
269-
Log.d(LOG_TAG, uri.getPath());
270-
player.setDataSource(this.context, uri);
271-
} catch (IOException e) {
272-
callback.invoke(errObj("invalidpath", e.toString()));
273-
return;
267+
if (path.startsWith("data:audio/")) {
268+
// Inline data
269+
try {
270+
player.setDataSource(path);
271+
} catch (IOException e) {
272+
callback.invoke(errObj("invalidpath", e.toString()));
273+
return;
274+
}
275+
} else {
276+
try {
277+
Uri uri = uriFromPath(path);
278+
Log.d(LOG_TAG, uri.getPath());
279+
player.setDataSource(this.context, uri);
280+
} catch (IOException e) {
281+
callback.invoke(errObj("invalidpath", e.toString()));
282+
return;
283+
}
274284
}
275285

276286
player.setOnErrorListener(this);
@@ -355,16 +365,13 @@ public void set(Integer playerId, ReadableMap options, Callback callback) {
355365
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && (options.hasKey("speed") || options.hasKey("pitch"))) {
356366
PlaybackParams params = new PlaybackParams();
357367

358-
boolean needToPauseAfterSet = false;
359368
if (options.hasKey("speed") && !options.isNull("speed")) {
360369
// If the player wasn't already playing, then setting the speed value to a non-zero value
361-
// will start it playing and we don't want that so we need to make sure to pause it straight
362-
// after setting the speed value
363-
boolean wasAlreadyPlaying = player.isPlaying();
370+
// will start it playing and we don't want that so we store and apply it later
364371
float speedValue = (float) options.getDouble("speed");
365-
needToPauseAfterSet = !wasAlreadyPlaying && speedValue != 0.0f;
366-
367-
params.setSpeed(speedValue);
372+
this.playerSpeed.put(playerId, speedValue);
373+
// Apply param only if isPlaying. If not, we defer it on start
374+
if (player.isPlaying()) params.setSpeed(speedValue);
368375
}
369376

370377
if (options.hasKey("pitch") && !options.isNull("pitch")) {
@@ -393,7 +400,23 @@ public void play(Integer playerId, Callback callback) {
393400
if (!this.mixWithOthers) {
394401
this.mAudioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
395402
}
396-
player.start();
403+
404+
// Let's start using setSpeed when supported
405+
Float speedValue = this.playerSpeed.get(playerId);
406+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && speedValue != null) {
407+
PlaybackParams params = new PlaybackParams();
408+
params.setSpeed(speedValue);
409+
player.setPlaybackParams(params);
410+
411+
// Check if device is honoring android spec: when setSpeed player should start
412+
// https://developer.android.com/reference/android/media/MediaPlayer#setPlaybackParams(android.media.PlaybackParams)
413+
// If that is not happening, explicitly call start
414+
if (!player.isPlaying()) {
415+
player.start();
416+
}
417+
} else {
418+
player.start();
419+
}
397420

398421
callback.invoke(null, getInfo(player));
399422
} catch (Exception e) {

android/src/main/java/com/reactnativecommunity/rctaudiotoolkit/AudioRecorderModule.java

Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,13 @@
22

33
import android.annotation.TargetApi;
44
import android.media.MediaRecorder;
5-
import android.os.Build;
6-
import android.os.Environment;
75
import android.util.Log;
86
import android.net.Uri;
97
import android.webkit.URLUtil;
108
import android.content.ContextWrapper;
119

1210
import com.facebook.react.bridge.Arguments;
1311
import com.facebook.react.bridge.ReactApplicationContext;
14-
import com.facebook.react.bridge.ReactContext;
1512
import com.facebook.react.bridge.ReactContextBaseJavaModule;
1613
import com.facebook.react.bridge.ReactMethod;
1714
import com.facebook.react.bridge.Callback;
@@ -23,11 +20,12 @@
2320
import java.io.IOException;
2421
import java.io.File;
2522
import java.lang.Thread;
26-
import java.net.URISyntaxException;
2723
import java.util.HashMap;
2824
import java.util.Map;
2925
import java.util.Map.Entry;
3026
import java.util.Objects;
27+
import java.util.Timer;
28+
import java.util.TimerTask;
3129

3230
public class AudioRecorderModule extends ReactContextBaseJavaModule implements
3331
MediaRecorder.OnInfoListener, MediaRecorder.OnErrorListener {
@@ -37,6 +35,11 @@ public class AudioRecorderModule extends ReactContextBaseJavaModule implements
3735
Map<Integer, Boolean> recorderAutoDestroy = new HashMap<>();
3836

3937
private ReactApplicationContext context;
38+
private Timer meteringUpdateTimer;
39+
private int meteringFrameId = 0;
40+
private Integer meteringRecorderId = null;
41+
private MediaRecorder meteringRecorder = null;
42+
private int meteringInterval = 0;
4043

4144
public AudioRecorderModule(ReactApplicationContext reactContext) {
4245
super(reactContext);
@@ -105,6 +108,8 @@ private int formatFromPath(String path) {
105108

106109
private int encoderFromName(String name) {
107110
switch (name) {
111+
case "aac-lc":
112+
return MediaRecorder.AudioEncoder.AAC;
108113
case "aac":
109114
return MediaRecorder.AudioEncoder.AAC;
110115
case "mp4":
@@ -140,6 +145,39 @@ private Uri uriFromPath(String path) {
140145

141146
return uri;
142147
}
148+
149+
// metering methods
150+
private void startMeteringTimer(int monitorInterval) {
151+
meteringUpdateTimer = new Timer();
152+
meteringUpdateTimer.scheduleAtFixedRate(new TimerTask() {
153+
@Override
154+
public void run() {
155+
if (meteringRecorderId != null && meteringRecorder != null) {
156+
WritableMap body = Arguments.createMap();
157+
body.putDouble("id", meteringFrameId++);
158+
159+
int amplitude = meteringRecorder.getMaxAmplitude();
160+
if (amplitude == 0) {
161+
body.putInt("value", -160);
162+
body.putInt("rawValue", 0);
163+
} else {
164+
body.putInt("rawValue", amplitude);
165+
body.putInt("value", (int) (20 * Math.log10(((double) amplitude) / 32767d)));
166+
}
167+
emitEvent(meteringRecorderId, "meter", body);
168+
}
169+
}
170+
}, 0, monitorInterval);
171+
}
172+
173+
private void stopMeteringTimer() {
174+
if (meteringUpdateTimer != null) {
175+
meteringUpdateTimer.cancel();
176+
meteringUpdateTimer.purge();
177+
meteringUpdateTimer = null;
178+
meteringFrameId = 0;
179+
}
180+
}
143181

144182
@ReactMethod
145183
public void destroy(Integer recorderId, Callback callback) {
@@ -149,6 +187,10 @@ public void destroy(Integer recorderId, Callback callback) {
149187
recorder.release();
150188
this.recorderPool.remove(recorderId);
151189
this.recorderAutoDestroy.remove(recorderId);
190+
if (recorderId == meteringRecorderId) {
191+
meteringRecorderId = null;
192+
meteringRecorder = null;
193+
}
152194

153195
WritableMap data = new WritableNativeMap();
154196
data.putString("message", "Destroyed recorder");
@@ -240,6 +282,20 @@ public void prepare(Integer recorderId, String path, ReadableMap options, Callba
240282
} catch (IOException e) {
241283
callback.invoke(errObj("preparefail", e.toString()));
242284
}
285+
286+
if (options.hasKey("meteringInterval")) {
287+
int meteringInterval = options.getInt("meteringInterval");
288+
if (meteringRecorderId != null) {
289+
Log.i(LOG_TAG, "multiple recorder metering are not currently supporter. Metering will be active on the last recorder.");
290+
}
291+
if (meteringInterval <= 0) {
292+
Log.w(LOG_TAG, "metering interval must be grater then 0. Ignoring metering");
293+
} else {
294+
meteringRecorder = recorder;
295+
meteringRecorderId = recorderId;
296+
this.meteringInterval = meteringInterval;
297+
}
298+
}
243299
}
244300

245301
@ReactMethod
@@ -251,6 +307,9 @@ public void record(Integer recorderId, Callback callback) {
251307
}
252308

253309
try {
310+
if (recorderId == meteringRecorderId) {
311+
startMeteringTimer(meteringInterval);
312+
}
254313
recorder.start();
255314

256315
callback.invoke();
@@ -268,6 +327,9 @@ public void stop(Integer recorderId, Callback callback) {
268327
}
269328

270329
try {
330+
if (recorderId == meteringRecorderId) {
331+
stopMeteringTimer();
332+
}
271333
recorder.stop();
272334
if (this.recorderAutoDestroy.get(recorderId)) {
273335
Log.d(LOG_TAG, "Autodestroying recorder...");
@@ -288,7 +350,6 @@ public void pause(Integer recorderId, Callback callback) {
288350
pause24(recorderId,callback);
289351
}
290352

291-
292353
@TargetApi(24)
293354
private void pause24(Integer recorderId, Callback callback) {
294355
MediaRecorder recorder = this.recorderPool.get(recorderId);
@@ -298,6 +359,9 @@ private void pause24(Integer recorderId, Callback callback) {
298359
}
299360

300361
try {
362+
if (recorderId == meteringRecorderId) {
363+
stopMeteringTimer();
364+
}
301365
recorder.pause();
302366
if (this.recorderAutoDestroy.get(recorderId)) {
303367
Log.d(LOG_TAG, "Autodestroying recorder...");
@@ -352,6 +416,5 @@ public void onInfo(MediaRecorder recorder, int what, int extra) {
352416
data.putString("message", "Android MediaRecorder info");
353417

354418
emitEvent(recorderId, "info", data);
355-
356419
}
357420
}

docs/API.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ Media methods
3131

3232
// (iOS only) Define the audio session category
3333
// Options: Playback, Ambient and SoloAmbient
34-
category : PlaybackCategory (default: PlaybackCategory.Playback)
34+
// More info about categories can be found here:
35+
// https://developer.apple.com/documentation/avfoundation/avaudiosession/category
36+
category : PlaybackCategories (default: PlaybackCategories.Playback)
3537

3638
// Boolean to determine whether other audio sources on the device will mix
3739
// with sounds being played back by this module. If this is not set, playback
@@ -220,6 +222,11 @@ Player.isPrepared true if player is prepared
220222
// Quality of the recording, iOS only.
221223
// Possible values: 'min', 'low', 'medium', 'high', 'max'
222224
quality : String (default: 'medium')
225+
226+
// Optional argument to activate metering events.
227+
// This will cause a 'meter' event to fire every given milliseconds,
228+
// e.g. 250 will fire 4 time in a second.
229+
meteringInterval : Number (default: undefined)
223230
}
224231
```
225232

@@ -310,6 +317,15 @@ are supported:
310317
311318
* `looped` - Playback of a file has looped.
312319
320+
* `meter` - Recurring event during recording session (see `meteringInterval` in `recorderOptions`). `data` associated to this event follows the format:
321+
```js
322+
{
323+
"id", // frame number
324+
"value", // sound level in decibels, -160 is a silence level
325+
"rawValue" // raw level value, OS-dependent
326+
}
327+
```
328+
**Currently, only one recored at a time generates meter events. Last prepared Recorder wins.**
313329
314330
Listen to these events with `player.on('eventname', callback(data))`. Data
315331
may contain additional information about the event, for example a more detailed

docs/SETUP.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,9 @@ React Native 0.59 and earlier:
111111
`libReactNativeAudioToolkit.a` from under Workspace.
112112
113113
4. Add a usage description to **Info.plist**.
114-
```<key>Privacy - Microphone Usage Description</key>
115-
<string>This app requires access to your microphone</string>
114+
```xml
115+
<key>NSMicrophoneUsageDescription</key>
116+
<string>This app requires access to your microphone</string>
116117
```
117118
118119
React Native 0.60 and later

0 commit comments

Comments
 (0)