1. 程式人生 > >【iOS】音訊播放之AVAudioPlayer,AVPlayer,AVQueuePlayer

【iOS】音訊播放之AVAudioPlayer,AVPlayer,AVQueuePlayer

前言

在婚語APP中,分別使用了AVAudioPlayer,AVPlayer,AVQueuePlayer來實現音訊播放功能,下面以婚語的實際需求分別介紹它們的使用方法和區別。

需求1 檔期備忘:使用者新建檔期記錄時,可以進行錄音備忘,錄音完成後可直接播放,儲存檔期時將錄音檔案上傳到伺服器。

分析1:因為錄音備忘一般時長較短檔案較小,所以錄音完將錄音檔案上傳到伺服器的同時,本地也保留錄音檔案,使用者檢視檔期並點選播放語音備忘時,先讀取本地錄音檔案,找不到時再到伺服器下載儲存到本地,然後再使用AVAudioPlayer實現本地音訊播放。

需求2 婚禮音樂播放:如圖,使用者可以線上試聽一些婚禮現場使用的背景音樂,只能選中某一首背景音樂進行播放,播放完成後停止,不能自動播放下一首。

婚語音樂

分析2:因為試聽的音樂是在伺服器上,而AVAudioPlayer只能播放本地音樂檔案,所以需要使用支援線上音樂的AVPlayer進行播放。

需求3 婚語開場白:如圖,在需求2的前提下,支援列表自動播放,類似於網易音樂。同時支援後臺播放、鎖屏歌曲資訊顯示和控制、耳機控制等

婚語開場白

分析3:此時可以使用AVPlayer的子類AVQueuePlayer進行列表播放。AVAudioPlayer,AVPlayer,AVQueuePlayer都支援後臺播放、鎖屏資訊、耳機控制等

一、 AVAudioPlayer

可以通過音訊的NSData或者本地音訊檔案的url,來建立一個AVAudioPlayer例項,如載入本地的music.mp3的音訊檔案:

NSURL *fileUrl = [[NSBundle mainBundle] URLForResource:@"music" withExtension:@"mp3"];
self.player = [[AVAudioPlayer alloc] initWithContentsOfURL:fileUrl error:nil];

if (self.player) {
    [self.player prepareToPlay];
}

載入音訊檔案後,可以呼叫prepareToPlay方法,這樣可以提前獲取需要的硬體支援,並載入音訊到緩衝區。在呼叫play方法時,減少開始播放的延遲。

當呼叫play方法後,開始播放音樂:

[self.player play];

可以呼叫pause或stop來暫停播放,這裡的stop方法的效果也只是暫停播放,不同之處是stop會撤銷prepareToPlay方法所做的準備。

[self.player stop];

另外,我們可以進行更多的操作:

單獨設定音樂的音量(預設1.0,可設定範圍為0.0至1.0,兩個極端為靜音、系統音量):

self.player.volume = 0.5;

修改左右聲道的平衡(預設0.0,可設定範圍為-1.0至1.0,兩個極端分別為只有左聲道、只有右聲道):

self.player.pan = -1;

設定播放速度(預設1.0,可設定範圍為0.5至2.0,兩個極端分別為一半速度、兩倍速度):

self.player.rate = 0.5;

設定迴圈播放(預設1,若設定值大於0,則為相應的迴圈次數,設定為-1可以實現無限迴圈):

self.player.numberOfLoops = -1;

二、 AVPlayer

AVPlayer支援播放本地、分步下載、或線上流媒體音視訊,不僅可以播放音訊,配合AVPlayerLayer類可實現視訊播放。另外支援播放進度監聽。

1.AVPlayer需要通過AVPlayerItem來關聯需要播放的媒體。

#import <AVFoundation/AVFoundation.h>
AVPlayerItem *item = [[AVPlayerItem alloc] initWithURL:[NSURL URLWithString:urlStr]];
AVPlayer *player = [[AVPlayer alloc] initWithPlayerItem:item];

2.在準備播放前,通過KVO新增播放狀態改變監聽

[self.player.currentItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];

處理KVO回撥事件:

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    if ([keyPath isEqualToString:@"status"]) {
        switch (self.player.status) {
            case AVPlayerStatusUnknown:
            {
                NSLog(@"未知轉態");
            }
                break;
            case AVPlayerStatusReadyToPlay:
            {
                NSLog(@"準備播放");
            }
                break;
            case AVPlayerStatusFailed:
            {
                NSLog(@"載入失敗");
            }
                break;

            default:
                break;
        }

    }
}

3.KVO監聽音樂緩衝狀態:

[self.player.currentItem addObserver:self
                          forKeyPath:@"loadedTimeRanges"
                             options:NSKeyValueObservingOptionNew
                             context:nil];



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

{
    //...

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

        NSArray * timeRanges = self.player.currentItem.loadedTimeRanges;
        //本次緩衝的時間範圍
        CMTimeRange timeRange = [timeRanges.firstObject CMTimeRangeValue];
        //緩衝總長度
        NSTimeInterval totalLoadTime = CMTimeGetSeconds(timeRange.start) + CMTimeGetSeconds(timeRange.duration);
        //音樂的總時間
        NSTimeInterval duration = CMTimeGetSeconds(self.player.currentItem.duration);
        //計算緩衝百分比例
        NSTimeInterval scale = totalLoadTime/duration;
        //更新緩衝進度條
        //        self.loadTimeProgress.progress = scale;
    }
}

4.開始播放後,通過KVO新增播放結束事件監聽

[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(playFinished:)
                                             name:AVPlayerItemDidPlayToEndTimeNotification
                                           object:_player.currentItem];

5.開始播放時,通過AVPlayer的方法監聽播放進度,並更新進度條(定期監聽的方法):

