基于系統(tǒng)AVplayer的音頻播放實(shí)現(xiàn)1.0-2.0倍速播放,鎖屏播放,記憶播放,斷點(diǎn)播放

音頻播放主要是基于系統(tǒng)的AVPlayerAVPlayerItem,不像視頻需要顯示畫面需要AVPlayerLayer

實(shí)現(xiàn)思路:

建一個(gè)繼承于NSObject類.h .m文件,或許你疑問為什么不寫在view或者viewcontrol中,因?yàn)橐纛l播放你只需要處理數(shù)據(jù)就行了,比如做快進(jìn)快退操作,下一曲上一曲操作,只需要把數(shù)據(jù)傳給它做數(shù)據(jù)操作就行了.下面看代碼;

建一個(gè)繼承與NSobject類起名叫MAudioPlayer的文件

導(dǎo)入

#import <AVFoundation/AVFoundation.h>
#import <MediaPlayer/MediaPlayer.h>

在.h中添加一些屬性,用于外部訪問,比如當(dāng)前播放器播放的model等,再寫一些方法用于外部調(diào)用,比如暫停,播放等

@property (nonatomic ,strong,readonly) AVPlayer *player;
@property (nonatomic, assign, readonly) NSInteger currentTime;/*!*當(dāng)前播放器時(shí)間*/
@property (nonatomic, assign, readonly) NSInteger totalTime;/*!*當(dāng)前播放器總時(shí)間*/
@property (nonatomic, assign,readonly) MAudioPlayState playerState;/*用枚舉定義了播放器狀態(tài)*/
@property (nonatomic, assign) float ratevalue;/*!*倍速*/

定義的枚舉類型,可自定義添加狀態(tài)

typedef NS_ENUM(NSUInteger, MAudioPlayState) {
MAudioStatePlaying = 1,
MAudioStatePaused,
MAudioStateWaiting,
};

還可以加一個(gè)model,在播放的時(shí)候?qū)?dāng)前播放的model傳給播放器,下面代碼中,在播放器代理里面有使用到這個(gè)model

/*
 * 當(dāng)前音頻模型
 */
@property (nonatomic, strong) DetailCourseListModel *currentAudioModel;

定義了一個(gè)播放器代理,實(shí)現(xiàn)兩個(gè)方法,一個(gè)是實(shí)時(shí)回傳播放器時(shí)間刷新UI,一個(gè)是播放完畢,做暫托В或者下一曲操作

@protocol MAudioPlayerDelegate <NSObject>

- (void)audioUpdateWith:(float)time Totaltime:(float)totalTime;

-(void)audioPlayEnd;

@end

在.m中聲明AVPlayerItem對象

@property (nonatomic ,strong) AVPlayerItem *playerItem;

再添加一個(gè)時(shí)間觀察,用于實(shí)時(shí)刷新UI數(shù)據(jù)
@property (nonatomic, strong) id timeObserve;// 時(shí)間觀察
還可以再加一些輔助對象,比如:電話監(jiān)聽,耳機(jī)拔插監(jiān)聽,來電前播放狀態(tài),鎖屏播放的存儲(chǔ)的字典等
@property (nonatomic, strong) CTCallCenter *callCenter ;/*!*監(jiān)聽電話*/
@property (nonatomic, assign) BOOL isPlay;/*!*播放或者暫停*/
@property (nonatomic, strong) NSMutableDictionary *imageSpaceDict;/*!*存圖片字典*/

用單例初始化保證全局只有一個(gè)播放器
+ (instancetype)sharedMPlayer

預(yù)留一些播放暫停,播放,切換下一曲等操作方法

初始化播放器方法

/**
初始化播放器

 @param url 播放地址
 @param recordTime 指定時(shí)間播放
 @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根據(jù)需求自定義

現(xiàn)在看一下.m中的代碼

單例初始化對象,保證整個(gè)app只存在一個(gè)播放器

+ (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];//監(jiān)聽電話
   //[self audioRouteChangeListener];//拔插耳機(jī)
   self.playerItem = [[AVPlayerItem alloc] initWithURL:[NSURL URLWithString:localString]];
    _ratevalue = ratevalue;//倍速
    //AVPlayer
    _player = [AVPlayer playerWithPlayerItem:self.playerItem];
    [_player play];
    //指定時(shí)間播放,類似記憶播放,拖動(dòng)進(jìn)度條也用這個(gè)方法
    [_player seekToTime:CMTimeMake(recordTime, 1)];
}

因?yàn)閟elf.playerItem用的是懶加載,看一下,item怎么初始化,做了哪些操作

/**
 *  根據(jù)playerItem樟遣,來添加移除觀察者
 *
 *  @param playerItem playerItem
 */
