做一個(gè)完整的音樂播放器3.歌曲切換

上周遲到了,周末去參加OSC源創(chuàng)會(huì)了,還是有點(diǎn)啟發(fā)的。但這不是重點(diǎn),重點(diǎn)是 上一篇我只是實(shí)現(xiàn)了一首歌曲的在線播放,這肯定是不夠的。這一篇博客主要是實(shí)現(xiàn)了多首歌曲的順序播放以及上一首和下一首切換。
先看一下效果圖

效果圖.png

1.準(zhǔn)備工作

(1)數(shù)據(jù)源
?? 我把歌曲列表存在本地songList.json文件里。用FHAlbumModel管理歌曲。
FHAlbumModel.h

#import <Foundation/Foundation.h>

@interface FHAlbumModel : NSObject

@property (nonatomic, copy) NSString *lrclink; // 歌詞
@property (nonatomic, copy) NSString *pic_big; // 背景圖
@property (nonatomic, copy) NSString *artist_name; // 歌手
@property (nonatomic, copy) NSString *title; // 歌名
@property (nonatomic, copy) NSString *song_id; // 歌曲地址


- (instancetype)initWithInfo: (NSDictionary *)InfoDic;
@end

FHAlbumModel.m

#import "FHAlbumModel.h"

@implementation FHAlbumModel

- (instancetype)initWithInfo: (NSDictionary *)InfoDic {
    
    FHAlbumModel *model = [[FHAlbumModel alloc] init];
   // 通過kvo為屬性賦值
    [model setValuesForKeysWithDictionary:InfoDic];
    return model;
}
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    
}
@end

(2)聲明的變量

#import "FHMusicPlayerViewController.h"
#import <AVFoundation/AVFoundation.h>
#import "UIColor+RGBHelper.h"
#import "FHCustomButton.h"
#import "Masonry.h"
#import "FHAlbumModel.h"
#import "FHLrcModel.h"

@interface FHMusicPlayerViewController ()<UITableViewDelegate, UITableViewDataSource>{
    
    UIImageView *_backImageView; // 背景圖
    UILabel *_album_titleLabel; // 標(biāo)題
    UILabel *_artist_nameLabel; // 副標(biāo)題
    UILabel *_currentLabel;  // 當(dāng)前時(shí)間
    UILabel *_durationLabel; // 總時(shí)間
    UIProgressView *_progressView; // 進(jìn)度條
    UISlider *_playerSlider;   // 播放控制器
    FHCustomButton *_playButton;  // 播放暫停
    FHCustomButton *_prevButton;  // 上一首
    FHCustomButton *_nextButton;  // 下一首
    BOOL _isPlay; // 記錄播放暫停狀態(tài)
    NSInteger _index; // 記錄播放到了第幾首歌
    FHAlbumModel *_currentModel;
    UITableView *_lrcTableView;  // 用于顯示歌詞
    int _row;  //記錄歌詞第幾行
}
@property (nonatomic, strong)NSMutableArray *albumArr; //歌曲
@property (nonatomic, strong)NSMutableArray *lrcArr;  // 歌詞
@property (nonatomic, strong)AVPlayer *avPlayer;
@property (nonatomic, strong)id timePlayProgerssObserver;// 播放器進(jìn)度觀察者

@end

UI的具體實(shí)現(xiàn)我就不一一介紹了,可以去我的GitUp下載源碼。只要記住每個(gè)變量的含義就好了,方便下面的觀看。
(3)懶加載變量

