iOS 播放器类详解

4,369 阅读12分钟

AVAudioSession


AVAudioSession

音频会话(AVAudioSession) 用来管理多个 APP 对音频硬件设备资源(麦克风,扬声器)的使用。

配置音频会话

// 设置后台播放功能,并调用 setActive 将会话激活才能起作用
AVAudioSession *session = [AVAudioSession sharedInstance];
[session setCategory:AVAudioSessionCategoryPlayback error:nil];
[session setActive:YES error:nil];
音频会话类型
  • AVAudioSessionCategoryAmbient 混间播放,应用会随着静音键和屏幕关闭而静音。并且不会中止其它应用播放声音,可以和其它自带应用如 iPod,safari 等同时播放声音。注意:该 Category 无法在后台播放声音。
  • AVAudioSessionCategorySoloAmbient 独占播放,应用会随着静音键和屏幕关闭而静音。不可以与其他声音混合播放,会中断其他声音。
  • AVAudioSessionCategoryPlayback 后台独占播放,当手机设置为静音或进入后台时会继续播放,如果让使声音在后台继续播放时,必须在 plist 文件里面添加 UIBackgroundModes 属性。默认情况下,使用这一类别意味着你的应用程序的音频是不可混合激活的,你的会话将中断任何其他非混合的音频会话。
  • AVAudioSessionCategoryRecord 录音模式,用于需要录音的应用,设置该 Category 后,除了来电铃声,闹钟或日历提醒之外的其它系统声音都不会被播放。该 Category 只提供单纯录音功能。如果让使声音在后台继续播放时,必须在 plist 文件里面添加 UIBackgroundModes 属性。
  • AVAudioSessionCategoryPlayAndRecord 播放和录音模式,允许播放时同时录制音频,比如 VoIP (Voice over Internet Protocol) 应用,通话时既播放音频也录制音频。除此之外同 AVAudioSessionCategoryPlayback 类似。支持 AirPlay 。但是如果设置 AVAudioSessionModeVoiceChat 属性, AirPlay 是不可用的。
  • AVAudioSessionCategoryMultiRoute 多线路模式,支持音频播放和录制。允许多条音频流的同步输入和输出。比如:USB 和耳机同时音频输出。
音频会话模式
模式 兼容的 Category 场景
AVAudioSessionModeDefault All 默认模式
AVAudioSessionModeVoiceChat AVAudioSessionCategoryPlayAndRecord VoIP
AVAudioSessionModeVideoChat AVAudioSessionCategoryPlayAndRecord 视频通话
AVAudioSessionModeGameChat AVAudioSessionCategoryPlayAndRecord 游戏录制,GKVoiceChat自动设置
AVAudioSessionModeVideoRecording AVAudioSessionCategoryPlayAndRecord AVAudioSessionCategoryRecord 录制视频
AVAudioSessionModeMoviePlayback AVAudioSessionCategoryPlayback 视频播放
AVAudioSessionModeMeasurement AVAudioSessionCategoryPlayAndRecord AVAudioSessionCategoryRecord AVAudioSessionCategoryPlayback 最小系统

