流媒體開發(一)音頻播放

序言

隨着人們學習、娛樂和工作的需要,人們對如何在網絡上傳輸海量的視頻、音頻等多媒體信息有了進一步的要求。在這種背景下,iOS流媒體技術應運而生。通俗的講,所謂的iOS流媒體技術,就是將視音頻文件經過壓縮處理後,放在網絡服務器上進行分段的傳輸,客戶端計算機不用將整個的視音頻文件下載到本地,便可以即時收聽和收看。

iOS流媒體格式的文件是經過了特殊的編碼,才能夠實現在網絡上邊邊下邊播,而不是將整個的影音文件全部下載後才能播放。通常,編碼由專門的壓縮編碼軟件來完成,使用者收聽和收看網絡影音文件則是一個解壓縮的過程,這是由專門的播放器來完成的。所以,iOS流媒體文件具有它特殊的格式。網絡上的影音服務,不同公司提供的格式、類型、以及傳輸方式都存在着差異。目前,在iOS流媒體領域中,參與競爭的公司主要有三個:微軟、Real Networks和蘋果公司,相應的iOS流媒體解決方案分別是:Windows Media、Real System和QuickTime.

在實際的開發工作中,流媒體開發是程序員必須掌握的開發技術。iOS SDK中提供了很多方便的方法來播放流媒體。iOS對於流媒體支持相當靈活和完善,既有高度封裝的類幫助我們實現流媒體的開發工作,也開放了大量的底層接口供我們使用,我們可以自行封裝實現我們需要的功能,開發的自由度非常高。

流媒體開發主要是對音頻文件和視頻文件的處理,下面我們先來看一下iOS中如何實現音頻播放。


音頻播放

在iOS中音頻播放從形式上可以分爲音效播放和音樂播放。前者主要指的是一些短音頻播放,通常作爲點綴音頻,對於這類音頻不需要進行進度、循環等控制;後者指的是一些較長的音頻,通常是主音頻,對於這些音頻的播放通常需要進行精確的控制。

在iOS中播放兩類音頻分別使用AudioToolbox.framework和AVFoundation.framework來完成音效和音樂播放。

1. 音效播放

在iOS開發中,有時候需要我們頻繁的播放某種提示聲音,比如微博的刷新提示聲音,QQ消息的提示聲音等,對於這些短小且頻繁的音頻,我們最好將其加入到系統聲音裏。

音頻數據可分爲壓縮和非壓縮的存儲類型。壓縮的音頻文件雖然文件體積較小(相對於非壓縮的),但需要耗費處理器的性能進行解壓和解碼。如果音頻文件體積較小,壓縮後的音頻文件,也不會節省較大的磁盤空間。像這一類小型非壓縮的文件可以註冊成爲系統聲音。

通過AudioToolbox.framework來完成系統聲音的註冊和播放:

  AudioToolbox.framework是一套基於C語言的框架,使用它來播放音效其本質是將短音頻註冊到系統聲音服務(System Sound Service)。
  System Sound Service是一種簡單、底層的聲音播放服務,但是它本身也存在着一些限制:
  1. 音頻播放時間不能超過30s
  2. 數據必須是PCM或者IMA4格式
  3. 音頻文件必須打包成.caf、.aif、.wav中的一種(注意這是官方文檔的說法,實際測試發現一些.mp3也可以播放)

使用System Sound Service 播放音效的步驟如下:

  1. 調用AudioServicesCreateSystemSoundID(CFURLRef inFileURL, SystemSoundID* outSystemSoundID) 函數獲得系統聲音ID。
  2. 如果需要監聽播放完成操作,則使用AudioServicesAddSystemSoundCompletion( SystemSoundID inSystemSoundID,CFRunLoopRef inRunLoop, CFStringRef inRunLoopMode, AudioServicesSystemSoundCompletionProc inCompletionRoutine, void* inClientData)方法註冊回調函數。
  3. 調用AudioServicesPlaySystemSound(SystemSoundID inSystemSoundID) 或者AudioServicesPlayAlertSound(SystemSoundID inSystemSoundID) 方法播放音效(後者帶有震動效果)。

示例代碼:

#import "ViewController.h"
#import <AudioToolbox/AudioToolbox.h>

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    // 1.獲取音頻文件
    NSString *filePath = [[NSBundle mainBundle] pathForResource:@"msgcome" ofType:@"wav"];
    NSURL *fileURL = [NSURL fileURLWithPath:filePath];

    // 2.註冊聲音到系統 -> 獲取系統聲音ID
    SystemSoundID soundID = 0;
    AudioServicesCreateSystemSoundID((__bridge CFURLRef)(fileURL), &soundID);
    unsigned long sID = soundID;//SystemSoundID不能直接打印,需要一次中轉
    NSLog(@"soundID:%lu",sID);
    /* 通過該函數將音頻文件註冊爲系統聲音
     * 該函數調用完成後,系統自動分配soundID
     * 參數一:音頻文件路徑
     <#CFURLRef  _Nonnull inFileURL#>
     * 參數二:註冊的系統聲音ID
     <#SystemSoundID * _Nonnull outSystemSoundID#>
     */

    // 3.播放系統聲音
    AudioServicesPlaySystemSound(soundID);// 播放音效
