一、前言
-
这段时间确实忙,好久没更新文章了,实在抱歉!中间换了工作环境,暂时稳定下来了,抽空整理了之前看的资料,具体分析了音频录制以及播放的实现,并封装了一个录音和播放音频的小轮子,欢迎star。
-
至于为什么写这个,因为我确实是对音视频方面比较感兴趣!而且之前也在看这部分的文档,我目的是要逐步研究音视频底层的东西,而
AudioToolbox
、VideoToolbox
就是底层实现了,在此之前,我觉得熟悉上层接口是很有必要的
- 那么本文研究的都是
AVFoundation
、AVKit
、MediaPlayer
这些上层接口类
二、上层接口以及基本用法
注意:此处只针对录音和播放的上层相关核心类,稍作解释,整理了两张图说明,图片尺寸有点大,建议新标签打开或者另存为打开。
录音
-
1、AVAudioRecorder 文档详细介绍了相关API以及使用,通过这个类,可简单实现音频录制、暂停、停止、指定时间录制等,缺点是:没办法设置录音时长,无法监听录音各种状态;当然,本框架中已经实现,可仔细阅读源码
-
2、Audio Queue Services 文档描述也是相当详细了,暂时没仔细研究,本框架中暂时没使用
播放
- 1、System Sound Services 提供C接口实现,可播放本地bundle的短暂音效(30秒以内)和震动效果,会马上播放,不支持多个同时播放,可通过
OSStatus
判断是否操作成功
摘自文档: System Sound Services provides a C interface for playing short sounds and for invoking vibration on iOS devices that support vibration.You can use System Sound Services to play short (30 seconds or shorter) sounds.
用法如下:
// kSystemSoundID_Vibrate 震动 iPod 无效
// kSystemSoundID_UserPreferredAlert 播放用户在系统设置的音效
SystemSoundID soundID;
// 必须是bundle文件
NSURL *soundUrl = [[NSBundle mainBundle] URLForResource:@"sound" withExtension:@"wav"];
// 创建
AudioServicesCreateSystemSoundID((__bridge CFURLRef)soundUrl, &soundID);
// 播放
//AudioServicesPlayAlertSound(soundID);// 提醒音效
AudioServicesPlayAlertSoundWithCompletion(soundID, ^{
});
//AudioServicesPlaySystemSound(soundID);// 普通系统声音
//AudioServicesPlaySystemSoundWithCompletion(soundID, ^{
//});
// 停止
//AudioServicesDisposeSystemSoundID(soundID);
// 播放完毕回调
AudioServicesAddSystemSoundCompletion(soundID, NULL, NULL, (void *)finishPlayCallback, NULL);
/**
回调
@param soundID ID
*/
static void finishPlayCallback(SystemSoundID soundID){
NSLog(@"stop");
}
- 2、AVAudioPlayer,可实现播放本地音频文件或缓存data音频数据、延时播放、循环播放、可同时播放多个音频、可控制播放优先级、播放速度等;支持播放iOS或macOS所支持的所有音频格式,具体API官方文档有详细介绍
用法如下:
NSURL* fileURL = [[NSBundle mainBundle] URLForResource:@"gitKong"withExtension:@"mp3"];
NSError *error = nil;
// 创建
AVAudioPlayer *player = [[AVAudioPlayer alloc] initWithContentsOfURL:fileURL error:&error];
if (error) {
NSLog(@"error:%@",error);
}
// 音频时长
//player.duration
// 循环次数,-1为无限循环
player.numberOfLoops = -1;
// 设置代理,监听播放完成、失败、被打断
player.delegate = self;
// 准备播放、开始、暂停、结束
[player prepareToPlay];
[player play];
[player pause];
[player stop];
- 3、MPMusicPlayerController,在
MediaPlayer
框架中,需要配合MPMediaPickerController
使用,支持列表播放,由于系统封装好,因此定制性不强,MPMusicPlayerController有两种播放器:applicationMusicPlayer和systemMusicPlayer,前者在应用退出后音乐播放会自动停止,后者在应用停止后不会退出播放状态。 具体用法可参考官方文档
用法:可参考:Paddy的一篇博客,页内搜 “扩展–播放音乐库中的音乐”
- 4、Audio Queue Services,支持网络流媒体播放、支持常用格式、支持列表播放、定制性十分强,因为功能都需要自行实现,作者暂时没在此框架中使用,其实在此之前,本人已经翻译了
Audio Queue Services
的Playing Audio
,推荐先看看Audio Queue Services 解读之 Playing Audio(上)、Audio Queue Services 解读之 Playing Audio(下),大家可对比官方文档查阅
- 5、MPMoviePlayerController,注意:这个类在iOS9.0之后就被废弃了,系统建议使用
AVPictureInPictureController
或AVPlayerViewController
代替,系统已经高度封装了,因此定制性不强,但支持播放音频,因此也可以使用此类来进行音频播放,至于view就可以不显示出来
摘自官方文档: The MPMoviePlayerController class is formally deprecated in iOS 9. (The MPMoviePlayerViewController class is also formally deprecated.) To play video content in iOS 9 and later, instead use the AVPictureInPictureController or AVPlayerViewController class from the AVKit framework, or the WKWebView class from WebKit.
用法如下:(代码摘自官方文档)
MPMoviePlayerController *player =
[[MPMoviePlayerController alloc] initWithContentURL: myURL];
[player prepareToPlay];
[player.view setFrame: myView.bounds]; // player's frame must match parent's
[myView addSubview: player.view];
// ...
[player play];
具体范例可参考:Paddy的一篇博客,页内搜 “** MPMoviePlayerController**”
- 6、MPMoviePlayerViewController,和
MPMoviePlayerController
一样,iOS9.0之后就被废弃了,具体用法和MPMoviePlayerController
类似,这里不作过多分析,用到的朋友可先查阅官方文档。
具体范例可参考:Paddy的一篇博客,页内搜 “** MPMoviePlayerViewController**”
- 7、AVPictureInPictureController,看命名就知道,这个类就是专门处理画中画的,目前暂时只支持iPad,在此就不作过多分析,有需求的朋友自行查阅官方文档。
摘自官方文档: An AVPictureInPictureController lets you respond to user-initiated playback of video in a floating, resizable window on iPad.
- 8、AVPlayerViewController,系统针对
AVPlayer
封装的一个视频播放器,当然支持播放音频,支持 .mov、.mp4、.mpv、.3gp 格式,其他格式没一一考究,按理来说,AVPlayerViewController
是对AVPlayer
进行封装的,而AVPlayer
支持多种格式,那么AVPlayerViewController
也应该支持,具体就交给各位去检验一下,有问题@我一下喔,此类具体用法在此也不作过多分析
- 9、AVPlayer,通过上表可以知道,
AVPlayer
功能确实已经足够强大,iOS4.0以上都支持,支持网络流媒体、支持多种常用类型,可在AVMediaFormat.h
通过 类AVURLAsset
调用 audiovisualTypes 方法返回支持类型;stack over flow 有提供答案,可参考。
下面介绍一下相关类:
-
AVAsset :是一个抽象类,不能直接使用,主要用于准确获取多媒体信息,例如媒体总时长duration、音量、播放速度等。
-
AVURLAsset :
AVAsset
的子类,可根据一个本地或网络URL路径创建一个包含多媒体信息的AVURLAsset
对象 -
AVAssetTrack :传输轨道,一般播放视频至少两个轨道,一个播放声音,一个播放画面;而
AVAssetTrack
就是专门管理这些轨道的,可以查看到媒体类型、轨道ID、采样数据的长度等 -
AVPlayerItem :媒体资源管理对象,管理视频或音频的一些基本信息和状态,通过KVO方便监听播放状态、缓冲进度等信息,一个
AVPlayerItem
对应一个视频或音频,可通过NSURL
或AVAsset
创建 -
AVQueuePlayer :
AVPlayer
的子类,通过此类可以方便实现多媒体列表播放、可切换下一条播放
三、需求是什么
- ####音频录制
由于不需要实时录制传输,只要录制到指定本地文件,保证格式可用,而且还没仔细研究
Audio Queue Services
,因此暂时使用AVAudioRecorder
,需要提供什么API?
-
1、一些基本配置参数,例如通道数、采样率、音频格式等
-
2、录音状态,正在录音、暂停录音、停止录音
-
3、录音结束时间,设置录音在某一时间停止
-
4、音频控制接口,准备播放(参数配置后)、开始播放、暂停播放、停止播放
-
5、音频播放状态监听,通过代理监听:录音开始播放、正在录音、结束录音、录音出现错误
-
####音频播放
涉及播放必须支持本地和网络媒体,支持常用音频格式、支持列表播放,而且可定制性必须强,通过上文表格可以发现,只有
Audio Queue Services
和AVPlayer
满足,当然,由于没有仔细研究Audio Queue Services
,因此暂时使用AVPlayer
,那么需要什么API呢?
-
1、根据URL创建播放器,可传入单个URL字符串或者URL字符串数组
-
2、动态添加新的URL地址,可传入单个URL字符串或者URL字符串数组
-
3、播放器当前状态,正在播放、暂停播放、停止播放
-
4、当前播放URL的信息,包括当前播放器音量大小、播放总时长、当前播放到的时间、当前播放URL在播放队列中的下标、播放器播放URL队列数组、剩余播放URL数
-
5、播放器控制接口,开始播放、暂停播放、停止播放、设置播放进度、移动播放指定下标开始播放(实现播放上一条、下一条)
-
6、播放器监听,通过代理监听:播放器开始播放、正在播放、当前缓冲进度、结束播放、播放错误
四、功能封装实现
- ####录音,AVAudioRecorder封装
大概流程:检测授权 - 创建录音器 - 配置基本信息 - 准备录音 - 录音 - 暂停/停止
- 1、检测授权
由于录音需要访问麦克风,需要在 Info.plist
添加 Privacy - Microphone Usage Description
key,但是添加后,系统会首次进入的时候提示,如果拒绝了就什么都不处理,此时需要通过 AVCaptureDevice
手动监听,如果之前同意授权,那么就正常创建录音器,否则提示用户去开启,具体代码如下:
- (void)checkAuth{
switch ([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio]) {
case AVAuthorizationStatusAuthorized:{
[self fl_createAudioRecorder];
break;
}
case AVAuthorizationStatusNotDetermined:{
[AVCaptureDevice requestAccessForMediaType:AVMediaTypeAudio completionHandler:^(BOOL granted) {
if (granted) {
[self fl_createAudioRecorder];
}
else{
[self fl_showAuthTip];
}
}];
break;
}
default:
[self fl_showAuthTip];
break;
}
}
- (void)fl_showAuthTip{
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"温馨提示" message:@"您还没开启授权麦克风,请打开--> 设置 -- > 隐私 --> 通用等权限设置" delegate:nil cancelButtonTitle:@"OK" otherButtonTitles: nil];
[alert show];
}
- 2、创建录音器
在这方法内部,就可以配置默认基本信息(例如音频支持类型、默认通道数、默认采样率等等)、创建高精度定时器、准备录音、监听相关事件(例如应用程序进入后台、前台处理、录音结束、录音编码失败、录音被打断、打断结束等)
- (void)fl_createAudioRecorder{
NSError *error;
AVAudioSession * audioSession = [AVAudioSession sharedInstance];
// 设置类别,表示该应用同时支持播放和录音
[audioSession setCategory:AVAudioSessionCategoryPlayAndRecord error: &error];
// 启动音频会话管理,此时会阻断后台音乐的播放.
[audioSession setActive:YES error: &error];
// 音频使用内置扬声器和麦克风
[audioSession overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:&error];
self.Recorder = [[AVAudioRecorder alloc] initWithURL:[NSURL fileURLWithPath:[self fl_filePath]] settings:[self fl_recorderSetting] error:&error];
if (error) {
[self fl_delegateResponseFailureWithCode:FLAudioRecorderErrorByRecorderIsNotInit];
return;
}
[self fl_createTimer];
self.Recorder.delegate = self;
// 开启音量检测
self.Recorder.meteringEnabled = YES;
[self fl_prepare];
[self fl_addObserver];
}
- 3、高精度定时器创建
由于 AVAudioRecorder
并没有提供类似 AVPlayer
的高精度监听正在播放机制,因此需要手动创建一个监听,内部计算,从而实现定时结束录音,此处使用 GCD
来创建定时器。
为啥不用 NSTimer
,首先 NSTimer
只提供创建和销毁,配合录音的开始、暂停就需要不断创建和销毁,相对来说消耗多点性能,其次回调都是使用 SEL
(iOS 10之前),相对来说,没那么方便;相反,使用 GCD
来创建定时器只需要创建一次,而且很方便地开始(dispatch_resume
)、暂停(dispatch_suspend
)和停止(dispatch_suspend
同时重置定时计数达到停止效果)定时器,和录音配合简直完美
为啥说是高精度,这个其实是相对而言的,一般录音、播放基本单位都是秒,那么此时录音定时则0.01s执行一次,就是说,定时器可获取到小数点后两位,部分具体代码如下:
- (void)fl_createTimer{
// 创建定时器对象
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
// 设置时间间隔
dispatch_source_set_timer(self.timer, DISPATCH_TIME_NOW, 0.01 * NSEC_PER_SEC, 0);
// 定时器回调
__weak typeof(self) weakSelf = self;
dispatch_source_set_event_handler(self.timer, ^{
typeof(self) strongSelf = weakSelf;
if (strongSelf.count >= strongSelf.endTime * 100) {
strongSelf.count = strongSelf.endTime * 100;
[strongSelf fl_stop:nil];
}
else{
FL_DELEGATE_RESPONSE(strongSelf.delegate, @selector(fl_audioRecorder:recordingWithCurrentTime:), @[strongSelf,@(strongSelf.count++ / 100)], nil);
}
});
}
- 4、开始录音、暂停录音、停止录音
这些API都是直接封装 AVAudioRecorder
提供的,只是在此基础上添加一些监听、状态更新、错误处理,这里谈谈代理相应如何处理,其他具体处理可查看源码。
也许大家看上面示例代码会发现有个C函数 FL_DELEGATE_RESPONSE(<#id delegate#>, <#SEL selector#>, <#NSArray<id> *objects#>, <#^(void)complete#>)
,这个是一个私有方法,专门处理代理回调实现,delegate就是要 response 的
target,selector 就是 response 的方法,objects 就是 selector 所需要的参数,complete是 response 后的操作回调。
一般做法是:
if (self.delegate && [self.delegate respondsToSelector:@selector(fl_audioRecorder:recordingWithCurrentTime:)]) {
[self.delegate fl_audioRecorder:self recordingWithCurrentTime:@(strongSelf.count++ / 100)];
}
当然,此时可以利用 performSelector
进行封装,变成如下:(其中 FLSuppressPerformSelectorLeakWarning
宏是用作忽略编译器警告)
if (self.delegate && [self.delegate respondsToSelector:@selector(fl_audioRecorder:recordingWithCurrentTime:)]) {
FLSuppressPerformSelectorLeakWarning(
[self.delegate performSelector:@selector(fl_audioRecorder:recordingWithCurrentTime:) withObject:self withObject:@(strongSelf.count++ / 100)];
);
}
很快就会发现, performSelector
就不适用了,因为某些代理需要传递的参数不止两个,可能是3个、4个或更多,那么此时就有个终极解决办法,就是通过获取方法签名 NSMethodSignature
,然后获取方法实现 NSInvocation
通过设置 target
、 selector
和 argument
,然后调用 invoke
方法去执行,因此参数可以通过数组传入,具体实现代码如下:
id FL_PERFORM_SELECTOR(id target,SEL selector,NSArray <id>* objects){
// 获取方法签名
NSMethodSignature *sig = [target methodSignatureForSelector:selector];
if (sig){
NSInvocation* invo = [NSInvocation invocationWithMethodSignature:sig];
[invo setTarget:target];
[invo setSelector:selector];
for (NSInteger index = 0; index < objects.count; index ++) {
id object = objects[index];
// 参数从下标2开始
[invo setArgument:&object atIndex:index + 2];
}
[invo invoke];
if (sig.methodReturnLength) {
id anObject;
[invo getReturnValue:&anObject];
return anObject;
}
else {
return nil;
}
}
else {
return nil;
}
}
- ####播放,AVPlayer封装
大概流程:创建播放器-设置监听-开始播放-暂停/停止播放
- 1、创建播放器
通过传入URL
字符串 或者 URL
字符串数组,将 URL
添加到内部队列中,并设置监听当前的 URL
,核心代码如下:
- (void)fl_createPlayWithItem:(AVPlayerItem *)item andStartImmediately:(BOOL)startImmediately{
// 销毁之前的
[self fl_removeObserve];
// 初始化播放器
if (!self.player) {
self.player = [AVPlayer playerWithPlayerItem:item];
}
else{
[self.player replaceCurrentItemWithPlayerItem:item];
}
// 监听
[self fl_addObserve];
self.playerStatus = Player_Stoping;
if (startImmediately) {
[self fl_startAtBegining];
}
}
- 2、设置监听
监听开始播放、正在播放(通过 addPeriodicTimeObserverForInterval
监听)、缓冲进度(通过 KVO
监听 AVPlayerItem
的 loadedTimeRanges
属性)、结束播放、播放失败、应用进入前后台、耳机拔插事件,核心代码如下:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
AVPlayerItem *playerItem = object;
if ([keyPath isEqualToString:@"status"]) {
AVPlayerStatus status= [[change objectForKey:@"new"] intValue];
if(status == AVPlayerStatusReadyToPlay){
}
else if(status == AVPlayerStatusUnknown){
[self fl_delegateResponseFailureWithCode:FLAudioPlayerErrorByUnknow];
}
else if (status == AVPlayerStatusFailed){
[self fl_delegateResponseFailureWithCode:FLAudioPlayerErrorByPlayerStatusFailed];
}
}
else if([keyPath isEqualToString:@"loadedTimeRanges"]){
NSArray *array = playerItem.loadedTimeRanges;
//本次缓冲时间范围
CMTimeRange timeRange = [array.firstObject CMTimeRangeValue];
float startSeconds = CMTimeGetSeconds(timeRange.start);
float durationSeconds = CMTimeGetSeconds(timeRange.duration);
//缓冲总长度
NSTimeInterval totalBuffer = startSeconds + durationSeconds;
CGFloat bufferProgress = totalBuffer / self.totalTime.doubleValue;
[self fl_delegateResponseToSelector:@selector(fl_audioPlayer:cacheToCurrentBufferProgress:) withObject:@[self,@(FL_SAVE_PROGRESS(bufferProgress))] complete:nil];
}
else if ([keyPath isEqualToString:@"playbackBufferEmpty"]){
}
else if ([keyPath isEqualToString:@"playbackLikelyToKeepUp"]){
}
}
__weak typeof(self) weakSelf = self;
self.timeObserver = [self.player addPeriodicTimeObserverForInterval:CMTimeMake(1, 1) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
typeof(self) strongSelf = weakSelf;
CGFloat progress = strongSelf.currentTime.doubleValue / strongSelf.totalTime.doubleValue;
[strongSelf fl_delegateResponseToSelector:@selector(fl_audioPlayer:playingToCurrentProgress:withBufferProgress:) withObject:@[strongSelf,@(FL_SAVE_PROGRESS(progress)),strongSelf.bufferProgress] complete:nil];
}];
- 3、播放器管理,开始、暂停、停止、新增播放URL地址、指定播放等
开始、暂停、seek都是针对 AVPlayer
提供的接口进行二次封装,添加一些事件监听、状态更新以及错误处理,停止则是通过 pause
和 seek
配合实现,这里主要谈谈队列播放的实现。
通过上文可以知道,系统提供的 AVQueuePlayer
能实现队列播放,只需要传入一个 AVPlayerItem
数组即可,能实现播放下一条,它是 AVPlayer
的子类,就是说, AVQueuePlayer
只不过是针对 AVPlayer
进行二次封装来实现队列播放而已,而且效果不太如意,通过 advanceToNextItem
方法播放下一条,会移除之前的 item
,那么就没办法实现播放上一条和指定播放任一条的需求,而且监听起来十分麻烦,因此自行实现队列播放
内部主要是通过两个数组实现,valiableItems
是所有添加进去的 AVPlayerItem
数组,而 lastItems
是剩下需要播放的 AVPlayerItem
数组,当前播放的Item永远都是 lastItems
的第一个元素,意味着,通过valiableItems
数组动态修改 lastItems
数组的元素,即可实现播放 valiableItems
数组中的任意一条地址,具体代码如下:
@property (nonatomic,strong)NSMutableArray<AVPlayerItem *> *valiableItems;
@property (nonatomic,strong)NSMutableArray<AVPlayerItem *> *lastItems;
- (void)fl_moveToIndex:(NSInteger)index andStartImmediately:(BOOL)startImmediately{
if (!self.player) {
[self fl_delegateResponseFailureWithCode:FLAudioPlayerErrorByPlayerIsNotInit];
return;
}
if (!self.valiableUrls.count) {
[self fl_delegateResponseFailureWithCode:FLAudioPlayerErrorByPlayerNoMoreValiableUrl];
return;
}
if (index < 0) {
index = 0;
}
else if (index > self.valiableUrls.count - 1){
index = self.valiableUrls.count - 1;
}
// stop current
[self fl_stop];
[self.lastItems removeAllObjects];
NSMutableArray <AVPlayerItem *>*tempArrM = self.valiableItems.mutableCopy;
NSIndexSet *se = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(index, tempArrM.count - index)];
[self.lastItems addObjectsFromArray:[tempArrM objectsAtIndexes:se]];
AVPlayerItem *firstItem = self.lastItems.firstObject;
self.currentUrl = self.valiableUrls[index];
[self fl_createPlayWithItem:firstItem andStartImmediately:startImmediately];
}
五、细节点
- 1、传入URL地址时候,不需要关心是本地还是网络,内部会自动处理,核心代码如下:
BOOL FL_ISNETURL(NSString *urlString){
return [[urlString substringToIndex:4] caseInsensitiveCompare:@"http"] == NSOrderedSame || [[urlString substringToIndex:5] caseInsensitiveCompare:@"https"] == NSOrderedSame;
}
- (NSURL *)fl_getSuitableUrl:(NSString *)urlString{
NSURL *url = nil;
if (FL_ISNETURL(urlString)) {
url = [NSURL URLWithString:urlString];
}
else{
url = [NSURL fileURLWithPath:urlString];
}
return url;
}
- 2、内置一个时间格式转换,通过C函数
FL_COVERTTIME
传入时间戳(单位秒),核心代码如下:
NSString *FL_COVERTTIME(CGFloat second){
NSDate *date = [NSDate dateWithTimeIntervalSince1970:second];
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
if (second/3600 >= 1) {
[formatter setDateFormat:@"HH:mm:ss"];
} else {
[formatter setDateFormat:@"mm:ss"];
}
NSString *showtimeNew = [formatter stringFromDate:date];
return showtimeNew;
}
- 3、内置音量控制,通过
MPVolumeView
控制系统声音来实现,外部只需要修改currentVolum
属性即可
/**
当前播放的音量大小(0.0-1.0),注意,播放音频的时候设置才生效
*/
@property (nonatomic,assign)CGFloat currentVolum;
-
4、框架内代理方法全部基本数据类型都包装成NSNumber,方便统一处理,因此外界获取时,可通过
.doubleValue
获取 -
5、统一处理了所有错误信息,有一一对应的error,可根据code判断,具体的code,可查看相应的错误代理方法解释
六、总结
-
1、通过研究录音播放的上层接口类,了解API的设计思路,可以更好的理解底层实现,以及为日后封装提供API设计规范
-
2、具体API以及实现、调用方法,可以去 Github clone 代码,里面有详细完整的Demo 演示
-
3、Demo中附带自定义滑动进度条,监听事件完善,带有缓冲进度,FLSlider Github 地址,详细使用Demo中也有演示
-
4、如果大家发现上文有哪里说得不对或者有更好的建议,欢迎去简书评论或简信我!如果你觉得写得不错,欢迎关注我!给个like 和 start,谢谢支持