基于系统AVplayer的音频播放实现1.0-2.0倍速播放,锁屏播放,记忆播放,断点播放

音频播放主要是基于系统的AVPlayerAVPlayerItem,不像视频需要显示画面需要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;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,378评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,356评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,702评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,259评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,263评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,036评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,349评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,979评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,469评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,938评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,059评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,703评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,257评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,262评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,501评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,792评论 2 345

推荐阅读更多精彩内容