//    AudioServicesPlayAlertSound(soundID);// 播放系統聲音並震動

    // 4.監聽播放完成的操作
    AudioServicesAddSystemSoundCompletion(soundID, NULL, NULL, soundCompletionCallback, (__bridge void *)self);
    /*
     *參數說明:
     * 1、剛剛播放完成自定義系統聲音的ID
     * 2、回調函數(playFinished)執行的run Loop,NULL表示main run loop
     * 3、回調函數執行所在run loop的模式,NULL表示默認的run loop mode
     * 4、需要回調的函數,按照要求的函數格式自定義回調函數
     * 5、傳入的參數, 此參數會被傳入回調函數裏
     */

}

// 音效播放結束的回調函數
void soundCompletionCallback (SystemSoundID mySSID, void* __nullable data) {

    NSLog(@"completion Callback");
    unsigned long sID = mySSID;//SystemSoundID不能直接打印,需要一次中轉
    NSLog(@"soundID:%lu",sID);

    /*ARC中C語言函數需要自己管理內存,所以這裏,我們需要回調完成後移除剛剛添加的操   作 ,並且確認當你以後不再需要播放該ID的音頻時,清理該ID音頻對應的所有資源
     */
    // 根據ID移除播放完成後回調執行的函數
    AudioServicesRemoveSystemSoundCompletion(mySSID);

    // 根據ID釋放自定義的系統聲音
    AudioServicesDisposeSystemSoundID(mySSID);
}

@end

注意:

  • 必須等到播放完成才能清理該ID對應的所有資源,也就是說,如果我們打算不再使用該音頻,可以在完成後調用的函數裏清理資源,如上例所示。 不過一般情況下,既然加到系統聲音裏的多是頻繁使用的,可以在程序結束或者某個控制器銷燬的時候再清理。
  • 如果在AudioServicesPlaySystemSound(ID)之後馬上調用 AudioServicesDisposeSystemSoundID(ID),你將聽不到任何聲音,並且也不會調用播放完成後的函數,這是因爲系統音頻播放的所有操作都是放到主線程之外執行的,當主線程執行到清理的時候,該音頻如果試圖播放ID對應的音頻,將無法找到。

2. 音樂播放

如果播放較大的音頻或者要對音頻有精確的控制則System Sound Service可能就很難滿足實際需求了,通常這種情況會選擇使用AVFoundation.framework中的AVAudioPlayer來實現。AVAudioPlayer可以看成一個播放器,它支持多種音頻格式,而且能夠進行進度、音量、播放速度等控制。

首先簡單看一下AVAudioPlayer常用的屬性和方法:

屬性 說明
@property(readonly, getter=isPlaying) BOOL playing 是否正在播放,只讀
@property(readonly) NSUInteger numberOfChannels 音頻聲道數,只讀
@property(readonly) NSTimeInterval duration 音頻時長
@property(readonly) NSURL *url 音頻文件路徑,只讀
@property(readonly) NSData *data 音頻數據,只讀
@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則正常播放,如果要修改播放速率則必須設置enableRate爲YES
@property NSTimeInterval currentTime 當前播放時長
@property(readonly) NSTimeInterval deviceCurrentTime 輸出設備播放音頻的時間,注意如果播放中被暫停此時間也會繼續累加
@property NSInteger numberOfLoops 循環播放次數,如果爲0則不循環,如果小於0則無限循環,大於0則表示循環次數
@property(readonly) NSDictionary *settings 音頻播放設置信息,只讀
@property(getter=isMeteringEnabled) BOOL meteringEnabled 是否啓用音頻測量,默認爲NO,一旦啓用音頻測量可以通過updateMeters方法更新測量值
@property(nonatomic, copy) NSArray *channelAssignments 獲得或設置播放聲道
對象方法 說明
-(instancetype)initWithContentsOfURL:(NSURL )url error:(NSError *)outError 使用文件URL初始化播放器,注意這個URL不能是HTTP URL,AVAudioPlayer不支持加載網絡媒體流,只能播放本地文件
-(instancetype)initWithData:(NSData )data error:(NSError *)outError 使用NSData初始化播放器,注意使用此方法時必須文件格式和文件後綴一致,否則出錯,所以相比此方法更推薦使用上述方法或- (instancetype)initWithData:(NSData )data fileTypeHint:(NSString )utiString error:(NSError **)outError方法進行初始化
-(BOOL)prepareToPlay; 加載音頻文件到緩衝區,注意即使在播放之前音頻文件沒有加載到緩衝區程序也會隱式調用此方法。
-(BOOL)play; 播放音頻文件
-(BOOL)playAtTime:(NSTimeInterval)time 在指定的時間開始播放音頻
-(void)pause; 暫停播放
-(void)stop; 停止播放
-(void)updateMeters 更新音頻測量值,注意如果要更新音頻測量值必須設置meteringEnabled爲YES,通過音頻測量值可以即時獲得音頻分貝等信息
-(float)peakPowerForChannel:(NSUInteger)channelNumber; 獲得指定聲道的分貝峯值,注意如果要獲得分貝峯值必須在此之前調用updateMeters方法
-(float)averagePowerForChannel:(NSUInteger)channelNumber 獲得指定聲道的分貝平均值,注意如果要獲得分貝平均值必須在此之前調用updateMeters方法
代理方法 說明
-(void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag 音頻播放完成
-(void)audioPlayerDecodeErrorDidOccur:(AVAudioPlayer )player error:(NSError )error 音頻解碼發生錯誤

下面是AVAudioPlayer的使用步驟:

  1. 初始化AVAudioPlayer對象,此時通常指定本地文件路徑。
    注意:需要設置爲全局變量,系統沒有任何的邏輯對其進行強引用,像視圖添加在父視圖上,會抑制存在,如果設置爲局部變量,音樂還沒有播放,就被自動釋放掉了。
  2. 設置播放器屬性,例如重複次數、音量大小等。
  3. 調用play方法播放。

示例代碼:

#import "ViewController.h"
#import <AVFoundation/AVFoundation.h>

@interface ViewController ()<AVAudioPlayerDelegate>
{
    AVAudioPlayer *_audioPlayer;//音頻播放器

    __weak IBOutlet UISlider *_progress;
    __weak IBOutlet UISlider *_volume;

}
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    // 1.加載音頻文件
    NSString *filePath = [[NSBundle mainBundle] pathForResource:@"東京不太熱" ofType:@"mp3"];
    NSURL *fileURL = [NSURL fileURLWithPath:filePath];

    // 2.初始化音頻播放器
    //使用文件URL初始化播放器,注意這個URL不能是HTTP URL,AVAudioPlayer不支持加載網絡媒體流,只能播放本地文件
    NSError *error = nil;
    _audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:fileURL error:&error];
    if (error) {

        NSLog(@"初始化音頻播放器出錯,error:%@",error);
        return;
    }
    // 3.設置播放器屬性
    {
        _audioPlayer.numberOfLoops = -1;// 循環次數
        _audioPlayer.delegate = self;// 代理對象

        CGFloat duration = _audioPlayer.duration;// 播放時長
        NSLog(@"音樂總時長:%.2f",duration);
        _audioPlayer.currentTime = 20;// 當前的播放時間
        _audioPlayer.volume = 0.5;// 音量 0.0~1.0

        _audioPlayer.enableRate = YES;// 允許改變播放速率
        _audioPlayer.rate = 1.0;// 正常速率

        [_audioPlayer prepareToPlay];// 加載音頻文件到播放隊列,準備播放
        [_audioPlayer play];// 開始播放
    }

    // UI設置
    [_volume setValue:_audioPlayer.volume];
    [_progress setValue:_audioPlayer.currentTime/_audioPlayer.duration];
    //添加計時器方法,刷新進度條
    CADisplayLink *displayLink  = [CADisplayLink displayLinkWithTarget:self selector:@selector(updataProgress)];
    //CADisplayLink需要將顯示鏈接添加到一個運行循環中,即用於處理系統事件的一個Cocoa Touch結構。
    [displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];

}

