I'm trying to figure out if this is possible - my app activates an audio session that is initialized as:
[[[AVAudioSession alloc] init] setCategory:AVAudioSessionCategoryPlayback withOptions:AVAudioSessionCategoryOptionMixWithOthers error:&error];
I would like to be able to understand when an additional audio session that originated from another app or the OS is playing.
I know about the ability to implement the delegate methods beginInterruption: and endInterruption but these won't get invoked because of the AVAudioSessionCategoryOptionMixWithOthers option I'm using.
Is there a way to achieve this without using private API?
Thanks in advance.
The way you manage your application's Audio Session has had some significant changes since iOS 6.0, and deserves a brief mention first. Before iOS 6.0 you would make use of AVAudioSession and AudioSessionServices classes, incorporating delegation and property listening respectively. From iOS 6.0 onwards use AVAudioSession class and incorporate notifications.
The following is for iOS 6.0 onwards.
To tell if other audio outside your applications sandbox is playing use -
// query if other audio is playing
BOOL isPlayingWithOthers = [[AVAudioSession sharedInstance] isOtherAudioPlaying];
// test it with...
(isPlayingWithOthers) ? NSLog(#"other audio is playing") : NSLog(#"no other audio is playing");
As for interruption handling you'll need to observe AVAudioSessionInterruptionNotification and AVAudioSessionRouteChangeNotification. So in the class that manages your audio session you could put something like the following - this should be called once at the start of the application lifecycle and don't forget to remove observer in the dealloc method of the same class.
// ensure we already have a singleton object
[AVAudioSession sharedInstance];
// register for notifications
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(interruption:)
name:AVAudioSessionInterruptionNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(routeChange:)
name:AVAudioSessionRouteChangeNotification
object:nil];
And finally add the following selectors interruption: and routeChange: - these will receive a NSNotification object that has a property called userInfo of type NSDictionary that you read to assist any conditionals your application has.
- (void)interruption:(NSNotification*)notification {
// get the user info dictionary
NSDictionary *interuptionDict = notification.userInfo;
// get the AVAudioSessionInterruptionTypeKey enum from the dictionary
NSInteger interuptionType = [[interuptionDict valueForKey:AVAudioSessionInterruptionTypeKey] integerValue];
// decide what to do based on interruption type here...
switch (interuptionType) {
case AVAudioSessionInterruptionTypeBegan:
NSLog(#"Audio Session Interruption case started.");
// fork to handling method here...
// EG:[self handleInterruptionStarted];
break;
case AVAudioSessionInterruptionTypeEnded:
NSLog(#"Audio Session Interruption case ended.");
// fork to handling method here...
// EG:[self handleInterruptionEnded];
break;
default:
NSLog(#"Audio Session Interruption Notification case default.");
break;
} }
And similarly...
- (void)routeChange:(NSNotification*)notification {
NSDictionary *interuptionDict = notification.userInfo;
NSInteger routeChangeReason = [[interuptionDict valueForKey:AVAudioSessionRouteChangeReasonKey] integerValue];
switch (routeChangeReason) {
case AVAudioSessionRouteChangeReasonUnknown:
NSLog(#"routeChangeReason : AVAudioSessionRouteChangeReasonUnknown");
break;
case AVAudioSessionRouteChangeReasonNewDeviceAvailable:
// a headset was added or removed
NSLog(#"routeChangeReason : AVAudioSessionRouteChangeReasonNewDeviceAvailable");
break;
case AVAudioSessionRouteChangeReasonOldDeviceUnavailable:
// a headset was added or removed
NSLog(#"routeChangeReason : AVAudioSessionRouteChangeReasonOldDeviceUnavailable");
break;
case AVAudioSessionRouteChangeReasonCategoryChange:
// called at start - also when other audio wants to play
NSLog(#"routeChangeReason : AVAudioSessionRouteChangeReasonCategoryChange");//AVAudioSessionRouteChangeReasonCategoryChange
break;
case AVAudioSessionRouteChangeReasonOverride:
NSLog(#"routeChangeReason : AVAudioSessionRouteChangeReasonOverride");
break;
case AVAudioSessionRouteChangeReasonWakeFromSleep:
NSLog(#"routeChangeReason : AVAudioSessionRouteChangeReasonWakeFromSleep");
break;
case AVAudioSessionRouteChangeReasonNoSuitableRouteForCategory:
NSLog(#"routeChangeReason : AVAudioSessionRouteChangeReasonNoSuitableRouteForCategory");
break;
default:
break;
} }
There is no need to poll anything as long as you check the state of your applications audio session say for example in the viewDidLoad of your root view controller, at the start of you apps lifecycle. Any changes from there onwards to your applications audio session will be known via these two main notifications. Replace the NSLog statements with what ever your code needs to do based on the cases contained in the switch.
You can find more information about AVAudioSessionInterruptionTypeKey and AVAudioSessionRouteChangeReasonKey in the AVAudioSession class reference documentation.
My apologies for the long answer but I think Audio Session management in iOS is rather fiddly and Apple's Audio Session Programming Guide, at the time of writing this, does not include code examples using notifications for interruption handling.
You can check if other audio is playing like this:
UInt32 otherAudioIsPlaying;
UInt32 propertySize = sizeof (otherAudioIsPlaying);
AudioSessionGetProperty (kAudioSessionProperty_OtherAudioIsPlaying, &propertySize, &otherAudioIsPlaying );
[self handleIfAudioIsPlaying: otherAudioIsPlaying];
Then you can add a loop and check every X second if something changed.
Related
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 trying to adapt my code from using only WCSessionDelegate callbacks in the foreground to accepting WKWatchConnectivityRefreshBackgroundTask via handleBackgroundTasks: in the background. The documentation states that background tasks may come in asynchronously and that one should not call setTaskCompleted until the WCSession's hasContentPending is NO.
If I put my watch app into the background and transferUserInfo: from an iPhone app, I am able to successfully receive my first WKWatchConnectivityRefreshBackgroundTask. However, hasContentPending is always YES, so I save away the task and simply return from my WCSessionDelegate method. If I transferUserInfo: again, hasContentPending is NO, but there is no WKWatchConnectivityRefreshBackgroundTask associated with this message. That is, subsequent transferUserInfo: do not trigger a call to handleBackgroundTask: –- they're simply handled by the WCSessionDelegate. Even if I immediately setTaskCompleted without checking hasContentPending, subsequent transferUserInfo: are handled by session:didReceiveUserInfo: without me needing to activate a WCSession again.
I'm not sure what to do here. There doesn't seem to be a way to force a WCSession to deactivate, and following the documentation about delaying setTaskCompleted seems to be getting me into trouble with the OS.
I've posted and documented a sample project illustrating my workflow on GitHub, pasting my WKExtensionDelegate code below. Am I making an incorrect choice or interpreting the documentation incorrectly somewhere along the line?
I've looked at the QuickSwitch 2.0 source code (after fixing the Swift 3 bugs, rdar://28503030), and their method simply doesn't seem to work (there's another SO thread about this). I've tried using KVO for WCSession's hasContentPending and activationState, but there's still never any WKWatchConnectivityRefreshBackgroundTask to complete, which makes sense to be given my current explanation of the issue.
#import "ExtensionDelegate.h"
#interface ExtensionDelegate()
#property (nonatomic, strong) WCSession *session;
#property (nonatomic, strong) NSMutableArray<WKWatchConnectivityRefreshBackgroundTask *> *watchConnectivityTasks;
#end
#implementation ExtensionDelegate
#pragma mark - Actions
- (void)handleBackgroundTasks:(NSSet<WKRefreshBackgroundTask *> *)backgroundTasks
{
NSLog(#"Watch app woke up for background task");
for (WKRefreshBackgroundTask *task in backgroundTasks) {
if ([task isKindOfClass:[WKWatchConnectivityRefreshBackgroundTask class]]) {
[self handleBackgroundWatchConnectivityTask:(WKWatchConnectivityRefreshBackgroundTask *)task];
} else {
NSLog(#"Handling an unsupported type of background task");
[task setTaskCompleted];
}
}
}
- (void)handleBackgroundWatchConnectivityTask:(WKWatchConnectivityRefreshBackgroundTask *)task
{
NSLog(#"Handling WatchConnectivity background task");
if (self.watchConnectivityTasks == nil)
self.watchConnectivityTasks = [NSMutableArray new];
[self.watchConnectivityTasks addObject:task];
if (self.session.activationState != WCSessionActivationStateActivated)
[self.session activateSession];
}
#pragma mark - Properties
- (WCSession *)session
{
NSAssert([WCSession isSupported], #"WatchConnectivity is not supported");
if (_session != nil)
return (_session);
_session = [WCSession defaultSession];
_session.delegate = self;
return (_session);
}
#pragma mark - WCSessionDelegate
- (void)session:(WCSession *)session activationDidCompleteWithState:(WCSessionActivationState)activationState error:(NSError *)error
{
switch(activationState) {
case WCSessionActivationStateActivated:
NSLog(#"WatchConnectivity session activation changed to \"activated\"");
break;
case WCSessionActivationStateInactive:
NSLog(#"WatchConnectivity session activation changed to \"inactive\"");
break;
case WCSessionActivationStateNotActivated:
NSLog(#"WatchConnectivity session activation changed to \"NOT activated\"");
break;
}
}
- (void)sessionWatchStateDidChange:(WCSession *)session
{
switch(session.activationState) {
case WCSessionActivationStateActivated:
NSLog(#"WatchConnectivity session activation changed to \"activated\"");
break;
case WCSessionActivationStateInactive:
NSLog(#"WatchConnectivity session activation changed to \"inactive\"");
break;
case WCSessionActivationStateNotActivated:
NSLog(#"WatchConnectivity session activation changed to \"NOT activated\"");
break;
}
}
- (void)session:(WCSession *)session didReceiveUserInfo:(NSDictionary<NSString *, id> *)userInfo
{
/*
* NOTE:
* Even if this method only sets the task to be completed, the default
* WatchConnectivity session delegate still picks up the message
* without another call to handleBackgroundTasks:
*/
NSLog(#"Received message with counter value = %#", userInfo[#"counter"]);
if (session.hasContentPending) {
NSLog(#"Task not completed. More content pending...");
} else {
NSLog(#"No pending content. Marking all tasks (%ld tasks) as complete.", (unsigned long)self.watchConnectivityTasks.count);
for (WKWatchConnectivityRefreshBackgroundTask *task in self.watchConnectivityTasks)
[task setTaskCompleted];
[self.watchConnectivityTasks removeAllObjects];
}
}
#end
From your description and my understanding it sounds like this is working correctly.
The way this was explained to me is that the new handleBackgroundTasks: on watchOS is intended to be a way for:
the system to communicate to the WatchKit extension why it is being launched/resumed in the background, and
a way for the WatchKit extension to let the system know when it has completed the work it wants to do and can therefore be terminated/suspended again.
This means that whenever an incoming WatchConnectivity payload is received on the Watch and your WatchKit extension is terminated or suspended you should expect one handleBackgroundTasks: callback letting you know why you are running in the background. This means you could receive 1 WKWatchConnectivityRefreshBackgroundTask but several WatchConnectivity callbacks (files, userInfos, applicationContext). The hasContentPending lets you know when your WCSession has delivered all of the initial, pending content (files, userInfos, applicationContext). At that point, you should call the setTaskCompleted on the WKWatchConnectivityRefreshBackgroundTask object.
Then you can expect that your WatchKit extension will soon thereafter be suspended or terminated unless you have received other handleBackgroundTasks: callbacks and therefore have other WK background task objects to complete.
I have found that when attaching to the processes with a debugger that OSs might not suspended them like they normally would, so it'd suggest inspecting the behavior here using logging if you want to be sure to avoid any of those kind of issues.
I would like my app to be notified when microphone interruption has ended before i start recording.
say for example my application is trying to start recording from background and during this time if any other applications like skype is accessing microphone and i shouldn't do. so i want to start register/observe for interruption so that when skype call ended, my app should be notified.
I can able to achieve only when app is recording and interruption began . i will be notified when interruption ended not on while about to start recording .
please help .
I tried to register using
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(handleAudioSessionInterruption:) name:AVAudioSessionInterruptionNotification object:[AVAudioSession sharedInstance]];
// observe the interruption begin / end
- (void)handleAudioSessionInterruption:(NSNotification*)notification
{
AVAudioSessionInterruptionType interruptionType = [notification.userInfo[AVAudioSessionInterruptionTypeKey] unsignedIntegerValue];
AVAudioSessionInterruptionOptions interruptionOption = [notification.userInfo[AVAudioSessionInterruptionOptionKey] unsignedIntegerValue];
NSLog(#"interruptionType:%lu",(unsigned long)interruptionType);
switch (interruptionType) {
case AVAudioSessionInterruptionTypeBegan:{
NSLog(#"interruption began");
break;
}
case AVAudioSessionInterruptionTypeEnded:{
NSLog(#"interruption ended");
break;
}
default:
break;
}
}
We are recording audio with AVAudioRecorder but when ever we receive any incoming call Recording gets stopped. If we try to Resume recording again then it will start from the beginning. I have checked the Voice Memos(iOS Apple Application) & find the same problem. I have also checked the Audio Queue services Sample Code (SpeakHere) But same problem is there.
So Please help how to resume audio recording after incoming call?
Note:- I Know there are many question related to audio recording But unable to find solution.So please Help, Your help is greatly appreciated. Thanks in advance.
It is possible in iOS,to resume a audio recording after incoming call.Basic concept behind this is the use of AVAudioSession and AudioQueue ,when you receive a incoming call on your device it would give a kAudioSessionBeginInterruption and on disconnecting the call you got a kAudioSessionEndInterruption......on receiving a incoming call ,just pause the audio queue and then [[AVAudioSession sharedInstance]setActive:NO error:nil];
......Then on the kAudioSessionEndInterruption ,when the call ends,just resume the paused queue and [[AVAudioSession sharedInstance]setActive:YES error:nil];
This will work......I have successfully run the program and able to continue the recording after receiving an incoming call......
Hope this will help you.......
Apple explain AVAudioSessionInterruption observer as a new method to handle interruptions. Apple explains it in Swift at this page but for someones that search for objective-c code this github issue must be useful.
You need to handle audio interrupts like
AVAudioSession *session = [AVAudioSession sharedInstance];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(audioSessionInterruptionNotification:)
name:AVAudioSessionInterruptionNotification
object:session];
where your audio session interruption notification AVAudioSessionInterruptionNotification is something like
-(void)audioSessionInterruptionNotification:(NSNotification*)notification {
NSString* seccReason = #"";
//Check the type of notification, especially if you are sending multiple AVAudioSession events here
NSLog(#"Interruption notification name %#", notification.name);
if ([notification.name isEqualToString:AVAudioSessionInterruptionNotification]) {
seccReason = #"Interruption notification received";
//Check to see if it was a Begin interruption
if ([[notification.userInfo valueForKey:AVAudioSessionInterruptionTypeKey] isEqualToNumber:[NSNumber numberWithInt:AVAudioSessionInterruptionTypeBegan]]) {
seccReason = #"Interruption began";
} else if([[notification.userInfo valueForKey:AVAudioSessionInterruptionTypeKey] isEqualToNumber:[NSNumber numberWithInt:AVAudioSessionInterruptionTypeEnded]]){
seccReason = #"Interruption ended!";
//Resume your audio
}
}
}
You need to write your handling code then for interruption begin and end.
Use the AVFoundation Delegate Methods,
-(void)audioRecorderBeginInterruption:(AVAudioRecorder *)recorder
{
[audiorecorder pause];
}
-(void)audioRecorderEndInterruption:(AVAudioRecorder *)recorder
{
[audiorecorder record];
}
I’m embedding YouTube videos in a UIWebView for an iOS app. I’m using “Method 2” at this YouTube blog post to embed the video. This works great, except that because iOS manages the media player, I can’t determine whether the video is playing or is done. I don’t want to swap that view out with another one while the video is playing, but I don’t see a good way to determine that. Any ideas? If there’s a way to get a JavaScript callback, that will work, or if there’s a way to embed YouTube videos using the HTML5 <video> tag, that will work as well (I’ve tried that and not gotten success).
Just add observer for MPAVControllerPlaybackStateChangedNotification.
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(playbackStateDidChange:)
name:#"MPAVControllerPlaybackStateChangedNotification"
object:nil];
then start listening:
- (void)playbackStateDidChange:(NSNotification *)note
{
NSLog(#"note.name=%# state=%d", note.name, [[note.userInfo objectForKey:#"MPAVControllerNewStateParameter"] intValue]);
int playbackState = [[note.userInfo objectForKey:#"MPAVControllerNewStateParameter"] intValue];
switch (playbackState) {
case 1: //end
;
break;
case 2: //start
;
break;
default:
break;
}
}
Explore other states if you're curious. Additionally everyone interested in bunch of other notifications can register to see all:
CFNotificationCenterAddObserver(CFNotificationCenterGetLocalCenter(),
NULL,
noteCallbackFunction,
NULL,
NULL,
CFNotificationSuspensionBehaviorDeliverImmediately);
then check what's coming on:
void noteCallbackFunction (CFNotificationCenterRef center,
void *observer,
CFStringRef name,
const void *object,
CFDictionaryRef userInfo)
{
NSLog(#"notification name: %#", name);
NSLog(#"notification info: %#", userInfo);
}
Have fun!
you can inject javascript into a UIWebView (see http://iphoneincubator.com/blog/windows-views/how-to-inject-javascript-functions-into-a-uiwebview)... other interesting stuff about javascript and UIWebView can be found here and here.
try using this together with this experimental youtube API (see http://code.google.com/apis/youtube/iframe_api_reference.html)... this should be able to get you what you want.
Another useful resource for this to make a callback from javascript to your code is here.
For iPhone I used some tricky method. You could get a notification when video modal view controller was dismissed.
-(void) onUIWebViewButtonTouch:(id) sender
{
self.isWatchForNotifications = YES;
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:#selector(windowNowVisible:)
name:UIWindowDidBecomeVisibleNotification
object:self.view.window
];
}
- (void)windowNowVisible:(NSNotification *)note
{
if (isWatchForNotifications == YES)
{
//modal viewcontroller was dismissed
}
self.isWatchForNotifications = NO;
}