iOS開發(fā)之網(wǎng)絡(luò)音樂(lè)播放器(SC音樂(lè))
前言
一直都想做一款自己的網(wǎng)絡(luò)音樂(lè)播放器,兩個(gè)月前做了一個(gè)swift版的網(wǎng)絡(luò)音樂(lè)播放器,但是那個(gè)播放器數(shù)據(jù)來(lái)源于我自己用VPS和nginx搭建的服務(wù)器,所有的文件都要自己準(zhǔn)備,包括mp3、歌詞、專輯圖片等,非常麻煩,有興趣的可以跟我要源碼。現(xiàn)在這款音樂(lè)播放器數(shù)據(jù)是來(lái)源于百度音樂(lè),前前后后花了一個(gè)多星期搞定,網(wǎng)上有一些音樂(lè)網(wǎng)站的API,有興趣的同學(xué)可以去查一下。我這里貼一下我自己用到的百度音樂(lè)API:http://blog.csdn.net/zuiaisha1/article/details/61200422。
正題
一、播放控制
SC音樂(lè)用的是AVPlayer,這個(gè)庫(kù)是蘋果自帶的視頻庫(kù),也可以播放音頻,可以支持邊播放邊緩存,使用也比較簡(jiǎn)單。詳細(xì)看蘋果官網(wǎng)介紹:https://developer.apple.com/documentation/avfoundation/avplayer。這里介紹一下要用到的東西。我們知道,播放器要有播放、暫停、上一曲、下一曲的功能,還要知道播放總時(shí)間,當(dāng)前時(shí)間,播放狀態(tài),能夠從歌曲的任意時(shí)間點(diǎn)開始播放。在AVPlayer庫(kù)中:
play ---- 播放
pause ---- 暫停
rate ---- 播放狀態(tài)(0.0代表當(dāng)前狀態(tài)是暫停, 1.0代表當(dāng)前狀態(tài)是播放)
seekToTime ---- 從某個(gè)時(shí)間點(diǎn)開始播放(拖動(dòng)進(jìn)度條用到)
duration ---- 歌曲總時(shí)間
currentTime ---- 當(dāng)前播放時(shí)間
上一曲和下一曲可以通過(guò)改變歌曲url來(lái)實(shí)現(xiàn)。
初始化一個(gè)AVPlayer需要一個(gè)playItem,所以先初始化一個(gè)playItem,再用這個(gè)playItem去實(shí)例化一個(gè)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傳進(jìn)play方法就可以實(shí)現(xiàn)播放音樂(lè)了。上一曲下一曲只是改變一下歌曲的url就可以實(shí)現(xiàn)。
歌曲總時(shí)長(zhǎng):
_play.currentItem.duration
當(dāng)前播放時(shí)間:
_play.currentTime
從某個(gè)時(shí)間點(diǎn)開始播放:
//播放器定位到對(duì)應(yīng)的位置
CMTime targetTime = CMTimeMake((int64_t)(currentTime), 1);
[musicPlayer.play seekToTime:targetTime];
播放狀態(tài):
//播放或者暫停按鍵按下,要判斷播放狀態(tài)
if (_play.rate == 0) {
// 當(dāng)前狀態(tài)為暫停
// 下面要執(zhí)行播放的代碼
} else {
// 當(dāng)前狀態(tài)為播放
// 下面要執(zhí)行暫停的代碼
}
監(jiān)管播放(更新播放進(jìn)度條和當(dāng)前時(shí)間):
_playerTimeObserver = [musicPlayer.play addPeriodicTimeObserverForInterval:CMTimeMake(1.0, 1.0) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
// 這里一秒進(jìn)來(lái)一次,可以更新時(shí)間、播放進(jìn)度條和歌詞
// 需要注意的是_playerTimeObserver必須要每首歌播放結(jié)束后清掉,不清會(huì)有問(wèn)題。
}
播放結(jié)束通知:
// 歌曲播放結(jié)束后會(huì)調(diào)用自定義的方法finishedPlaying
[[NSNotificationCenter defaultCenter] ?addObserver:self selector:@selector(finishedPlaying) name:AVPlayerItemDidPlayToEndTimeNotification object:_play.currentItem];
二、獲取百度音樂(lè)數(shù)據(jù)
百度音樂(lè)的全接口:http://tingapi.ting.baidu.com/v1/restserver/ting。所有的數(shù)據(jù)都是以這個(gè)為開頭,后面加一些其他東西。
可以請(qǐng)求到的數(shù)據(jù)有很多,這里只說(shuō)幾個(gè):
一、獲取歌曲列表(新歌榜、熱歌榜、經(jīng)典老歌榜等)
例:method=baidu.ting.billboard.billList&type=1&size=10&offset=0
完整的請(qǐng)求地址: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熱門歌曲,要獲取哪個(gè)榜只需要改變一下type的值就行了)
參數(shù): type = 1-新歌榜,2-熱歌榜,11-搖滾榜,12-爵士,16-流行,21-歐美金曲榜,22-經(jīng)典老歌榜,23-情歌對(duì)唱榜,24-影視金曲榜,25-網(wǎng)絡(luò)歌曲榜
size = 10 //返回條目數(shù)量
offset = 0 //獲取偏移
獲取到的數(shù)據(jù)如截圖所示:
我們只需要“song_list”里面的數(shù)據(jù),點(diǎn)開后發(fā)現(xiàn)“song_list“就是一個(gè)字典:
繼續(xù)點(diǎn)開[0],里面是一首歌的信息,包括歌名、歌手名、專輯名等等,但是并沒(méi)有歌曲url,別急這個(gè)要另外獲取,需要用到這里的"song_id".
二、獲取歌曲url
例:method=baidu.ting.song.lry&songid=877578
完整的請(qǐng)求地址:http://tingapi.ting.baidu.com/v1/restserver/ting?from=qianqian&version=2.1.0&method=baidu.ting.song.play&songid=877578
參數(shù):songid = 877578 //假設(shè)這個(gè)是歌曲id
獲取到的數(shù)據(jù)如圖所示:
我們只關(guān)注”bitrate“中的數(shù)據(jù),點(diǎn)開發(fā)現(xiàn)里面有”file_link“,這個(gè)就是歌曲的url:
網(wǎng)絡(luò)請(qǐng)求我用大名鼎鼎的AFNetworking,獲取到的數(shù)據(jù)解析我用MJExtension,主要將數(shù)據(jù)轉(zhuǎn)成NSArray,將這兩個(gè)庫(kù)拉入自己的工程,添加頭文件#import "AFNetworking.h" ? #import "MJExtension.h"即可。
// 獲取歌曲信息請(qǐng)求
// 新歌榜
- (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請(qǐng)求:
-(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圓角設(shè)置為正方形邊長(zhǎng)的一半就得到圓形的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、專輯圖片旋轉(zhuǎn)
我封裝了一個(gè)UIImageView的旋轉(zhuǎn)動(dòng)畫類,代碼如下:
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
// 開始旋轉(zhuǎn)
-(void) startRotating {
? ? CABasicAnimation* rotateAnimation = [CABasicAnimation animationWithKeyPath:@"transform.rotation"];
? ? rotateAnimation.fromValue = [NSNumber numberWithFloat:0.0];
? ? rotateAnimation.toValue = [NSNumber numberWithFloat:M_PI * 2];? // 旋轉(zhuǎn)一周
? ? rotateAnimation.duration = 20.0;? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? // 旋轉(zhuǎn)時(shí)間20秒
? ? rotateAnimation.repeatCount = MAXFLOAT;? ? ? ? ? ? ? ? ? ? ? ? ? // 重復(fù)次數(shù),這里用最大次數(shù)
? ? [self.layer addAnimation:rotateAnimation forKey:nil];
}
// 停止旋轉(zhuǎn)
-(void) stopRotating {
? ? CFTimeInterval pausedTime = [self.layer convertTime:CACurrentMediaTime() fromLayer:nil];
? ? self.layer.speed = 0.0;? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? // 停止旋轉(zhuǎn)
? ? self.layer.timeOffset = pausedTime;? ? ? ? ? ? ? ? ? ? ? ? ? ? ? // 保存時(shí)間,恢復(fù)旋轉(zhuǎn)需要用到
}
// 恢復(fù)旋轉(zhuǎn)
-(void) resumeRotate {
? ? If (self.layer.timeOffset == 0) {
? ? ? ? [self startRotating];
? ? ? ? return;
? ? }
? ? CFTimeInterval pausedTime = self.layer.timeOffset;
? ? self.layer.speed = 1.0;? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? // 開始旋轉(zhuǎn)
? ? self.layer.timeOffset = 0.0;
? ? self.layer.beginTime = 0.0;
? ? CFTimeInterval timeSincePause = [self.layer convertTime:CACurrentMediaTime() fromLayer:nil] - ? ?pausedTime;? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? // 恢復(fù)時(shí)間
? ? self.layer.beginTime = timeSincePause;? ? ? ? ? ? ? ? ? ? ? ? ? // 從暫停的時(shí)間點(diǎn)開始旋轉(zhuǎn)
}
@end
歌曲剛開始播放調(diào)用startRotating,開始旋轉(zhuǎn),點(diǎn)擊暫停按鍵時(shí)調(diào)用stopRotating停止旋轉(zhuǎn),點(diǎn)擊播放按鍵時(shí)調(diào)用resumeRotate恢復(fù)旋轉(zhuǎn)(如果調(diào)用startRotating則又從頭開始旋轉(zhuǎn))。
3、歌詞解析和歌詞滾動(dòng)
首先我們先來(lái)看一下lrc文件的格式,如圖:
不難發(fā)現(xiàn),除去歌詞頭部信息后,正文前面[]里面是時(shí)間點(diǎn),右邊才是歌詞,也就是一段歌詞對(duì)應(yīng)一個(gè)時(shí)間點(diǎn),這個(gè)時(shí)間點(diǎn)是開始點(diǎn),也就是說(shuō)當(dāng)歌曲播放到00:49.65這個(gè)時(shí)間點(diǎn)的時(shí)候,歌詞應(yīng)該滾動(dòng)到“因?yàn)樵?一千年以后”這段。知道原理就好辦了。先解析歌詞。
// 解析歌詞
.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) {
// 首先處理歌詞中無(wú)用的東西
// [ti:][ar:][al:]這類的直接跳過(guò)
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存放配對(duì)時(shí)間點(diǎn)和歌詞段,mTimeArray存放時(shí)間點(diǎn),通過(guò)時(shí)間點(diǎn)來(lái)找到相應(yīng)的歌詞段,不過(guò)這個(gè)時(shí)間點(diǎn)是NSString格式,需要轉(zhuǎn)成int(我的精度要求不高,只到秒,后面的小數(shù)沒(méi)要)
NSString轉(zhuǎn)int
-(int) stringToInt: (NSString *)timeString {
NSArray *strTemp = [timeString componentsSeparatedByString:@":"];
int time = [strTemp.firstObject intValue] * 60 + [strTemp.lastObject intValue];
return time;
}
int轉(zhuǎn)NSString(顯示當(dāng)前播放時(shí)間要用到)
-(NSString *)intToString: (int)needTransformInteger {
//實(shí)現(xiàn)00:00這種格式播放時(shí)間
int wholeTime = needTransformInteger;
int min? = wholeTime / 60;
int sec = wholeTime % 60;
NSString *str = [NSString stringWithFormat:@"%02d:%02d", min , sec];
return str;
}
歌詞滾動(dòng):
// songInfo.lrcIndex記錄歌詞第幾行,用currentTime 和 mTimeArray中第幾行歌詞的時(shí)間相比較,大于那個(gè)時(shí)間歌詞tableView滾動(dòng)到那一行。
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;
}
}
先寫到這里,后續(xù)還會(huì)補(bǔ)充鎖屏播放設(shè)置,后臺(tái)播放設(shè)置,手勢(shì)操作等。如果各位覺(jué)得還可以,別忘了加個(gè)星星哦!
CSDN地址:http://blog.csdn.net/u014636932/article/details/77622358
這里附上github地址:https://github.com/Mozartisnotmyname/SCMusic.git