// 即時調用刷新進度條的value值
- (void)updataProgress {

    // 計算播放進度 = 當前播放時間/播放總時長
    CGFloat progress = _audioPlayer.currentTime/_audioPlayer.duration;
    [_progress setValue:progress];// 設置進度條的播放進度

}

// 拖動進度條,改變音頻的播放音量
- (IBAction)volumeAction:(UISlider *)sender {

    // 設置音量
    _audioPlayer.volume = sender.value;
}

// 拖動進度條,改變音頻的播放進度
- (IBAction)progress:(UISlider *)sender {

    // 進度條的拖動進度
    CGFloat progress = sender.value;

    // 設置當前的播放時間 = 總時長 * 播放進度
    _audioPlayer.currentTime = _audioPlayer.duration * progress;

}
- (IBAction)stop:(UIButton *)sender {

    // 判斷播放器是否處於播放狀態
    BOOL playing = _audioPlayer.playing;
    if (playing) {

        [_audioPlayer stop];//停止播放
    }
}

// 點擊按鈕,切換音頻的播放狀態
- (IBAction)playOrPause:(UIButton *)sender {

    //判斷當前的播放狀態
    BOOL isplay = _audioPlayer.playing;
    if (isplay) {

        [_audioPlayer pause];// 暫停

        [sender setTitle:@"play" forState:UIControlStateNormal];
    }else {

        //播放
        [_audioPlayer play];

        [sender setTitle:@"pause" forState:UIControlStateNormal];
    }
}

#pragma mark - AVAudioPlayerDelegate
// 播放結束
- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag {

    NSLog(@"player finished");
    NSLog(@"%f",_progress.value);

    _progress.value = 0.0f;
}

// 音頻解碼出錯
- (void)audioPlayerDecodeErrorDidOccur:(AVAudioPlayer *)player error:(NSError * __nullable)error {

    NSLog(@"player decode error");
}

@end

由於AVAudioPlayer一次只能播放一個音頻文件,所有上一曲、下一曲其實可以通過創建多個播放器對象來完成,這裏暫不實現。播放進度的實現主要依靠一個定時器實時計算當前播放時長和音頻總時長的比例,另外爲了演示委託方法,上面的代碼中也實現了播放完成委託方法,通常如果有下一曲功能的話播放完可以觸發下一曲音樂播放。

注意:

  • AVAudioPlayer必須設置爲全局變量,否則音頻無法播放。
  • 播放音頻是不能添加全局斷點,否則會導致程序崩潰。
  • AVAudioPlayer的委託方法,通常如果有下一曲功能的話播放完可以觸發下一曲音樂播放。當然由於AVAudioPlayer一次只能播放一個音頻文件,所有上一曲、下一曲其實可以通過創建多個播放器對象來完成。

3. 網絡音頻播放

