音频播放主要是基于系统的AVPlayer
和AVPlayerItem
,不像视频需要显示画面需要AVPlayerLayer
实现思路:
建一个继承于NSObject
类.h .m文件,或许你疑问为什么不写在view或者viewcontrol中,因为音频播放你只需要处理数据就行了,比如做快进快退操作,下一曲上一曲操作,只需要把数据传给它做数据操作就行了.下面看代码;
建一个继承与NSobject
类起名叫MAudioPlayer
的文件
导入
#import <AVFoundation/AVFoundation.h>
#import <MediaPlayer/MediaPlayer.h>
在.h中添加一些属性,用于外部访问,比如当前播放器播放的model等,再写一些方法用于外部调用,比如暂停,播放等
@property (nonatomic ,strong,readonly) AVPlayer *player;
@property (nonatomic, assign, readonly) NSInteger currentTime;/*!*当前播放器时间*/
@property (nonatomic, assign, readonly) NSInteger totalTime;/*!*当前播放器总时间*/
@property (nonatomic, assign,readonly) MAudioPlayState playerState;/*用枚举定义了播放器状态*/
@property (nonatomic, assign) float ratevalue;/*!*倍速*/
定义的枚举类型,可自定义添加状态
typedef NS_ENUM(NSUInteger, MAudioPlayState) {
MAudioStatePlaying = 1,
MAudioStatePaused,
MAudioStateWaiting,
};
还可以加一个model,在播放的时候将当前播放的model传给播放器,下面代码中,在播放器代理里面有使用到这个model
/*
* 当前音频模型
*/
@property (nonatomic, strong) DetailCourseListModel *currentAudioModel;
定义了一个播放器代理,实现两个方法,一个是实时回传播放器时间刷新UI,一个是播放完毕,做暂停或者下一曲操作
@protocol MAudioPlayerDelegate <NSObject>
- (void)audioUpdateWith:(float)time Totaltime:(float)totalTime;
-(void)audioPlayEnd;
@end
在.m中声明AVPlayerItem对象
@property (nonatomic ,strong) AVPlayerItem *playerItem;
再添加一个时间观察,用于实时刷新UI数据
@property (nonatomic, strong) id timeObserve;// 时间观察
还可以再加一些辅助对象,比如:电话监听,耳机拔插监听,来电前播放状态,锁屏播放的存储的字典等
@property (nonatomic, strong) CTCallCenter *callCenter ;/*!*监听电话*/
@property (nonatomic, assign) BOOL isPlay;/*!*播放或者暂停*/
@property (nonatomic, strong) NSMutableDictionary *imageSpaceDict;/*!*存图片字典*/
用单例初始化保证全局只有一个播放器
+ (instancetype)sharedMPlayer
预留一些播放暂停,播放,切换下一曲等操作方法
初始化播放器方法
/**
初始化播放器
@param url 播放地址
@param recordTime 指定时间播放
@param ratevalue 倍速,1.0~2.0倍速播放
*/
- (void)initWithUrl:(NSString *)url seekTotime:(NSInteger)recordTime rateValue:(float)ratevalue;
- (void)playerPaused;//暂停播放方法
- (void)playerPlay//开启播放
- (void)closePlayer//移除播放器
-(void)SetlockScreenInformation:(DetailCourseListModel *)model;//锁屏方法,model根据需求自定义
现在看一下.m中的代码
单例初始化对象,保证整个app只存在一个播放器
+ (instancetype)sharedMPlayer
{
static MAudioPlayer *audioPlayer = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
audioPlayer = [[MAudioPlayer alloc] init];
});
return audioPlayer;
}
初始化播放器
- (void)initWithUrl:(NSString *)url seekTotime :(NSInteger)recordTime rateValue:(float)ratevalue{
//[self callStatCenter];//监听电话
//[self audioRouteChangeListener];//拔插耳机
self.playerItem = [[AVPlayerItem alloc] initWithURL:[NSURL URLWithString:localString]];
_ratevalue = ratevalue;//倍速
//AVPlayer
_player = [AVPlayer playerWithPlayerItem:self.playerItem];
[_player play];
//指定时间播放,类似记忆播放,拖动进度条也用这个方法
[_player seekToTime:CMTimeMake(recordTime, 1)];
}
因为self.playerItem用的是懒加载,看一下,item怎么初始化,做了哪些操作
/**
* 根据playerItem,来添加移除观察者
*
* @param playerItem playerItem
*/
- (void)setPlayerItem:(AVPlayerItem *)playerItem
{
//如果初始化的item与当前item相等,则不做操作
if (_playerItem == playerItem) {return;}
//如果当前item不为空,移除里面的属性观察
if (_playerItem) {
[[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:_playerItem];
[_playerItem removeObserver:self forKeyPath:@"status"];
}
_playerItem = playerItem;
if (playerItem) {
//当前音频播放完毕监听,我这里写的代理,方便数据传递
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(moviePlayDidEnd:) name:AVPlayerItemDidPlayToEndTimeNotification object:playerItem];
//监听播放器状态
[playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
//除了播放器状态,还可以监听缓冲状态:无缓冲playbackBufferEmpty,缓冲足够可以播放:playbackBufferEmpty等,具体状态可以百度查找
}
}
增加一个观察,用来观察播放器,暂停,播放等状态,方便刷新UI
//观察播放器状态
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSString*, id> *)change context:(nullable void *)context {
if ([keyPath isEqualToString:@"status"]) {
AVPlayerItemStatus status = _playerItem.status;
switch (status) {
case AVPlayerItemStatusReadyToPlay:
{
self.playerItem.audioTimePitchAlgorithm = AVAudioTimePitchAlgorithmTimeDomain;
[_player play];
//如果想实现倍速播放,必须调用此方法
[self enableAudioTracks:YES inPlayerItem:_playerItem];
self.player.rate = _ratevalue;
//增加一个时间观察,为了实时拿到当前播放时间,刷新UI,锁屏操作等
[self addTimeObserve];
}
break;
case AVPlayerItemStatusUnknown:
{
BLLog(@"AVPlayerItemStatusUnknown");
}
break;
case AVPlayerItemStatusFailed:
{
BLLog(@"AVPlayerItemStatusFailed");
BLLog(@"%@",_playerItem.error);
}
break;
default:
break;
}
}
}
每一秒刷新UI
- (void)addTimeObserve{
__weak typeof(self) weakSelf = self;
self.timeObserve = [self.player addPeriodicTimeObserverForInterval:CMTimeMakeWithSeconds(1, 1) queue:nil usingBlock:^(CMTime time){
AVPlayerItem *currentItem = weakSelf.playerItem;
NSArray *loadedRanges = currentItem.seekableTimeRanges;
NSInteger currentTime = (NSInteger)CMTimeGetSeconds([currentItem currentTime]);
CGFloat totalTime = (CGFloat)currentItem.duration.value / currentItem.duration.timescale;
if (self.delegateM && [self.delegateM respondsToSelector:@selector(audioUpdateWith:Totaltime:)]) {
//播放器时间代理
[weakSelf.delegateM audioUpdateWith:currentTime Totaltime:totalTime];
}
//根据系统方法来判断播放器状态,供外部属性调用实时刷新UI,比如:外部播放器按钮状态可根据可状态播放,点击播放还是暂停,也可以通过此状态判断
if (self.player.timeControlStatus == AVPlayerTimeControlStatusPlaying) {
_playerState = MAudioStatePlaying;
}
if (self.player.timeControlStatus == AVPlayerTimeControlStatusPaused) {
_playerState = MAudioStatePaused;
}
if (self.player.timeControlStatus == AVPlayerTimeControlStatusWaitingToPlayAtSpecifiedRate) {
_playerState = MAudioStateWaiting;
}
if (loadedRanges.count > 0 && currentItem.duration.timescale != 0) {
_totalTime = totalTime;
_currentTime = currentTime;
}
}];
}
倍速切换方法
- (void)enableAudioTracks:(BOOL)enable inPlayerItem:(AVPlayerItem*)playerItem
{
for (AVPlayerItemTrack *track in playerItem.tracks)
{
if ([track.assetTrack.mediaType isEqual:AVMediaTypeAudio])
{
track.enabled = enable;
}
}
}
下面就是实现播放器播放,暂停等方法了
//播放暂停
- (void)playerPaused {
[self.player pause];
}
//播放继续
- (void)playerPlay {
[self.player play];
self.player.rate = _ratevalue;
}
关闭播放器,记得移除通知,置空播放器
- (void)closePlayer{
[self.player.currentItem cancelPendingSeeks];
[self.player.currentItem.asset cancelLoading];
self.playerItem = nil;
[self.player replaceCurrentItemWithPlayerItem:nil];
_player = nil;
self.ratevalue = 1.0;
self.callCenter = nil;
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
对,还有播放器锁屏的实现方法,可在.h里面写一个锁屏方法供外部调用,锁屏方法就是在外部播放器代理里面调用,如果锁屏中加载图片为网络图片的话,最好做一个字典通过key-value来存储
-(void)SetlockScreenInformation:(DetailCourseListModel *)model{
//model是项目中用到的,可根据自己需求定义
NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithDictionary:[[MPNowPlayingInfoCenter defaultCenter] nowPlayingInfo]];
[dict setObject:model.teacherName==nil?@"1":model.name forKey:MPMediaItemPropertyTitle];
//此表现形式 name-title 副标题
// [dict setObject:recordUploadModel.teacherName==nil?@"1":recordUploadModel.teacherName forKey:MPMediaItemPropertyArtist];
[dict setObject:model.name==nil?@"1":model.name forKey:MPMediaItemPropertyAlbumTitle];
NSString *imageUrl;
//判断当前是个链接还是上传路径
if ([model.imgUrl hasPrefix:@"http"]) {
imageUrl = model.imgUrl;
}else{
//拼接图片url
imageUrl = [NSString stringWithFormat:@"%@%@", imageUrlString, model.imgUrl];
}
if (![self.imageSpaceDict objectForKey:imageUrl]) {
NSLog(@"走了几次啊");
UIImage *imageM = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:imageUrl]]];
[self.imageSpaceDict setValue:imageM forKey:imageUrl];
}
UIImage *tempImage = [UIImage imageNamed:@"加载中"];
[dict setObject:[[MPMediaItemArtwork alloc] initWithImage:[self.imageSpaceDict objectForKey:imageUrl] == nil?tempImage:[self.imageSpaceDict objectForKey:imageUrl]] forKey:MPMediaItemPropertyArtwork];
//当前已经过时间
[dict setObject:[NSNumber numberWithDouble:_currentTime] forKey:MPNowPlayingInfoPropertyElapsedPlaybackTime];
//总时间
[dict setObject:[NSNumber numberWithDouble:_totalTime] forKey:MPMediaItemPropertyPlaybackDuration];
[[MPNowPlayingInfoCenter defaultCenter] setNowPlayingInfo:dict];
}
写了那么多了,估计看的也没有耐心了.锁屏后怎么在锁屏界面或控制中心对播放器做暂停,上一曲下一曲等操作呢,可以在控制器中加入以下代码:
-(void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
// 开始接受远程控制
[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
//成为第一响应者
[self becomeFirstResponder];
// 开启界面常亮
[[UIApplication sharedApplication] setIdleTimerDisabled:YES];
}
-(void)viewWillDisappear:(BOOL)animated{
[super viewWillDisappear:animated];
//接触远程控制
[[UIApplication sharedApplication] endReceivingRemoteControlEvents];
[self resignFirstResponder];
// 关闭界面常亮
[[UIApplication sharedApplication] setIdleTimerDisabled:NO];
}
重写父类成为响应者方法
// 重写父类成为响应者方法
- (BOOL)canBecomeFirstResponder
{
return YES;
}
//重写父类方法,接受外部事件的处理
- (void)remoteControlReceivedWithEvent: (UIEvent *) receivedEvent {
if (receivedEvent.type == UIEventTypeRemoteControl) {
switch (receivedEvent.subtype) { // 得到事件类型
case UIEventSubtypeRemoteControlTogglePlayPause: // 暂停ios6
break;
case UIEventSubtypeRemoteControlPreviousTrack: // 上一首
break;
case UIEventSubtypeRemoteControlNextTrack: // 下一首
break;
case UIEventSubtypeRemoteControlPlay: //播放
break;
case UIEventSubtypeRemoteControlPause: // 暂停 ios7
break;
default:
break;
}
}
}
给锁屏界面实时传数据呢,就在播放器代理里面
#pragma mark - 播放器代理时间
- (void)audioUpdateWith:(float)time Totaltime:(float)totalTime{
//滑动进度条
_slider.value = time/totalTime;
//刷新当前时间,通过扩展方法转化成00:00:00格式
_currentTimeLB.text = [NSString timeTransformString:(float)time];
//刷新总时间
_totalTimeLB.text = [NSString timeTransformString:(float)totalTime];
//当前标题,就是model里面的
_titleLB.text = [NSString stringWithFormat:@"%@",MAudioPlay.currentAudioModel.name];
//在代理里面调用锁屏方法,传数据
[MAudioPlay SetlockScreenInformation:MAudioPlay.currentAudioModel];
}
秒转换成00:00:00格式,我写的是NSString的扩展属性,此方法也是在网上找的,用到手动释放,需要在工程TARGETS->Build Phases 找到你写此方法的文件,双击加入-fno-objc-arc
方法如下:
+(NSString *)timeTransformString:(unsigned long)ms
{
unsigned long seconds, h, m, s;
char buff[128] = { 0 };
NSString *time = nil;
seconds = ms ;
h = seconds / 3600;
m = (seconds - h * 3600) / 60;
s = seconds - h * 3600 - m * 60;
snprintf(buff, sizeof(buff), "%02ld:%02ld:%02ld", h, m, s);
time = [[[NSString alloc] initWithCString:buff
encoding:NSUTF8StringEncoding] autorelease];
return time;
}