美文网首页iOS视频开发音频&视频
基于系统AVplayer的音频播放实现1.0-2.0倍速播放,锁

基于系统AVplayer的音频播放实现1.0-2.0倍速播放,锁

作者: ControlM | 来源:发表于2017-06-21 09:52 被阅读794次

    音频播放主要是基于系统的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;
    }

    相关文章

      网友评论

      • 梁森的简书:AVPlayerTimeControlStatusPaused是在ios10之后才能用的,ios10之前怎么通过类似的状态进行播放和暂停呢?
      • MNCode:有demo嘛?可以发一份参考不?
        ControlM:https://github.com/mahuiying0126/MPlayerDemo 只有 swift 版本的
      • 魔魔571:大佬,情况是这样的,我根据你的这个类,来获取 ‘音频的总时长’
        CGFloat totalTime = (CGFloat)currentItem.duration.value / currentItem.duration.timescale;

        可是,获取出来的音频时长不对啊
        魔魔571:音频是网络音频
      • 魔魔571:大佬,能指点一下吗,有没有demo

      本文标题:基于系统AVplayer的音频播放实现1.0-2.0倍速播放,锁

      本文链接:https://www.haomeiwen.com/subject/iqezqxtx.html