I am trying to get device motion updates on an iPhone or iPad in the background using CMMotionManager. I have reviewed all previous posts on this topic and thought that I had code that would work. My app also uses background audio, and this works properly in the foreground and background. In Info.plist, I have background audio and background location updates enabled.
For testing purposes, I have declared "var motionManager = CMMotionManager()" in AppDelegate, and included the following code in didFinishLaunchingWithOptions:
motionManager.deviceMotionUpdateInterval = 0.10
let queue = NSOperationQueue()
motionManager.startDeviceMotionUpdatesToQueue(queue, withHandler: {
data, error in
let accelerationVector = sqrt(pow(data!.userAcceleration.x, 2) + pow(data!.userAcceleration.y, 2) + pow(data!.userAcceleration.z, 2))
print("\(accelerationVector)")
})
When I run the app on my device, the code executes in the foreground as expected, but when I press the home button, I get about 10 more readings before it stops. When I tap on the app icon, the readings start again. I have also placed breakpoints on the code in the handler, and get similar results.
What am I missing?
Do NOT trust the NSLog or other similar log output
I used to meet this problem. I want collect the motion data in background and I made a demo about this. I found I can get all the data and log when my app in active, but the xCode log console output nothing when the application in background.
I used to think the problem is the CoreMotion data can only be collected in foreground, but I am wrong. All the callbacks still working when application enter background, Just the NSLog stop telling me the data.
If you don't believe, just collect all the data into a NSMutableArray or other collections, then check the data collected when app in background.
e.g.
#property (nonatomic, strong) NSMutableArray *arrAltimeters;
...
[self.altimeter startRelativeAltitudeUpdatesToQueue:self.operationQueue withHandler:^(CMAltitudeData * _Nullable altitudeData, NSError * _Nullable error) {
[self.arrAltimeters addObject:altitudeData];
NSLog(#"Altimate data count = %ld", self.arrAltimeters.count);
}];
I have declared the motion manager in ViewDidlLoad
CMMotionManager *motionManager;
/*--Initialising Motion Manager--*/
motionManager = [[CMMotionManager alloc] init];
motionManager.deviceMotionUpdateInterval = 1.0;
As whenever the app goes in background the motionManager stop providing callbacks in this case , You need to restart the motionmanager when the app goes in background or in foreground.To do this we need to follow the process below:
1) we first need to Register for the app transition notification :
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(appDidEnterBackground) name:UIApplicationDidEnterBackgroundNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(appDidBecomeActive) name:UIApplicationDidBecomeActiveNotification object:nil];
2)Notification callback functions, in which you will restart the motion manager
-(void)appDidEnterBackground{
[self restartMotionUpdates];
}
-(void)appDidBecomeActive{
[self restartMotionUpdates];
}
3)This function which will restart the motion manager
-(void)restartMotionUpdates{
[motionManager stopDeviceMotionUpdates];
[motionManager startDeviceMotionUpdatesToQueue:[NSOperationQueue mainQueue] withHandler:^(CMDeviceMotion *motion, NSError *error) {
NSLog(#"x=%f y=%f z=%f",fabs(motion.userAcceleration.x),fabs(motion.userAcceleration.y),fabs(motion.userAcceleration.z));
}];
}
After doing more research, I figured out the problem. I was using mpmusicplayercontroller to play background audio. This will play the audio in the background, but this does not keep my app running in the background. When I switched to using avplayer to play audio, the app runs in the background including the device motion updates.
Related
I have created a camera using AVCaptureSession. I have configured that for both Photo and Video recording modes.
Camera and App is running fine. Also I allowed background music play (If user play song using Music App in iPhone) while open camera or recording video. It is also working fine. (Attached image 2)
I allowed background Music play with the help of this code
AVAudioSession *session1 = [AVAudioSession sharedInstance];
[session1 setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:AVAudioSessionCategoryOptionMixWithOthers|AVAudioSessionCategoryOptionDefaultToSpeaker|AVAudioSessionCategoryOptionAllowBluetooth error:nil];
[session1 setActive:YES error:nil];
[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
Now if i receive a call, minimize phone call screen by tapping on Home button and open app and want to open camera screen to capture image / record video, It opens but freeze with a image (Attached image(1)).
Now my requirement is, i want to capture image / record video while on phone call. I looked for another apps, and Snapchat is doing same, and i am able to record video while i am on call.
please help me, how can i achieve this.
You need to use the AVCaptureSessionWasInterruptedNotification and AVCaptureSessionInterruptionEndedNotification callbacks and disconnect the audio capture while the session is interrupted:
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(sessionWasInterrupted:) name:AVCaptureSessionWasInterruptedNotification object:self.session];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(sessionInterruptionEnded:) name:AVCaptureSessionInterruptionEndedNotification object:self.session];
// note that self.session is an AVCaptureSession
-
- (void)sessionWasInterrupted:(NSNotification *)notification {
NSLog(#"session was interrupted");
AVCaptureDevice *device = [[self audioInput] device];
if ([device hasMediaType:AVMediaTypeAudio]) {
[[self session] removeInput:[self audioInput]];
[self setAudioInput:nil];
}
}
- (void)sessionInterruptionEnded:(NSNotification *)notification {
NSLog(#"session interuption ended");
}
// note that [self audioInput] is a getter for an AVCaptureDeviceInput
This will allow the camera to continue running and allows it to capture stills / silent video
Now as for how to reconnect the audio after the call ends.. let me know if you figure it out, seemed impossible as of iOS 10: Callback when phone call ends? (to resume AVCaptureSession)
I have added an observer for the interrupt notification when recording audio.
This works fine when performing an outgoing-call, getting an incoming call and not answering, Siri, etc..
Now my app is running in the background with the red bar at the top of the screen, and continuing the recording in the states described above is not a problem.
But when I actually answer an incoming-call. I get another AVAudioSessionInterruptionTypeBegan notification and then when I stop the call, I never get a notification AVAudioSessionInterruptionTypeEnded type.
I have tried using the CTCallCenter to detect when a call has started, but I am unable to restart the recording from that callback.
Does anyone know how to get the interrupt mechanism to work with an incoming call that is actually getting answered?
This is (part of) the code I am using;
CFNotificationCenterAddObserver(
CFNotificationCenterGetLocalCenter(),
this,
&handle_interrupt,
(__bridge CFStringRef) AVAudioSessionInterruptionNotification,
NULL,
CFNotificationSuspensionBehaviorDeliverImmediately );
...
static void handle_interrupt( CFNotificationCenterRef center, void *observer, CFStringRef name, const void *object, CFDictionaryRef userInfo )
{
au_recorder *recorder = static_cast<au_recorder*>( observer );
NSNumber* interruptionType = [( ( __bridge NSDictionary* ) userInfo ) objectForKey:AVAudioSessionInterruptionTypeKey];
switch ( [interruptionType unsignedIntegerValue] )
{
case AVAudioSessionInterruptionTypeBegan:
{
// pause recorder without stopping recording (already done by OS)
recorder->pause();
break;
}
case AVAudioSessionInterruptionTypeEnded:
{
NSNumber* interruptionOption = [( ( __bridge NSDictionary* ) userInfo ) objectForKey:AVAudioSessionInterruptionOptionKey];
if ( interruptionOption.unsignedIntegerValue == AVAudioSessionInterruptionOptionShouldResume )
{
recorder->resume();
}
break;
}
}
}
I have tried binding the notification to either the AppDelegate, a UIViewController and a separate class, but that doesn't appear to help.
Edit
This is what I tried using the CTCallCenter, but this is very flaky. When recorder->resume() is called from the callback, it either works, crashes violently or doesn't do anything at all until the app is put back in the foreground again manually.
callCenter = [[CTCallCenter alloc] init];
callCenter.callEventHandler = ^(CTCall *call)
{
if ([call.callState isEqualToString:CTCallStateDisconnected])
{
recorder->resume();
}
};
UPDATE
If you hackily wait for a second or so, you can restart the recording from your callEventHandler (although you haven't described your violent crashes, it works fine for me):
if ([call.callState isEqualToString:CTCallStateDisconnected])
{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
self.recorder->resume();
});
}
This is all without a background task or changing to an exclusive audio session. The delay works around the fact that the call ending notification comes in before Phone.app deactivates its audio session & 1 second seems to be small enough to fall into some kind of background descheduling grace period. I've lost count of how many implementation details are assumed in this, so maybe you'd like to read on to the
Solution that seems to be backed by documentation:
N.B While there's a link to "suggestive" documentation for this solution, it was mostly guided by these two errors popping up and their accompanying comments in the header files:
AVAudioSessionErrorCodeCannotInterruptOthers
The app's audio session is non-mixable and trying to go active while in the background.
This is allowed only when the app is the NowPlaying app.
AVAudioSessionErrorInsufficientPriority
The app was not allowed to set the audio category because another app (Phone, etc.) is controlling it.
You haven't shown how your AVAudioSession is configured, but the fact that you're not getting an AVAudioSessionInterruptionTypeEnded notification suggests you're not using an exclusive audio session (i.e. you're setting "mix with others"):
[session setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:AVAudioSessionCategoryOptionMixWithOthers error:&error];
The obsoleted CTCallCenter and newer CXCallObserver callbacks seem to happen too early, before the interruption ends, and maybe with some further work you could find a way to run a background task that restarts the recording a little while after the call ends (my initial attempts at this failed).
So right now, the only way I know to restart the recording after receiving a phone call is:
Use an exclusive audio session (this gives you an end interruption):
if (!([session setCategory:AVAudioSessionCategoryPlayAndRecord error:&error]
&& [session setActive:YES error:&error])) {
NSLog(#"Audio session error: %#", error);
}
and integrate with "Now Playing" (not joking, this is a structural part of the solution):
MPRemoteCommandCenter *commandCenter = [MPRemoteCommandCenter sharedCommandCenter];
[commandCenter.playCommand addTargetWithHandler:^(MPRemoteCommandEvent *event) {
NSLog(#"PLAY");
return MPRemoteCommandHandlerStatusSuccess;
}];
[commandCenter.pauseCommand addTargetWithHandler:^(MPRemoteCommandEvent *event) {
NSLog(#"PAUSE");
return MPRemoteCommandHandlerStatusSuccess;
}];
// isn't this mutually exclusive with mwo? or maybe that's remote control keys
// not seeing anything. maybe it needs actual playback?
NSDictionary* nowPlaying = #{
MPMediaItemPropertyTitle: #"foo",
MPMediaItemPropertyArtist: #"Me",
MPMediaItemPropertyAlbumTitle: #"brown album",
};
[MPNowPlayingInfoCenter defaultCenter].nowPlayingInfo = nowPlaying;
Pieces of this solution were cherry picked from the Audio Guidelines for User-Controlled Playback and Recording Apps documentation.
p.s. maybe the lock screen integration or non-exclusive audio session are deal breakers for you, but there are other situations where you'll never get an end interruption, e.g. launching another exclusive app, like Music (although this may not be an issue with mixWithOthers, so maybe you should go with the code-smell delay solution.
I'm new to iOS programming. I'm wanting to write an app that has constant access to the accelerometer, and barometric pressure sensor of the iPhone 6, while running in the background.
In my research, I've found that people have done it using the Location Updates background mode, but this only worked for 10 minutes before the app was suspended (and this changed to 3 minutes in iOS 6?). I'd also read about using the audio background mode playing a mute sound as a way to work around that time limit. This seems like a substandard solution though.
I'm wondering then how an app something like Sleep Cycle does this, as it seems like it must have constant access to the accelerometer.
It can be done by using CoreMotion framework.
You have to import CoreMotion framework, then #import <CoreMotion/CoreMotion.h> in your appdelegate.
Here motionManager is object of CMMotionManager.
xData, yData, zData are double values to store accelerometer data.
if (motionManager ==nil) {
motionManager= [[CMMotionManager alloc]init];
}
[motionManager startAccelerometerUpdates];
[motionManager startAccelerometerUpdatesToQueue:[NSOperationQueue mainQueue] withHandler:^(CMAccelerometerData *accelerometerData, NSError *error) {
xData = accelerometerData.acceleration.x;
yData = accelerometerData.acceleration.y;
zData = accelerometerData.acceleration.z;
}];
You have to do it in - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions.
You can then use these value of xData, yData, zData where ever you want by appdelegate object, even in background.
I have an app with a view that, when loaded, gets regular device motion updates (20 per second) and uses them to display things on the screen. This was working fine using the old (deprecated) UIAccelerometer code.
I have since ported it to use Core Motion and the CMMotionManager but am having the problem that every time (except the first time) one of these views is loaded, only one or two updates are received in the first second or so, and then they start being received as expected (20 per second).
Relevant code in the view:
- (void)viewDidLoad {
[super viewDidLoad];
// Register to receive acceleration from motion manager.
motionManager = [[CMMotionManager alloc] init];
motionManager.deviceMotionUpdateInterval = 0.05;
[motionManager startDeviceMotionUpdatesToQueue:[NSOperationQueue currentQueue]
withHandler:^(CMDeviceMotion *motion, NSError *error) {
// Handle motion and update UI here.
}
];
}
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
// Stop the motion updates.
[motionManager stopDeviceMotionUpdates];
}
- (IBAction)exitView:(id)sender {
// Stop the motion updates.
[motionManager stopDeviceMotionUpdates];
// Pop the view from the navigation controller.
}
I've added some NSLog statements and can confirm that motion updates are being requested as soon as the view loads, and that the handler is only receiving updates as I described above.
Is there anything I'm missing that I can do to get the CoreMotion version to behave the same as the old version, i.e. without the delay?
Currently I'm able to get updates from the accelerometer using CMAccelerometer
_motionManager.accelerometerUpdateInterval = kUpdateInterval;
[_motionManager startAccelerometerUpdatesToQueue:self.queue withHandler:
^(CMAccelerometerData *accelerometerData, NSError *error) {
[(id) self setAcceleration:accelerometerData.acceleration];
[self performSelectorOnMainThread:#selector(update) withObject:nil waitUntilDone:NO];
}];
- (void)update {
... code to handle data ...
}
And this is working fine, however when I close my app (i.e. put it into the background not kill the instance), I no longer receive updates from my _motionManager. How do I handle this?
You can register your app to listen to location, and use CoreMotion. Then you can get accelerometer data in background.
Basically your app needs to be allowed to run in the background for other reasons.
Example: