一.前言
首先做一个项目我们最好先分析我们要做哪些功能,按功能模块一个个划分好结构。每个功能模块都有相对应的职责。切入正题,我做的这款音乐播放器,实现的是播放本地音乐。有以下几个要点:
1.如何实现播放音乐?
2.如何切换当前正在播放的音乐资源?
3.如何监听音乐播放器的各种状态(播放器状态,播放的进度,缓冲的进度,播放是否完成)?
4.如何手动监控播放进度?
5.如何在后台模式下或者锁屏模式下播放音乐、显示音乐播放信息和远程操控音乐?
二.网络音乐播放器的核心技术点
iOS自带的AVFoundation框架的AVPlayer类,KVO和KVC,通知机制,远程控制,SDWebImage
三.开始工作
1.创建应用程序
导入图片资源并设置icon图标,command+r运行程序后,再按command + shift + h 可以看到icon图片,如下所示
2.使用storyboard搭建主界面
storyboard是主流的开发方式,开发速度快,降低理解难度。如下所示:
3.界面的专辑图片的旋转效果
我们容易想到用计时器,但是你是否遇到过,self 强引用NStimer,NStimer对self强引用。这里互相强引用不释放。如果用__weak typeof(self) wself = self;解决,这是有bu g的,因为你不知道self何时会释放,加入timer在运行时,self就释放了会引起野指针错误。因此最有效的解决办法如下:
#pragma mark 专辑图片的旋转
//实现图片的旋转效果 弧度 = 度数 / 180 * M_PI
-(void)rotate {
_musicIcon.transform = CGAffineTransformRotate(_musicIcon.transform, 0.5/180 * M_PI );
}
-(void)reloadUI:(MusicModel*)model
{
self.musicName.text = model.name;
self.artist.text = model.artist;
[self.musicIcon sd_setImageWithURL:[NSURL URLWithString:model.cover]];
//创建一个定时器,每隔一段时间去访问rotate方法
self.rotatingTimer = [NSTimer scheduledTimerWithTimeInterval:0.2 target:self selector:@selector(rotate) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:_rotatingTimer forMode:NSRunLoopCommonModes];
self.duration.text = model.duration;
self.playBtn.selected = YES;
self.playSlider.value = 0;
self.loadTimeProgress.progress = 0;
}
//**在视图消失时,要释放计时器**
- (void)viewWillDisappear:(BOOL)animated{
[self.rotatingTimer invalidate];
self.rotatingTimer = nil;
}
4.导入AVFoundation框架,创建AVPlayer播放器
- (void)viewDidLoad {
[super viewDidLoad];
[self loadData]; //加载数据,把数据转成模型,存放在dataSource数组中
self.currentIndex = 0;
[self playBtnAction:self.playBtn];
#pragma mark - 加载数据,存在数组里
-(void)loadData
{
NSString *path = [[NSBundle mainBundle] pathForResource:@"music" ofType:@"json"];
NSData *data = [NSData dataWithContentsOfFile:path];
//反序列化
NSArray *datas = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:nil];
//字典转模型
for (NSDictionary *dic in datas) {
MusicModel *model = [[MusicModel alloc] initWithDic:dic];
[self.dataSource addObject:model]; //把模型存到数组里
}
}
//播放,播放音乐时要监听音乐的状态,进而调用playWithUrl:
- (IBAction)playBtnAction:(UIButton *)sender
{
if (!sender.selected) {
[self playWithUrl:self.dataSource[self.currentIndex]];
sender.selected = YES;
}else{
[self.player pause];
[self removePlayStatus];
[self removePlayLoadTime];
self.currentModel = nil;
sender.selected = NO;
}
}
#pragma mark- 音乐播放相关
//播放音乐
-(void)playWithUrl:(MusicModel*)model
{
AVPlayerItem *item = [[AVPlayerItem alloc] initWithURL:[NSURL URLWithString:model.url]];
//替换当前音乐资源
[self.player replaceCurrentItemWithPlayerItem:item];
//刷新界面UI
[self reloadUI:model];
//监听音乐播放完成通知
[self addNSNotificationForPlayMusicFinish];
//开始播放
[self.player play];
//监听播放器状态
[self addPlayStatus];
//监听音乐缓冲进度
[self addPlayLoadTime];
//监听音乐播放的进度
[self addMusicProgressWithItem:item];
//记录当前播放音乐的索引
self.currentIndex = [model.Id integerValue];
self.currentModel = model;
//音乐锁屏信息展示
[self setupLockScreenInfo];
}
我们来一个个分析playWithUrl:里的方法
1)替换当前音乐
[self.player replaceCurrentItemWithPlayerItem:item];
2)刷新界面UI
上面讲过的设置定时器的那一部分,重新设置歌名,歌手,专辑图片,音乐播放的当前时间和总时间,音乐播放进度和缓冲进度,按钮的状态
-(void)reloadUI:(MusicModel*)model
{
self.musicName.text = model.name;
self.artist.text = model.artist;
[self.musicIcon sd_setImageWithURL:[NSURL URLWithString:model.cover]];
//创建一个定时器,每隔一段时间去访问rotate方法
self.rotatingTimer = [NSTimer scheduledTimerWithTimeInterval:0.2 target:self selector:@selector(rotate) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:_rotatingTimer forMode:NSRunLoopCommonModes];
self.duration.text = model.duration;
self.playBtn.selected = YES;
self.playSlider.value = 0;
self.loadTimeProgress.progress = 0;
}
3)监听音乐播放完成通知
#pragma mark - NSNotification
-(void)addNSNotificationForPlayMusicFinish
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
//给AVPlayerItem添加播放完成通知,播放完成放下一首的通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playFinished:) name:AVPlayerItemDidPlayToEndTimeNotification object:_player.currentItem];
}
-(void)playFinished:(NSNotification*)notification
{
//播放下一首
[self nextBtnAction:nil];
}
4).开始播放
//开始播放
[self.player play];
5).监听播放器状态
#pragma mark - 监听音乐各种状态
//通过KVO监听播放器状态
-(void)addPlayStatus
{
[self.player.currentItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
}
//移除监听播放器状态1
-(void)removePlayStatus
{
if (self.currentModel == nil) {return;}
[self.player.currentItem removeObserver:self forKeyPath:@"status"];
}
6).监听音乐缓冲进度
//KVO监听音乐缓冲状态
-(void)addPlayLoadTime
{
[self.player.currentItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];
}
//移除监听音乐缓冲状态
-(void)removePlayLoadTime
{
if (self.currentModel == nil) {return;}
[self.player.currentItem removeObserver:self forKeyPath:@"loadedTimeRanges"];
}
7).监听音乐播放的进度
//监听音乐播放的进度
-(void)addMusicProgressWithItem:(AVPlayerItem *)item
{
//移除监听音乐播放进度
[self removeTimeObserver];
__weak typeof(self) weakSelf = self;
self.timeObserver = [self.player addPeriodicTimeObserverForInterval:CMTimeMake(1.0, 1.0) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
//当前播放的时间
float current = CMTimeGetSeconds(time);
//总时间
float total = CMTimeGetSeconds(item.duration);
if (current) {
float progress = current / total;
//更新播放进度条
weakSelf.playSlider.value = progress;
weakSelf.currentTime.text = [weakSelf timeFormatted:current];
}
}];
}
8).观察者回调
//观察者回调
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
if ([keyPath isEqualToString:@"status"]) {
switch (self.player.status) {
case AVPlayerStatusUnknown:
{
NSLog(@"未知转态");
}
break;
case AVPlayerStatusReadyToPlay:
{
NSLog(@"准备播放");
}
break;
case AVPlayerStatusFailed:
{
NSLog(@"加载失败");
}
break;
default:
break;
}
}
if ([keyPath isEqualToString:@"loadedTimeRanges"]) {
NSArray * timeRanges = self.player.currentItem.loadedTimeRanges;
//本次缓冲的时间范围
CMTimeRange timeRange = [timeRanges.firstObject CMTimeRangeValue];
//缓冲总长度
NSTimeInterval totalLoadTime = CMTimeGetSeconds(timeRange.start) + CMTimeGetSeconds(timeRange.duration);
//音乐的总时间
NSTimeInterval duration = CMTimeGetSeconds(self.player.currentItem.duration);
//计算缓冲百分比例
NSTimeInterval scale = totalLoadTime/duration;
//更新缓冲进度条
self.loadTimeProgress.progress = scale;
}
}
9).记录当前音乐播放的索引
//记录当前播放音乐的索引
self.currentIndex = [model.Id integerValue];
self.currentModel = model;
10).音乐锁屏信息 [self setupLockScreenInfo];
#pragma mark - 设置锁屏信息
//音乐锁屏信息展示
- (void)setupLockScreenInfo
{
// 1.获取锁屏中心
MPNowPlayingInfoCenter *playingInfoCenter = [MPNowPlayingInfoCenter defaultCenter];
//初始化一个存放音乐信息的字典
NSMutableDictionary *playingInfoDict = [NSMutableDictionary dictionary];
// 2、设置歌曲名
if (self.currentModel.name) {
[playingInfoDict setObject:self.currentModel.name forKey:MPMediaItemPropertyAlbumTitle];
}
// 设置歌手名
if (self.currentModel.artist) {
[playingInfoDict setObject:self.currentModel.artist forKey:MPMediaItemPropertyArtist];
}
// 3设置封面的图片
UIImage *image = [self getMusicImageWithMusicId:self.currentModel];
if (image) {
MPMediaItemArtwork *artwork = [[MPMediaItemArtwork alloc] initWithImage:image];
[playingInfoDict setObject:artwork forKey:MPMediaItemPropertyArtwork];
}
// 4设置歌曲的总时长
[playingInfoDict setObject:self.currentModel.detailDuration forKey:MPMediaItemPropertyPlaybackDuration];
//音乐信息赋值给获取锁屏中心的nowPlayingInfo属性
playingInfoCenter.nowPlayingInfo = playingInfoDict;
// 5.开启远程交互
[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
}
//获取远程网络图片,如有缓存取缓存,没有缓存,远程加载并缓存
-(UIImage*)getMusicImageWithMusicId:(MusicModel*)model
{
UIImage *image;
NSString *key = [model.Id stringValue];
UIImage *cacheImage = self.musicImageDic[key];
if (cacheImage) {
image = cacheImage;
}else{
//这里用了非常规的做法,仅用于demo快速测试,实际开发不推荐,会堵塞主线程
//建议加载歌曲时先把网络图片请求下来再设置
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:model.cover]];
image = [UIImage imageWithData:data];
if (image) {
[self.musicImageDic setObject:image forKey:key];
}
}
return image;
}
//监听远程交互方法
- (void)remoteControlReceivedWithEvent:(UIEvent *)event
{
switch (event.subtype) {
//播放
case UIEventSubtypeRemoteControlPlay:{
[self.player play];
}
break;
//停止
case UIEventSubtypeRemoteControlPause:{
[self.player pause];
}
break;
//下一首
case UIEventSubtypeRemoteControlNextTrack:
[self nextBtnAction:nil];
break;
//上一首
case UIEventSubtypeRemoteControlPreviousTrack:
[self lastBtnAction:nil];
break;
default:
break;
}
}
附上github地址:(https://github.com/lizichenzi/onlionmusic/tree/master)