iOS音視頻:音頻

原創(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.frameworkAVFoundation.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播放音效的步驟
  1. 調(diào)用AudioServicesCreateSystemSoundID( CFURLRef inFileURL缎罢, SystemSoundID* outSystemSoundID)函數(shù)獲得系統(tǒng)聲音ID伊群。
  2. 如果需要監(jiān)聽播放完成操作,則使用AudioServicesAddSystemSoundCompletion( SystemSoundID inSystemSoundID策精,CFRunLoopRef inRunLoop舰始,CFStringRef inRunLoopModeAudioServicesSystemSoundCompletionProc inCompletionRoutine咽袜,void* inClientData)方法注冊回調(diào)函數(shù)丸卷。
  3. 調(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)步驟如下:

  1. 初始化AVAudioPlayer對象氏淑,此時通常指定本地文件路徑勃蜘。
  2. 設(shè)置播放器屬性,例如重復(fù)次數(shù)假残、音量大小等元旬。
  3. 調(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 0App plays audio or streams audio/video using AirPlay(其實可以直接通過XcodeProject 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的重要組成播放靖诗,無論是iPodiTouch支示、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有兩種播放器:applicationMusicPlayersystemMusicPlayer驰唬,前者在應(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)行音樂選擇的令宿,但是仍然提供了兩個方法getLocalMediaQuerygetLocalMediaItemCollection來演示如何直接通過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ù)隊列

一個音頻服務(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ù)的流程示意圖:

官方關(guān)于音頻隊列服務(wù)的流程示意圖

類似的疾党,看一下音頻播放緩沖隊列音诫,其組成部分和錄音緩沖隊列類似。

音頻播放緩沖隊列

但是在音頻播放緩沖隊列中雪位,回調(diào)函數(shù)調(diào)用的時機不同于音頻錄制緩沖隊列竭钝,流程剛好相反。將音頻讀取到緩沖器中雹洗,一旦一個緩沖器填充滿之后就放到緩沖隊列中愚臀,然后繼續(xù)填充其他緩沖器先慷。當(dāng)開始播放時惹恃,則從第一個緩沖器中讀取音頻進(jìn)行播放恃锉,一旦播放完之后就會觸發(fā)回調(diào)函數(shù),開始播放下一個緩沖器中的音頻螃成,同時填充第一個緩沖器旦签,填充滿之后再次放回到緩沖隊列。

下面是詳細(xì)的流程圖:

詳細(xì)的流程圖

當(dāng)然寸宏,要明白音頻隊列服務(wù)的原理并不難宁炫,問題是如何實現(xiàn)這個自定義的回調(diào)函數(shù),這其中我們有大量的工作要做氮凝,控制播放狀態(tài)羔巢、處理異常中斷、進(jìn)行音頻編碼等等。由于牽扯內(nèi)容過多竿秆,而且不是本文目的启摄,如果以后有時間將另開一篇文章重點介紹,目前有很多第三方優(yōu)秀框架可以直接使用幽钢,例如FreeStreamer歉备。

使用FreeStreamer之前要做如下準(zhǔn)備工作:

  1. 拷貝FreeStreamer中的Reachability.hReachability.mCommon搅吁、astreamer兩個文件夾中的內(nèi)容到項目中威创。
  2. 添加FreeStreamer使用的類庫:CFNetwork.framework落午、AudioToolbox.framework谎懦、AVFoundation.frameworklibxml2.dylib溃斋、MediaPlayer.framework界拦。
  3. 如果引用libxml2.dylib編譯不通過,需要在Xcode的Targets-Build Settings-Header Build Path中添加$(SDKROOT)/usr/include/libxml2梗劫。
  4. 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

教育場景下主要使用到的音頻場景有:

image.png
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)播放音頻皇忿,播放器底層 AudioUnitdescriptionVoiceProcessingIO芹啥。RTC SDK 內(nèi)部維護(hù)了一個 AudioUnit傀履,通話音量下 AudioUnitdescriptionRemoteIO,媒體音量下為 VoiceProcessingIO,當(dāng)出現(xiàn)模式切換時忍饰,會銷毀原來的 AudioUnit,再創(chuàng)建新的 AudioUnit,始終保持一個 AudioUnit來進(jìn)行音頻播放俱两。

通話音量下俊柔,AVPlayer 內(nèi) VoiceProcessingIOAudioUnit 聲音會被抑制白指。同樣的赋焕,在媒體音量下蜜氨,RTC SDK 內(nèi)的 AudioUnitdescription 設(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)前 categoryPlayAndRecordPlaybackAmbient的情況下不需要 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)該包含 AVAudioSessionCategoryOptionDefaultToSpeakerAVAudioSessionCategoryOptionMixWithOthers湿酸,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ù)功能不斷迭代儒鹿,無法完全保證線上不出問題约炎,因此一套可靠的兜底策略顯得非常有必要圾浅。

