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

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/dolacmeng/article/details/77430108

前言

在婚语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

猜你喜欢

转载自blog.csdn.net/dolacmeng/article/details/77430108