#pragma - mark 懶加載歌曲
- (NSMutableArray *)albumArr {
    
    if (!_albumArr) {
        
        _albumArr = [NSMutableArray new];
        // 從本地獲取json數(shù)據(jù)
        NSData *jsonData = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"songList" ofType:@"json"]];
        // 把json數(shù)據(jù)轉(zhuǎn)換成字典
        NSDictionary *rootDic  = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingAllowFragments error:nil];
        NSArray *albumArr = [NSArray arrayWithArray:rootDic[@"song_list"]];
        for (NSDictionary *dic in albumArr) {
            FHAlbumModel *albumModel = [[FHAlbumModel alloc] initWithInfo:dic];
            [_albumArr addObject:albumModel];
        }
    }
    return _albumArr;
    
}
#pragma - mark 懶加載歌詞
- (NSMutableArray *)lrcArr{
    
    if (!_lrcArr) {
        _lrcArr = [NSMutableArray new];
    }
    return _lrcArr;
    
}
#pragma - mark 懶加載AVPlayer 
- (AVPlayer *)avPlayer {
    
    if (!_avPlayer) {
        AVPlayerItem *item = [AVPlayerItem new];
        _avPlayer = [[AVPlayer alloc] initWithPlayerItem:item];
    }
    return _avPlayer;
}

2.歌曲輪播

#pragma mark - 播放暫停
- (void)playAction:(UIButton *)button {
    
    _isPlay = !_isPlay;
    if (_isPlay) {
        _playButton.imageView.image = [UIImage imageNamed:@"play"];
        if (_currentModel) {
            [self.avPlayer play];
        }else {
            [self playMusic];
        }
      }else {
        _playButton.imageView.image = [UIImage imageNamed:@"stop"];
        [self.avPlayer pause];
    }
    
}

當(dāng)沒有歌曲播放時(shí)候,添加歌曲。當(dāng)有歌曲播放時(shí),不添加歌曲。這樣可以保證暫停之后繼續(xù)播放。