- (void)setPlayerItem:(AVPlayerItem *)playerItem
{
    //如果初始化的item與當(dāng)前item相等,則不做操作
    if (_playerItem == playerItem) {return;}
    //如果當(dāng)前item不為空,移除里面的屬性觀察
    if (_playerItem) {
        [[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:_playerItem];
        [_playerItem removeObserver:self forKeyPath:@"status"];
    }
    _playerItem = playerItem;
    if (playerItem) {
    //當(dāng)前音頻播放完畢監(jiān)聽,我這里寫的代理,方便數(shù)據(jù)傳遞
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(moviePlayDidEnd:) name:AVPlayerItemDidPlayToEndTimeNotification object:playerItem];
        //監(jiān)聽播放器狀態(tài)
        [playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
      //除了播放器狀態(tài),還可以監(jiān)聽緩沖狀態(tài):無緩沖playbackBufferEmpty,緩沖足夠可以播放:playbackBufferEmpty等,具體狀態(tài)可以百度查找 
    }
 }

增加一個(gè)觀察,用來觀察播放器,暫停,播放等狀態(tài),方便刷新UI

//觀察播放器狀態(tài)
- (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];
                 //如果想實(shí)現(xiàn)倍速播放,必須調(diào)用此方法
                [self enableAudioTracks:YES inPlayerItem:_playerItem];
                self.player.rate = _ratevalue;
                //增加一個(gè)時(shí)間觀察,為了實(shí)時(shí)拿到當(dāng)前播放時(shí)間,刷新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:)]) {
 //播放器時(shí)間代理
    [weakSelf.delegateM audioUpdateWith:currentTime Totaltime:totalTime];
        }
        //根據(jù)系統(tǒng)方法來判斷播放器狀態(tài),供外部屬性調(diào)用實(shí)時(shí)刷新UI,比如:外部播放器按鈕狀態(tài)可根據(jù)可狀態(tài)播放,點(diǎn)擊播放還是暫停,也可以通過此狀態(tài)判斷
        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;
        }
    }
}

下面就是實(shí)現(xiàn)播放器播放,暫停等方法了

//播放暫停
- (void)playerPaused {
    [self.player pause];
    
}
//播放繼續(xù)
- (void)playerPlay {
    [self.player play];
    self.player.rate = _ratevalue;
}

關(guān)閉播放器,記得移除通知,置空播放器

- (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];
    
}

對,還有播放器鎖屏的實(shí)現(xiàn)方法,可在.h里面寫一個(gè)鎖屏方法供外部調(diào)用,鎖屏方法就是在外部播放器代理里面調(diào)用,如果鎖屏中加載圖片為網(wǎng)絡(luò)圖片的話,最好做一個(gè)字典通過key-value來存儲(chǔ)

-(void)SetlockScreenInformation:(DetailCourseListModel *)model{
    //model是項(xiàng)目中用到的,可根據(jù)自己需求定義
    NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithDictionary:[[MPNowPlayingInfoCenter defaultCenter] nowPlayingInfo]];
    
    [dict setObject:model.teacherName==nil?@"1":model.name forKey:MPMediaItemPropertyTitle];
    //此表現(xiàn)形式 name-title 副標(biāo)題
    //    [dict setObject:recordUploadModel.teacherName==nil?@"1":recordUploadModel.teacherName forKey:MPMediaItemPropertyArtist];
    
    [dict setObject:model.name==nil?@"1":model.name forKey:MPMediaItemPropertyAlbumTitle];
    NSString *imageUrl;
    //判斷當(dāng)前是個(gè)鏈接還是上傳路徑
    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];
    //當(dāng)前已經(jīng)過時(shí)間
    [dict setObject:[NSNumber numberWithDouble:_currentTime] forKey:MPNowPlayingInfoPropertyElapsedPlaybackTime];
    //總時(shí)間
    [dict setObject:[NSNumber numberWithDouble:_totalTime] forKey:MPMediaItemPropertyPlaybackDuration];
    [[MPNowPlayingInfoCenter defaultCenter] setNowPlayingInfo:dict];
}

寫了那么多了,估計(jì)看的也沒有耐心了.鎖屏后怎么在鎖屏界面或控制中心對播放器做暫停,上一曲下一曲等操作呢,可以在控制器中加入以下代碼:

