feat(Ios): handle audio interruptions (#2565)

* This update tries to improve audio recording interruptions on iOS due to phone calls or background music.

- Use a more generic event to handle session interruptions. This removes the need to listen to foreground/background events, and stopping the session this way was actually redundant/wrong (see https://forums.developer.apple.com/thread/61406). This also makes session stopping detection more reliable (calls, suspension due to a call or notification, etc., which would previously not set the recording interrupted flag on every case)

From the above docs: "No, incorrect. You _never_ need to stop your capture session. The capture session automatically stops itself when your app goes to the background and resumes itself when you come back to the foreground."

- Allow for `captureAudio` updates to also update the audio connections internally so the prop can be correctly updated on the fly without remounting.

- add onAudioInterrupted and onAudioConnected events so the UI can handle scenarios where audio is wanted but not available. This should also help in keeping the preview active even if audio is interrupted and we have captureAudio={true}. Lastly, it can be used to detect if we can record audio or not due to the dummy implementation of the audio permission on iOS always returning true.

- check, activate, and release audio sessions (if captureAudio) so we can detect early if audio is available before attempting to connect the input. This will also allow us to detect if we can record even if there was already a call before opening the camera.

- use proper observer for session error instead of of the strong self block. No benefit, but makes code more readable and allows access to instance variables

- getDeviceOrientationWithBlock might fire more than once under some circumstances, ending up taking a picture or video twice. Add a lock and additional check to prevent this.

* no need for change check,

* do not resume audio if we were hinted not to (e.g., music playback happening)

* start session here also on session queue.

* check for session running before trying to record or capture.
This should fix a possible race condition where both the session start call happens at the same time as the record call

* no need to set orientation on constructor, and set it on session queue to prevent race conditions

* move device init and checks also to session queue. This prevents possible double initializations.
This commit is contained in:
cristianoccazinsp 2019-11-04 18:39:37 -03:00 committed by Sibelius Seraphini
parent 8f4601b3b0
commit 59dfdb649a
7 changed files with 318 additions and 152 deletions

View File

@ -378,6 +378,15 @@ Event contains the following fields:
- `cameraStatus` - one of the [CameraStatus](#status) values
- `recordAudioPermissionStatus` - one of the [RecordAudioPermissionStatus](#recordAudioPermissionStatus) values
### `iOS` `onAudioInterrupted`
iOS only. Function to be called when the camera audio session is interrupted or fails to start for any reason (e.g., in use or not authorized). For example, this might happen due to another app taking exclusive control over the microphone (e.g., a phone call) if `captureAudio={true}`. When this happens, any active audio input will be temporarily disabled and cause a flicker on the preview screen. This will fire any time an attempt to connect the audio device fails. Use this to update your UI to indicate that audio recording is not currently possible.
### `iOS` `onAudioConnected`
iOS only. Function to be called when the camera audio session is connected. This will be fired the first time the camera is mounted with `captureAudio={true}`, and any time the audio device connection is established. Note that this event might not always fire after an interruption due to iOS' behavior. For example, if the audio was already interrupted before the camera was mounted, this event will only fire once a recording is attempted.
### `Android` `onPictureTaken`
Function to be called when native code emit onPictureTaken event, when camera has taken a picture.

View File

@ -64,6 +64,7 @@
- (void)updateWhiteBalance;
- (void)updateExposure;
- (void)updatePictureSize;
- (void)updateCaptureAudio;
// Face Detection props
- (void)updateTrackingEnabled:(id)requestedTracking;
- (void)updateFaceDetectionMode:(id)requestedMode;

View File

@ -11,7 +11,6 @@
@property (nonatomic, weak) RCTBridge *bridge;
@property (nonatomic,strong) RNSensorOrientationChecker * sensorOrientationChecker;
@property (nonatomic, assign, getter=isSessionPaused) BOOL paused;
@property (nonatomic, strong) RCTPromiseResolveBlock videoRecordedResolve;
@property (nonatomic, strong) RCTPromiseRejectBlock videoRecordedReject;
@ -20,6 +19,8 @@
@property (nonatomic, strong) id barcodeDetector;
@property (nonatomic, copy) RCTDirectEventBlock onCameraReady;
@property (nonatomic, copy) RCTDirectEventBlock onAudioInterrupted;
@property (nonatomic, copy) RCTDirectEventBlock onAudioConnected;
@property (nonatomic, copy) RCTDirectEventBlock onMountError;
@property (nonatomic, copy) RCTDirectEventBlock onBarCodeRead;
@property (nonatomic, copy) RCTDirectEventBlock onTextRecognized;
@ -44,6 +45,7 @@
static NSDictionary *defaultFaceDetectorOptions = nil;
BOOL _recordRequested = NO;
BOOL _sessionInterrupted = NO;
- (id)initWithBridge:(RCTBridge *)bridge
@ -68,7 +70,6 @@ BOOL _recordRequested = NO;
self.previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
self.previewLayer.needsDisplayOnBoundsChange = YES;
#endif
self.paused = NO;
self.rectOfInterest = CGRectMake(0, 0, 1.0, 1.0);
self.autoFocus = -1;
self.exposure = -1;
@ -76,8 +77,8 @@ BOOL _recordRequested = NO;
self.cameraId = nil;
self.isFocusedOnPoint = NO;
self.isExposedOnPoint = NO;
[self changePreviewOrientation:[UIApplication sharedApplication].statusBarOrientation];
_recordRequested = NO;
_sessionInterrupted = NO;
// we will do other initialization after
@ -87,8 +88,6 @@ BOOL _recordRequested = NO;
// and we need to also add/remove event listeners.
}
return self;
}
@ -167,19 +166,17 @@ BOOL _recordRequested = NO;
name:UIApplicationDidChangeStatusBarOrientationNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(bridgeDidBackground:)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(sessionWasInterrupted:) name:AVCaptureSessionWasInterruptedNotification object:self.session];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(sessionDidStartRunning:) name:AVCaptureSessionDidStartRunningNotification object:self.session];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(sessionRuntimeError:) name:AVCaptureSessionRuntimeErrorNotification object:self.session];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(bridgeDidForeground:)
name:UIApplicationWillEnterForegroundNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(audioDidInterrupted:)
name:AVAudioSessionInterruptionNotification
object:nil];
selector:@selector(audioDidInterrupted:)
name:AVAudioSessionInterruptionNotification
object:[AVAudioSession sharedInstance]];
// this is not needed since RN will update our type value
// after mount to set the camera's default, and that will already
@ -190,11 +187,13 @@ BOOL _recordRequested = NO;
else{
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidChangeStatusBarOrientationNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidEnterBackgroundNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:AVCaptureSessionWasInterruptedNotification object:self.session];
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationWillEnterForegroundNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:AVCaptureSessionDidStartRunningNotification object:self.session];
[[NSNotificationCenter defaultCenter] removeObserver:self name:AVAudioSessionInterruptionNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:AVCaptureSessionRuntimeErrorNotification object:self.session];
[[NSNotificationCenter defaultCenter] removeObserver:self name:AVAudioSessionInterruptionNotification object:[AVAudioSession sharedInstance]];
[self stopSession];
}
@ -254,7 +253,7 @@ BOOL _recordRequested = NO;
{
AVCaptureDevice *device = [self.videoCaptureDeviceInput device];
NSError *error = nil;
if(device == nil){
return;
}
@ -344,7 +343,7 @@ BOOL _recordRequested = NO;
- (void)defocusPointOfInterest
{
AVCaptureDevice *device = [self.videoCaptureDeviceInput device];
if (self.isFocusedOnPoint) {
@ -353,7 +352,7 @@ BOOL _recordRequested = NO;
if(device == nil){
return;
}
device.subjectAreaChangeMonitoringEnabled = NO;
[[NSNotificationCenter defaultCenter] removeObserver:self name:AVCaptureDeviceSubjectAreaDidChangeNotification object:device];
@ -375,7 +374,7 @@ BOOL _recordRequested = NO;
if(self.isExposedOnPoint){
self.isExposedOnPoint = NO;
if(device == nil){
return;
}
@ -395,7 +394,7 @@ BOOL _recordRequested = NO;
if(self.isExposedOnPoint){
self.isExposedOnPoint = NO;
if(device == nil){
return;
}
@ -413,7 +412,7 @@ BOOL _recordRequested = NO;
{
AVCaptureDevice *device = [self.videoCaptureDeviceInput device];
NSError *error = nil;
if(device == nil){
return;
}
@ -492,7 +491,7 @@ BOOL _recordRequested = NO;
{
AVCaptureDevice *device = [self.videoCaptureDeviceInput device];
NSError *error = nil;
if(device == nil){
return;
}
@ -547,7 +546,7 @@ BOOL _recordRequested = NO;
- (void)updateZoom {
AVCaptureDevice *device = [self.videoCaptureDeviceInput device];
NSError *error = nil;
if(device == nil){
return;
}
@ -577,7 +576,7 @@ BOOL _recordRequested = NO;
{
AVCaptureDevice *device = [self.videoCaptureDeviceInput device];
NSError *error = nil;
if(device == nil){
return;
}
@ -634,7 +633,7 @@ BOOL _recordRequested = NO;
{
AVCaptureDevice *device = [self.videoCaptureDeviceInput device];
NSError *error = nil;
if(device == nil){
return;
}
@ -689,6 +688,19 @@ BOOL _recordRequested = NO;
}
}
- (void)updateCaptureAudio
{
dispatch_async(self.sessionQueue, ^{
if(self.captureAudio){
[self initializeAudioCaptureSessionInput];
}
else{
[self removeAudioCaptureSessionInput];
}
});
}
- (void)takePictureWithOrientation:(NSDictionary *)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject{
[self.sensorOrientationChecker getDeviceOrientationWithBlock:^(UIInterfaceOrientation orientation) {
NSMutableDictionary *tmpOptions = [options mutableCopy];
@ -703,7 +715,7 @@ BOOL _recordRequested = NO;
- (void)takePicture:(NSDictionary *)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
{
// if video device is not set, reject
if(self.videoCaptureDeviceInput == nil){
if(self.videoCaptureDeviceInput == nil || !self.session.isRunning){
reject(@"E_IMAGE_CAPTURE_FAILED", @"Camera is not ready.", nil);
return;
}
@ -827,7 +839,7 @@ BOOL _recordRequested = NO;
}
- (void)record:(NSDictionary *)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
{
if(self.videoCaptureDeviceInput == nil){
if(self.videoCaptureDeviceInput == nil || !self.session.isRunning){
reject(@"E_VIDEO_CAPTURE_FAILED", @"Camera is not ready.", nil);
return;
}
@ -838,13 +850,13 @@ BOOL _recordRequested = NO;
}
NSInteger orientation = [options[@"orientation"] integerValue];
// some operations will change our config
// so we batch config updates, even if inner calls
// might also call this, only the outermost commit will take effect
// making the camera changes much faster.
[self.session beginConfiguration];
if (_movieFileOutput == nil) {
// At the time of writing AVCaptureMovieFileOutput and AVCaptureVideoDataOutput (> GMVDataOutput)
@ -923,40 +935,39 @@ BOOL _recordRequested = NO;
}
}
}
BOOL recordAudio = [options valueForKey:@"mute"] == nil || ([options valueForKey:@"mute"] != nil && ![options[@"mute"] boolValue]);
// sound recording connection, we can easily turn it on/off without manipulating inputs, this prevents flickering.
// note that mute will also be set to true
// if captureAudio is set to false on the JS side.
// Check the property anyways just in case it is manipulated
// with setNativeProps
if(recordAudio && self.captureAudio){
// if we haven't initialized our capture session yet
// initialize it. This will cause video to flicker.
if(self.audioCaptureDeviceInput == nil){
[self initializeAudioCaptureSessionInput];
}
[self initializeAudioCaptureSessionInput];
// finally, make sure we got access to the capture device
// and turn the connection on.
if(self.audioCaptureDeviceInput != nil){
AVCaptureConnection *audioConnection = [self.movieFileOutput connectionWithMediaType:AVMediaTypeAudio];
audioConnection.enabled = YES;
}
}
// if we have a capture input but are muted
// disable connection. No flickering here.
else if(self.audioCaptureDeviceInput != nil){
AVCaptureConnection *audioConnection = [self.movieFileOutput connectionWithMediaType:AVMediaTypeAudio];
audioConnection.enabled = NO;
}
dispatch_async(self.sessionQueue, ^{
NSString *path = nil;
@ -973,44 +984,44 @@ BOOL _recordRequested = NO;
[connection setVideoMirrored:YES];
}
}
// finally, commit our config changes before starting to record
[self.session commitConfiguration];
// and update flash in case it was turned off automatically
// due to session/preset changes
[self updateFlashMode];
// after everything is set, start recording with a tiny delay
// to ensure the camera already has focus and exposure set.
double delayInSeconds = 0.5;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
// we will use this flag to stop recording
// if it was requested to stop before it could even start
_recordRequested = YES;
dispatch_after(popTime, self.sessionQueue, ^(void){
// our session might have stopped in between the timeout
// so make sure it is still valid, otherwise, error and cleanup
if(self.movieFileOutput != nil && self.videoCaptureDeviceInput != nil && self.session.isRunning && _recordRequested){
if(self.movieFileOutput != nil && self.videoCaptureDeviceInput != nil && _recordRequested){
NSURL *outputURL = [[NSURL alloc] initFileURLWithPath:path];
[self.movieFileOutput startRecordingToOutputFileURL:outputURL recordingDelegate:self];
self.videoRecordedResolve = resolve;
self.videoRecordedReject = reject;
}
else{
reject(@"E_VIDEO_CAPTURE_FAILED", !_recordRequested ? @"Recording request cancelled." : @"Camera is not ready.", nil);
[self cleanupCamera];
}
// reset our flag
_recordRequested = NO;
});
});
}
@ -1081,18 +1092,7 @@ BOOL _recordRequested = NO;
}
[self setupOrDisableBarcodeScanner];
__weak RNCamera *weakSelf = self;
[self setRuntimeErrorHandlingObserver:
[NSNotificationCenter.defaultCenter addObserverForName:AVCaptureSessionRuntimeErrorNotification object:self.session queue:nil usingBlock:^(NSNotification *note) {
RNCamera *strongSelf = weakSelf;
dispatch_async(strongSelf.sessionQueue, ^{
// Manually restarting the session since it must
// have been stopped due to an error.
[strongSelf.session startRunning];
[strongSelf onReady:nil];
});
}]];
_sessionInterrupted = NO;
[self.session startRunning];
[self onReady:nil];
});
@ -1116,6 +1116,7 @@ BOOL _recordRequested = NO;
[self.previewLayer removeFromSuperlayer];
[self.session commitConfiguration];
[self.session stopRunning];
for (AVCaptureInput *input in self.session.inputs) {
[self.session removeInput:input];
}
@ -1123,7 +1124,11 @@ BOOL _recordRequested = NO;
for (AVCaptureOutput *output in self.session.outputs) {
[self.session removeOutput:output];
}
// cleanup audio input if any, and release
// audio session so other apps can continue playback.
[self removeAudioCaptureSessionInput];
// clean these up as well since we've removed
// all inputs and outputs from session
self.videoCaptureDeviceInput = nil;
@ -1136,58 +1141,128 @@ BOOL _recordRequested = NO;
// Note: Ensure this is called within a a session configuration block
- (void)initializeAudioCaptureSessionInput
{
NSError *error = nil;
AVCaptureDevice *audioCaptureDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
AVCaptureDeviceInput *audioDeviceInput = [AVCaptureDeviceInput deviceInputWithDevice:audioCaptureDevice error:&error];
// only initialize if not initialized already
if(self.audioCaptureDeviceInput == nil){
NSError *error = nil;
if (error || audioDeviceInput == nil) {
RCTLogWarn(@"%s: %@", __func__, error);
}
else{
if ([self.session canAddInput:audioDeviceInput]) {
[self.session addInput:audioDeviceInput];
self.audioCaptureDeviceInput = audioDeviceInput;
AVCaptureDevice *audioCaptureDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
AVCaptureDeviceInput *audioDeviceInput = [AVCaptureDeviceInput deviceInputWithDevice:audioCaptureDevice error:&error];
if (error || audioDeviceInput == nil) {
RCTLogWarn(@"%s: %@", __func__, error);
}
else{
RCTLog(@"Cannot add audio input");
// test if we can activate the device input.
// If we fail, means it is already being used
BOOL setActive = [[AVAudioSession sharedInstance] setActive:YES error:&error];
if (!setActive) {
RCTLogWarn(@"Audio device could not set active: %s: %@", __func__, error);
}
else if ([self.session canAddInput:audioDeviceInput]) {
[self.session addInput:audioDeviceInput];
self.audioCaptureDeviceInput = audioDeviceInput;
// inform that audio has been resumed
if(self.onAudioConnected){
self.onAudioConnected(nil);
}
}
else{
RCTLog(@"Cannot add audio input");
}
}
// if we failed to get the audio device, fire our interrupted event
if(self.audioCaptureDeviceInput == nil && self.onAudioInterrupted){
self.onAudioInterrupted(nil);
}
}
}
// Removes audio capture from the session, allowing the session
// to resume if it was interrupted, and stopping any
// recording in progress with the appropriate flags.
- (void)removeAudioCaptureSessionInput
{
if(self.audioCaptureDeviceInput != nil){
BOOL audioRemoved = NO;
if ([self.session.inputs containsObject:self.audioCaptureDeviceInput]) {
if ([self isRecording]) {
self.isRecordingInterrupted = YES;
}
[self.session removeInput:self.audioCaptureDeviceInput];
self.audioCaptureDeviceInput = nil;
// update flash since it gets reset when
// we change the session inputs
dispatch_async(self.sessionQueue, ^{
[self updateFlashMode];
});
audioRemoved = YES;
}
// Deactivate our audio session so other audio can resume
// playing, if any. E.g., background music.
NSError *error = nil;
BOOL setInactive = [[AVAudioSession sharedInstance] setActive:NO withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:&error];
if (!setInactive) {
RCTLogWarn(@"Audio device could not set inactive: %s: %@", __func__, error);
}
self.audioCaptureDeviceInput = nil;
// inform that audio was interrupted
if(audioRemoved && self.onAudioInterrupted){
self.onAudioInterrupted(nil);
}
}
}
- (void)initializeCaptureSessionInput
{
AVCaptureDevice *captureDevice = [self getDevice];
// if setting a new device is the same we currently have, nothing to do
// return.
if(self.videoCaptureDeviceInput != nil && captureDevice != nil && [self.videoCaptureDeviceInput.device.uniqueID isEqualToString:captureDevice.uniqueID]){
return;
}
// if the device we are setting is also invalid/nil, return
if(captureDevice == nil){
return;
}
__block UIInterfaceOrientation interfaceOrientation;
void (^statusBlock)(void) = ^() {
interfaceOrientation = [[UIApplication sharedApplication] statusBarOrientation];
};
if ([NSThread isMainThread]) {
statusBlock();
} else {
dispatch_sync(dispatch_get_main_queue(), statusBlock);
}
AVCaptureVideoOrientation orientation = [RNCameraUtils videoOrientationForInterfaceOrientation:interfaceOrientation];
dispatch_async(self.sessionQueue, ^{
// Do all camera initialization in the session queue
// to prevent it from
AVCaptureDevice *captureDevice = [self getDevice];
// if setting a new device is the same we currently have, nothing to do
// return.
if(self.videoCaptureDeviceInput != nil && captureDevice != nil && [self.videoCaptureDeviceInput.device.uniqueID isEqualToString:captureDevice.uniqueID]){
return;
}
// if the device we are setting is also invalid/nil, return
if(captureDevice == nil){
return;
}
// get orientation also in our session queue to prevent
// race conditions and also blocking the main thread
__block UIInterfaceOrientation interfaceOrientation;
dispatch_sync(dispatch_get_main_queue(), ^{
interfaceOrientation = [[UIApplication sharedApplication] statusBarOrientation];
});
AVCaptureVideoOrientation orientation = [RNCameraUtils videoOrientationForInterfaceOrientation:interfaceOrientation];
[self.session beginConfiguration];
NSError *error = nil;
@ -1230,7 +1305,7 @@ BOOL _recordRequested = NO;
[self.session addInput:captureDeviceInput];
self.videoCaptureDeviceInput = captureDeviceInput;
// Update all these async after our session has commited
// since some values might be changed on session commit.
dispatch_async(self.sessionQueue, ^{
@ -1242,15 +1317,15 @@ BOOL _recordRequested = NO;
[self updateWhiteBalance];
[self updateFlashMode];
});
[self.previewLayer.connection setVideoOrientation:orientation];
[self _updateMetadataObjectsToRecognize];
}
else{
RCTLog(@"The selected device does not work with the Preset [%@] or configuration provided", self.session.sessionPreset);
}
// if we have not yet set our audio capture device,
// set it. Setting it early will prevent flickering when
// recording a video
@ -1261,7 +1336,7 @@ BOOL _recordRequested = NO;
// the captureAudio prop by a simple permission check;
// for example, checking
// [[AVAudioSession sharedInstance] recordPermission] == AVAudioSessionRecordPermissionGranted
if(self.audioCaptureDeviceInput == nil && self.captureAudio){
if(self.captureAudio){
[self initializeAudioCaptureSessionInput];
}
@ -1301,50 +1376,99 @@ BOOL _recordRequested = NO;
}
- (void)bridgeDidForeground:(NSNotification *)notification
{
// do not run in async queue because we might end up with a race condition
// leaving the camera stuck after a resume. Queue is also not needed.
if (![self.session isRunning] && [self isSessionPaused]) {
self.paused = NO;
[self.session startRunning];
[self updateFlashMode]; // flash is disabled when session is paused
}
}
- (void)bridgeDidBackground:(NSNotification *)notification
{
if ([self isRecording]) {
self.isRecordingInterrupted = YES;
}
if ([self.session isRunning] && ![self isSessionPaused]) {
self.paused = YES;
[self.session stopRunning];
}
}
// We are using this event to detect audio interruption ended
// events since we won't receive it on our session
// after disabling audio.
- (void)audioDidInterrupted:(NSNotification *)notification
{
NSDictionary *userInfo = notification.userInfo;
NSInteger type = [[userInfo valueForKey:AVAudioSessionInterruptionTypeKey] integerValue];
switch (type) {
case AVAudioSessionInterruptionTypeBegan:
[self bridgeDidBackground: notification];
break;
case AVAudioSessionInterruptionTypeEnded:
[self bridgeDidForeground: notification];
break;
default:
break;
// if our audio interruption ended
if(type == AVAudioSessionInterruptionTypeEnded){
// and the end event contains a hint that we should resume
// audio. Then re-connect our audio session if we are
// capturing audio.
// Sometimes we are hinted to not resume audio; e.g.,
// when playing music in background.
NSInteger option = [[userInfo valueForKey:AVAudioSessionInterruptionOptionKey] integerValue];
if(self.captureAudio && option == AVAudioSessionInterruptionOptionShouldResume){
dispatch_async(self.sessionQueue, ^{
// initialize audio if we need it
// check again captureAudio in case it was changed
// in between
if(self.captureAudio){
[self initializeAudioCaptureSessionInput];
}
});
}
}
}
// session interrupted events
- (void)sessionWasInterrupted:(NSNotification *)notification
{
// Mark session interruption
_sessionInterrupted = YES;
// Turn on video interrupted if our session is interrupted
// for any reason
if ([self isRecording]) {
self.isRecordingInterrupted = YES;
}
// prevent any video recording start that we might have on the way
_recordRequested = NO;
// get event info and fire RN event if our session was interrupted
// due to audio being taken away.
NSDictionary *userInfo = notification.userInfo;
NSInteger type = [[userInfo valueForKey:AVCaptureSessionInterruptionReasonKey] integerValue];
if(type == AVCaptureSessionInterruptionReasonAudioDeviceInUseByAnotherClient){
// if we have audio, stop it so preview resumes
// it will eventually be re-loaded the next time recording
// is requested, although it will flicker.
[self removeAudioCaptureSessionInput];
}
}
// update flash and our interrupted flag on session resume
- (void)sessionDidStartRunning:(NSNotification *)notification
{
//NSLog(@"sessionDidStartRunning Was interrupted? %d", _sessionInterrupted);
if(_sessionInterrupted){
// resume flash value since it will be resetted / turned off
dispatch_async(self.sessionQueue, ^{
[self updateFlashMode];
});
}
_sessionInterrupted = NO;
}
- (void)sessionRuntimeError:(NSNotification *)notification
{
// Manually restarting the session since it must
// have been stopped due to an error.
dispatch_async(self.sessionQueue, ^{
_sessionInterrupted = NO;
[self.session startRunning];
[self onReady:nil];
});
}
- (void)orientationChanged:(NSNotification *)notification
{
UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation];

View File

@ -13,6 +13,8 @@
RCT_EXPORT_MODULE(RNCameraManager);
RCT_EXPORT_VIEW_PROPERTY(onCameraReady, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onAudioInterrupted, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onAudioConnected, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onMountError, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onBarCodeRead, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onFacesDetected, RCTDirectEventBlock);
@ -299,6 +301,7 @@ RCT_CUSTOM_VIEW_PROPERTY(textRecognizerEnabled, BOOL, RNCamera)
RCT_CUSTOM_VIEW_PROPERTY(captureAudio, BOOL, RNCamera)
{
[view setCaptureAudio:[RCTConvert BOOL:json]];
[view updateCaptureAudio];
}
RCT_CUSTOM_VIEW_PROPERTY(rectOfInterest, CGRect, RNCamera)

View File

@ -60,11 +60,16 @@
{
__weak __typeof(self) weakSelf = self;
self.orientationCallback = ^(UIInterfaceOrientation orientation) {
if (callback) {
callback(orientation);
// Synchronized because this might fire more than once
// under some circumstances, causing a very bad loop
// to people that uses it.
@synchronized (weakSelf) {
if (callback && weakSelf.orientationCallback) {
callback(orientation);
}
weakSelf.orientationCallback = nil;
[weakSelf pause];
}
weakSelf.orientationCallback = nil;
[weakSelf pause];
};
[self resume];
}

View File

@ -245,6 +245,8 @@ type PropsType = typeof View.props & {
focusDepth?: number,
type?: number | string,
onCameraReady?: Function,
onAudioInterrupted?: Function,
onAudioConnected?: Function,
onStatusChange?: Function,
onBarCodeRead?: Function,
onPictureSaved?: Function,
@ -381,6 +383,8 @@ export default class Camera extends React.Component<PropsType, StateType> {
focusDepth: PropTypes.number,
onMountError: PropTypes.func,
onCameraReady: PropTypes.func,
onAudioInterrupted: PropTypes.func,
onAudioConnected: PropTypes.func,
onStatusChange: PropTypes.func,
onBarCodeRead: PropTypes.func,
onPictureSaved: PropTypes.func,
@ -613,6 +617,18 @@ export default class Camera extends React.Component<PropsType, StateType> {
}
};
_onAudioInterrupted = () => {
if (this.props.onAudioInterrupted) {
this.props.onAudioInterrupted();
}
};
_onAudioConnected = () => {
if (this.props.onAudioConnected) {
this.props.onAudioConnected();
}
};
_onStatusChange = () => {
if (this.props.onStatusChange) {
this.props.onStatusChange({
@ -775,6 +791,8 @@ export default class Camera extends React.Component<PropsType, StateType> {
ref={this._setReference}
onMountError={this._onMountError}
onCameraReady={this._onCameraReady}
onAudioInterrupted={this._onAudioInterrupted}
onAudioConnected={this._onAudioConnected}
onGoogleVisionBarcodesDetected={this._onObjectDetected(
this.props.onGoogleVisionBarcodesDetected,
)}
@ -845,6 +863,8 @@ const RNCamera = requireNativeComponent('RNCamera', Camera, {
onBarCodeRead: true,
onGoogleVisionBarcodesDetected: true,
onCameraReady: true,
onAudioInterrupted: true,
onAudioConnected: true,
onPictureSaved: true,
onFaceDetected: true,
onLayout: true,

4
types/index.d.ts vendored
View File

@ -152,6 +152,10 @@ export interface RNCameraProps {
}): void;
onMountError?(error: { message: string }): void;
/** iOS only */
onAudioInterrupted?(): void;
onAudioConnected?(): void;
/** Value: float from 0 to 1.0 */
zoom?: number;
/** iOS only. float from 0 to any. Locks the max zoom value to the provided value