大家應該已經注意到了,使用AVAudioPlayer播放音頻並不支持網絡流媒體播放,但是對於音頻播放來說網絡流媒體播放有時候是很有必要的。AVAudioPlayer只能播放本地文件,並且是一次性加載所以音頻數據,初始化AVAudioPlayer時指定的URL也只能是File URL而不能是HTTP URL。當然,將音頻文件下載到本地然後再調用AVAudioPlayer來播放也是一種播放網絡音頻的辦法,但是這種方式最大的弊端就是必須等到整個音頻播放完成才能播放,而不能使用流式播放,這往往在實際開發中是不切實際的。

那麼在iOS中如何播放網絡流媒體呢?蘋果公司提供了功能強大的AVPlayer用於播放網絡音頻(既流媒體),效果和可控性都比較好。

AVPlayer是一個全功能影音播放器,可以播放任何格式的音頻或視頻,支持當前流行的幾乎所有的多媒體格式:
 支持視頻格式: WMV,AVI,MKV,RMVB,RM,XVID,MP4,3GP,MPG …
 支持音頻格式: MP3,WMA,RM,ACC,OGG,APE,FLAC,FLV…
 支持外部字幕: smi,srt,ass,sub,txt…

下面我們先來重點介紹下AVPlayer中網絡音頻的播放。

示例代碼:

#import "ViewController.h"
#import <AVFoundation/AVFoundation.h>

@interface ViewController ()
{
    NSArray *_playList;
    AVPlayer *_avPlayer;// 音頻播放器
}
@property (weak, nonatomic) IBOutlet UISlider *volume;
@property (weak, nonatomic) IBOutlet UISlider *progress;
@property (weak, nonatomic) IBOutlet UIButton *play;
@property (strong, nonatomic) IBOutlet UIProgressView *progressView;

- (IBAction)volume:(UISlider *)sender;
- (IBAction)progress:(UISlider *)sender;
- (IBAction)playOrPause:(UIButton *)sender;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    NSLog(@"%@",[NSHomeDirectory() stringByAppendingString:@"/Documents"]);

    // 1.加載網絡音頻網址
    NSString *str1 = @"http://www.300.la/filestores/2016/02/20/692de68936d75eea6da4a75f87f3ab2f.mp3";
    NSString *str2 = @"http://www.300.la/filestores/2014/08/20/93fde37c2aeae22c61a9c7b70b247f92.mp3";
    NSURL *url1 = [NSURL URLWithString:str1];
    NSURL *url2 = [NSURL URLWithString:str2];
    _playList = @[url1,url2];

    //使用AVPlayer同樣可以播放本地的音頻文件
//    NSURL *fileURL = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"PartOfMe" ofType:@"mp3"]];

    // 2.初始化AVPlayer音頻播放器
    AVPlayerItem *item = [[AVPlayerItem alloc] initWithURL:_playList[0]];
    _avPlayer = [[AVPlayer alloc] initWithPlayerItem:item];

    // 3.播放音頻
    [_avPlayer play];

    {// 常用屬性設置

        _avPlayer.rate = 1;// 播放速度
        _avPlayer.volume = 0.8f;// 播放音量
        _avPlayer.muted = NO;// 靜音

        // 播放完成的操作
        _avPlayer.actionAtItemEnd = AVPlayerActionAtItemEndPause;
        /* AVPlayerActionAtItemEnd
         AVPlayerActionAtItemEndAdvance = 0,// 不要使用
         AVPlayerActionAtItemEndPause = 1,
         AVPlayerActionAtItemEndNone    = 2,
         */
    }

    {// 常用方法
        // 指定播放時間
        [_avPlayer seekToTime:CMTimeMakeWithSeconds(20, 24)];// 從指定時間的前一秒開始播放
    }

    // 監測播放狀態,
    [_avPlayer addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];

    // 註冊通知 AVPlayerItemDidPlayToEndTimeNotification:音頻播放完成
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playDidEnd) name:AVPlayerItemDidPlayToEndTimeNotification object:nil];
    /* 音頻播放通知屬性名
     AVF_EXPORT NSString *const AVPlayerItemTimeJumpedNotification NS_AVAILABLE(10_7, 5_0); // 項目的當前時間間斷地發生了變化
     AVF_EXPORT NSString *const AVPlayerItemDidPlayToEndTimeNotification      NS_AVAILABLE(10_7, 4_0);   // 項目中結束時間
     AVF_EXPORT NSString *const AVPlayerItemFailedToPlayToEndTimeNotification NS_AVAILABLE(10_7, 4_3);   // 項目未能發揮它的結束時間
     AVF_EXPORT NSString *const AVPlayerItemPlaybackStalledNotification       NSvh_AVAILABLE(10_9, 6_0);    // 媒體沒有及時趕到繼續播放
     AVF_EXPORT NSString *const AVPlayerItemNewAccessLogEntryNotification NS_AVAILABLE(10_9, 6_0); // 一個新的訪問日誌條目已被添加
     AVF_EXPORT NSString *const AVPlayerItemNewErrorLogEntryNotification NS_AVAILABLE(10_9, 6_0); // 添加了一個新的錯誤日誌條目
     AVF_EXPORT NSString *const AVPlayerItemFailedToPlayToEndTimeErrorKey     NS_AVAILABLE(10_7, 4_3);   // 項目中結束時間發生錯誤
     */

    // 實時監聽音頻文件的播放進度
    __block ViewController *blockSelf = self;
    __block AVPlayer *blockPlayer = _avPlayer;
    [_avPlayer addPeriodicTimeObserverForInterval:CMTimeMake(1, 1) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {

        // 週期性回調該block的代碼塊
        NSLog(@"播放狀態:%ld",item.status);
        CGFloat seconds = time.value/time.timescale;
        NSLog(@"%f",seconds);

        /* CMTime : value/timescale = seconds.
         time指的就是時間(不是秒),
         而時間要換算成秒就要看第二個參數timeScale了.
         timeScale指的是1秒需要由幾個frame構成(可以視為fps,幀數),
         因此真正要表達的時間就會是 time / timeScale 才會是秒.
         */

        // 統計時長
        CMTime duration = blockPlayer.currentItem.duration;
        CGFloat durationSec = CMTimeGetSeconds(duration);
        NSLog(@"總時長:%f",durationSec);

        CMTime current = blockPlayer.currentItem.currentTime;
        CGFloat currentSec = CMTimeGetSeconds(current);
        NSLog(@"當前時間:%f",currentSec);

        //計算緩存時間
        NSTimeInterval timeInterval = [blockSelf availabelDuration];
        NSLog(@"time interval :%f",timeInterval);

        // 刷新播放進度
        [blockSelf.progress setValue:currentSec/durationSec animated:YES];

        // 刷新下載進度
        [blockSelf.progressView setProgress:timeInterval/durationSec animated:YES];

    }];

    // 限定時間監聽,可以設置具體的回調時間