下面逐一介绍下每个Mode:

  • AVAudioSessionModeDefault,默认模式,与所有的 Category 兼容
  • AVAudioSessionModeVoiceChat,适用于 VoIP 类型的应用。只能是 AVAudioSessionCategoryPlayAndRecord Category下。在这个模式系统会自动配置 AVAudioSessionCategoryOptionAllowBluetooth 这个选项。系统会自动选择最佳的内置麦克风组合支持语音聊天。
  • AVAudioSessionModeVideoChat,用于视频聊天类型应用,只能是 AVAudioSessionCategoryPlayAndRecord Category下。适在这个模式系统会自动配置 AVAudioSessionCategoryOptionAllowBluetoothAVAudioSessionCategoryOptionDefaultToSpeaker 选项。系统会自动选择最佳的内置麦克风组合支持视频聊天。
  • AVAudioSessionModeGameChat,适用于游戏类应用。使用 GKVoiceChat 对象的应用会自动设置这个模式和 AVAudioSessionCategoryPlayAndRecord Category。实际参数和AVAudioSessionModeVideoChat 一致。
  • AVAudioSessionModeVideoRecording,适用于使用摄像头采集视频的应用。只能是 AVAudioSessionCategoryPlayAndRecordAVAudioSessionCategoryRecord 这两个 Category下。这个模式搭配 AVCaptureSession API 结合来用可以更好地控制音视频的输入输出路径。(例如,设置 automaticallyConfiguresApplicationAudioSession 属性,系统会自动选择最佳输出路径。
  • AVAudioSessionModeMoviePlayback,适用于播放视频的应用。只用于 AVAudioSessionCategoryPlayback 这个Category。
  • AVAudioSessionModeMeasurement,最小化系统。只用于 AVAudioSessionCategoryPlayAndRecordAVAudioSessionCategoryRecordAVAudioSessionCategoryPlayback 这几种 Category。
音频会话选项

我们还可以使用options去微调Category行为,如下表:

Option Option功能说明 兼容的 Category
AVAudioSessionCategoryOptionMixWithOthers 支持和其他APP音频 mix AVAudioSessionCategoryPlayAndRecord AVAudioSessionCategoryPlayback AVAudioSessionCategoryMultiRoute
AVAudioSessionCategoryOptionDuckOthers 系统智能调低其他APP音频音量 AVAudioSessionCategoryPlayAndRecord AVAudioSessionCategoryPlayback AVAudioSessionCategoryMultiRoute
AVAudioSessionCategoryOptionAllowBluetooth 支持蓝牙音频输入 AVAudioSessionCategoryRecord AVAudioSessionCategoryPlayAndRecord
AVAudioSessionCategoryOptionDefaultToSpeaker 设置默认输出音频到扬声器 AVAudioSessionCategoryPlayAndRecord
调优我们的 Category

通过 Category 和合适的 ModeOptions 的搭配我们可以调优出我们的效果,下面举两个应用场景:用过高德地图的都知道,在后台播放 QQ 音乐的时候,如果导航语音出来,QQ 音乐不会停止,而是被智能压低和混音,等导航语音播报完后,QQ 音乐正常播放,这里我们需要后台播放音乐,所以 Category 使用AVAudioSessionCategoryPlayback,需要混音和智能压低其他 APP 音量,所以 Options 选用 AVAudioSessionCategoryOptionMixWithOthersAVAudioSessionCategoryOptionDuckOthers

后台播放

选择 Target 的 Capabilities,在 Background Modes 中选中 Audio, AirPlay, and Picture in Picture。或在 info.plist里面配置:

<key>UIBackgroundModes</key>
<array>
    <string>audio</string>
</array>
音频中断处理

其他 App 或者电话会中断我们的 App 音频,所以相应的我们要做出处理。可以通过监听 AVAudioSessionInterruptionNotification 这个 key 获取音频中断事件。 回调回来 Userinfo 有键值:

  • AVAudioSessionInterruptionTypeKey 取值 AVAudioSessionInterruptionTypeBegan 表示中断开始 取值 AVAudioSessionInterruptionTypeEnded 表示中断结束 中断开始:我们需要做的是保存好播放状态,上下文,更新用户界面等 中断结束:我们要做的是恢复好状态和上下文,更新用户界面,根据需求准备好之后选择是否激活 session。

选择不同的音频播放技术,处理中断方式也有差别,具体如下:

  • System Sound Services:使用 System Sound Services 播放音频,系统会自动处理,不受 App 控制,当中断发生时,音频播放会静音,当中断结束后,音频播放会恢复。
  • AV Foundation frameworkAVAudioPlayer 类和 AVAudioRecorder 类提供了中断开始和结束的 delegate 回调方法来处理中断。中断发生,系统会自动停止播放,需要做的是记录播放时间等状态,更新用户界面,等中断结束后,再次调用播放方法,系统会自动激活 session。
  • Audio Queue Services, I/O audio unit:使用 aduio unit 这些技术需要处理中断,需要做的是记录播放或者录制的位置,中断结束后自己恢复 session。
  • OpenAL:使用 OpenAL 播放时,同样需要自己监听中断。管理 OpenAL上下文,用户中断结束后恢复 session。

AVAudioPlayer


AVAudioPlayer 属于 AVFundation.framework 的类,是一个提供音频播放相关功能的播放器类。除非你要从网络流中播放音频、需要访问原始音频样本或者需要非常低的时延,否则AVAudioPlayer都能胜任。

AVAudioPlayer 构建于 Core Audio 中的 C-based Audio Queue Services 的最顶层。所以他可以提供所有在 Audio Queue Services 中所能找到的核心功能,比如播放、循环、甚至音频计量,但是使用的是非常友好的 OC 接口。

AVAudioPlayer 每次播放都需要将上一个 player 释放掉,然后重新创建 player 来进行播放。

AVAudioPlayer 支持以下音频格式:

  • ACC
  • AMR (Adaptive multi-Rate,一种语音格式)
  • ALAC (Apple lossless Audio Codec)
  • iLBC (internet Low Bitrate Codec,一种语音格式)
  • IMA4 (IMA/ADPCM)
  • linearPCM (uncompressed)
  • u-lawa-law
  • MP3 (MPEG-Laudio Layer 3)

AVAudioPlayer 主要属性:

playing  // 播放器是否正在播放 获取通过isPlaying
numberOfChannels  // 该音频的声道次数,只读
duration  // 该音频播放时长
url  // 音频文件路径, 只读
data  // 音频数据,只读
pan NS_AVAILABLE(10_7, 4_0)  // 立体声设置 设为 -1.0 则左边播放,设为 0.0 则中央播放,设为 1.0 则右边播放
enableRate NS_AVAILABLE(10_8, 5_0);   // 是否允许改变播放速率
rate NS_AVAILABLE(10_8, 5_0);  // 播放速率 0.5 (半速播放) ~ 2.0(倍速播放) 注1.0 是正常速度
currentTime  // 该音频的播放点
deviceCurrentTime  // 输出设备播放音频的时间,注意如果播放中被暂停此时间也会继续累加
numberOfLoops  // 循环次数,如果要单曲循环,设置为负数
volume  // 播放器的音频增益,值:0.0~1.0
channelAssignments  // 获得或设置播放声道

AVAudioPlayer 初始化:

// 利用文件初始化
- (instancetype)initWithContentsOfURL:(NSURL *)url error:(NSError * _Nullable *)outError;
// 利用音频数据来初始化
- (instancetype)initWithData:(NSData *)data error:(NSError * _Nullable *)outError;
// 利用文件初始化
- (instancetype)initWithContentsOfURL:(NSURL *)url fileTypeHint:(NSString *)utiString error:(NSError * _Nullable *)outError;
// 利用音频数据来初始化
- (instancetype)initWithData:(NSData *)data fileTypeHint:(NSString *)utiString error:(NSError * _Nullable *)outError;

AVAudioPlayer 主要方法:

// 准备播放,取得音频文件并预加载 Audio Queue 缓存区,降低调用play方法和听到声音输出之间的延时,在使用play方法时如果没有做好准备会自动调用该方法
- (BOOL)prepareToPlay;
// 异步播放
- (BOOL)play; 
// 在某个时间点播放
- (BOOL)playAtTime:(NSTimeInterval)time NS_AVAILABLE(10_7, 4_0); 
// 暂停播放,但仍然可以播放
- (void)pause; 
// 停止播放,不再准备播放了
- (void)stop; 
// 更新音频测量值,注意如果要更新音频测量值必须设置meteringEnabled为YES,通过音频测量值可以即时获得音频分贝等信息
- (void)updateMeters; 
// 返回给定通道的分贝峰值功率
- (float)peakPowerForChannel:(NSUInteger)channelNumber; 
// 获得指定声道的分贝峰值,注意如果要获得分贝峰值必须在此之前调用updateMeters方法
- (float)averagePowerForChannel:(NSUInteger)channelNumber;

AVAudioPlayer 代理方法:

// 音频播放完成
- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag;
// 音频解码发生错误
- (void)audioPlayerDecodeErrorDidOccur:(AVAudioPlayer *)player error:(NSError *)error;

AVPlayer


AVPlayer 支持播放本地、分步下载、或在线流媒体音视频,不仅可以播放音频,配合 AVPlayerLayer 类可实现视频播放。另外支持播放进度监听。 AVPlayer 只支持单个媒体资源播放,需要配合 AVPlayerItem 关联来进行媒体播放。

AVPlayer 初始化:

// 类方法,从url加载音频
+ (instancetype)playerWithURL:(NSURL *)URL
// 类方法,配合AVPlayerItem使用
+ (instancetype)playerWithPlayerItem:(nullable AVPlayerItem *)item
// 实例方法,从url加载音频
- (instancetype)initWithURL:(NSURL *)URL
// 实例方法,配合AVPlayerItem使用
- (instancetype)initWithPlayerItem:(nullable AVPlayerItem *)item

AVPlayer 添加监听:

【播放状态】

// 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;
        }
    }
}

