官方文档镇楼
https://developer.apple.com/library/archive/documentation/Audio/Conceptual/AudioSessionProgrammingGuide/Introduction/Introduction.html
音频在iOS,tvOS
是一种托管服务。系统通过使用音频会话管理应用程序、应用程序间和设备级别的音频行为。
我们使用AVAudioSession
来与系统沟通在我们的应用程序中使用音频。我们可以做的交互如下
Category
)和模式(mode
),以便与系统通信,了解您打算如何在应用程序中使用音频Category
)和模式(mode
)配置放入操作中AVAudioSessionInterruptionNotification
)和路由更改(AVAudioSessionRouteChangeNotification
)采样速率
、I/O
缓冲区持续时间和通道数量AVAudioSession
是App和System之间的中介,在App启动的时候,应用程序会自动提供一个AVAudioSession
单例,你通过设置这个单例来实现音频配置。
https://developer.apple.com/videos/play/wwdc2012/505/
https://developer.apple.com/library/archive/qa/qa1754/_index.html
官方文档
https://developer.apple.com/library/archive/documentation/Audio/Conceptual/AudioSessionProgrammingGuide/ConfiguringanAudioSession/ConfiguringanAudioSession.html#//apple_ref/doc/uid/TP40007875-CH2-SW1
当我们的App启动时,内置的应用程序或者其他App(消息、音乐、Safari、手机)可能正在后台运行。它们中的每一个都可能产生音频:一条文本消息到达,音乐后台播放。
Q1:当QQ音乐后台播放时,你的App需要播放声音,如何处理这种竞争关系呢?
背景:Music App正在占用音频播放。此时我们的MyApp需要进行音频播放
1-2.MyApp请求CoreAudio控制中心,我请求激活音频会话
//get your app's audioSession singleton object
AVAudioSession* session = [AVAudioSession sharedInstance];
[session setCategory:AVAudioSessionCategoryPlayAndRecord
withOptions:AVAudioSessionCategoryOptionDefaultToSpeaker |AVAudioSessionCategoryOptionAllowBluetooth
error:nil];
[session setMode:AVAudioSessionModeDefault error:&error];
[session setActive:YES error:&error];
3-4.通过MyApp的设置代码,CoreAudio考虑其类别,对其他音频进行处理
如果,您的应用程序使用了一个类别(不是AVAudioSessionCategoryOptionMixWithOthers),该类别要求对其他音频进行静音,系统将关闭音乐应用程序的音频会话,停止其音频播放。
5.系统(CoreAudio)激活您的应用程序的音频会话和播放可以开始。
Q2:当你的App正在播放,此时来了一个电话,接听电话后,如何让App恢复音频播放?
https://developer.apple.com/library/archive/documentation/Audio/Conceptual/AudioSessionProgrammingGuide/HandlingAudioInterruptions/HandlingAudioInterruptions.html
当电话打断,短信打断时,此类音频打断我们都会受到一个AVAudioSessionInterruptionNotification
通知,通过监听这个通知,我们处理App的音频恢复播放
AVAudioSession *session = [AVAudioSession sharedInstance];
NSNotificationCenter *noficationCenter = [NSNotificationCenter defaultCenter];
[noficationCenter addObserver: self
selector: @selector(handleRouteChange:)
name: AVAudioSessionRouteChangeNotification
object: session]; //这是route改变通知,例如耳机插拔
[noficationCenter addObserver: self
selector: @selector(handleInterruption:)
name: AVAudioSessionInterruptionNotification
object: session]; //这是音频打断通知
以下是我的项目中处理打断通知的模板,[AVAudioSession sharedInstance].secondaryAudioShouldBeSilencedHint
指示另一应用程序是否正在播放音频,与otherAudioPlaying
一样,只是在iOS 8
之后,其判断更加严格
- (void)handleInterruption:(NSNotification*)notification {
NSInteger reason = 0;
NSString *reasonStr = @"";
if ([notification.name isEqualToString:AVAudioSessionInterruptionNotification]) {
//Posted when an audio interruption occurs.
//bug https://blog.csdn.net/shengpeng3344/article/details/83617979
if ([[[UIDevice currentDevice] systemVersion] compare:@"10.3" options:NSNumericSearch] != NSOrderedAscending) {
BOOL isSuspend = [[[notification userInfo] objectForKey:AVAudioSessionInterruptionWasSuspendedKey] boolValue];
if (isSuspend) {
LogInfo(@"[GSAudioStreamer] AVAudioSessionInterruptionWasSuspendedKey is YES");
return;
}
} else {
// Fallback on earlier versions
}
reason = [[[notification userInfo] objectForKey:AVAudioSessionInterruptionTypeKey] integerValue];
if (reason == AVAudioSessionInterruptionTypeBegan) { //在非后台模式下 打断处理 - 后台打断处理会导致底层线程卡死
reasonStr = @"AVAudioSessionInterruptionTypeBegan";
if ([self pause]) {
pausedByInterruption = YES;
}
//should stop whatever in background or app is not background audio type
}
if (reason == AVAudioSessionInterruptionTypeEnded) {
reasonStr = @"AVAudioSessionInterruptionTypeEnded";
NSNumber *seccondReason = [[notification userInfo] objectForKey:AVAudioSessionInterruptionOptionKey];
switch ([seccondReason integerValue]) {
case AVAudioSessionInterruptionOptionShouldResume:{ //在非后台模式下 打断处理 - 后台打断处理会导致底层线程卡死
reasonStr = @"AVAudioSessionInterruptionTypeEnded - resume ";
}
case 0:{
reasonStr = @"AVAudioSessionInterruptionTypeEnded - unknown";
}
// Indicates that the audio session is active and immediately ready to be used. Your app can resume the audio operation that was interrupted.
break;
default:
break;
}
if (pausedByInterruption) {
[self resume];
pausedByInterruption = NO;
}
}
}
LogInfo(@"[GSAudioStreamer] handleInterruption reason %@ , userInfo : %@.", reasonStr,[notification userInfo]);
}
其他App调用[[AVAudioSession sharedInstance] setActive:NO withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:nil];
,自己的App会收到AVAudioSessionInterruptionNotification
,且在音频打断通知AVAudioSessionInterruptionNotification
的AVAudioSessionInterruptionOptionKey
设置为AVAudioSessionInterruptionOptionShouldResume
,这就是当接入电话后,QQ音乐会自动恢复播放的原因。如果不设置AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation
,则无法做到这点。
附上handleRouteChange
的通知处理,这个处理尤其重要,大部分音频App
都需要很好的处理这个通知
参考
https://developer.apple.com/library/archive/documentation/Audio/Conceptual/AudioSessionProgrammingGuide/HandlingAudioHardwareRouteChanges/HandlingAudioHardwareRouteChanges.html
最重要的是理解这几个枚举
/* values for AVAudioSessionRouteChangeReasonKey in AVAudioSessionRouteChangeNotification用户信息字典
AVAudioSessionRouteChangeReasonUnknown
原因不明。
AVAudioSessionRouteChangeReasonNewDeviceAvailable
一种新的设备出现了(例如,耳机已经插好)。例如蓝牙耳机连上
AVAudioSessionRouteChangeReasonOldDeviceUnavailable
旧设备无法使用(如耳机未插拔)。例如蓝牙耳机airpod从耳朵上拿下来
AVAudioSessionRouteChangeReasonCategoryChange
音频类别已更改(例如AVAudioSessionCategoryPlayback已更改为AVAudioSessionCategoryPlayAndRecord)。
一般由[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:newOption error:nil];触发
AVAudioSessionRouteChangeReasonOverride
路由已被覆盖(例如,category是AVAudioSessionCategoryPlayAndRecord和输出
已从默认的接收方更改为扬声器)。一般由[[AVAudioSession sharedInstance] overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:nil];触发
AVAudioSessionRouteChangeReasonWakeFromSleep
设备从休眠中醒来。
AVAudioSessionRouteChangeReasonNoSuitableRouteForCategory
当当前类别没有路由时返回(例如,该类别是AVAudioSessionCategoryRecord)
但是没有输入设备可用)。
AVAudioSessionRouteChangeReasonRouteConfigurationChange
表示输入和/或输出端口的集合没有改变,只是改变了它们的某些方面
配置发生了变化。例如,端口选择的数据源发生了更改。
*/
typedef NS_ENUM(NSUInteger, AVAudioSessionRouteChangeReason)
{
AVAudioSessionRouteChangeReasonUnknown = 0,
AVAudioSessionRouteChangeReasonNewDeviceAvailable = 1,
AVAudioSessionRouteChangeReasonOldDeviceUnavailable = 2,
AVAudioSessionRouteChangeReasonCategoryChange = 3,
AVAudioSessionRouteChangeReasonOverride = 4,
AVAudioSessionRouteChangeReasonWakeFromSleep = 6,
AVAudioSessionRouteChangeReasonNoSuitableRouteForCategory = 7,
AVAudioSessionRouteChangeReasonRouteConfigurationChange = 8 // added in iOS 7
};
值得注意的是
overrideOutputAudioPort:
会触发AVAudioSessionRouteChangeReasonOverride
setCategory: withOptions:error:
会触发AVAudioSessionRouteChangeReasonCategoryChange
两者都可以改变音频输出为听筒/蓝牙(Receiver)
或者扬声器(Speaker)
,例如:
//切听筒 - override
[[AVAudioSession sharedInstance] overrideOutputAudioPort:AVAudioSessionPortOverrideNone error:nil];
//切听筒 - category
AVAudioSessionCategoryOptions newOption = (self.sessionCategoryOption | AVAudioSessionCategoryOptionDefaultToSpeaker)^AVAudioSessionCategoryOptionDefaultToSpeaker;
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:newOption error:nil];
//切扬声器 - override
[[AVAudioSession sharedInstance] overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:nil];
//切扬声器 - category
AVAudioSessionCategoryOptions newOption = self.sessionCategoryOption | AVAudioSessionCategoryOptionDefaultToSpeaker;
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:newOption error:nil];
//区别在于,category方式,在蓝牙连接的情况下,改变是无效的,而override是默认会调用,还会收到通知,随后会收到newDevice的通知,表示收到蓝牙设备。但override不会改变category,仅仅改变输出port
我们一般都需要处理打断通知
和路由改变
通知
这里对inputs和outputs数组进行了空值判断,是存在为空的情况,请开发者注意
//MARK:AVAudioSession
//FIXME: due to audio session route change, will cause audio queue slient while playing, to fix this bug we handler this notification
//由于音频会话路由的改变,将导致音频队列在播放时保持静音,为了修复这个bug我们处理这个通知
- (void)handleRouteChange:(NSNotification *)notification {
AVAudioSession *session = [ AVAudioSession sharedInstance];
NSString *seccReason = @"";
NSInteger reason = [[[notification userInfo] objectForKey:AVAudioSessionRouteChangeReasonKey] integerValue];
AVAudioSessionRouteDescription* prevRoute = [[notification userInfo] objectForKey:AVAudioSessionRouteChangePreviousRouteKey];
switch (reason) {
case AVAudioSessionRouteChangeReasonNoSuitableRouteForCategory:
seccReason = @"The route changed because no suitable route is now available for the specified category.";
break;
case AVAudioSessionRouteChangeReasonWakeFromSleep:
seccReason = @"The route changed when the device woke up from sleep.";
break;
case AVAudioSessionRouteChangeReasonOverride:
seccReason = @"The output route was overridden by the app.";
break;
case AVAudioSessionRouteChangeReasonCategoryChange:
seccReason = @"The category of the session object changed.";
break;
case AVAudioSessionRouteChangeReasonOldDeviceUnavailable:
seccReason = @"The previous audio output path is no longer available.";
break;
case AVAudioSessionRouteChangeReasonNewDeviceAvailable:
seccReason = @"A preferred new audio output path is now available.";
break;
case AVAudioSessionRouteChangeReasonRouteConfigurationChange:
seccReason = @"The route configuration changed.";
break;
case AVAudioSessionRouteChangeReasonUnknown:
default:
seccReason = [NSString stringWithFormat:@"The reason for the change is unknown.(%ld)",(long)reason];
break;
}
// if ([[[UIDevice currentDevice] systemVersion] compare:@"8.3" options:NSNumericSearch] != NSOrderedAscending) {
// shouldResume = NO;
// }
struct timeval time = {
.tv_sec = 0
};
gettimeofday(&time, NULL);
NSTimeInterval nowval = time.tv_sec + time.tv_usec/1e9;
if (category_time == 0 || nowval > category_time + 2) {
category_time = nowval;
[self reset];
[self resume];
}
LogInfo(@"[GSAudioStreamer] handleRouteChange session %@ %@ %lu", session.mode,session.category,(unsigned long)session.categoryOptions);
LogInfo(@"[GSAudioStreamer] handleRouteChange reason is %@", seccReason);
if (session.currentRoute) {
if (session.currentRoute.outputs) {
NSArray<AVAudioSessionPortDescription *>*outputs = session.currentRoute.outputs;
if (outputs.count > 0) {
AVAudioSessionPortDescription *output = [outputs firstObject];
LogInfo(@"[GSAudioStreamer] inport port type is %@ , name : %@", output.portType,output.portType);
if (output.portType == AVAudioSessionPortBuiltInReceiver) {
NSError *error = nil;
//这里强制转为扬声器
[session overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:&error];
if (!error) {
LogInfo(@"[GSAudioStreamer] overrideOutputAudioPort Error : %@",error);
}
}
}
}
}
}
另外远程控制的监听需要 MPRemoteCommandCenter
相关的知识 https://developer.apple.com/documentation/mediaplayer/mpremotecommandcenter
http://blog.cocoachina.com/article/29610
这篇文章写得不错,注意的是MPRemoteCommand
是屏保时的控制按键事件,如果你需要监听airpod
的从耳取下的事件,是需要处理MPRemoteCommand
的pauseCommand
和AVAudioSessionRouteChangeReasonOldDeviceUnavailable
的
媒体服务器通过共享服务器进程提供音频和其他多媒体功能。虽然很少见,但媒体服务器可以在您的应用程序处于活动状态时重置。注册AVAudioSessionMediaServicesWereResetNotification
通知,以监视媒体服务器重置。收到通知后,您的app需要做以下操作:
重要提示:应用程序不需要重新注册任何AVAudioSession通知或重置AVAudioSession属性上的key-value observer。
如果您想知道媒体服务器何时首次不可用,还可以注册AVAudioSessionMediaServicesWereLostNotification
通知。然而,大多数应用程序只需要响应重置通知。只有当应用程序必须响应在媒体服务器丢失后但在媒体服务器重置之前发生的用户事件时,才使用丢失的通知。
如何测试:您可以通过在“设置”->“开发者”->Reset Media Services模拟。使用此实用程序可以方便地确保您的应用程序在重置媒体服务时作出适当的响应。
关于Category和Mode的配置参数,官方文档说明了
https://developer.apple.com/library/archive/documentation/Audio/Conceptual/AudioSessionProgrammingGuide/AudioSessionCategoriesandModes/AudioSessionCategoriesandModes.html#//apple_ref/doc/uid/TP40007875-CH10-SW3
值得注意的几个点,例如AVAudioSessionCategoryPlayAndRecord
会默认切换到AVAudioSessionModeVoiceChat
文档中说明如下:
/* Only valid with AVAudioSessionCategoryPlayAndRecord. Appropriate for Voice over IP
(VoIP) applications. Reduces the number of allowable audio routes to be only those
that are appropriate for VoIP applications and may engage appropriate system-supplied
signal processing. Has the side effect of setting AVAudioSessionCategoryOptionAllowBluetooth */
extern AVAudioSessionMode const AVAudioSessionModeVoiceChat API_AVAILABLE(ios(5.0), watchos(2.0), tvos(9.0)) API_UNAVAILABLE(macos);
所以当你切换AVAudioSessionCategoryPlayAndRecord
和AVAudioSessionCategoryPlayBack
时,默认是切换AVAudioSessionModeDefault
和AVAudioSessionModeVoiceChat
,如果你的app需要同时录音和播放,你就一直指定为AVAudioSessionCategoryPlayAndRecord
就好了,如果需要响应MPNowPlayingInfoCenter
在锁屏界面进行显示播放信息以及控制MPCommand
,你则需要设置为AVAudioSessionCategoryPlayBack
if (session.mode != AVAudioSessionModeDefault) {
success = [session setMode:AVAudioSessionModeDefault error:&error];
if (!success) MLog(@"AVAudioSession setMode error: %@",error);
}
在各个App的使用中,如果场景比较复杂,是可以切换Category
来实现功能,但同样不因太频繁
对于开发对应mode的音视频应用,各有差异,例如:
对于AVAudioSessionModeDefault
和AVAudioSessionModeVoiceChat
两种模式,AVAudioSessionModeVoiceChat
会隐式采用outport
为Receiver(AVAudioSessionPortBuiltInReceiver)
,在AVAudioSessionModeDefault
会隐式使用Speaker(AVAudioSessionPortBuiltInSpeaker)
,你可以自己监听RouteChanged通知来判断它默认采用的哪种。
这两种的表现为AVAudioSessionModeVoiceChat
声音特别小,因为是从听筒出来的。
大同小异,需要开发者自己注意。
只有特定的类别和模式支持AirPlay。以下类别同时支持AirPlay的镜像和非镜像版本
AVAudioSessionCategoryPlayAndRecord类别和以下模式只支持AirPlay的镜像版本:
为了保护用户隐私,您的应用程序在录制音频之前必须征得用户的同意。如果用户不授予权限,则只记录静默。当你使用一个支持记录的类别,而应用程序试图使用输入路径时,系统会自动提示用户获得许可。
您可以使用requestRecordPermission:
方法手动请求权限,而不是等待系统提示用户记录权限。使用这种方法,您的应用程序可以在不中断应用程序的自然流程的情况下获得许可,从而获得更好的用户体验。
AVAudioSession.sharedInstance().requestRecordPermission { granted in
if granted {
// User granted access. Present recording interface.
} else {
// Present message to user indicating that recording
// can't be performed until they change their preference
// under Settings -> Privacy -> Microphone
}
}
重要提示:从iOS 10开始,所有访问设备麦克风的应用程序都必须静态声明它们的意图。要做到这一点,应用程序现在必须在其信息中包含NSMicrophoneUsageDescription键。并为该键提供一个目的字符串。当系统提示用户允许访问时,此字符串将作为警告的一部分显示。
如果一个应用程序试图在没有该密钥和值的情况下访问设备的任何麦克风,该应用程序将终止。
即应该是在plist中需要进行设置键值对
如果一个应用程序试图在没有该密钥和值的情况下访问设备的任何麦克风,该应用程序将终止。
OS
和tvOS
应用程序要求您为某些后台操作启用某些功能。回放应用程序需要的一个常见功能是播放背景音频。启用此功能后,当用户切换到其他应用程序或锁定iOS设备时,应用程序的音频可以继续。这一功能也需要支持先进的播放功能,如AirPlay流媒体和图片中的图片在iOS
中播放。
配置这些功能的最简单方法是使用Xcode
。在Xcode
中选择应用程序的目标并选择capability
选项卡。在capability
选项卡下,将背景模式切换为ON
,并从可用模式列表中选择“Audio, AirPlay, and Picture in Picture
”选项。
以下内容都摘自官方文档
https://developer.apple.com/library/archive/documentation/Audio/Conceptual/AudioSessionProgrammingGuide/AudioGuidelinesByAppType/AudioGuidelinesByAppType.html#//apple_ref/doc/uid/TP40007875-CH11-SW1
本人加上了自己的理解,翻译水平有限
大多数游戏都需要用户交互才能让游戏中发生任何事情。在设计游戏时使用AVAudioSessionCategoryAmbient或AVAudioSessionCategorySoloAmbient
类别。当用户打开另一个应用程序或锁定屏幕时,他们不希望该应用程序继续播放。通常他们希望在游戏程序播放时,来自另一个应用程序的音频能够继续播放。
以下是官方文档的指南:
applicationDidBecomeActive:
方法中激活音频会话。Tips: 一般使用AVAudioSessionCategoryOptionDuckOthers
AVAudioSessionInterruptionNotification
)secondaryAudioShouldBeSilencedHint
属性,以确定是否应该重新播放游戏的原声带。播放和录制类App,应该选用AVAudioSessionCategoryRecord、AVAudioSessionCategoryPlayAndRecord或AVAudioSessionCategoryPlayback
类别,当它们的音频会话被激活时,通常会中断其他系统音频。
以下是官方文档的指南:
当你的app是非后台模式时,你需要在进入后台时,显式的关闭AudioSession,否则会收到不必要的打断通知,详情见: https://blog.csdn.net/shengpeng3344/article/details/83617979
AVAudioSessionInterruptionNotification
类型的通知,以便在音频会话中断时得到通知。当中断结束时,不要再开始播放或录制音频,除非应用程序在中断之前已经这样做了audio
UIBackgroundModes
标志,即后台允许音频播放。[MPRemoteCommandCenter](https://developer.apple.com/documentation/mediaplayer/mpremotecommandcenter)
),并为您的媒体提供适当的NowPlaying信息(请参阅MPNowPlayingInfoCenter
)。requestRecordPermission:
来向用户请求记录权限。不要依赖操作系统来提示用户。AVAudioSessionCategoryPlayAndRecord
类别而不是AVAudioSessionCategoryRecord
类别。只记录的类别实际上会使所有系统输出静默,而且通常对大多数应用程序来说限制太大。VoIP和聊天应用程序要求输入和输出路由(input and output routes
)都可用。这类应用程序使用AVAudioSessionCategoryPlayAndRecord
类别,不与其他应用程序混合。
AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation
常量禁用音频会话。
Tips
:意为调用[[AVAudioSession sharedInstance] setActive:NO withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:nil];,会告诉CoreAudio自己的App音频关闭了,CoreAudio会恢复其他需要播放的App,这就是当接入电话后,QQ音乐会自动恢复播放的原因。如果不设置AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation,则无法做到这点。
audio UIBackgroundModes
标志。MPVolumeView
对象来显示系统音量调节和路由选择器。requestRecordPermission:
来向用户请求记录权限。不要依赖操作系统来提示用户。从iOS 10开始,要构建与内置电话应用(电话和FaceTime应用)具有相同功能的VoIP应用程序,请使用CallKit框架
。有关更多信息,请参见CallKit框架参考。
计量应用程序需要对输入和输出路由应用最少的系统提供的信号处理。设置AVAudioSessionCategoryPlayAndRecord
类别和测量模式,以最小化信号处理。此外,这类应用程序不会与其他应用程序混合。
requestRecordPermission:
来向用户请求记录权限。不要依赖操作系统来提示用户。社交媒体或其他类似浏览器的应用程序经常播放短视频。它们使用AVAudioSessionCategoryPlayback
类别,不服从铃声开关。这些应用程序也不与其他应用程序混合。
导航和锻炼应用程序使用AVAudioSessionCategoryPlayback
或AVAudioSessionCategoryPlayAndRecord
类别。这些应用程序的音频通常由简短的语音提示组成。当播放时,这些提示会中断语音,比如播客或有声书,并与(和鸭子)其他音频混合,比如音乐应用程序的播放。
AVAudioSessionCategoryOptionInterruptSpokenAudioAndMixWithOthers
和AVAudioSessionCategoryOptionDuckOthers
选项激活音频会话。即开启混合,或者使得其他App的音频变小
合作音乐应用程序是为在其他应用程序播放时播放而设计的。这些类型的应用程序使用AVAudioSessionCategoryPlayback
或AVAudioSessionCategoryPlayAndRecord`类别,并与其他应用程序混合使用。
requestRecordPermission:
来向用户请求记录权限。不要依赖操作系统来提示用户。推荐文章:
https://juejin.im/post/5b3c40bef265da0f774a8a4d
学习不断,有错误请指出,如果对你有帮助,点个赞