//    NSValue *value = [NSValue valueWithCMTime:CMTimeMake(10, 2)];
    NSValue *value = [NSValue valueWithCMTime:CMTimeMakeWithSeconds(5, 24)];
    [_avPlayer addBoundaryTimeObserverForTimes:@[value] queue:dispatch_get_main_queue() usingBlock:^{
        NSLog(@“第5秒。。。。。");

    }];

    {// UI設置
        self.volume.value = _avPlayer.volume;

        self.progress.value = 0;
        self.progressView.progress = 0;
        [self.play setTitle:@"pause" forState:UIControlStateNormal];
    }
}

// 播放結束後執行該方法
- (void)playDidEnd {

    NSLog(@"play finished");

    // 播放下一首
    AVPlayerItem *currentItem = [[AVPlayerItem alloc] initWithURL:_playList[1]];
    [_avPlayer replaceCurrentItemWithPlayerItem:currentItem];
    [_avPlayer play];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {

    if ([keyPath isEqualToString:@"status"]) {

        /* AVPlayerItemStatus
         AVPlayerItemStatusUnknown,
         AVPlayerItemStatusReadyToPlay,
         AVPlayerItemStatusFailed
         */
        if (_avPlayer.status == AVPlayerStatusReadyToPlay) {

            [self.play setTitle:@"pause" forState:UIControlStateNormal];
        }
    }
}
- (IBAction)volume:(UISlider *)sender {

    _avPlayer.volume = sender.value;
}
- (IBAction)progress:(UISlider *)sender {

    // 總時長
    float duration = _avPlayer.currentItem.duration.value/_avPlayer.currentItem.duration.timescale;
    float currentTime = duration * sender.value;
    [_avPlayer seekToTime:CMTimeMake(currentTime, 1)];

}
static bool playing = YES;
- (IBAction)playOrPause:(UIButton *)sender {

    if (playing) {
        [_avPlayer pause]; //暫停播放
        playing = NO;
        [sender setTitle:@"play" forState:UIControlStateNormal];
    }else {

        [_avPlayer play]; //暫停播放
        playing = YES;
        [sender setTitle:@"pause" forState:UIControlStateNormal];
    }
}

#pragma mark 計算網絡音頻文件緩衝
- (NSTimeInterval)availabelDuration {

    NSArray *loadedTimeRanges = [[_avPlayer currentItem] loadedTimeRanges];
    CMTimeRange timeRange = [loadedTimeRanges.firstObject CMTimeRangeValue];// 獲取緩存區域
    /* typedef struct
      {
         CMTime start;
         CMTime duration;
      } CMTimeRange;
     */
    float startSeconds = CMTimeGetSeconds(timeRange.start);
    float durationSeconds = CMTimeGetSeconds(timeRange.duration);

    // 計算緩存事件

    NSTimeInterval result = startSeconds + durationSeconds;
    return result;
}

@end

事實上,AVPlayer一般用來完成視頻播放,上面的代碼稍作修改後可以用來完成視頻的播放,具體的操作我們在後面講到視頻播放時再具體說明。


4. 音頻隊列

上面我們使用了AVPlayer完成網絡音頻的播放,除此之外,我們也可以使用AudioToolbox框架中的音頻隊列服務Audio Queue Services。
使用音頻隊列服務完全可以做到音頻播放以及音頻的錄製,首先看一下錄音音頻服務隊列:
這裏寫圖片描述

 音頻錄製的音頻服務隊列Audio Queue有三部分組成:
 1. 三個緩衝器Buffers:每個緩衝器都是一個存儲音頻數據的臨時倉庫。
 2. 一個緩衝隊列Buffer Queue:一個包含音頻緩衝器的有序隊列。
 3. 一個回調Callback:一個自定義的隊列回調函數。

聲音通過輸入設備進入緩衝隊列中,首先填充第一個緩衝器;當第一個緩衝器填充滿之後自動填充下一個緩衝器,同時會調用回調函數;在回調函數中需要將緩衝器中的音頻數據寫入磁盤,同時將緩衝器放回到緩衝隊列中以便重用。下面是Apple官方關於音頻隊列服務的流程示意圖:
這裏寫圖片描述

類似的,看一下音頻播放緩衝隊列,其組成部分和錄音緩衝隊列類似。
這裏寫圖片描述

但是在音頻播放緩衝隊列中,回調函數調用的時機不同於音頻錄製緩衝隊列,流程剛好相反。將音頻讀取到緩衝器中,一旦一個緩衝器填充滿之後就放到緩衝隊列中,然後繼續填充其他緩衝器;當開始播放時,則從第一個緩衝器中讀取音頻進行播放;一旦播放完之後就會觸發回調函數,開始播放下一個緩衝器中的音頻,同時填充第一個緩衝器放;填充滿之後再次放回到緩衝隊列。下面是詳細的流程:
這裏寫圖片描述

當然,要明白音頻隊列服務的原理並不難,問題是如何實現這個自定義的回調函數,這其中我們有大量的工作要做,控制播放狀態、處理異常中斷、進行音頻編碼等等。由於牽扯內容過多,本文不對音頻隊列的使用做更多的介紹,目前有很多第三方優秀框架可以直接使用,例如AudioStreamer、FreeStreamer。由於前者當前只有非ARC版本,所以下面使用FreeStreamer來簡單演示在線音頻播放的過程,當然在使用之前要做如下準備工作:

  1. 拷貝FreeStreamer中的Reachability.h、Reachability.m和Common、astreamer兩個文件夾中的內容到項目中。
  2. 添加FreeStreamer使用的類庫:CFNetwork.framework、AudioToolbox.framework、AVFoundation.framework、libxml2.dylib、MediaPlayer.framework。
  3. 如果引用libxml2.dylib編譯不通過,需要在Xcode的Targets-Build Settings-Header Search Path中添加$(SDKROOT)/usr/include/libxml2。
  4. 導入頭文件 FSAudioStream.h

示例代碼:

#import "ViewController.h"
#import "FSAudioStream.h"

@interface ViewController ()

@property (nonatomic,strong)FSAudioStream *audioStream;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];


    [self.audioStream play];
}
//加載本地音頻
- (NSURL *)getFileURL {

    NSString *filePath = [[NSBundle mainBundle] pathForResource:@"東京不太熱" ofType:@"mp3"];

    NSURL *fileURL = [NSURL fileURLWithPath:filePath];

    return fileURL;
}
// 加載網絡音頻
- (NSURL *)getHttpURL {
//    NSString *str = @"http://www.300.la/filestores/2016/02/20/692de68936d75eea6da4a75f87f3ab2f.mp3";

    NSString *str = @"http://www.300.la/filestores/2014/08/20/93fde37c2aeae22c61a9c7b70b247f92.mp3";
    NSURL *url = [NSURL URLWithString:str];

    return url;
}