兜底策略的基本邏輯是 hookAVAudioSession 的變化喷鸽,當(dāng)各模塊對 AVAudioSession 的設(shè)置不符合規(guī)范要求時做祝,我們在不影響功能的前提下強制進(jìn)行修正剖淀,比如對 options 補充上混音模式翻诉。

通過方法交換我們可以 hookAVAudioSession 的更改碰煌。比如用 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)警告錯誤碼(如agorawarningCode 為 1025),當(dāng)出現(xiàn)對應(yīng)的警告碼時拧咳,結(jié)合 slardar 的報警功能伯顶,在飛書群里以消息的形式進(jìn)行同步。同時在 hookAVAudioSession 的變更時,通過獲取堆棧信息祭衩,可以定位到是哪個模塊觸發(fā)的更改灶体,結(jié)合報警用戶信息,可以更方便的定位問題掐暮。

媒體聲音被抑制

媒體聲音在媒體音量下開啟播放蝎抽,播放途中因為連麥而切換到了通話音量,此時因為系統(tǒng)特性路克,媒體音量會被通話音量抑制而導(dǎo)致聲音變小樟结。針對該問題,我們使用音視頻 SDK 提供的混音精算、混流功能來規(guī)避瓢宦。基本原理是播放媒體資源時灰羽,我們拿到資源的 pcm 音頻數(shù)據(jù)驮履,將數(shù)據(jù)拋給 RTCaudioUnit 進(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

參考文獻(xiàn)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
禁止轉(zhuǎn)載旷赖,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者顺又。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市等孵,隨后出現(xiàn)的幾起案子稚照,更是在濱河造成了極大的恐慌昌渤,老刑警劉巖曹锨,帶你破解...
    沈念sama閱讀 219,039評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡埠对,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評論 3 395
  • 文/潘曉璐 我一進(jìn)店門询刹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來烘跺,“玉大人,你說我怎么就攤上這事斤彼》质荩” “怎么了?”我有些...
    開封第一講書人閱讀 165,417評論 0 356
  • 文/不壞的土叔 我叫張陵琉苇,是天一觀的道長嘲玫。 經(jīng)常有香客問我,道長并扇,這世上最難降的妖魔是什么去团? 我笑而不...
    開封第一講書人閱讀 58,868評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮穷蛹,結(jié)果婚禮上土陪,老公的妹妹穿的比我還像新娘。我一直安慰自己肴熏,他們只是感情好鬼雀,可當(dāng)我...
    茶點故事閱讀 67,892評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蛙吏,像睡著了一般源哩。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上鸦做,一...
    開封第一講書人閱讀 51,692評論 1 305
  • 那天励烦,我揣著相機與錄音,去河邊找鬼泼诱。 笑死坛掠,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的治筒。 我是一名探鬼主播屉栓,決...
    沈念sama閱讀 40,416評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼耸袜!你這毒婦竟也來了系瓢?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,326評論 0 276
  • 序言:老撾萬榮一對情侶失蹤句灌,失蹤者是張志新(化名)和其女友劉穎夷陋,沒想到半個月后欠拾,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,782評論 1 316
  • 正文 獨居荒郊野嶺守林人離奇死亡骗绕,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,957評論 3 337
  • 正文 我和宋清朗相戀三年藐窄,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片酬土。...
    茶點故事閱讀 40,102評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡荆忍,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出撤缴,到底是詐尸還是另有隱情刹枉,我是刑警寧澤,帶...
    沈念sama閱讀 35,790評論 5 346
  • 正文 年R本政府宣布屈呕,位于F島的核電站微宝,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏虎眨。R本人自食惡果不足惜蟋软,卻給世界環(huán)境...
    茶點故事閱讀 41,442評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望嗽桩。 院中可真熱鬧岳守,春花似錦、人聲如沸碌冶。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽扑庞。三九已至譬重,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間嫩挤,已是汗流浹背害幅。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評論 1 272
  • 我被黑心中介騙來泰國打工消恍, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留岂昭,地道東北人。 一個月前我還...
    沈念sama閱讀 48,332評論 3 373
  • 正文 我出身青樓狠怨,卻偏偏與公主長得像约啊,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子佣赖,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,044評論 2 355