iOS开发之网络音乐播放器(SC音乐)
前言
一直都想做一款自己的网络音乐播放器,两个月前做了一个swift版的网络音乐播放器,但是那个播放器数据来源于我自己用VPS和nginx搭建的服务器,所有的文件都要自己准备,包括mp3、歌词、专辑图片等,非常麻烦,有兴趣的可以跟我要源码。现在这款音乐播放器数据是来源于百度音乐,前前后后花了一个多星期搞定,网上有一些音乐网站的API,有兴趣的同学可以去查一下。我这里贴一下我自己用到的百度音乐API:http://blog.csdn.net/zuiaisha1/article/details/61200422。
正题
一、播放控制
SC音乐用的是AVPlayer,这个库是苹果自带的视频库,也可以播放音频,可以支持边播放边缓存,使用也比较简单。详细看苹果官网介绍:https://developer.apple.com/documentation/avfoundation/avplayer。这里介绍一下要用到的东西。我们知道,播放器要有播放、暂停、上一曲、下一曲的功能,还要知道播放总时间,当前时间,播放状态,能够从歌曲的任意时间点开始播放。在AVPlayer库中:
play ---- 播放
pause ---- 暂停
rate ---- 播放状态(0.0代表当前状态是暂停, 1.0代表当前状态是播放)
seekToTime ---- 从某个时间点开始播放(拖动进度条用到)
duration ---- 歌曲总时间
currentTime ---- 当前播放时间
上一曲和下一曲可以通过改变歌曲url来实现。
初始化一个AVPlayer需要一个playItem,所以先初始化一个playItem,再用这个playItem去实例化一个play,具体代码:
MusicPlayerManager.h
//
// MusicPlayerManager.h
// BaiduMusic
//
// Created by 凌 陈 on 8/21/17.
// Copyright © 2017 凌 陈. All rights reserved.
//
#import
#import
@interface MusicPlayerManager : NSObject
typedef enum : NSUInteger {
RepeatPlayMode,
RepeatOnlyOnePlayMode,
ShufflePlayMode,
} ShuffleAndRepeatState;
@property (nonatomic,strong) AVPlayer *play;
@property (nonatomic,strong) AVPlayerItem *playItem;
@property (nonatomic,assign) ShuffleAndRepeatState shuffleAndRepeatState;
@property (nonatomic,assign) NSInteger playingIndex;
+ (MusicPlayerManager *)sharedManager;
-(void) setPlayItem: (NSString *)songURL;
-(void) setPlay;
-(void) startPlay;
-(void) stopPlay;
-(void) play: (NSString *)songURL;
@end
MusicPlayerManager.m
//
// MusicPlayerManager.m
// BaiduMusic
//
// Created by 凌 陈 on 8/21/17.
// Copyright © 2017 凌 陈. All rights reserved.
//
#import "MusicPlayerManager.h"
@implementation MusicPlayerManager
static MusicPlayerManager *_sharedManager = nil;
+(MusicPlayerManager *)sharedManager {
@synchronized( [MusicPlayerManager class] ){
if(!_sharedManager)
_sharedManager = [[self alloc] init];
return _sharedManager;
}
return nil;
}
-(void) setPlayItem: (NSString *)songURL {
NSURL * url = [NSURL URLWithString:songURL];
_playItem = [[AVPlayerItem alloc] initWithURL:url];
}
-(void) setPlay {
_play = [[AVPlayer alloc] initWithPlayerItem:_playItem];
}
-(void) startPlay {
[_play play];
}
-(void) stopPlay {
[_play pause];
}
-(void) play: (NSString *)songURL {
[self setPlayItem:songURL];
[self setPlay];
[self startPlay];
}
@end
将一首歌的url传进play方法就可以实现播放音乐了。上一曲下一曲只是改变一下歌曲的url就可以实现。
歌曲总时长:
_play.currentItem.duration
当前播放时间:
_play.currentTime
从某个时间点开始播放:
//播放器定位到对应的位置
CMTime targetTime = CMTimeMake((int64_t)(currentTime), 1);
[musicPlayer.play seekToTime:targetTime];
播放状态:
//播放或者暂停按键按下,要判断播放状态
if (_play.rate == 0) {
// 当前状态为暂停
// 下面要执行播放的代码
} else {
// 当前状态为播放
// 下面要执行暂停的代码
}
监管播放(更新播放进度条和当前时间):
_playerTimeObserver = [musicPlayer.play addPeriodicTimeObserverForInterval:CMTimeMake(1.0, 1.0) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
// 这里一秒进来一次,可以更新时间、播放进度条和歌词
// 需要注意的是_playerTimeObserver必须要每首歌播放结束后清掉,不清会有问题。
}
播放结束通知:
// 歌曲播放结束后会调用自定义的方法finishedPlaying
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(finishedPlaying) name:AVPlayerItemDidPlayToEndTimeNotification object:_play.currentItem];
二、获取百度音乐数据
百度音乐的全接口:http://tingapi.ting.baidu.com/v1/restserver/ting。所有的数据都是以这个为开头,后面加一些其他东西。
可以请求到的数据有很多,这里只说几个:
一、获取歌曲列表(新歌榜、热歌榜、经典老歌榜等)
例:method=baidu.ting.billboard.billList&type=1&size=10&offset=0
完整的请求地址:http://tingapi.ting.baidu.com/v1/restserver/ting?from=qianqian&version=2.1.0&method=baidu.ting.billboard.billList&format=json&type=1&offset=0&size=100 (前100热门歌曲,要获取哪个榜只需要改变一下type的值就行了)
参数: type = 1-新歌榜,2-热歌榜,11-摇滚榜,12-爵士,16-流行,21-欧美金曲榜,22-经典老歌榜,23-情歌对唱榜,24-影视金曲榜,25-网络歌曲榜
size = 10 //返回条目数量
offset = 0 //获取偏移
获取到的数据如截图所示:
我们只需要“song_list”里面的数据,点开后发现“song_list“就是一个字典:
继续点开[0],里面是一首歌的信息,包括歌名、歌手名、专辑名等等,但是并没有歌曲url,别急这个要另外获取,需要用到这里的"song_id".
二、获取歌曲url
例:method=baidu.ting.song.lry&songid=877578
完整的请求地址:http://tingapi.ting.baidu.com/v1/restserver/ting?from=qianqian&version=2.1.0&method=baidu.ting.song.play&songid=877578
参数:songid = 877578 //假设这个是歌曲id
获取到的数据如图所示:
我们只关注”bitrate“中的数据,点开发现里面有”file_link“,这个就是歌曲的url:
网络请求我用大名鼎鼎的AFNetworking,获取到的数据解析我用MJExtension,主要将数据转成NSArray,将这两个库拉入自己的工程,添加头文件#import "AFNetworking.h" #import "MJExtension.h"即可。
// 获取歌曲信息请求
// 新歌榜
- (void)loadNewSongs
{
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
NSString *path = @"http://tingapi.ting.baidu.com/v1/restserver/ting?from=qianqian&version=2.1.0&method=baidu.ting.billboard.billList&format=json&type=1&offset=0&size=100";//前100热门歌曲
[manager GET:path parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
if ([responseObject isKindOfClass:[NSDictionary class]])
{
NSArray *array = [responseObject objectForKey:@"song_list"];
songInfo.OMSongs = [OMHotSongInfo mj_objectArrayWithKeyValuesArray:array];
// [self reloadTableView:_radioAndMusicTableView];
[_mytableView reloadData];
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(@"error--%@",error);
}];
}
其它的榜单改一下type的值就可以了。
获取歌曲url请求:
-(void)getSelectedSong: (NSString *)songID index: (long)index {
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
NSString *path = [@"http://tingapi.ting.baidu.com/v1/restserver/ting?from=qianqian&version=2.1.0&method=baidu.ting.song.play&songid=" stringByAppendingString:songID];
[manager GET:path parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
if ([responseObject isKindOfClass:[NSDictionary class]])
{
NSDictionary *array = [responseObject objectForKey:@"bitrate"];
self.file_link = [array objectForKey:@"file_link"];
self.file_size = [array objectForKey:@"file_size"];
self.file_duration = [array objectForKey:@"file_duration"];
self.playSongIndex = index;
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(@"error--%@",error);
}];
}
三、播放界面
1、圆形专辑图片(原先是矩形的,需要处理一下)
.h文件
@property (nonatomic, strong) UIView *playControllerView;
@property (nonatomic, strong) UIImageView *currentPlaySongImage;
.m文件
// 专辑图片
// 先将专辑图片放到正方形UIImageView, 再将UIImageView圆角设置为正方形边长的一半就得到圆形的UIImageView了
_currentPlaySongImage = [[SCImageView alloc] initWithFrame:CGRectMake(10, 10 , _playControllerView.frame.size.height - 20 , _playControllerView.frame.size.height - 20)];
_currentPlaySongImage.image = [UIImage imageNamed:@"album_default"];
_currentPlaySongImage.clipsToBounds = true;
_currentPlaySongImage.layer.cornerRadius = (_playControllerView.frame.size.height - 20) * 0.5;
[_playControllerView addSubview:_currentPlaySongImage];
2、专辑图片旋转
我封装了一个UIImageView的旋转动画类,代码如下:
SCImageView.h
//
// SCImageView.h
// BaiduMusic
//
// Created by 凌 陈 on 8/22/17.
// Copyright © 2017 凌 陈. All rights reserved.
//
#import
@interface SCImageView : UIImageView
-(void) startRotating;
-(void) stopRotating;
-(void) resumeRotate;
@end
SCImageView.c
//
// SCImageView.m
// BaiduMusic
//
// Created by 凌 陈 on 8/22/17.
// Copyright © 2017 凌 陈. All rights reserved.
//
#import "SCImageView.h"
@implementation SCImageView
// 开始旋转
-(void) startRotating {
CABasicAnimation* rotateAnimation = [CABasicAnimation animationWithKeyPath:@"transform.rotation"];
rotateAnimation.fromValue = [NSNumber numberWithFloat:0.0];
rotateAnimation.toValue = [NSNumber numberWithFloat:M_PI * 2]; // 旋转一周
rotateAnimation.duration = 20.0; // 旋转时间20秒
rotateAnimation.repeatCount = MAXFLOAT; // 重复次数,这里用最大次数
[self.layer addAnimation:rotateAnimation forKey:nil];
}
// 停止旋转
-(void) stopRotating {
CFTimeInterval pausedTime = [self.layer convertTime:CACurrentMediaTime() fromLayer:nil];
self.layer.speed = 0.0; // 停止旋转
self.layer.timeOffset = pausedTime; // 保存时间,恢复旋转需要用到
}
// 恢复旋转
-(void) resumeRotate {
If (self.layer.timeOffset == 0) {
[self startRotating];
return;
}
CFTimeInterval pausedTime = self.layer.timeOffset;
self.layer.speed = 1.0; // 开始旋转
self.layer.timeOffset = 0.0;
self.layer.beginTime = 0.0;
CFTimeInterval timeSincePause = [self.layer convertTime:CACurrentMediaTime() fromLayer:nil] - pausedTime; // 恢复时间
self.layer.beginTime = timeSincePause; // 从暂停的时间点开始旋转
}
@end
歌曲刚开始播放调用startRotating,开始旋转,点击暂停按键时调用stopRotating停止旋转,点击播放按键时调用resumeRotate恢复旋转(如果调用startRotating则又从头开始旋转)。
3、歌词解析和歌词滚动
首先我们先来看一下lrc文件的格式,如图:
不难发现,除去歌词头部信息后,正文前面[]里面是时间点,右边才是歌词,也就是一段歌词对应一个时间点,这个时间点是开始点,也就是说当歌曲播放到00:49.65这个时间点的时候,歌词应该滚动到“因为在 一千年以后”这段。知道原理就好办了。先解析歌词。
// 解析歌词
.h文件
@property (nonatomic,strong) NSMutableDictionary *mLRCDictinary;
@property (nonatomic,strong) NSMutableArray *mTimeArray;
@property (nonatomic, assign) BOOL mIsLRCPrepared;
.m文件
-(void) AnalysisLRC: (NSString *)lrcStr {
NSString* contentStr = lrcStr;
NSArray *lrcArray = [contentStr componentsSeparatedByString:@"\n"];
[mLRCDictinary removeAllObjects];
[mTimeArray removeAllObjects];
for (NSString *line in lrcArray) {
// 首先处理歌词中无用的东西
// [ti:][ar:][al:]这类的直接跳过
if ([line containsString:@"[0"] || [line containsString:@"[1"] || [line containsString:@"[2"] || [line containsString:@"[3"]) {
NSArray *lineArr = [line componentsSeparatedByString:@"]"];
NSString *str1 = [line substringWithRange:NSMakeRange(3, 1)];
NSString *str2 = [line substringWithRange:NSMakeRange(6, 1)];
if ([str1 isEqualToString:@":"] && [str2 isEqualToString:@"."]) {
NSString *lrcStr = lineArr[1];
NSString *timeStr = [lineArr[0] substringWithRange:NSMakeRange(1, 5)];
[songInfo.mLRCDictinary setObject:lrcStr forKey:timeStr];
[songInfo.mTimeArray addObject:timeStr];
}
} else {
continue;
}
}
_mIsLRCPrepared = true;
[self.tableView reloadData];
}
mLRCDictinary存放配对时间点和歌词段,mTimeArray存放时间点,通过时间点来找到相应的歌词段,不过这个时间点是NSString格式,需要转成int(我的精度要求不高,只到秒,后面的小数没要)
NSString转int
-(int) stringToInt: (NSString *)timeString {
NSArray *strTemp = [timeString componentsSeparatedByString:@":"];
int time = [strTemp.firstObject intValue] * 60 + [strTemp.lastObject intValue];
return time;
}
int转NSString(显示当前播放时间要用到)
-(NSString *)intToString: (int)needTransformInteger {
//实现00:00这种格式播放时间
int wholeTime = needTransformInteger;
int min = wholeTime / 60;
int sec = wholeTime % 60;
NSString *str = [NSString stringWithFormat:@"%02d:%02d", min , sec];
return str;
}
歌词滚动:
// songInfo.lrcIndex记录歌词第几行,用currentTime 和 mTimeArray中第几行歌词的时间相比较,大于那个时间歌词tableView滚动到那一行。
if (songInfo.lrcIndex <= songInfo.mLRCDictinary.count - 1) {
if ((int)currentTime >= [songInfo stringToInt:songInfo.mTimeArray[songInfo.lrcIndex]]) {
_deliverView.midView.midLrcView.currentRow = songInfo.lrcIndex;
//
[_deliverView.midView.midLrcView.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:_deliverView.midView.midLrcView.currentRow inSection:0] atScrollPosition:UITableViewScrollPositionMiddle animated:YES];
[_deliverView.midView.midLrcView.tableView reloadData];
songInfo.lrcIndex = songInfo.lrcIndex + 1;
}
}
先写到这里,后续还会补充锁屏播放设置,后台播放设置,手势操作等。如果各位觉得还可以,别忘了加个星星哦!
CSDN地址:http://blog.csdn.net/u014636932/article/details/77622358
这里附上github地址:https://github.com/Mozartisnotmyname/SCMusic.git