- (FSAudioStream *)audioStream{

    if (!_audioStream) {

//        NSURL *URL = [self getFileURL];
        NSURL *URL = [self getHttpURL];

        _audioStream = [[FSAudioStream alloc] initWithUrl:URL];

        //播放失敗block
        _audioStream.onFailure = ^(FSAudioStreamError error, NSString *errorDescription) {

            NSLog(@"播放失敗:error:%d \n str:%@",error,errorDescription);
        };

        //播放完成的回調block
        _audioStream.onCompletion = ^(){

            NSLog(@"已經播放完成");
        };
        [_audioStream setVolume:0.8];// 設置聲音
    }
    return _audioStream;
}

@end

其實FreeStreamer的功能很強大,不僅僅是播放本地、網絡音頻那麼簡單,它還支持播放列表、檢查包內容、RSS訂閱、播放中斷等很多強大的功能,甚至還包含了一個音頻分析器,有興趣的朋友可以訪問官網查看詳細用法


拓展:播放音樂庫中的音樂

衆所周知音樂是iOS的重要組成播放,無論是iPod、iTouch、iPhone還是iPad都可以在iTunes購買音樂或添加本地音樂到音樂庫中同步到你的iOS設備。在MediaPlayer.frameowork中有一個MPMusicPlayerController用於播放音樂庫中的音樂。

下面先來看一下MPMusicPlayerController的常用屬性和方法:

