iOS开发之网络音乐播放器(SC音乐)(一)
ios开发之网络音乐播放器(sc音乐)(一)
前言
一直都想做一款自己的网络音乐播放器,两个月前做了一个swift版的网络音乐播放器,但是那个播放器数据来源于我自己用vps和nginx搭建的服务器,所有的文件都要自己准备,包括mp3、歌词、专辑图片等,非常麻烦,有兴趣的可以跟我要。现在这款音乐播放器数据是来源于百度音乐,前前后后花了一个多星期搞定,网上有一些音乐网站的api,有兴趣的同学可以去查一下。
一、播放控制
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];
二、获取百度音乐数据
百度音乐的全接口:https://tingapi.ting.baidu.com/v1/restserver/ting。所有的数据都是以这个为开头,后面加一些其他东西。
可以请求到的数据有很多,这里只说几个:
一、获取歌曲列表(新歌榜、热歌榜、经典老歌榜等)
例:method=baidu.ting.billboard.billlist&type=1&size=10&offset=0完整的请求地址:https://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完整的请求地址:https://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 = @"https://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 = [@"https://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; } }
先写到这里,后续还会补充锁屏播放设置,后台播放设置,手势操作等。