-(void)viewWillAppear:(BOOL)animated{
    [super viewWillAppear:animated];
    // 開始接受遠(yuǎn)程控制
    [[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
    //成為第一響應(yīng)者
    [self becomeFirstResponder];
    //  開啟界面常亮
    [[UIApplication sharedApplication] setIdleTimerDisabled:YES];
}
  
-(void)viewWillDisappear:(BOOL)animated{
    [super viewWillDisappear:animated];
    //接觸遠(yuǎn)程控制
    [[UIApplication sharedApplication] endReceivingRemoteControlEvents];
    [self resignFirstResponder];
    //  關(guān)閉界面常亮
    [[UIApplication sharedApplication] setIdleTimerDisabled:NO];

}

重寫父類成為響應(yīng)者方法

// 重寫父類成為響應(yīng)者方法
- (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;
        }
    }
}

給鎖屏界面實(shí)時(shí)傳數(shù)據(jù)呢,就在播放器代理里面

#pragma mark - 播放器代理時(shí)間
- (void)audioUpdateWith:(float)time Totaltime:(float)totalTime{
    //滑動(dòng)進(jìn)度條
    _slider.value = time/totalTime;
    //刷新當(dāng)前時(shí)間,通過擴(kuò)展方法轉(zhuǎn)化成00:00:00格式
    _currentTimeLB.text = [NSString timeTransformString:(float)time];
    //刷新總時(shí)間
    _totalTimeLB.text = [NSString timeTransformString:(float)totalTime];
    //當(dāng)前標(biāo)題,就是model里面的
    _titleLB.text = [NSString stringWithFormat:@"%@",MAudioPlay.currentAudioModel.name];
    //在代理里面調(diào)用鎖屏方法,傳數(shù)據(jù)
    [MAudioPlay SetlockScreenInformation:MAudioPlay.currentAudioModel];
}

秒轉(zhuǎn)換成00:00:00格式,我寫的是NSString的擴(kuò)展屬性,此方法也是在網(wǎng)上找的,用到手動(dòng)釋放,需要在工程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;
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末痘括,一起剝皮案震驚了整個(gè)濱河市枝冀,隨后出現(xiàn)的幾起案子衅鹿,更是在濱河造成了極大的恐慌颜启,老刑警劉巖愕够,帶你破解...
    沈念sama閱讀 211,042評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件走贪,死亡現(xiàn)場離奇詭異,居然都是意外死亡惑芭,警方通過查閱死者的電腦和手機(jī)坠狡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評論 2 384
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來遂跟,“玉大人逃沿,你說我怎么就攤上這事』盟” “怎么了凯亮?”我有些...
    開封第一講書人閱讀 156,674評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長哄尔。 經(jīng)常有香客問我假消,道長,這世上最難降的妖魔是什么岭接? 我笑而不...
    開封第一講書人閱讀 56,340評論 1 283
  • 正文 為了忘掉前任富拗,我火速辦了婚禮臼予,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘啃沪。我一直安慰自己粘拾,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,404評論 5 384
  • 文/花漫 我一把揭開白布谅阿。 她就那樣靜靜地躺著半哟,像睡著了一般。 火紅的嫁衣襯著肌膚如雪签餐。 梳的紋絲不亂的頭發(fā)上寓涨,一...
    開封第一講書人閱讀 49,749評論 1 289
  • 那天,我揣著相機(jī)與錄音氯檐,去河邊找鬼戒良。 笑死,一個(gè)胖子當(dāng)著我的面吹牛冠摄,可吹牛的內(nèi)容都是我干的糯崎。 我是一名探鬼主播,決...
    沈念sama閱讀 38,902評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼河泳,長吁一口氣:“原來是場噩夢啊……” “哼沃呢!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起拆挥,我...
    開封第一講書人閱讀 37,662評論 0 266
  • 序言:老撾萬榮一對情侶失蹤薄霜,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后纸兔,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體惰瓜,經(jīng)...
    沈念sama閱讀 44,110評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年汉矿,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了崎坊。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,577評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡洲拇,死狀恐怖奈揍,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情赋续,我是刑警寧澤男翰,帶...
    沈念sama閱讀 34,258評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站蚕捉,受9級特大地震影響奏篙,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,848評論 3 312
  • 文/蒙蒙 一秘通、第九天 我趴在偏房一處隱蔽的房頂上張望为严。 院中可真熱鬧,春花似錦肺稀、人聲如沸第股。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽夕吻。三九已至,卻和暖如春繁仁,著一層夾襖步出監(jiān)牢的瞬間涉馅,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評論 1 264
  • 我被黑心中介騙來泰國打工黄虱, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留稚矿,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,271評論 2 360
  • 正文 我出身青樓捻浦,卻偏偏與公主長得像晤揣,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子朱灿,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,452評論 2 348

推薦閱讀更多精彩內(nèi)容