屬性 說明
@property (nonatomic, readonly) MPMusicPlaybackState playbackState 播放器狀態,枚舉類型: MPMusicPlaybackStateStopped:停止播放 MPMusicPlaybackStatePlaying:正在播放MPMusicPlaybackStatePaused:暫停播放MPMusicPlaybackStateInterrupted:播放中斷MPMusicPlaybackStateSeekingForward:向前查找MPMusicPlaybackStateSeekingBackward:向後查找
@property (nonatomic) MPMusicRepeatMode repeatMode 重複模式,枚舉類型:MPMusicRepeatModeDefault:默認模式,使用用戶的首選項(系統音樂程序設置)MPMusicRepeatModeNone:不重複MPMusicRepeatModeOne:單曲循環MPMusicRepeatModeAll:在當前列表內循環
@property (nonatomic) MPMusicShuffleMode shuffleMode 隨機播放模式,枚舉類型:MPMusicShuffleModeDefault:默認模式,使用用戶首選項(系統音樂程序設置)MPMusicShuffleModeOff:不隨機播放MPMusicShuffleModeSongs:按歌曲隨機播放MPMusicShuffleModeAlbums:按專輯隨機播放
@property (nonatomic, copy) MPMediaItem *nowPlayingItem 正在播放的音樂項
@property (nonatomic, readonly) NSUInteger indexOfNowPlayingItem 當前正在播放的音樂在播放隊列中的索引
@property(nonatomic, readonly) BOOL isPreparedToPlay 是否準好播放準備
@property(nonatomic) NSTimeInterval currentPlaybackTime 當前已播放時間,單位:秒
@property(nonatomic) float currentPlaybackRate 當前播放速度,是一個播放速度倍率,0表示暫停播放,1代表正常速度
類方法 說明
+(MPMusicPlayerController *)applicationMusicPlayer; 獲取應用播放器,注意此類播放器無法在後臺播放
+(MPMusicPlayerController *)systemMusicPlayer 獲取系統播放器,支持後臺播放
對象方法 說明
-(void)setQueueWithQuery:(MPMediaQuery *)query 使用媒體隊列設置播放源媒體隊列
-(void)setQueueWithItemCollection:(MPMediaItemCollection *)itemCollection 使用媒體項集合設置播放源媒體隊列
-(void)skipToNextItem 下一曲
-(void)skipToBeginning 從起始位置播放
-(void)skipToPreviousItem 上一曲
-(void)beginGeneratingPlaybackNotifications 開啓播放通知,注意不同於其他播放器,MPMusicPlayerController要想獲得通知必須首先開啓,默認情況無法獲得通知
-(void)endGeneratingPlaybackNotifications 關閉播放通知
-(void)prepareToPlay 做好播放準備(加載音頻到緩衝區),在使用play方法播放時如果沒有做好準備回自動調用該方法
-(void)play 開始播放
-(void)pause 暫停播放
-(void)stop 停止播放
-(void)beginSeekingForward 開始向前查找(快進)
-(void)beginSeekingBackward 開始向後查找(快退)
-(void)endSeeking 結束查找
通知 說明(注意:要想獲得MPMusicPlayerController通知必須首先調用beginGeneratingPlaybackNotifications開啓通知)
MPMusicPlayerControllerPlaybackStateDidChangeNotification 播放狀態改變
MPMusicPlayerControllerNowPlayingItemDidChangeNotification 當前播放音頻改變
MPMusicPlayerControllerVolumeDidChangeNotification}聲音大小改變
MPMediaPlaybackIsPreparedToPlayDidChangeNotification 準備好播放

- MPMusicPlayerController有兩種播放器:applicationMusicPlayer和systemMusicPlayer,前者在應用退出後音樂播放會自動停止,後者在應用停止後不會退出播放狀態。
- 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;

有了這些方法,就可以很容易獲到歌曲、播放列表、專輯媒體等媒體隊列了,這樣就可以通過setQueueWithQuery:query方法設置音樂來源了。又或者得到MPMediaQueue之後創建MPMediaItemCollection,使用setQueueWithItemCollection:itemCollection設置音樂來源。

有時候可能希望用戶自己來選擇要播放的音樂,這時可以使用MPMediaPickerController,它是一個視圖控制器,類似於UIImagePickerController,選擇完播放來源後可以在其代理方法中獲得MPMediaItemCollection對象。

無論是通過哪種方式獲得MPMusicPlayerController的媒體源,可能都希望將每個媒體的信息顯示出來,這時候可以通過MPMediaItem對象獲得。一個MPMediaItem代表一個媒體文件,通過它可以訪問媒體標題、專輯名稱、專輯封面、音樂時長等等。無論是MPMediaQueue還是MPMediaItemCollection都有一個items屬性,它是MPMediaItem數組,通過這個屬性可以獲得MPMediaItem對象。

下面就簡單看一下MPMusicPlayerController的使用,在下面的例子中簡單演示了音樂的選擇、播放、暫停、通知、下一曲、上一曲功能,相信有了上面的概念,代碼讀起來並不複雜(示例中是直接通過MPMeidaPicker進行音樂選擇的,但是仍然提供了兩個方法getLocalMediaQuery和getLocalMediaItemCollection來演示如何直接通過MPMediaQueue獲得媒體隊列或媒體集合):

示例代碼:

#import "ViewController.h"
#import <MediaPlayer/MediaPlayer.h>

@interface ViewController ()<MPMediaPickerControllerDelegate>

@property (nonatomic,strong) MPMediaPickerController *mediaPicker; //媒體選擇控制器
@property (nonatomic,strong) MPMusicPlayerController *musicPlayer; //音樂播放器

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    /*
     1. 集成 <MediaPlayer/MediaPlayer.h>的頭文件
     2. 初始化媒體選擇控制器和音樂播放器

     */

}

// 懶加載媒體選擇器
- (MPMediaPickerController *)mediaPicker {

    //初始化媒體選擇器,這裏需要設置媒體類型爲音樂,也可以選擇視頻、廣播等
    if (!_mediaPicker) {

        _mediaPicker=[[MPMediaPickerController alloc]initWithMediaTypes:MPMediaTypeMusic];

//        _mediaPicker=[[MPMediaPickerController alloc]initWithMediaTypes:MPMediaTypeAny];
        _mediaPicker.allowsPickingMultipleItems=YES;//允許多選
//        _mediaPicker.showsCloudItems=YES;
        // 顯示icloud選項
        _mediaPicker.prompt=@"請選擇要播放的音樂";
        _mediaPicker.delegate=self;// 設置選擇器代理

    }

    return _mediaPicker;
}