【缓冲状态】

// KVO监听音乐缓冲状态
[self.player.currentItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];

// 处理KVO回调事件
- (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;
        NSLog(@"---%f,---%f,---%f",totalLoadTime,duration,scale);
        //更新缓冲进度条
        //        self.loadProgress.progress = scale;
    }
}

【播放结束事件的监听】

//播放结束事件的监听
[[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(playFinied:) name:AVPlayerItemDidPlayToEndTimeNotification
                                               object:self.player.currentItem];

//播放结束回调
- (void)playFinied:(AVPlayerItem *)item {
    NSLog(@"播放结束");
    self.slider.value = 0;
}

【监听播放进度】

// 开始播放时,通过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.slider.value = progress;
    }
}];

【拖动进度条改变播放进度】

// 拖动进度条改变播放进度
- (void)playValueChange:(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
- (BOOL)canInsertItem:(AVPlayerItem *)item afterItem:(nullable AVPlayerItem *)afterItem;
// 往播放队列中插入新的 AVPlayerItem
- (void)insertItem:(AVPlayerItem *)item afterItem:(nullable AVPlayerItem *)afterItem;
// 移除指定的 AVPlayerItem
- (void)removeItem:(AVPlayerItem *)item;
// 移除所有播放队列中的AVPlayerItem
- (void)removeAllItems;

官方 API 中没找到播放上一首的方法,所以其实直接用 AVPlayer 做列表播放也是可以的,自己维护一个播放列表数组,监听用户点击上一首和下一首按钮,并监听播放结束事件,调用 AVPlayer 实例的 replaceCurrentItemWithPlayerItem: 方法传入播放列表中的上一首或下一首就行。

锁屏信息

  1. 在播放控制界面接受远程控制
- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:YES];
    // 开始接受远程控制
    [[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
    [self resignFirstResponder];  
}

- (void)viewWillDisappear:(BOOL)animated {
// 接触远程控制
    [[UIApplication sharedApplication] endReceivingRemoteControlEvents];
    [self becomeFirstResponder];
}

// 重写父类成为响应者方法
- (BOOL)canBecomeFirstResponder {
    return YES;
}
  1. 对远程控制事件做出相应的操作
//重写父类方法,接受外部事件的处理
- (void) remoteControlReceivedWithEvent: (UIEvent *) receivedEvent {
    NSLog(@"remote");
    if (receivedEvent.type == UIEventTypeRemoteControl) {
        
        switch (receivedEvent.subtype) { // 得到事件类型
                
            case UIEventSubtypeRemoteControlTogglePlayPause: // 暂停 ios6
                [self.player pause]; // 调用你所在项目的暂停按钮的响应方法 下面的也是如此
                break;
                
            case UIEventSubtypeRemoteControlPreviousTrack:  // 上一首
                
                [self lastMusic:nil];
                break;
                
            case UIEventSubtypeRemoteControlNextTrack: // 下一首
                [self nextMusic:nil];
                break;
                
            case UIEventSubtypeRemoteControlPlay: //播放
                [self playMusic:nil];
                break;
                
            case UIEventSubtypeRemoteControlPause: // 暂停 ios7
                [self playMusic:nil];
                break;
                
            default:
                break;
        }
    }
}
  1. 设置锁屏主题
//锁屏信息
NSMutableDictionary *songInfo = [ [NSMutableDictionary alloc] init];
//锁屏图片
UIImage *img = [UIImage imageNamed:@"iPhoneX"];
if (img) {
    MPMediaItemArtwork *albumArt = [[MPMediaItemArtwork alloc]initWithImage:img];
    [songInfo setObject: albumArt forKey:MPMediaItemPropertyArtwork ];
}
//锁屏标题
NSString *title = @"music";
[songInfo setObject:[NSNumber numberWithFloat:100] forKey:MPMediaItemPropertyPlaybackDuration];
[songInfo setObject:title forKey:MPMediaItemPropertyTitle];
[songInfo setObject:title forKey:MPMediaItemPropertyAlbumTitle];
[[MPNowPlayingInfoCenter defaultCenter] setNowPlayingInfo:songInfo ];

通过耳机、锁屏界面控制

// 直接使用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:)];

如何做输出改变监听(拔出耳机音乐暂停播放)

iOS 6.0 后还可以监听输出改变通知。通俗来说,就是拔出耳机,音乐播放暂停。

// 添加观察者
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(routeChange:) name:AVAudioSessionRouteChangeNotification object:nil];

// 通知方法
- (void)routeChange:(NSNotification*)notification {
    NSDictionary *dic = notification.userInfo;
    int changeReason = [dic[AVAudioSessionRouteChangeReasonKey] intValue];

    // 等于AVAudioSessionRouteChangeReasonOldDeviceUnavailable表示旧输出不可用
    if (changeReason == AVAudioSessionRouteChangeReasonOldDeviceUnavailable) {
        AVAudioSessionRouteDescription *routeDescription = dic[AVAudioSessionRouteChangePreviousRouteKey];
        AVAudioSessionPortDescription *portDescription = [routeDescription.outputs firstObject];
        // 原设备为耳机则暂停
        if ([portDescription.portType isEqualToString:@"Headphones"]) {
            // 这边必须回调到主线程
            dispatch_async(dispatch_get_main_queue(), ^{
                self.playOrPause.selected = NO;
            });

            [self pauseMusic];
        }
    }
}

参考