- (void)playMusic {
   // 1.移除觀察者
   [self removeObserver];
   // 2.修改播放按鈕的圖片
   _playButton.imageView.image = [UIImage imageNamed:@"play"];
   // 3.獲取歌曲
   FHAlbumModel *albumModel = self.albumArr[_index];
   // 4.修改標(biāo)題
   _album_titleLabel.text = albumModel.title;
   // 5.修改副標(biāo)題
   _artist_nameLabel.text = [NSString stringWithFormat:@"%@ - 經(jīng)典老歌榜",albumModel.artist_name];
   // 6. 實(shí)例化新的playerItem
   AVPlayerItem *playerItem = [[AVPlayerItem alloc] initWithURL:[NSURL URLWithString:albumModel.song_id]];
   // 7.取代舊的playerItem
   [self.avPlayer replaceCurrentItemWithPlayerItem:playerItem];
   // 8.開始播放
   [self.avPlayer play];
   // 9.添加緩存狀態(tài)的觀察者
   [self addObserverOfLoadedTimeRanges];
   // 10.添加播放進(jìn)度的觀察者
   [self addTimePlayProgerssObserver];
   // 11.記錄當(dāng)前播放的歌曲
   _currentModel = self.albumArr[_index];
   // 12.獲取歌詞
   [self getAlbumLrc];
}
```
**分析**:1.**添加觀察者之前需要把以前的觀察者移除**。如果不移除self.avPlayer.currentItem 的觀察者,就會(huì)報(bào)“An instance 0x174009380 of class AVPlayerItem was deallocated while key value observers were still registered with it”。意思是觀察的對(duì)象已經(jīng)釋放,還對(duì)它進(jìn)行觀察。我們切換歌曲時(shí),原來的歌曲對(duì)象已經(jīng)釋放了,所以對(duì)原來歌曲對(duì)象添加的觀察者也應(yīng)該移除;雖然self.avPlayer一直存在,但是如果對(duì)它一直添加觀察者,會(huì)耗費(fèi)大量?jī)?nèi)存,為了防止內(nèi)存溢出所以也應(yīng)該移除。
```
#pragma mark - 移除觀察者
- (void)removeObserver {
// 沒添加之前不能移除否則會(huì)崩潰
   if (!_currentModel) {
       return;
   }else {
        [self.avPlayer.currentItem removeObserver:self forKeyPath:@"loadedTimeRanges"];
       [self.avPlayer removeTimeObserver:self.timePlayProgerssObserver];
   }
}
```
```
#pragma mark - 監(jiān)聽緩存狀態(tài) 
- (void)addObserverOfLoadedTimeRanges {
   
   [self.avPlayer.currentItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
   
   if ([keyPath isEqualToString:@"loadedTimeRanges"]) {
       NSArray * timeRanges = self.avPlayer.currentItem.loadedTimeRanges;
       //本次緩沖的時(shí)間范圍
       CMTimeRange timeRange = [timeRanges.firstObject CMTimeRangeValue];
       //緩沖總長(zhǎng)度
       NSTimeInterval totalLoadTime = CMTimeGetSeconds(timeRange.start) + CMTimeGetSeconds(timeRange.duration);
       //音樂的總時(shí)間
       NSTimeInterval duration = CMTimeGetSeconds(self.avPlayer.currentItem.duration);
       //計(jì)算緩沖百分比例
       NSTimeInterval scale = totalLoadTime/duration;
       //更新緩沖進(jìn)度條
       _progressView.progress = scale;
       
       _durationLabel.text = [NSString stringWithFormat:@"%d:%@",(int)duration/60,[self FormatTime:(int)duration%60]];
   }
}
```
```
#pragma mark - 添加播放進(jìn)度的觀察者
- (void)addTimePlayProgerssObserver {
   
   __block UISlider *weakPregressSlider = _playerSlider;
   __weak UILabel *waekCurrentLabel = _currentLabel;
   __block int weakRow = _row;
   __weak typeof(self) weakSelf = self;
   self.timePlayProgerssObserver = [self.avPlayer addPeriodicTimeObserverForInterval:CMTimeMake(1.0, 1.0) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
       
       // 當(dāng)前播放的時(shí)間
       float current = CMTimeGetSeconds(time);
       // 更新歌詞
       if (weakRow < weakSelf.lrcArr.count) {
           FHLrcModel *model = weakSelf.lrcArr[weakRow];
           if (model.presenTime == (int)current) {
               [weakSelf reloadTabelViewWithRow:weakRow];
               weakRow++;
           }
       }
       // 總時(shí)間
       float total = CMTimeGetSeconds(weakSelf.avPlayer.currentItem.duration);
       // 更改當(dāng)前播放時(shí)間
       NSString *currentSStr = [weakSelf FormatTime: (int)current % 60];
       waekCurrentLabel.text = [NSString stringWithFormat:@"%d:%@",(int)current / 60,currentSStr];
       // 更新播放進(jìn)度條
       weakPregressSlider.value = current / total;
           
   }];
}
```
```
 // 播放完成通知
   [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(nextButtonClick:) name:AVPlayerItemDidPlayToEndTimeNotification object:nil];
```
??寫在viewDidLoad里,因?yàn)樘砑右淮尉涂梢?。播放完成直接播放下一首?```
#pragma mark - 上一首
- (void)prevButtonClick :(UIButton *)button {
   _index--;
   if (_index < 0) {
     
       _index = self.albumArr.count - 1;
   }
   [self playMusic];
}
#pragma mark - 下一首
- (void)nextButtonClick :(UIButton *)button {
   _index++;
       if (_index >= self.albumArr.count) {
       
       _index = 0;
   }
   [self playMusic];
}
```
??當(dāng)播放第一首歌曲時(shí),點(diǎn)擊上一首播放最后一首歌曲。當(dāng)播放最后一首歌曲時(shí),點(diǎn)擊下一首播放第一首歌曲。
??由于篇幅的原因,下一篇博客再介紹歌詞的實(shí)現(xiàn)。重要的事情說三遍:項(xiàng)目地址[GitUp](https://github.com/haichong/iOSPlayerStudy) ,歡迎下載。

















??
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容