// 懶加載音樂播放器
- (MPMusicPlayerController *)musicPlayer {

    if (!_musicPlayer) {

        _musicPlayer = [MPMusicPlayerController systemMusicPlayer];// 在應用停止後不會退出播放狀態。
        _musicPlayer = [MPMusicPlayerController applicationMusicPlayer];// 在應用退出後音樂播放會自動停止

        //開啓通知,否則監控不到MPMusicPlayerController的通知
        [_musicPlayer beginGeneratingPlaybackNotifications];

        [self addNotification];//添加通知

        //如果不使用MPMediaPickerController可以使用如下方法獲得音樂庫媒體隊列
        //[_musicPlayer setQueueWithItemCollection:[self getLocalMediaItemCollection]];
    }

    return _musicPlayer;

}

#pragma mark - 通知
// 添加通知
- (void)addNotification {

    // 播放裝填改變的通知
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playbackStateChange:) name:MPMusicPlayerControllerPlaybackStateDidChangeNotification object:self.musicPlayer];
}

// 播放狀態改變時調用該方法
- (void)playbackStateChange:(NSNotification *)notification {

    switch (self.musicPlayer.playbackState) {
        case MPMusicPlaybackStatePlaying:

            NSLog(@"正在播放...");
        break;
        case MPMusicPlaybackStatePaused:

            NSLog(@"播放暫停.");
        break;
        case MPMusicPlaybackStateStopped:

            NSLog(@"播放停止.");
        break;
        default:
        break;
    }
}
-(void)dealloc{

    [self.musicPlayer endGeneratingPlaybackNotifications];
}

// 取得媒體隊列
- (MPMediaQuery *)getLocalMediaQuery {

    MPMediaQuery *mediaQueue=[MPMediaQuery songsQuery];
    for (MPMediaItem *item in mediaQueue.items) {
        NSLog(@"標題:%@,%@",item.title,item.albumTitle);
    }
    return mediaQueue;
}

// 獲取媒體集合
- (MPMediaItemCollection *)getLocalMediaItemCollection {

    MPMediaQuery *mediaQueue = [MPMediaQuery songsQuery];
    NSMutableArray *items = [NSMutableArray array];

    for (MPMediaItem *item in mediaQueue.items) {

        [items addObject:item];
        NSLog(@"標題:%@,%@",item.title,item.albumTitle);
    }

    MPMediaItemCollection *mediaItemCollection=[[MPMediaItemCollection alloc]initWithItems:items];

    return mediaItemCollection;
}

#pragma mark - MPMediaPickerControllerDelegate
//選擇完成
- (void)mediaPicker:(MPMediaPickerController *)mediaPicker didPickMediaItems:(MPMediaItemCollection *)mediaItemCollection {
    MPMediaItem *mediaItem=[mediaItemCollection.items firstObject];//第一個播放音樂
    //注意很多音樂信息如標題、專輯、表演者、封面、時長等信息都可以通過MPMediaItem的valueForKey:方法得到,但是從iOS7開始都有對應的屬性可以直接訪問
//    NSString *title= [mediaItem valueForKey:MPMediaItemPropertyAlbumTitle];
//    NSString *artist= [mediaItem valueForKey:MPMediaItemPropertyAlbumArtist];
//    MPMediaItemArtwork *artwork= [mediaItem valueForKey:MPMediaItemPropertyArtwork];
//    UIImage *image=[artwork imageWithSize:CGSizeMake(100, 100)];
    //專輯圖片
    NSLog(@"標題:%@,表演者:%@,專輯:%@",mediaItem.title ,mediaItem.artist,mediaItem.albumTitle); [self.musicPlayer setQueueWithItemCollection:mediaItemCollection];

    [self dismissViewControllerAnimated:YES completion:nil];
}

//取消選擇
- (void)mediaPickerDidCancel:(MPMediaPickerController *)mediaPicker {
    [self dismissViewControllerAnimated:YES completion:nil];
}

#pragma mark - UI事件
- (IBAction)selectClick:(UIButton *)sender {

    [self presentViewController:self.mediaPicker animated:YES completion:nil];
}

- (IBAction)playClick:(UIButton *)sender {

    [self.musicPlayer play];
}

- (IBAction)puaseClick:(UIButton *)sender {

    [self.musicPlayer pause];
}

- (IBAction)stopClick:(UIButton *)sender {

    [self.musicPlayer stop];
}

- (IBAction)nextClick:(UIButton *)sender {

    [self.musicPlayer skipToNextItem];
}

- (IBAction)prevClick:(UIButton *)sender {

    [self.musicPlayer skipToPreviousItem];
}

@end

小結

系統提供的音頻播放形式主要有:AudioToolbox、AVAudioPlayer、AVPlayer、音頻隊列對比他們的優缺點:

  • AudioToolbox:主要播放比較小且需要頻繁播放的系統聲音。
  • AVAudioPlayer:使用簡單方便,但只能播放本地音頻,不支持流媒體播放
  • AVPlayer:iOS4.0以後,可以使用AVPlayer播放本地音頻和支持流媒體播放,但提供接口較少,處理音頻不夠靈活,當然AVPlayer主要是用來播放視頻的。
  • 音頻隊列:主要處理流媒體播放,提供了強大且靈活的API接口(C函數的接口),但處理起來也較爲複雜,需要自己去調用接口封裝出需要的播放服務,我們介紹了幾個開源框架來實現音頻隊列的服務。
發佈了20 篇原創文章 · 獲贊 1 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章