做一个完整的音乐播放器3.歌曲切换

上周迟到了,周末去参加OSC源创会了,还是有点启发的。但这不是重点,重点是 上一篇我只是实现了一首歌曲的在线播放,这肯定是不够的。这一篇博客主要是实现了多首歌曲的顺序播放以及上一首和下一首切换。
先看一下效果图

效果图.png

1.准备工作

(1)数据源
   我把歌曲列表存在本地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; // 标题
    UILabel *_artist_nameLabel; // 副标题
    UILabel *_currentLabel;  // 当前时间
    UILabel *_durationLabel; // 总时间
    UIProgressView *_progressView; // 进度条
    UISlider *_playerSlider;   // 播放控制器
    FHCustomButton *_playButton;  // 播放暂停
    FHCustomButton *_prevButton;  // 上一首
    FHCustomButton *_nextButton;  // 下一首
    BOOL _isPlay; // 记录播放暂停状态
    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;// 播放器进度观察者

@end

UI的具体实现我就不一一介绍了,可以去我的GitUp下载源码。只要记住每个变量的含义就好了,方便下面的观看。
(3)懒加载变量

#pragma - mark 懒加载歌曲
- (NSMutableArray *)albumArr {
    
    if (!_albumArr) {
        
        _albumArr = [NSMutableArray new];
        // 从本地获取json数据
        NSData *jsonData = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"songList" ofType:@"json"]];
        // 把json数据转换成字典
        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];
    }
    
}

当没有歌曲播放时候,添加歌曲。当有歌曲播放时,不添加歌曲。这样可以保证暂停之后继续播放。

- (void)playMusic {
   // 1.移除观察者
   [self removeObserver];
   // 2.修改播放按钮的图片
   _playButton.imageView.image = [UIImage imageNamed:@"play"];
   // 3.获取歌曲
   FHAlbumModel *albumModel = self.albumArr[_index];
   // 4.修改标题
   _album_titleLabel.text = albumModel.title;
   // 5.修改副标题
   _artist_nameLabel.text = [NSString stringWithFormat:@"%@ - 经典老歌榜",albumModel.artist_name];
   // 6. 实例化新的playerItem
   AVPlayerItem *playerItem = [[AVPlayerItem alloc] initWithURL:[NSURL URLWithString:albumModel.song_id]];
   // 7.取代旧的playerItem
   [self.avPlayer replaceCurrentItemWithPlayerItem:playerItem];
   // 8.开始播放
   [self.avPlayer play];
   // 9.添加缓存状态的观察者
   [self addObserverOfLoadedTimeRanges];
   // 10.添加播放进度的观察者
   [self addTimePlayProgerssObserver];
   // 11.记录当前播放的歌曲
   _currentModel = self.albumArr[_index];
   // 12.获取歌词
   [self getAlbumLrc];
}
```
**分析**:1.**添加观察者之前需要把以前的观察者移除**。如果不移除self.avPlayer.currentItem 的观察者,就会报“An instance 0x174009380 of class AVPlayerItem was deallocated while key value observers were still registered with it”。意思是观察的对象已经释放,还对它进行观察。我们切换歌曲时,原来的歌曲对象已经释放了,所以对原来歌曲对象添加的观察者也应该移除;虽然self.avPlayer一直存在,但是如果对它一直添加观察者,会耗费大量内存,为了防止内存溢出所以也应该移除。
```
#pragma mark - 移除观察者
- (void)removeObserver {
// 没添加之前不能移除否则会崩溃
   if (!_currentModel) {
       return;
   }else {
        [self.avPlayer.currentItem removeObserver:self forKeyPath:@"loadedTimeRanges"];
       [self.avPlayer removeTimeObserver:self.timePlayProgerssObserver];
   }
}
```
```
#pragma mark - 监听缓存状态 
- (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;
       //本次缓冲的时间范围
       CMTimeRange timeRange = [timeRanges.firstObject CMTimeRangeValue];
       //缓冲总长度
       NSTimeInterval totalLoadTime = CMTimeGetSeconds(timeRange.start) + CMTimeGetSeconds(timeRange.duration);
       //音乐的总时间
       NSTimeInterval duration = CMTimeGetSeconds(self.avPlayer.currentItem.duration);
       //计算缓冲百分比例
       NSTimeInterval scale = totalLoadTime/duration;
       //更新缓冲进度条
       _progressView.progress = scale;
       
       _durationLabel.text = [NSString stringWithFormat:@"%d:%@",(int)duration/60,[self FormatTime:(int)duration%60]];
   }
}
```
```
#pragma mark - 添加播放进度的观察者
- (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) {
       
       // 当前播放的时间
       float current = CMTimeGetSeconds(time);
       // 更新歌词
       if (weakRow < weakSelf.lrcArr.count) {
           FHLrcModel *model = weakSelf.lrcArr[weakRow];
           if (model.presenTime == (int)current) {
               [weakSelf reloadTabelViewWithRow:weakRow];
               weakRow++;
           }
       }
       // 总时间
       float total = CMTimeGetSeconds(weakSelf.avPlayer.currentItem.duration);
       // 更改当前播放时间
       NSString *currentSStr = [weakSelf FormatTime: (int)current % 60];
       waekCurrentLabel.text = [NSString stringWithFormat:@"%d:%@",(int)current / 60,currentSStr];
       // 更新播放进度条
       weakPregressSlider.value = current / total;
           
   }];
}
```
```
 // 播放完成通知
   [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(nextButtonClick:) name:AVPlayerItemDidPlayToEndTimeNotification object:nil];
```
  写在viewDidLoad里,因为添加一次就可以。播放完成直接播放下一首。
```
#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];
}
```
  当播放第一首歌曲时,点击上一首播放最后一首歌曲。当播放最后一首歌曲时,点击下一首播放第一首歌曲。
  由于篇幅的原因,下一篇博客再介绍歌词的实现。重要的事情说三遍:项目地址[GitUp](https://github.com/haichong/iOSPlayerStudy) ,欢迎下载。

















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

推荐阅读更多精彩内容