原創(chuàng):知識探索型文章
創(chuàng)作不易,請珍惜,之后會持續(xù)更新涂佃,不斷完善
個人比較喜歡做筆記和寫總結(jié),畢竟好記性不如爛筆頭哈哈蜈敢,這些文章記錄了我的IOS成長歷程辜荠,希望能與大家一起進(jìn)步
溫馨提示:由于簡書不支持目錄跳轉(zhuǎn),大家可通過command + F 輸入目錄標(biāo)題后迅速尋找到你所需要的內(nèi)容
目錄
- 一抓狭、音效
- 二伯病、音樂
- 三、音頻會話
- 四否过、播放音樂庫中的音樂
- 五午笛、音頻隊列服務(wù)
- 六惭蟋、在線教室聲音問題
- Demo
- 參考文獻(xiàn)
一、音效
1药磺、簡介
在iOS中音頻播放從形式上可以分為音效播放和音樂播放告组。前者主要指的是一些短音頻播放,通常作為點綴音頻与涡,對于這類音頻不需要進(jìn)行進(jìn)度惹谐、循環(huán)等控制。后者指的是一些較長的音頻驼卖,通常是主音頻氨肌,對于這些音頻的播放通常需要進(jìn)行精確的控制。在iOS中播放兩類音頻分別使用AudioToolbox.framework
和AVFoundation.framework
來完成音效和音樂播放酌畜。AudioToolbox.framework
是一套基于C
語言的框架怎囚,使用它來播放音效其本質(zhì)是將短音頻注冊到系統(tǒng)聲音服務(wù)(System Sound Service
)。
System Sound Service的使用限制
- 音頻播放時間不能超過30s桥胞。
- 數(shù)據(jù)必須是
PCM
或者IMA4
格式恳守。 - 音頻文件必須打包成
.caf
、.aif
贩虾、.wav
中的一種(注意這是官方文檔的說法催烘,實際測試發(fā)現(xiàn).mp3
也可以播放)。
使用System Sound Service播放音效的步驟
- 調(diào)用
AudioServicesCreateSystemSoundID
(CFURLRef inFileURL
缎罢,SystemSoundID* outSystemSoundID
)函數(shù)獲得系統(tǒng)聲音ID
伊群。 - 如果需要監(jiān)聽播放完成操作,則使用
AudioServicesAddSystemSoundCompletion
(SystemSoundID inSystemSoundID
策精,CFRunLoopRef inRunLoop
舰始,CFStringRef inRunLoopMode
,AudioServicesSystemSoundCompletionProc inCompletionRoutine
咽袜,void* inClientData
)方法注冊回調(diào)函數(shù)丸卷。 - 調(diào)用
AudioServicesPlaySystemSound
(SystemSoundID inSystemSoundID
) 或者AudioServicesPlayAlertSound
(SystemSoundID inSystemSoundID
) 方法播放音效(后者帶有震動效果)。
2询刹、Demo演示
a谜嫉、播放音效文件
#import <AudioToolbox/AudioToolbox.h>
-(void)playSoundEffect:(NSString *)name
{
NSString *audioFile = [[NSBundle mainBundle] pathForResource:name ofType:nil];
NSURL *fileUrl = [NSURL fileURLWithPath:audioFile];
//1.獲得系統(tǒng)聲音ID
SystemSoundID soundID = 0;
/**
* inFileUrl:音頻文件url
* outSystemSoundID:聲音id(此函數(shù)會將音效文件加入到系統(tǒng)音頻服務(wù)中并返回一個長整形ID)
*/
AudioServicesCreateSystemSoundID((__bridge CFURLRef)(fileUrl), &soundID);
//如果需要在播放完之后執(zhí)行某些操作,可以調(diào)用如下方法注冊一個播放完成回調(diào)函數(shù)
AudioServicesAddSystemSoundCompletion(soundID, NULL, NULL, soundCompleteCallback, NULL);
//2.播放音頻
AudioServicesPlaySystemSound(soundID);//播放音效
AudioServicesPlayAlertSound(soundID);//播放音效并震動
}
b凹联、 播放完成回調(diào)函數(shù)
void soundCompleteCallback(SystemSoundID soundID,void * clientData)
{
NSLog(@"播放完成...");
}
c沐兰、調(diào)用方式
[self playSoundEffect:@"videoRing.caf"]; // 傳入音頻文件名稱
輸出結(jié)果為:
2020-09-01 09:53:33.464405+0800 AVAudioRecorderDemo[5612:339649] 播放完成...
d、遇到的問題
這里會遇到一個問題匕垫,下面這行代碼返回的path
為空僧鲁,而筆者已經(jīng)把Sound.caf
文件添加到項目中,最后在項目設(shè)置的Build Phases
頁面驾讲,在Copy Bundle Resources
欄目下添加該Sound.caf
文件就解決了倔约。
NSString *path = [[NSBundle mainBundle] pathForResource:@"Sound.caf" ofType:nil];
二掌栅、音樂
1葛作、簡介
如果播放較大的音頻或者要對音頻有精確的控制則System Sound Service
可能就很難滿足實際需求了庇楞,通常這種情況會選擇使用AVFoundation.framework
中的AVAudioPlayer
來實現(xiàn)步脓。AVAudioPlayer
可以看成一個播放器取视,它支持多種音頻格式甥材,而且能夠進(jìn)行進(jìn)度绑改、音量谢床、播放速度等控制。
AVAudioPlayer的屬性
@property(readonly, getter=isPlaying) BOOL playing //是否正在播放厘线,只讀
@property(readonly) NSUInteger numberOfChannels //音頻聲道數(shù)识腿,只讀
@property(readonly) NSTimeInterval duration //音頻時長
@property(readonly) NSURL *url //音頻文件路徑,只讀
@property(readonly) NSData *data //音頻數(shù)據(jù)造壮,只讀
@property float pan //立體聲平衡渡讼,如果為-1.0則完全左聲道,如果0.0則左右聲道平衡耳璧,如果為1.0則完全為右聲道
@property float volume //音量大小成箫,范圍0-1.0
@property BOOL enableRate //是否允許改變播放速率
@property float rate //播放速率,范圍0.5-2.0旨枯,如果為1.0則正常播放蹬昌,如果要修改播放速率則必須設(shè)置enableRate為YES
@property NSTimeInterval currentTime //當(dāng)前播放時長
@property(readonly) NSTimeInterval deviceCurrentTime //輸出設(shè)備播放音頻的時間,注意如果播放中被暫停此時間也會繼續(xù)累加
@property NSInteger numberOfLoops //循環(huán)播放次數(shù)攀隔,如果為0則不循環(huán)皂贩,如果小于0則無限循環(huán),大于0則表示循環(huán)次數(shù)
@property(readonly) NSDictionary *settings //音頻播放設(shè)置信息竞慢,只讀
@property(getter=isMeteringEnabled) BOOL meteringEnabled //是否啟用音頻測量先紫,默認(rèn)為NO治泥,一旦啟用音頻測量可以通過updateMeters方法更新測量值
AVAudioPlayer的方法
- (instancetype)initWithContentsOfURL:(NSURL *)url error:(NSError **)outError //使用文件URL初始化播放器筹煮,注意這個URL不能是HTTP URL,AVAudioPlayer不支持加載網(wǎng)絡(luò)媒體流居夹,只能播放本地文件
- (instancetype)initWithData:(NSData *)data error:(NSError **)outError //使用NSData初始化播放器败潦,注意使用此方法時必須文件格式和文件后綴一致,否則出錯准脂,所以相比此方法更推薦使用上述方法
- (BOOL)prepareToPlay; //加載音頻文件到緩沖區(qū)劫扒,注意即使在播放之前音頻文件沒有加載到緩沖區(qū)程序也會隱式調(diào)用此方法
- (BOOL)play; //播放音頻文件
- (BOOL)playAtTime:(NSTimeInterval)time //在指定的時間開始播放音頻
- (void)pause; //暫停播放
- (void)stop; //停止播放
- (void)updateMeters; //更新音頻測量值,注意如果要更新音頻測量值必須設(shè)置meteringEnabled為YES狸膏,通過音頻測量值可以即時獲得音頻分貝等信息
(float)peakPowerForChannel:(NSUInteger)channelNumber; //獲得指定聲道的分貝峰值沟饥,注意如果要獲得分貝峰值必須在此之前調(diào)用updateMeters方法
- (float)averagePowerForChannel:(NSUInteger)channelNumber; //獲得指定聲道的分貝平均值,注意如果要獲得分貝平均值必須在此之前調(diào)用updateMeters方法
AVAudioPlayer的代理方法
- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag; //音頻播放完成
2、Demo
a贤旷、功能點
下面就使用AVAudioPlayer
實現(xiàn)一個簡單播放器广料,在這個播放器中實現(xiàn)了播放、暫停幼驶、顯示播放進(jìn)度功能艾杏,當(dāng)然例如調(diào)節(jié)音量、設(shè)置循環(huán)模式盅藻、甚至是聲波圖像(通過分析音頻分貝值)等功能都可以實現(xiàn)购桑,這里就不再一一演示。實現(xiàn)步驟如下:
- 初始化
AVAudioPlayer
對象氏淑,此時通常指定本地文件路徑勃蜘。 - 設(shè)置播放器屬性,例如重復(fù)次數(shù)假残、音量大小等元旬。
- 調(diào)用
play
方法播放。
運行效果如下:
2020-09-01 11:22:00.863313+0800 AVAudioRecorderDemo[6245:396604] 音樂播放完成...
b守问、擴展和頭文件
當(dāng)然由于AVAudioPlayer
一次只能播放一個音頻文件匀归,所以上一曲、下一曲其實可以通過創(chuàng)建多個播放器對象來完成耗帕,這里暫不實現(xiàn)穆端。播放進(jìn)度的實現(xiàn)主要依靠一個定時器實時計算當(dāng)前播放時長和音頻總時長的比例,另外為了演示委托方法仿便,下面的代碼中也實現(xiàn)了播放完成委托方法体啰,通常如果有下一曲功能的話播放完可以觸發(fā)下一曲音樂播放。
#import "MusicViewController.h"
#import <AVFoundation/AVFoundation.h>
#define kMusicFile @"桜道.mp3"
#define kMusicSinger @"歌手:Jusqu'à Grand-Père"
#define kMusicTitle @"歌曲:桜道"
@interface MusicViewController ()<AVAudioPlayerDelegate>
@property (nonatomic,strong) AVAudioPlayer *audioPlayer; //播放器
@property (strong, nonatomic) UILabel *controlPanel; //控制面板
@property (strong, nonatomic) UIProgressView *playProgress; //播放進(jìn)度
@property (strong, nonatomic) UILabel *musicSinger; //演唱者
@property (strong, nonatomic) UIButton *playOrPause; //播放/暫停按鈕(如果tag為0認(rèn)為是暫停狀態(tài)嗽仪,1是播放狀態(tài))
@property (weak ,nonatomic) NSTimer *timer; //進(jìn)度更新定時器
@end
c荒勇、播放控制
播放音頻
-(void)play
{
if (![self.audioPlayer isPlaying])
{
[self.audioPlayer play];
self.timer.fireDate = [NSDate distantPast];//恢復(fù)定時器
}
}
暫停播放
-(void)pause
{
if ([self.audioPlayer isPlaying])
{
[self.audioPlayer pause];
self.timer.fireDate = [NSDate distantFuture];//暫停定時器,注意不能調(diào)用invalidate方法闻坚,此方法會取消沽翔,之后無法恢復(fù)
}
}
點擊播放/暫停按鈕
- (void)playClick:(UIButton *)sender
{
if(sender.tag)
{
sender.tag = 0;
[sender setImage:[UIImage imageNamed:@"playing_btn_play_n"] forState:UIControlStateNormal];
[self pause];
}
else
{
sender.tag = 1;
[sender setImage:[UIImage imageNamed:@"playing_btn_pause_n"] forState:UIControlStateNormal];
[self play];
}
}
更新播放進(jìn)度
-(void)updateProgress
{
float progress = self.audioPlayer.currentTime / self.audioPlayer.duration;
[self.playProgress setProgress:progress animated:true];
}
d、AVAudioPlayerDelegate
-(void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag
{
NSLog(@"音樂播放完成...");
// 下一首
}
e窿凤、創(chuàng)建播放器和計時器
創(chuàng)建播放器
-(AVAudioPlayer *)audioPlayer
{
if (!_audioPlayer)
{
NSString *urlStr = [[NSBundle mainBundle] pathForResource:kMusicFile ofType:nil];
NSURL *url = [NSURL fileURLWithPath:urlStr];
NSError *error = nil;
// 初始化播放器仅偎,注意這里的Url參數(shù)只能是文件路徑,不支持HTTP Url
_audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:url error:&error];
// 設(shè)置播放器屬性
_audioPlayer.numberOfLoops = 0; //設(shè)置為0不循環(huán)播放
_audioPlayer.delegate = self;
[_audioPlayer prepareToPlay]; //加載音頻文件到緩存
if(error)
{
NSLog(@"初始化播放器過程發(fā)生錯誤,錯誤信息:%@",error.localizedDescription);
return nil;
}
}
return _audioPlayer;
}
創(chuàng)建計時器
-(NSTimer *)timer
{
if (!_timer)
{
_timer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(updateProgress) userInfo:nil repeats:true];
}
return _timer;
}
三雳殊、音頻會話
1橘沥、簡介
事實上上面的播放器還存在一些問題,例如通常我們看到的播放器即使退出到后臺也是可以播放的夯秃,而這個播放器如果退出到后臺它會自動暫停座咆。
a痢艺、支持后臺播放的條件
AVAudioSession *audioSession = [AVAudioSession sharedInstance];
[audioSession setCategory:AVAudioSessionCategoryPlayback error:nil];
[audioSession setActive:YES error:nil];
- 設(shè)置后臺運行模式:在
plist
文件中添加Required background modes
,并且設(shè)置item 0
為App plays audio or streams audio/video using AirPlay
(其實可以直接通過Xcode
在Project Targets-Capabilities-Background Modes
中設(shè)置)介陶。 - 設(shè)置
AVAudioSession
的類型為AVAudioSessionCategoryPlayback
并且調(diào)用setActive:
方法啟動會話腹备。 - 為了能夠讓應(yīng)用退到后臺之后支持耳機控制,建議添加遠(yuǎn)程控制事件(這一步不是后臺播放必須的)斤蔓。
前兩步是后臺播放所必須設(shè)置的植酥,第三步主要用于接收遠(yuǎn)程事件,如果這一步不設(shè)置雖然也能夠在后臺播放弦牡,但是無法獲得音頻控制權(quán)(如果在使用當(dāng)前應(yīng)用之前使用其他播放器播放音樂的話友驮,此時如果按耳機播放鍵或者控制中心的播放按鈕則會播放前一個應(yīng)用的音頻),并且不能使用耳機進(jìn)行音頻控制驾锰。
第一步操作相信大家都很容易理解卸留,如果應(yīng)用程序要允許運行到后臺必須設(shè)置,正常情況下應(yīng)用如果進(jìn)入后臺會被掛起椭豫,通過該設(shè)置可以讓應(yīng)用程序繼續(xù)在后臺運行耻瑟。但是第二步使用的AVAudioSession
有必要進(jìn)行一下詳細(xì)的說明。
在iOS中每個應(yīng)用都有一個音頻會話赏酥,這個會話就通過AVAudioSession
來表示喳整。AVAudioSession
同樣存在于AVFoundation
框架中,它是單例模式設(shè)計裸扶,通過sharedInstance
進(jìn)行訪問框都。在使用Apple
設(shè)備時大家會發(fā)現(xiàn)有些應(yīng)用只要打開其他音頻播放就會終止,而有些應(yīng)用卻可以和其他應(yīng)用同時播放呵晨,在多種音頻環(huán)境中如何去控制播放的方式就是通過音頻會話來完成的魏保。
b、音頻會話的幾種會話模式:
會話類型 | 說明 | 是否要求輸入 | 是否要求輸出 | 是否遵從靜音鍵 |
---|---|---|---|---|
AVAudioSessionCategoryAmbient | 混音播放摸屠,可以與其他音頻應(yīng)用同時播放 | 否 | 是 | 是 |
AVAudioSessionCategorySoloAmbient | 獨占播放 | 否 | 是 | 是 |
AVAudioSessionCategoryPlayback | 后臺播放谓罗,也是獨占的 | 否 | 是 | 否 |
AVAudioSessionCategoryRecord | 錄音模式,用于錄音時使用 | 是 | 否 | 否 |
AVAudioSessionCategoryPlayAndRecord | 播放和錄音季二,此時可以錄音也可以播放 | 是 | 是 | 否 |
AVAudioSessionCategoryAudioProcessing | 硬件解碼音頻檩咱,此時不能播放和錄制 | 否 | 否 | 否 |
AVAudioSessionCategoryMultiRoute | 多種輸入輸出,例如可以耳機戒傻、USB設(shè)備同時播放 | 是 | 是 | 否 |
注意:是否遵循靜音鍵表示在播放過程中如果用戶通過硬件設(shè)置為靜音是否能關(guān)閉聲音税手。
根據(jù)前面對音頻會話的理解蜂筹,相信大家開發(fā)出能夠在后臺播放的音頻播放器并不難需纳,但是注意一下,在前面的代碼中也提到設(shè)置完音頻會話類型之后需要調(diào)用setActive:
方法將會話激活才能起作用艺挪。類似的不翩,如果一個應(yīng)用已經(jīng)在播放音頻兵扬,打開我們的應(yīng)用之后設(shè)置了在后臺播放的會話類型,此時其他應(yīng)用的音頻會停止而播放我們的音頻口蝠,如果希望我們的程序音頻播放完之后(關(guān)閉或退出到后臺之后)能夠繼續(xù)播放其他應(yīng)用的音頻的話則可以調(diào)用setActive:
方法關(guān)閉會話器钟。
2、Demo演示
a妙蔗、擴展和頭文件
#import "AudioSessionViewController.h"
#import <AVFoundation/AVFoundation.h>
#define kMusicFile @"桜道.mp3"
#define kMusicSinger @"歌手:Jusqu'à Grand-Père"
#define kMusicTitle @"歌曲:桜道"
@interface AudioSessionViewController ()<AVAudioPlayerDelegate>
@property (nonatomic,strong) AVAudioPlayer *audioPlayer; //播放器
@property (strong, nonatomic) UILabel *controlPanel; //控制面板
@property (strong, nonatomic) UIProgressView *playProgress; //播放進(jìn)度
@property (strong, nonatomic) UILabel *musicSinger; //演唱者
@property (strong, nonatomic) UIButton *playOrPause; //播放/暫停按鈕(如果tag為0認(rèn)為是暫停狀態(tài)傲霸,1是播放狀態(tài))
@property (weak ,nonatomic) NSTimer *timer; //進(jìn)度更新定時器
@end
b、耳機控制
實現(xiàn)了拔出耳機暫停音樂播放的功能眉反,這也是一個比較常見的功能昙啄。可以通過通知獲得輸出改變的通知寸五,然后拿到通知對象后根據(jù)userInfo
獲得是何種改變類型梳凛,進(jìn)而根據(jù)情況對音樂進(jìn)行暫停操作。
-(void)routeChange:(NSNotification *)notification
{
NSDictionary *dictionary = notification.userInfo;
[dictionary enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
NSLog(@"notification userInfo梳杏,key:%@韧拒,value:%@",key,obj);
}];
int changeReason = [dictionary[AVAudioSessionRouteChangeReasonKey] intValue];
// 等于AVAudioSessionRouteChangeReasonOldDeviceUnavailable 表示舊輸出不可用
if (changeReason == AVAudioSessionRouteChangeReasonOldDeviceUnavailable)
{
AVAudioSessionRouteDescription *routeDescription = dictionary[AVAudioSessionRouteChangePreviousRouteKey];
AVAudioSessionPortDescription *portDescription= [routeDescription.outputs firstObject];
//原設(shè)備為耳機則暫停
if ([portDescription.portType isEqualToString:@"Headphones"])
{
[self pause];
}
}
}
顯示當(dāng)前視圖控制器時注冊遠(yuǎn)程事件
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
//開啟遠(yuǎn)程控制
[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
//作為第一響應(yīng)者
//[self becomeFirstResponder];
}
當(dāng)前控制器視圖不顯示時取消遠(yuǎn)程控制
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[[UIApplication sharedApplication] endReceivingRemoteControlEvents];
//[self resignFirstResponder];
}
c、播放控制
播放音頻
- (void)play
{
if (![self.audioPlayer isPlaying])
{
[self.audioPlayer play];
self.timer.fireDate = [NSDate distantPast];//恢復(fù)定時器
}
}
暫停播放
- (void)pause
{
if ([self.audioPlayer isPlaying])
{
[self.audioPlayer pause];
self.timer.fireDate = [NSDate distantFuture];//暫停定時器十性,注意不能調(diào)用invalidate方法叛溢,此方法會取消,之后無法恢復(fù)
}
}
點擊播放/暫停按鈕
- (void)playClick:(UIButton *)sender
{
if(sender.tag)
{
sender.tag = 0;
[sender setImage:[UIImage imageNamed:@"playing_btn_play_n"] forState:UIControlStateNormal];
[self pause];
}
else
{
sender.tag = 1;
[sender setImage:[UIImage imageNamed:@"playing_btn_pause_n"] forState:UIControlStateNormal];
[self play];
}
}
更新播放進(jìn)度
- (void)updateProgress
{
float progress = self.audioPlayer.currentTime / self.audioPlayer.duration;
[self.playProgress setProgress:progress animated:true];
}
d劲适、AVAudioPlayerDelegate
-(void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag
{
NSLog(@"音樂播放完成...");
// 根據(jù)實際情況播放完成可以將會話關(guān)閉雇初,其他音頻應(yīng)用繼續(xù)播放
[[AVAudioSession sharedInstance] setActive:NO error:nil];
}
四、播放音樂庫中的音樂
1减响、屬性和方法
眾所周知音樂是iOS的重要組成播放靖诗,無論是iPod
、iTouch
支示、iPhone
還是iPad
都可以在iTunes
購買音樂或添加本地音樂到音樂庫中同步到你的iOS設(shè)備刊橘。在MediaPlayer.frameowork
中有一個MPMusicPlayerController
用于播放音樂庫中的音樂。
屬性
播放器狀態(tài)颂鸿,枚舉類型
@property (nonatomic, readonly) MPMusicPlaybackState playbackState
MPMusicPlaybackStateStopped:停止播放
MPMusicPlaybackStatePlaying:正在播放
MPMusicPlaybackStatePaused:暫停播放
MPMusicPlaybackStateInterrupted:播放中斷
MPMusicPlaybackStateSeekingForward:向前查找
MPMusicPlaybackStateSeekingBackward:向后查找
重復(fù)模式促绵,枚舉類型
@property (nonatomic) MPMusicRepeatMode repeatMode
MPMusicRepeatModeDefault:默認(rèn)模式,使用用戶的首選項(系統(tǒng)音樂程序設(shè)置)
MPMusicRepeatModeNone:不重復(fù)
MPMusicRepeatModeOne:單曲循環(huán)
MPMusicRepeatModeAll:在當(dāng)前列表內(nèi)循環(huán)
隨機播放模式嘴纺,枚舉類型
@property (nonatomic) MPMusicShuffleMode shuffleMode
MPMusicShuffleModeDefault:默認(rèn)模式败晴,使用用戶首選項(系統(tǒng)音樂程序設(shè)置)
MPMusicShuffleModeOff:不隨機播放
MPMusicShuffleModeSongs:按歌曲隨機播放
MPMusicShuffleModeAlbums:按專輯隨機播放
常用屬性
@property (nonatomic, copy) MPMediaItem *nowPlayingItem //正在播放的音樂項
@property (nonatomic, readonly) NSUInteger indexOfNowPlayingItem //當(dāng)前正在播放的音樂在播放隊列中的索引
@property(nonatomic, readonly) BOOL isPreparedToPlay //是否做好播放準(zhǔn)備
@property(nonatomic) NSTimeInterval currentPlaybackTime //當(dāng)前已播放時間,單位:秒
@property(nonatomic) float currentPlaybackRate //當(dāng)前播放速度栽渴,是一個播放速度倍率尖坤,0表示暫停播放,1代表正常速度
類方法
+ (MPMusicPlayerController *)applicationMusicPlayer; //獲取應(yīng)用播放器闲擦,注意此類播放器無法在后臺播放
+ (MPMusicPlayerController *)systemMusicPlayer //獲取系統(tǒng)播放器慢味,支持后臺播放
對象方法
- (void)setQueueWithQuery:(MPMediaQuery *)query //使用媒體隊列設(shè)置播放源媒體隊列
- (void)setQueueWithItemCollection:(MPMediaItemCollection *)itemCollection //使用媒體項集合設(shè)置播放源媒體隊列
- (void)skipToNextItem //下一曲
- (void)skipToBeginning //從起始位置播放
- (void)skipToPreviousItem //上一曲
- (void)beginGeneratingPlaybackNotifications //開啟播放通知场梆,注意不同于其他播放器,MPMusicPlayerController要想獲得通知必須首先開啟纯路,默認(rèn)情況無法獲得通知
- (void)endGeneratingPlaybackNotifications //關(guān)閉播放通知
- (void)prepareToPlay //做好播放準(zhǔn)備(加載音頻到緩沖區(qū))或油,在使用play方法播放時如果沒有做好準(zhǔn)備回自動調(diào)用該方法
- (void)play //開始播放
- (void)pause //暫停播放
- (void)stop //停止播放
- (void)beginSeekingForward //開始向前查找(快進(jìn))
- (void)beginSeekingBackward //開始向后查找(快退)
- (void)endSeeking //結(jié)束查找
通知
要想獲得MPMusicPlayerController
通知必須首先調(diào)用beginGeneratingPlaybackNotifications
開啟通知
MPMusicPlayerControllerPlaybackStateDidChangeNotification //播放狀態(tài)改變
MPMusicPlayerControllerNowPlayingItemDidChangeNotification //當(dāng)前播放音頻改變
MPMusicPlayerControllerVolumeDidChangeNotification //聲音大小改變
MPMediaPlaybackIsPreparedToPlayDidChangeNotification //準(zhǔn)備好播放
2、獲取媒體文件列表
MPMusicPlayerController
有兩種播放器:applicationMusicPlayer
和systemMusicPlayer
驰唬,前者在應(yīng)用退出后音樂播放會自動停止顶岸,后者在應(yīng)用停止后不會退出播放狀態(tài)。
MPMusicPlayerController
加載音樂不同于前面的AVAudioPlayer
是通過一個文件路徑來加載叫编,而是需要一個播放隊列蜕琴。在MPMusicPlayerController
中提供了兩個方法來加載播放隊列:- (void)setQueueWithQuery:(MPMediaQuery *)query
和- (void)setQueueWithItemCollection:(MPMediaItemCollection *)itemCollection
,正是由于它的播放音頻來源是一個隊列宵溅,因此MPMusicPlayerController
支持上一曲凌简、下一曲等操作。
那么接下來的問題就是如何獲取MPMediaQueue
或者MPMediaItemCollection
恃逻?MPMediaQueue
對象有一系列的類方法來獲得媒體隊列:
+ (MPMediaQuery *)albumsQuery;
+ (MPMediaQuery *)artistsQuery;
+ (MPMediaQuery *)songsQuery;
+ (MPMediaQuery *)playlistsQuery;
+ (MPMediaQuery *)podcastsQuery;
+ (MPMediaQuery *)audiobooksQuery;
+ (MPMediaQuery *)compilationsQuery;
+ (MPMediaQuery *)composersQuery;
+ (MPMediaQuery *)genresQuery;
有了這些方法雏搂,就可以很容易獲到歌曲、播放列表寇损、專輯媒體等媒體隊列了凸郑,這樣就可以通過: - (void)setQueueWithQuery:(MPMediaQuery *)query
方法設(shè)置音樂來源了。又或者得到MPMediaQueue
之后創(chuàng)建MPMediaItemCollection
矛市,使用- (void)setQueueWithItemCollection:(MPMediaItemCollection *)itemCollection設(shè)
置音樂來源芙沥。
有時候可能希望用戶自己來選擇要播放的音樂,這時可以使用MPMediaPickerController
浊吏,它是一個視圖控制器而昨,類似于UIImagePickerController
,選擇完播放來源后可以在其代理方法中獲得MPMediaItemCollection
對象找田。
無論是通過哪種方式獲得MPMusicPlayerController
的媒體源歌憨,可能都希望將每個媒體的信息顯示出來,這時候可以通過MPMediaItem
對象獲得墩衙。一個MPMediaItem
代表一個媒體文件务嫡,通過它可以訪問媒體標(biāo)題、專輯名稱漆改、專輯封面心铃、音樂時長等等。無論是MPMediaQueue
還是MPMediaItemCollection
都有一個items
屬性挫剑,它是MPMediaItem
數(shù)組去扣,通過這個屬性可以獲得MPMediaItem
對象。
3暮顺、Demo演示
a厅篓、功能點
下面就簡單看一下MPMusicPlayerController
的使用秀存,在下面的例子中簡單演示了音樂的選擇捶码、播放羽氮、暫停、通知惫恼、下一曲档押、上一曲功能,相信有了上面的概念祈纯,代碼讀起來并不復(fù)雜(示例中是直接通過MPMeidaPicker
進(jìn)行音樂選擇的令宿,但是仍然提供了兩個方法getLocalMediaQuery
和getLocalMediaItemCollection
來演示如何直接通過MPMediaQueue
獲得媒體隊列或媒體集合)。在Info.plist
文件中添加Privacy - Media Library Usage Description
訪問權(quán)限腕窥,提示語可為:訪問音樂庫權(quán)限粒没。
a、數(shù)據(jù)源
獲得媒體隊列
- (MPMediaQuery *)getLocalMediaQuery
{
MPMediaQuery *mediaQueue = [MPMediaQuery songsQuery];
for (MPMediaItem *item in mediaQueue.items)
{
NSLog(@"item 標(biāo)題:%@簇爆,albumTitle 專輯標(biāo)題:%@",item.title,item.albumTitle);
}
return mediaQueue;
}
獲取媒體集合
-(MPMediaItemCollection *)getLocalMediaItemCollection
{
MPMediaQuery *mediaQueue = [MPMediaQuery songsQuery];
NSMutableArray *array = [NSMutableArray array];
for (MPMediaItem *item in mediaQueue.items)
{
[array addObject:item];
NSLog(@"item 標(biāo)題:%@癞松,albumTitle 專輯標(biāo)題:%@",item.title,item.albumTitle);
}
MPMediaItemCollection *mediaItemCollection = [[MPMediaItemCollection alloc] initWithItems:[array copy]];
return mediaItemCollection;
}
b、MPMediaPickerControllerDelegate
選擇完成
-(void)mediaPicker:(MPMediaPickerController *)mediaPicker didPickMediaItems:(MPMediaItemCollection *)mediaItemCollection
{
MPMediaItem *mediaItem = [mediaItemCollection.items firstObject];// 播放第一個音樂
//注意很多音樂信息如標(biāo)題入蛆、專輯响蓉、表演者、封面哨毁、時長等信息都可以通過MPMediaItem的valueForKey:方法得到枫甲,也都有對應(yīng)的屬性可以直接訪問
//NSString *title = [mediaItem valueForKey:MPMediaItemPropertyAlbumTitle];
//NSString *artist = [mediaItem valueForKey:MPMediaItemPropertyAlbumArtist];
//MPMediaItemArtwork *artwork = [mediaItem valueForKey:MPMediaItemPropertyArtwork];
//UIImage *image = [artwork imageWithSize:CGSizeMake(100, 100)];//專輯圖片
NSLog(@"標(biāo)題:%@,表演者:%@,專輯:%@",mediaItem.title ,mediaItem.artist,mediaItem.albumTitle);
[self.musicPlayer setQueueWithItemCollection:mediaItemCollection];
[self dismissViewControllerAnimated:YES completion:nil];
}
取消選擇
-(void)mediaPickerDidCancel:(MPMediaPickerController *)mediaPicker
{
[self dismissViewControllerAnimated:YES completion:nil];
}
c、通知
添加通知
-(void)addNotification
{
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter addObserver:self selector:@selector(playbackStateChange:) name:MPMusicPlayerControllerPlaybackStateDidChangeNotification object:self.musicPlayer];
}
播放狀態(tài)改變通知
-(void)playbackStateChange:(NSNotification *)notification
{
switch (self.musicPlayer.playbackState)
{
case MPMusicPlaybackStatePlaying:
NSLog(@"正在播放...");
break;
case MPMusicPlaybackStatePaused:
NSLog(@"播放暫停.");
break;
case MPMusicPlaybackStateStopped:
NSLog(@"播放停止.");
break;
default:
break;
}
}
d扼褪、創(chuàng)建媒體播發(fā)器
-(MPMusicPlayerController *)musicPlayer
{
if (!_musicPlayer)
{
_musicPlayer = [MPMusicPlayerController systemMusicPlayer];
// 開啟通知想幻,否則監(jiān)控不到MPMusicPlayerController的通知
[_musicPlayer beginGeneratingPlaybackNotifications];
// 添加通知
[self addNotification];
// 如果不使用MPMediaPickerController可以使用如下方法獲得音樂庫媒體隊列
//[_musicPlayer setQueueWithItemCollection:[self getLocalMediaItemCollection]];
}
return _musicPlayer;
}
e、創(chuàng)建媒體選擇器
-(MPMediaPickerController *)mediaPicker
{
if (!_mediaPicker)
{
// 初始化媒體選擇器话浇,這里設(shè)置媒體類型為音樂举畸,其實這里也可以選擇視頻、廣播等
//_mediaPicker = [[MPMediaPickerController alloc] initWithMediaTypes:MPMediaTypeMusic];
_mediaPicker = [[MPMediaPickerController alloc] initWithMediaTypes:MPMediaTypeAny];
// 允許多選
_mediaPicker.allowsPickingMultipleItems = YES;
// 顯示iCloud選項
_mediaPicker.showsCloudItems = YES;
_mediaPicker.prompt = @"請選擇要播放的音樂";
//設(shè)置選擇器代理
_mediaPicker.delegate = self;
}
return _mediaPicker;
}
@end
注意:模擬器和沒有安裝Apple music app
的真機都會報錯凳枝,必須在真機上調(diào)試抄沮,且安裝蘋果自帶的音樂APP。
The requested app extension could not be found
五岖瑰、音頻隊列服務(wù)
1叛买、簡介
無論是前面的錄音還是音頻播放均不支持網(wǎng)絡(luò)流媒體播放,當(dāng)然對于錄音來說這種需求可能不大蹋订,但是對于音頻播放來說有時候就很有必要了率挣。AVAudioPlayer
只能播放本地文件,并且是一次性加載所有音頻數(shù)據(jù)露戒,初始化AVAudioPlayer
時指定的URL
也只能是File URL
而不能是HTTP URL
椒功。
當(dāng)然捶箱,將音頻文件下載到本地然后再調(diào)用AVAudioPlayer
來播放也是一種播放網(wǎng)絡(luò)音頻的辦法,但是這種方式最大的弊端就是必須等到整個音頻下載完成才能播放动漾,而不能使用流式播放丁屎,這往往在實際開發(fā)中是不切實際的。那么在iOS中如何播放網(wǎng)絡(luò)流媒體呢旱眯?就是使用AudioToolbox
框架中的音頻隊列服務(wù)Audio Queue Services
晨川。
使用音頻隊列服務(wù)完全可以做到音頻播放和錄制,首先看一下錄音音頻服務(wù)隊列:
一個音頻服務(wù)隊列Audio Queue
由三部分組成:
- 三個緩沖器
Buffers
:每個緩沖器都是一個存儲音頻數(shù)據(jù)的臨時倉庫删豺。 - 一個緩沖隊列
Buffer Queue
:一個包含音頻緩沖器的有序隊列共虑。 - 一個回調(diào)
Callback
:一個自定義的隊列回調(diào)函數(shù)。
聲音通過輸入設(shè)備進(jìn)入緩沖隊列中呀页,首先填充第一個緩沖器妈拌;當(dāng)?shù)谝粋€緩沖器填充滿之后自動填充下一個緩沖器,同時會調(diào)用回調(diào)函數(shù)蓬蝶,在回調(diào)函數(shù)中需要將緩沖器中的音頻數(shù)據(jù)寫入磁盤尘分,同時將緩沖器放回到緩沖隊列中以便重用。
下面是Apple官方關(guān)于音頻隊列服務(wù)的流程示意圖:
類似的疾党,看一下音頻播放緩沖隊列音诫,其組成部分和錄音緩沖隊列類似。
但是在音頻播放緩沖隊列中雪位,回調(diào)函數(shù)調(diào)用的時機不同于音頻錄制緩沖隊列竭钝,流程剛好相反。將音頻讀取到緩沖器中雹洗,一旦一個緩沖器填充滿之后就放到緩沖隊列中愚臀,然后繼續(xù)填充其他緩沖器先慷。當(dāng)開始播放時惹恃,則從第一個緩沖器中讀取音頻進(jìn)行播放恃锉,一旦播放完之后就會觸發(fā)回調(diào)函數(shù),開始播放下一個緩沖器中的音頻螃成,同時填充第一個緩沖器旦签,填充滿之后再次放回到緩沖隊列。
下面是詳細(xì)的流程圖:
當(dāng)然寸宏,要明白音頻隊列服務(wù)的原理并不難宁炫,問題是如何實現(xiàn)這個自定義的回調(diào)函數(shù),這其中我們有大量的工作要做氮凝,控制播放狀態(tài)羔巢、處理異常中斷、進(jìn)行音頻編碼等等。由于牽扯內(nèi)容過多竿秆,而且不是本文目的启摄,如果以后有時間將另開一篇文章重點介紹,目前有很多第三方優(yōu)秀框架可以直接使用幽钢,例如FreeStreamer歉备。
使用FreeStreamer
之前要做如下準(zhǔn)備工作:
- 拷貝
FreeStreamer
中的Reachability.h
、Reachability.m
和Common
搅吁、astreamer
兩個文件夾中的內(nèi)容到項目中威创。 - 添加
FreeStreamer
使用的類庫:CFNetwork.framework
落午、AudioToolbox.framework
谎懦、AVFoundation.framework
、libxml2.dylib
溃斋、MediaPlayer.framework
界拦。 - 如果引用
libxml2.dylib
編譯不通過,需要在Xcode的Targets-Build Settings-Header Build Path
中添加$(SDKROOT)/usr/include/libxml2
梗劫。 - 將
FreeStreamer
中的FreeStreamerMobile-Prefix.pch
文件添加到項目中并將Targets-Build Settings-Precompile Prefix Header
設(shè)置為YES
享甸,在Targets-Build Settings-Prefix Header
設(shè)置為$(SRCROOT)/項目名稱/FreeStreamerMobile-Prefix.pch
2蛉威、Demo演示
然后就可以編寫代碼播放網(wǎng)絡(luò)音頻了蚯嫌。
#import "FreeStreamerViewController.h"
#import "FSAudioStream.h"
@interface FreeStreamerViewController ()
@property (nonatomic,strong) FSAudioStream *audioStream;
@end
@implementation FreeStreamerViewController
- (void)viewDidLoad
{
[super viewDidLoad];
[self.audioStream play];
}
// 取得本地文件路徑
-(NSURL *)getFileUrl
{
NSString *urlStr = [[NSBundle mainBundle]pathForResource:@"桜道.mp3" ofType:nil];
NSURL *url = [NSURL fileURLWithPath:urlStr];
return url;
}
// 取得網(wǎng)絡(luò)文件路徑
-(NSURL *)getNetworkUrl
{
NSString *urlStr = @"http://192.168.1.102/liu.mp3";
NSURL *url = [NSURL URLWithString:urlStr];
return url;
}
// 創(chuàng)建FSAudioStream對象
-(FSAudioStream *)audioStream
{
if (!_audioStream)
{
NSURL *url = [self getNetworkUrl];
// 創(chuàng)建FSAudioStream對象
_audioStream=[[FSAudioStream alloc] initWithUrl:url];
_audioStream.onFailure=^(FSAudioStreamError error,NSString *description){
NSLog(@"播放過程中發(fā)生錯誤晒旅,錯誤信息:%@",description);
};
_audioStream.onCompletion=^(){
NSLog(@"播放完成!");
};
[_audioStream setVolume:0.5];//設(shè)置聲音
}
return _audioStream;
}
@end
其實FreeStreamer
的功能很強大废恋,不僅僅是播放本地谈秫、網(wǎng)絡(luò)音頻那么簡單拟烫,它還支持播放列表、檢查包內(nèi)容蚓哩、RSS
訂閱、播放中斷等很多強大的功能喜颁,甚至還包含了一個音頻分析器半开。
六、在線教室聲音問題
1奢米、問題描述
在線教室場景下鬓长,聲音是最重要的內(nèi)容傳輸渠道之一涉波,保障聲音的穩(wěn)定可靠炭序,是在線教室質(zhì)量非常重要的一環(huán)惭聂。同時在線教室里許多功能模塊都與聲音有關(guān)聯(lián)辜纲,如何處理好各個模塊間的聲音沖突成為一個重要話題侨歉。
AVAudioSession
在 iOS 端炮温,說到聲音的話題就繞不開 AVAudioSession
担巩。AVAudioSession
的作用是管理音頻這一唯一硬件資源的分配拳话,通過調(diào)優(yōu)合適的 AVAudioSession
來適配我們的 APP 對于音頻的功能需求岸裙。切換音頻場景的時候判呕,需要相應(yīng)的切換 AVAudioSession
。
AVAudioSessionCategory
教育場景下主要使用到的音頻場景有:
AVAudioSessionMode
iOS 提供 AVAudioSessionMode
用于與 AVAudioSessionCategory
搭配使用园爷,教育場景下使用到的音頻模式主要有:
AVAudioSessionOptions
我們可以使用 options
去微調(diào) Category
行為美浦,教育場景下常用的有:
通話音量與媒體音量
一般而言庆冕,通話音量指的是進(jìn)行語音拷姿、視頻通話時的音量棒妨。媒體音量指的是播放音樂枕扫、視頻或游戲的音效参滴、背景音的音量。在實際使用中酷勺,兩者的差異在于击胜,通話音量有較好的回聲消除辰斋,媒體音量有較好的聲音表現(xiàn)力藕夫。媒體音量可以調(diào)整到 0誉尖,而通話音量不可以。
通話音量與媒體音量只能二選一柬甥,因此需要區(qū)分系統(tǒng)音量走的是通話音量還是媒體音量。系統(tǒng)音量走通話音量,是指在設(shè)備上調(diào)整音量時,調(diào)整的是通話音量洗显。媒體音量同理损搬。媒體音量和通話音量分別屬于 2 個不同的、獨立的系統(tǒng)剩瓶,一個設(shè)置不會影響到另外一個枝缔。
進(jìn)入通話后趴荸,音效的播放音量由通話音量控制。退出通話后,則由媒體音量控制晌涕。一般在教育場景下巡扇,學(xué)生作為觀眾拉流時刀闷,使用的媒體音量筒扒,老師說話的聲音更加立體飽滿,當(dāng)學(xué)生連麥時武氓,使用的通話音量属提,以保證通話聲音的質(zhì)量。簡單來說,非連麥模式下會使用媒體音量控制寿弱,連麥模式下會使用通話音量控制噪矛,兩者有獨立的音量控制機制缩滨。
當(dāng)播放媒體資源時,使用播放器(如 AVPlayer
)播放音頻皇忿,播放器底層 AudioUnit
的 description
為 VoiceProcessingIO
芹啥。RTC SDK
內(nèi)部維護(hù)了一個 AudioUnit
傀履,通話音量下 AudioUnit
的 description
為 RemoteIO
,媒體音量下為 VoiceProcessingIO
,當(dāng)出現(xiàn)模式切換時忍饰,會銷毀原來的 AudioUnit
,再創(chuàng)建新的 AudioUnit
,始終保持一個 AudioUnit
來進(jìn)行音頻播放俱两。
通話音量下俊柔,AVPlayer
內(nèi) VoiceProcessingIO
的 AudioUnit
聲音會被抑制白指。同樣的赋焕,在媒體音量下蜜氨,RTC SDK
內(nèi)的 AudioUnit
的 description
設(shè)置為 VoiceProcessingIO
郎汪,如果此時其他模塊通過設(shè)置 AVAudioSession
切換到通話音量,RTC
的聲音也會被抑制波俄。
行業(yè)現(xiàn)狀
在線教室場景下,很多功能都需要播放聲音浸踩,包括課中音視頻直播折剃、課后回放奏甫、webview
內(nèi)嵌課件聲音(包括音頻挠进、視頻澎办、音效)至会、課堂音頻凑懂、課堂視頻巷帝、課堂游戲聲音、音效聲音等脱衙。除此之外瞧预,教室內(nèi)還包括很多需要聲音錄制的功能,包括連麥盆驹、跟讀躯喇、集體發(fā)言廉丽、聊天語音輸入正压、語音識別等雏逾。教室內(nèi)這些功能存在各種組合蠢正,且對 AVAudioSession
的設(shè)置要求存在差異,而 AVAudioSession
又是一個單例,如果沒有一個統(tǒng)一管理的邏輯,很容易就出現(xiàn)設(shè)置混亂的問題。目前行業(yè)內(nèi)碰到的比較多的問題主要是聽不見 RTC
聲音與媒體聲音被抑制几于。
聽不見 RTC 聲音
聽不見 RTC
聲音的主要原因是其他功能在設(shè)置 AVAudioSession
時推沸,AVAudioSessionOptions
未包含 AVAudioSessionCategoryOptionMixWithOthers
混音模式,導(dǎo)致 RTC
聲音被高優(yōu)進(jìn)程打斷饰剥。比如在非混音模式下播放 webview
的內(nèi)嵌音頻龄章,因為 webview
是使用系統(tǒng)進(jìn)程來播放聲音凰盔,優(yōu)先級最高严卖,所以 APP 進(jìn)程下的 RTC
聲音就會被抑制導(dǎo)致無法正常發(fā)聲。
這類問題一般都比較隱蔽狮辽,因為簡單的場景如果有問題怎棱,在上線之前一般都能測試出來蝗肪,而當(dāng)多個功能場景串起來之后才觸發(fā)問題,往往就很難在測試期間發(fā)現(xiàn)亏掀,且如果線上沒有完備的日志查詢體系闻丑,針對線上這類問題排查起來難度也非常大,往往因為定位不到原因而長期遺留挂滓。
媒體聲音被抑制
在通話音量模式下冬阳,媒體聲音會被壓低熊楼,導(dǎo)致聲音變小。比較常見的場景是在小班場景下哭靖,學(xué)生在推流時播放課堂音視頻等媒體資源钝诚,聲音會比 RTC
的聲音要小凝颇,導(dǎo)致媒體聲音聽不清楚拧略。通話模式下(連麥時)媒體聲音會被壓低禽最,原因是 iOS 手機系統(tǒng)會開啟回聲消除以保證人聲體驗川无,因此會壓低媒體通道的聲音懦趋,也會壓低背景音效愕够。
教育行業(yè)內(nèi)部分頭部 APP 也沒有從根本上解決該問題惑芭,很多都是通過從產(chǎn)品功能層面上規(guī)避問題遂跟,通過產(chǎn)品妥協(xié)來為技術(shù)問題讓步幻锁。比如在播放課堂音視頻資源時哄尔,默認(rèn)將所有學(xué)生都強制關(guān)麥岭接,關(guān)麥時學(xué)生處于媒體音量鸣戴,就不存在被壓低的問題了窄锅,等到課堂音視頻播放結(jié)束后入偷,再允許學(xué)生開麥疏之。這種通過規(guī)避問題場景來解決問題的方式体捏,不具有可復(fù)制性河泳。
RTC 聲音變小
RTC
聲音變小拆挥,主要原因是聲音通過聽筒發(fā)聲纸兔,而沒有正常通過揚聲器發(fā)聲汉矿,造成聲音變小的假象洲拇。另外在 iOS14 系統(tǒng)下赋续,使用過 RTC
的通話模式并切回媒體模式后纽乱,再調(diào)用 setCategory:PlayAndRecord + DefaultToSpeaker
就會必現(xiàn)聲音小的問題鸦列。
2敛熬、解決方案
針對上述行業(yè)痛點,通過底層原理的分析與實際項目經(jīng)驗诲锹,從代碼規(guī)范归园、問題兜底庸诱、問題報警梳理出一套可行的解決方案桥爽。
聽不見 RTC 聲音钠四、RTC 聲音變小
RTC
的聲音問題基本是因為其他模塊功能對 AVAudioSession
進(jìn)行了更改缀去,且在功能結(jié)束之后褥影,也沒有將 AVAudioSession
重置到 RTC
需要的設(shè)置伪阶。本身音視頻 SDK(如agora
栅贴、zego
等)對這種情況會有一定的兜底邏輯檐薯,但是這種兜底如果存在侵入性坛缕,也是不合理的,因此具有一定的局限性宠页。
AudioSession 修改規(guī)范
由于系統(tǒng)無法區(qū)分同一個進(jìn)程中是哪個模塊對 AudioSession
進(jìn)行了更改举户,所以為了避免聽不見 RTC
聲音的問題俭嘁,在使用 RTC
時供填,其它模塊對 AudioSession
的調(diào)用更改慨丐,需要遵循以下原則:
- 模塊調(diào)用
setCategory
前先判斷下,當(dāng)前AudioSession
如已滿足使用需要晌端,不用再次設(shè)置咧纠,避免觸發(fā) iOS 14 系統(tǒng) Bug - 模塊需要錄音時,
Category
應(yīng)該使用PlayAndRecord
(為了防止打斷正在播放的音頻演痒,不要使用僅錄音的CategoryRecord
)鸟顺,當(dāng)前category
不是PlayAndRecord
的情況下再調(diào)用setCategory
- 模塊僅需要播放時讯嫂,當(dāng)前
category
為PlayAndRecord
或Playback
、Ambient
的情況下不需要setCategory
- 若當(dāng)前的
category
不滿足模塊使用千扔,在setCategory
之前應(yīng)該先保存當(dāng)前的AudioSession
狀態(tài)昏鹃,然后再setCategory
洞渤、使用音頻功能载迄,使用結(jié)束后魂迄,應(yīng)該重新setCategory
恢復(fù)到之前的AudioSession
狀態(tài) - 在設(shè)置
audioSession
時捣炬,categoryOptions
都應(yīng)該包含AVAudioSessionCategoryOptionDefaultToSpeaker
與AVAudioSessionCategoryOptionMixWithOthers
湿酸,iOS10 系統(tǒng)及以上還應(yīng)包含AVAudioSessionCategoryOptionAllowBluetooth
。
//需要錄音時铁坎,AudioSession的設(shè)置代碼如下:
if ([AVAudioSession sharedInstance].category != AVAudioSessionCategoryPlayAndRecord) {
[RTCAudioSessionCacheManager cacheCurrentAudioSession];
AVAudioSessionCategoryOptions categoryOptions = AVAudioSessionCategoryOptionDefaultToSpeaker | AVAudioSessionCategoryOptionMixWithOthers;
if (@available(iOS 10.0, *)) {
categoryOptions |= AVAudioSessionCategoryOptionAllowBluetooth;
}
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:categoryOptions error:nil];
[[AVAudioSession sharedInstance] setActive:YES error:nil];
}
//功能結(jié)束時重置audioSession
[RTCAudioSessionCacheManager resetToCachedAudioSession];
static AVAudioSessionCategory cachedCategory = nil;
static AVAudioSessionCategoryOptions cachedCategoryOptions = nil;
@implementation RTCAudioSessionCacheManager
......
@end
更改audioSession
前緩存RTC
當(dāng)下的設(shè)置:
+ (void)cacheCurrentAudioSession {
if (![[AVAudioSession sharedInstance].category isEqualToString:AVAudioSessionCategoryPlayback] && ![[AVAudioSession sharedInstance].category isEqualToString:AVAudioSessionCategoryPlayAndRecord]) {
return;
}
@synchronized (self) {
cachedCategory = [AVAudioSession sharedInstance].category;
cachedCategoryOptions = [AVAudioSession sharedInstance].categoryOptions;
}
}
重置到緩存的audioSession
設(shè)置:
+ (void)resetToCachedAudioSession {
if (!cachedCategory || !cachedCategoryOptions) {
return;
}
BOOL needResetAudioSession = ![[AVAudioSession sharedInstance].category isEqualToString:cachedCategory] || [AVAudioSession sharedInstance].categoryOptions != cachedCategoryOptions;
if (needResetAudioSession) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[[AVAudioSession sharedInstance] setCategory:cachedCategory withOptions:cachedCategoryOptions error:nil];
[[AVAudioSession sharedInstance] setActive:YES error:nil];
@synchronized (self) {
cachedCategory = nil;
cachedCategoryOptions = nil;
}
});
}
}
兜底策略
考慮到在線教室場景的復(fù)雜度围详,讓教室內(nèi)所有功能代碼都遵循 AVAudioSession
的修改規(guī)范短曾,雖然有嚴(yán)格的codeReview
嫉拐,但是也存在一定的人為因素風(fēng)險漠嵌,隨著業(yè)務(wù)功能不斷迭代儒鹿,無法完全保證線上不出問題约炎,因此一套可靠的兜底策略顯得非常有必要圾浅。
兜底策略的基本邏輯是 hook
到 AVAudioSession
的變化喷鸽,當(dāng)各模塊對 AVAudioSession
的設(shè)置不符合規(guī)范要求時做祝,我們在不影響功能的前提下強制進(jìn)行修正剖淀,比如對 options
補充上混音模式翻诉。
通過方法交換我們可以 hook
到 AVAudioSession
的更改碰煌。比如用 kk_setCategory:withOptions: error:
與系統(tǒng)的 setCategory:withOptions: error:
進(jìn)行交換芦圾,在交換的方法里洪乍,我們判斷 options
是否包含 AVAudioSessionCategoryOptionMixWithOthers
壳澳,如果沒有包含我們就進(jìn)行追加巷波。在需要進(jìn)行對audioSession
進(jìn)行修正的場景下(RTC
直播),修改options
時未包含mixWithOther
垮耳,則給options
追加mixWithOther
氨菇。
- (BOOL)kk_setCategory:(AVAudioSessionCategory)category withOptions:(AVAudioSessionCategoryOptions)options error:(NSError **)outError {
BOOL addMixWithOthersEnable = shouldFixAudioSession && !(options & AVAudioSessionCategoryOptionMixWithOthers)];
if (addMixWithOthersEnable) {
return [self kk_setCategory:category withOptions:options | AVAudioSessionCategoryOptionMixWithOthers error:outError];;
}
return [self kk_setCategory:category withOptions:options error:outError];
}
但上述方法只對通過調(diào)用 setCategory:withOptions: error:
來設(shè)置 audioSession
有效乌询,如果調(diào)用了 setCategory:error:
來更改audioSession
豌研,則會造成調(diào)用死循環(huán)的問題妹田。在 iOS 底層實現(xiàn)中,調(diào)用 setCategory:error:
時鹃共,內(nèi)部會再調(diào)用 setCategory:withOptions: error:
方法鬼佣,因為進(jìn)行了方法交換霜浴,從而出現(xiàn)嵌套調(diào)用問題晶衷。
針對該問題,我們通過監(jiān)聽 AVAudioSessionRouteChangeNotification
通知阴孟,來 hookcategory
的變化晌纫,AVAudioSessionRouteChangeNotification
在調(diào)用 setCategory:error:
時會觸發(fā),而不會在調(diào)用 setCategory:withOptions: error:
時直接觸發(fā)永丝,進(jìn)而與上述方法形成了很好的互補锹漱。
//添加對AVAudioSessionRouteChange的監(jiān)聽
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleRouteChangeNotification:) name:AVAudioSessionRouteChangeNotification object:nil];
- (void)handleRouteChangeNotification:(NSNotification *)notification {
NSNumber* reasonNumber =
notification.userInfo[AVAudioSessionRouteChangeReasonKey];
AVAudioSessionRouteChangeReason reason =
(AVAudioSessionRouteChangeReason)reasonNumber.unsignedIntegerValue;
if (reason == AVAudioSessionRouteChangeReasonCategoryChange) {
AVAudioSessionCategoryOptions currentCategoryOptions = [AVAudioSession sharedInstance].categoryOptions;
AVAudioSessionCategory currentCategory = [AVAudioSession sharedInstance].category;
//在需要進(jìn)行對audioSession進(jìn)行修正的場景下(RTC直播),修改category時options未包含mixWithOther慕嚷,則給options追加mixWithOther
if (shouldFixAudioSession && !(currentCategoryOptions & AVAudioSessionCategoryOptionMixWithOthers)) {
[[AVAudioSession sharedInstance] setCategory:currentCategory withOptions:currentCategoryOptions | AVAudioSessionCategoryOptionMixWithOthers error:nil];
}
}
}
報警機制
即使有修改規(guī)范與兜底策略的保障哥牍,隨著教室業(yè)務(wù)迭代與 iOS 系統(tǒng)升級,也無法保證線上完全不出問題喝检,因此我們建立了問題報警機制嗅辣,當(dāng)線上出現(xiàn)問題時,能在工作群里及時收到警報挠说,根據(jù)警報的問題信息辩诞,通過日志進(jìn)一步排查問題。通過報警機制纺涤,我們可以更快速的對線上問題作出反應(yīng)译暂,不被動依賴于學(xué)生的投訴反饋,以最快的速度推進(jìn)問題解決撩炊。
當(dāng) RTC
聲音被打斷時外永,底層音視頻 SDK 會回調(diào)警告錯誤碼(如agora
的 warningCode
為 1025),當(dāng)出現(xiàn)對應(yīng)的警告碼時拧咳,結(jié)合 slardar
的報警功能伯顶,在飛書群里以消息的形式進(jìn)行同步。同時在 hook
到 AVAudioSession
的變更時,通過獲取堆棧信息祭衩,可以定位到是哪個模塊觸發(fā)的更改灶体,結(jié)合報警用戶信息,可以更方便的定位問題掐暮。
媒體聲音被抑制
媒體聲音在媒體音量下開啟播放蝎抽,播放途中因為連麥而切換到了通話音量,此時因為系統(tǒng)特性路克,媒體音量會被通話音量抑制而導(dǎo)致聲音變小樟结。針對該問題,我們使用音視頻 SDK 提供的混音精算、混流功能來規(guī)避瓢宦。基本原理是播放媒體資源時灰羽,我們拿到資源的 pcm
音頻數(shù)據(jù)驮履,將數(shù)據(jù)拋給 RTC
的 audioUnit
進(jìn)行混合,由 RTC
音頻播放單元統(tǒng)一播放廉嚼,如果此時 RTC
使用的是通話音量玫镐,則媒體資源也是使用的通話音量播放,反之亦然前鹅。以此來保證媒體資源與 RTC
始終保持統(tǒng)一的音量控制機制,而避免聲音大小存在差異峭梳。
混音是指給到音頻的本地文件路徑舰绘,或者播放的 url
,由 SDK 進(jìn)行數(shù)據(jù)讀取與播放葱椭∥媸伲混流是指針對視頻文件,播放器只解碼播放視頻數(shù)據(jù)孵运,將音頻數(shù)據(jù)實時拋出來給到 SDK秦陋,SDK 將傳入的實時音頻數(shù)據(jù)與RTC
音頻數(shù)據(jù)進(jìn)行混合與播放。項目中我們使用點播 SDK TTVideoEngine
來實現(xiàn)視頻播放與音頻外拋治笨。
Demo
Demo在我的Github上驳概,歡迎下載。
Multi-MediaDemo