__weak typeof(self) weakSelf = self;
[self.player addPeriodicTimeObserverForInterval:CMTimeMake(1.0, 1.0) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
    //當前播放的時間
    float current = CMTimeGetSeconds(time);
    //總時間
    float total = CMTimeGetSeconds(item.duration);
    if (current) {
        float progress = current / total;
        //更新播放進度條
        weakSelf.playSlider.value = progress;
    }
}];

6.使用者拖動進度條,修改播放進度

- (void)playSliderValueChange:(UISlider *)sender
{
    //根據值計算時間
    float time = sender.value * CMTimeGetSeconds(self.player.currentItem.duration);
    //跳轉到當前指定時間
    [self.player seekToTime:CMTimeMake(time, 1)];
}

三、 AVQueuePlayer

AVPlayer只支援單個媒體資源的播放,我們可以使用AVPlayer的子類AVQueuePlayer實現列表播放。在AVPlayer的基礎上,增加以下方法:

//通過給定的AVPlayerItem陣列建立一個AVQueuePlayer例項
+ (instancetype)queuePlayerWithItems:(NSArray<AVPlayerItem *> *)items;

//通過給定的AVPlayerItem陣列初始化AVQueuePlayer例項
- (AVQueuePlayer *)initWithItems:(NSArray<AVPlayerItem *> *)items;

//獲得當前的播放佇列陣列
- (NSArray<AVPlayerItem *> *)items;

//停止播放當前音樂,並播放佇列中的下一首
- (void)advanceToNextItem;

//往播放佇列中插入新的AVPlayerItem
- (void)insertItem:(AVPlayerItem *)item afterItem:(nullable AVPlayerItem *)afterItem;

//從播放佇列中移除指定AVPlayerItem
- (void)removeItem:(AVPlayerItem *)item;

//清空播放佇列
- (void)removeAllItems;

*官方API中沒找到播放上一首的方法,所以其實直接用AVPlayer做列表播放也是可以的,自己維護一個播放列表陣列,監聽使用者點選上一首和下一首按鈕,並監聽播放結束事件,呼叫 AVPlayer 例項的replaceCurrentItemWithPlayerItem:方法傳入播放列表中的上一首或下一首就行。

四、 後臺播放

首先在AppDelegate.m的- (BOOL)application:didFinishLaunchingWithOptions:方法中新增程式碼:

AVAudioSession *session = [AVAudioSession sharedInstance];
[session setCategory:AVAudioSessionCategoryPlayback error:nil];
[session setActive:YES error:nil];

然後在Info.plist檔案中新增:

<key>UIBackgroundModes</key>
<array>
    <string>audio</string>
</array>

五、 鎖屏資訊

在每次準備播放下一首時,更新鎖屏資訊:

MPNowPlayingInfoCenter *infoCenter = [MPNowPlayingInfoCenter defaultCenter];
    MPMediaItemArtwork *artwork = [[MPMediaItemArtwork alloc] initWithImage:[UIImage imageNamed:@"封面圖片"]];
    infoCenter.nowPlayingInfo = @{
                                  MPMediaItemPropertyTitle :@"歌曲名",
                                  MPMediaItemPropertyArtist :@"歌手名",
                                  MPMediaItemPropertyPlaybackDuration :歌曲時間長度,
                                  MPNowPlayingInfoPropertyElapsedPlaybackTime : @(已播放時間長度),
                                  MPMediaItemPropertyArtwork : artwork
                                  };

六、 通過耳機、鎖屏介面控制

// 在需要處理遠端控制事件的具體控制器或其它類中呼叫下面這個方法
#import <MediaPlayer/MPRemoteCommandCenter.h>
#import <MediaPlayer/MPRemoteCommand.h>
- (void)remoteControlEventHandler
{
    // 直接使用sharedCommandCenter來獲取MPRemoteCommandCenter的shared例項
    MPRemoteCommandCenter *commandCenter = [MPRemoteCommandCenter sharedCommandCenter];

// 啟用播放命令 (鎖屏介面和上拉快捷功能選單處的播放按鈕觸發的命令)
commandCenter.playCommand.enabled = YES;
// 為播放命令新增響應事件, 在點選後觸發
[commandCenter.playCommand addTarget:self action:@selector(playAction:)];

// 播放, 暫停, 上下曲的命令預設都是啟用狀態, 即enabled預設為YES
// 為暫停, 上一曲, 下一曲分別新增對應的響應事件
[commandCenter.pauseCommand addTarget:self action:@selector(pauseAction:)];
[commandCenter.previousTrackCommand addTarget:self action:@selector(previousTrackAction:)];
[commandCenter.nextTrackCommand addTarget:self action:@selector(nextTrackAction:)];

// 啟用耳機的播放/暫停命令 (耳機上的播放按鈕觸發的命令)
commandCenter.togglePlayPauseCommand.enabled = YES;
// 為耳機的按鈕操作新增相關的響應事件
[commandCenter.togglePlayPauseCommand addTarget:self action:@selector(playOrPauseAction:)];
}

-(void)playAction:(id)obj{
    [[HYPlayerTool sharePlayerTool] play];
}
-(void)pauseAction:(id)obj{
    [[HYPlayerTool sharePlayerTool] pause];
}
-(void)nextTrackAction:(id)obj{
    [[HYPlayerTool sharePlayerTool] playNext];
}
-(void)previousTrackAction:(id)obj{
    [[HYPlayerTool sharePlayerTool] playPre];
}
-(void)playOrPauseAction:(id)obj{
    if ([[HYPlayerTool sharePlayerTool] isPlaying]) {
        [[HYPlayerTool sharePlayerTool] pause];
    }else{
        [[HYPlayerTool sharePlayerTool] play];
    }
}

程式碼demo:https://github.com/dolacmeng/AVAudioDemo