iOS之基于FreeStreamer的簡單音樂播放器示例
前提準(zhǔn)備
為了能夠有明確的思路來做這個(gè)demo,我下載了QQ音樂和網(wǎng)易云音樂,然后分別對比,最終選擇了QQ音樂來參照,先是獲取了其中的所有資源文件(如果有不知道怎么提取資源文件的,可以參考iOS提取APP中的圖片資源),在這之后就是研究使用技術(shù),這里我選擇了FreeStreamer,雖然系統(tǒng)也有,但是該框架可能更好用點(diǎn)。
實(shí)現(xiàn)部分
在這之前,先來看看大概效果圖吧


再看完效果圖之后,我們就來看看這其中涉及到的幾個(gè)難點(diǎn)吧(在我看開~)
1、先讓播放器跑起來
這里我使用的是pods來管理三方庫,代碼如下
platform:ios,'8.0' target "GLMusicBox" do pod 'FreeStreamer', '~> 3.7.3' pod 'SDWebImage', '~> 4.0.0' pod 'MJRefresh', '~> 3.1.11' pod 'Masonry', '~> 1.0.2' pod 'Reachability', '~> 3.2' pod 'AFNetworking', '~> 3.0' pod 'IQKeyboardManager', '~> 3.3.2' end
針對FreeStreamer我簡單進(jìn)行了封裝下
#import "FSAudioStream.h"
@class GLMusicLRCModel;
typedef NS_ENUM(NSInteger,GLLoopState){
GLSingleLoop = 0,//單曲循環(huán)
GLForeverLoop,//重復(fù)循環(huán)
GLRandomLoop,//隨機(jī)播放
GLOnceLoop//列表一次順序播放
};
@protocol GLMusicPlayerDelegate/**
*
實(shí)時(shí)更新
*
**/
- (void)updateProgressWithCurrentPosition:(FSStreamPosition)currentPosition endPosition:(FSStreamPosition)endPosition;
- (void)updateMusicLrc;
@end
@interface GLMusicPlayer : FSAudioStream
/**
*
播放列表
*
**/
@property (nonatomic,strong) NSMutableArray *musicListArray;
/**
當(dāng)前播放歌曲的歌詞
*/
@property (nonatomic,strong) NSMutableArray *musicLRCArray;
/**
*
當(dāng)前播放
*
**/
@property (nonatomic,assign,readonly) NSUInteger currentIndex;
/**
*
當(dāng)前播放的音樂的標(biāo)題
*
**/
@property (nonatomic,strong) NSString *currentTitle;
/**
是否是暫停狀態(tài)
*/
@property (nonatomic,assign) BOOL isPause;
@property (nonatomic,weak) idglPlayerDelegate;
//默認(rèn) 重復(fù)循環(huán) GLForeverLoop
@property (nonatomic,assign) GLLoopState loopState;
/**
*
單例播放器
*
**/
+ (instancetype)defaultPlayer;
/**
播放隊(duì)列中的指定的文件
@param index 序號(hào)
*/
- (void)playMusicAtIndex:(NSUInteger)index;
/**
播放前一首
*/
- (void)playFont;
/**
播放下一首
*/
- (void)playNext;
@end
這里繼承了FSAudioStream,并且采用了單例模式
+ (instancetype)defaultPlayer
{
static GLMusicPlayer *player = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
FSStreamConfiguration *config = [[FSStreamConfiguration alloc] init];
config.httpConnectionBufferSize *=2;
config.enableTimeAndPitchConversion = YES;
player = [[super alloc] initWithConfiguration:config];
player.delegate = (id)self;
player.onFailure = ^(FSAudioStreamError error, NSString *errorDescription) {
//播放錯(cuò)誤
//有待解決
};
player.onCompletion = ^{
//播放完成
NSLog(@" 打印信息: 播放完成1");
};
player.onStateChange = ^(FSAudioStreamState state) {
switch (state) {
case kFsAudioStreamPlaying:
{
NSLog(@" 打印信息 playing.....");
player.isPause = NO;
[GLMiniMusicView shareInstance].palyButton.selected = YES;
}
break;
case kFsAudioStreamStopped:
{
NSLog(@" 打印信息 stop.....%@",player.url.absoluteString);
}
break;
case kFsAudioStreamPaused:
{
//pause
player.isPause = YES;
[GLMiniMusicView shareInstance].palyButton.selected = NO;
NSLog(@" 打印信息: pause");
}
break;
case kFsAudioStreamPlaybackCompleted:
{
NSLog(@" 打印信息: 播放完成2");
[player playMusicForState];
}
break;
default:
break;
}
};
//設(shè)置音量
[player setVolume:0.5];
//設(shè)置播放速率
[player setPlayRate:1];
player.loopState = GLForeverLoop;
});
return player;
}
然后實(shí)現(xiàn)了播放方法
- (void)playFromURL:(NSURL *)url
{
//根據(jù)地址 在本地找歌詞
NSMutableDictionary *dic = [NSMutableDictionary dictionaryWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"musiclist" ofType:@"plist"]];
for (NSString *playStringKey in dic.allKeys) {
if ([[dic valueForKey:playStringKey] isEqualToString:url.absoluteString]) {
self.currentTitle = playStringKey;
break;
}
}
[self stop];
if (![url.absoluteString isEqualToString:self.url.absoluteString]) {
[super playFromURL:url];
}else{
[self play];
}
NSLog(@" 當(dāng)前播放歌曲:%@",self.currentTitle);
[GLMiniMusicView shareInstance].titleLable.text = self.currentTitle;
//獲取歌詞
NSString *lrcFile = [NSString stringWithFormat:@"%@.lrc",self.currentTitle];
self.musicLRCArray = [NSMutableArray arrayWithArray:[GLMusicLRCModel musicLRCModelsWithLRCFileName:lrcFile]];
if (![self.musicListArray containsObject:url]) {
[self.musicListArray addObject:url];
}
//更新主界面歌詞UI
if (self.glPlayerDelegate && [self.glPlayerDelegate respondsToSelector:@selector(updateMusicLrc)])
{
[self.glPlayerDelegate updateMusicLrc];
}
_currentIndex = [self.musicListArray indexOfObject:url];
if (!_progressTimer) {
_progressTimer = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateProgress)];
[_progressTimer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}
}
在上面的代碼中,有許多邏輯是后面加的,比如更新UI界面,獲取歌詞等處理,如果要實(shí)現(xiàn)簡單的播放,則可以不用重寫該方法,直接通過playFromURL就可以實(shí)現(xiàn)我們的播放功能。
2、更新UI
這里的UI暫不包括歌詞的更新,而只是進(jìn)度條的更新,要更新進(jìn)度條,比不可少的是定時(shí)器,這里我沒有選擇NSTimer,而是選擇了CADisplayLink,至于為什么,我想大家應(yīng)該都比較了解,可以這么來對比,下面引用一段其他博客的對比:
iOS設(shè)備的屏幕刷新頻率是固定的,CADisplayLink在正常情況下會(huì)在每次刷新結(jié)束都被調(diào)用,精確度相當(dāng)高。
NSTimer的精確度就顯得低了點(diǎn),比如NSTimer的觸發(fā)時(shí)間到的時(shí)候,runloop如果在阻塞狀態(tài),觸發(fā)時(shí)間就會(huì)推遲到下一個(gè)runloop周期。并且 NSTimer新增了tolerance屬性,讓用戶可以設(shè)置可以容忍的觸發(fā)的時(shí)間的延遲范圍。
CADisplayLink使用場合相對專一,適合做UI的不停重繪,比如自定義動(dòng)畫引擎或者視頻播放的渲染。NSTimer的使用范圍要廣泛的多,各種需要單次或者循環(huán)定時(shí)處理的任務(wù)都可以使用。在UI相關(guān)的動(dòng)畫或者顯示內(nèi)容使用 CADisplayLink比起用NSTimer的好處就是我們不需要在格外關(guān)心屏幕的刷新頻率了,因?yàn)樗旧砭褪歉聊凰⑿峦降?/p>
使用方法
if (!_progressTimer) {
_progressTimer = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateProgress)];
[_progressTimer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}
更新進(jìn)度
- (void)updateProgress
{
if (self.glPlayerDelegate && [self.glPlayerDelegate respondsToSelector:@selector(updateProgressWithCurrentPosition:endPosition:)])
{
[self.glPlayerDelegate updateProgressWithCurrentPosition:self.currentTimePlayed endPosition:self.duration];
}
[self showLockScreenCurrentTime:(self.currentTimePlayed.second + self.currentTimePlayed.minute * 60) totalTime:(self.duration.second + self.duration.minute * 60)];
}
在這里有兩個(gè)屬性:currentTimePlayed和duration,分別保存著當(dāng)前播放時(shí)間和總時(shí)間,是如下的結(jié)構(gòu)體
typedef struct {
unsigned minute;
unsigned second;
/**
* Playback time in seconds.
*/
float playbackTimeInSeconds;
/**
* Position within the stream, where 0 is the beginning
* and 1.0 is the end.
*/
float position;
} FSStreamPosition;
我們在更新UI的時(shí)候,主要可以根據(jù)其中的minute和second來,如果播放了90s,那么minute就為1,而second為30,所以我們在計(jì)算的時(shí)候,應(yīng)該是這樣的(self.currentTimePlayed.second + self.currentTimePlayed.minute * 60)
當(dāng)然在更新進(jìn)度條的時(shí)候,我們也可以通過position直接來給slider進(jìn)行賦值,這表示當(dāng)前播放的比例
#pragma mark == GLMusicPlayerDelegate
- (void)updateProgressWithCurrentPosition:(FSStreamPosition)currentPosition endPosition:(FSStreamPosition)endPosition
{
//更新進(jìn)度條
self.playerControlView.slider.value = currentPosition.position;
self.playerControlView.leftTimeLable.text = [NSString translationWithMinutes:currentPosition.minute seconds:currentPosition.second];
self.playerControlView.rightTimeLable.text = [NSString translationWithMinutes:endPosition.minute seconds:endPosition.second];
//更新歌詞
[self updateMusicLrcForRowWithCurrentTime:currentPosition.position *(endPosition.minute *60 + endPosition.second)];
self.playerControlView.palyMusicButton.selected = [GLMusicPlayer defaultPlayer].isPause;
}
本項(xiàng)目中,slider控件沒有用系統(tǒng)的,而是簡單的寫了一個(gè),大概如下
@interface GLSlider : UIControl
//進(jìn)度條顏色
@property (nonatomic,strong) UIColor *progressColor;
//緩存條顏色
@property (nonatomic,strong) UIColor *progressCacheColor;
//滑塊顏色
@property (nonatomic,strong) UIColor *thumbColor;
//設(shè)置進(jìn)度值 0-1
@property (nonatomic,assign) CGFloat value;
//設(shè)置緩存進(jìn)度值 0-1
@property (nonatomic,assign) CGFloat cacheValue;
@end
static CGFloat const kProgressHeight = 2;
static CGFloat const kProgressLeftPadding = 2;
static CGFloat const kThumbHeight = 16;
@interface GLSlider()
//滑塊 默認(rèn)
@property (nonatomic,strong) CALayer *thumbLayer;
//進(jìn)度條
@property (nonatomic,strong) CALayer *progressLayer;
//緩存進(jìn)度條
@property (nonatomic,strong) CALayer *progressCacheLayer;
@property (nonatomic,assign) BOOL isTouch;
@end
@implementation GLSlider
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self addSubLayers];
}
return self;
}
....
這里是添加了緩存進(jìn)度條的,但是由于時(shí)間關(guān)系,代碼中還未實(shí)時(shí)更新緩存進(jìn)度
3、更新歌詞界面
說到歌詞界面,我們看到QQ音樂的效果是這樣的,逐行逐字進(jìn)行更新,注意不是逐行更新??紤]到逐字進(jìn)行更新,那么我們必須要對lable進(jìn)行干點(diǎn)什么,這里對其進(jìn)行了繼承,并添加了些方法
@interface GLMusicLrcLable : UILabel //進(jìn)度 @property (nonatomic,assign) CGFloat progress; @end
#import "GLMusicLrcLable.h"
@implementation GLMusicLrcLable
- (void)setProgress:(CGFloat)progress
{
_progress = progress;
//重繪
[self setNeedsDisplay];
}
- (void)drawRect:(CGRect)rect
{
[super drawRect:rect];
CGRect fillRect = CGRectMake(0, 0, self.bounds.size.width * _progress, self.bounds.size.height);
[UICOLOR_FROM_RGB(45, 185, 105) set];
UIRectFillUsingBlendMode(fillRect, kCGBlendModeSourceIn);
}
@end
注意UIRectFillUsingBlendMode該方法能夠?qū)崿F(xiàn)逐字進(jìn)行漸變的效果
逐字的問題解決了,那么就剩下逐行問題了,逐行的問題應(yīng)該不難,是的。我們只需要在指定的時(shí)間內(nèi)將其滾動(dòng)就行,如下
[self.lrcTableView scrollToRowAtIndexPath:currentIndexPath atScrollPosition:UITableViewScrollPositionMiddle animated:YES]
但是這中要注意一個(gè)問題,那就是必須做到,在下一行進(jìn)行展示的時(shí)候,取消上一行的效果,如下
//設(shè)置當(dāng)前行的狀態(tài)
[currentCell reloadCellForSelect:YES];
//取消上一行的選中狀態(tài)
[previousCell reloadCellForSelect:NO];
- (void)reloadCellForSelect:(BOOL)select
{
if (select) {
_lrcLable.font = [UIFont systemFontOfSize:17];
}else{
_lrcLable.font = [UIFont systemFontOfSize:14];
_lrcLable.progress = 0;
}
}
其中_lrcLable.progress = 0;必須要,否則我們的文字顏色不會(huì)改變
在大問題已經(jīng)解決的情況下,我們就需要關(guān)心另一個(gè)重要的問題了,那就是歌詞。這里先介紹一個(gè)網(wǎng)站,可以獲取歌曲名和歌詞的
(找了好久....) 歌曲歌詞獲取 ,不過好多好聽的歌曲居然播放不了,你懂得,大天朝版權(quán)問題....找一首歌,播放就能看到看到歌詞了。關(guān)于歌詞,有許多格式,這里我用的是lrc格式,應(yīng)該還算比較主流,格式大概如下
[ti:老人與海] [ar:海鳴威 ] [al:單曲] [by:www.5nd.com From 那時(shí)花開] [00:04.08]老人與海 海鳴威 [00:08.78]海鳴威 [00:37.06]秋天的夜凋零在漫天落葉里面 [00:42.43]泛黃世界一點(diǎn)一點(diǎn)隨風(fēng)而漸遠(yuǎn) [00:47.58]冬天的雪白色了你我的情人節(jié) [00:53.24]消失不見 愛的碎片 [00:57.87]Rap: [00:59.32]翻開塵封的相片 [01:00.87]想起和你看過 的那些老舊默片 [01:02.50]老人與海的情節(jié) [01:04.23]畫面中你卻依稀 在浮現(xiàn)
在有了格式后,我們就需要一個(gè)模型,來分離歌曲信息了,下面是我建的模型
#import @interface GLMusicLRCModel : NSObject //該段歌詞對應(yīng)的時(shí)間 @property (nonatomic,assign) NSTimeInterval time; //歌詞 @property (nonatomic,strong) NSString *title; /** * 將特點(diǎn)的歌詞格式進(jìn)行轉(zhuǎn)換 * **/ + (id)musicLRCWithString:(NSString *)string; /** * 根據(jù)歌詞的路徑返回歌詞模型數(shù)組 * **/ + (NSArray *)musicLRCModelsWithLRCFileName:(NSString *)name; @end
#import "GLMusicLRCModel.h"
@implementation GLMusicLRCModel
+(id)musicLRCWithString:(NSString *)string
{
GLMusicLRCModel *model = [[GLMusicLRCModel alloc] init];
NSArray *lrcLines =[string componentsSeparatedByString:@"]"];
if (lrcLines.count == 2) {
model.title = lrcLines[1];
NSString *timeString = lrcLines[0];
timeString = [timeString stringByReplacingOccurrencesOfString:@"[" withString:@""];
timeString = [timeString stringByReplacingOccurrencesOfString:@"]" withString:@""];
NSArray *times = [timeString componentsSeparatedByString:@":"];
if (times.count == 2) {
NSTimeInterval time = [times[0] integerValue]*60 + [times[1] floatValue];
model.time = time;
}
}else if(lrcLines.count == 1){
}
return model;
}
+(NSArray *)musicLRCModelsWithLRCFileName:(NSString *)name
{
NSString *lrcPath = [[NSBundle mainBundle] pathForResource:name ofType:nil];
NSString *lrcString = [NSString stringWithContentsOfFile:lrcPath encoding:NSUTF8StringEncoding error:nil];
NSArray *lrcLines = [lrcString componentsSeparatedByString:@"\n"];
NSMutableArray *lrcModels = [NSMutableArray array];
for (NSString *lrcLineString in lrcLines) {
if ([lrcLineString hasPrefix:@"[ti"] || [lrcLineString hasPrefix:@"[ar"] || [lrcLineString hasPrefix:@"[al"] || ![lrcLineString hasPrefix:@"["]) {
continue;
}
GLMusicLRCModel *lrcModel = [GLMusicLRCModel musicLRCWithString:lrcLineString];
[lrcModels addObject:lrcModel];
}
return lrcModels;
}
@end
在歌詞模型準(zhǔn)備好之后,我們要展示歌詞,這里我選擇的是tableview,通過每一個(gè)cell來加載不同的歌詞,然后通過歌詞的時(shí)間信息來更新和滾動(dòng)
#pragma mark == UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [GLMusicPlayer defaultPlayer].musicLRCArray.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
MusicLRCTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"musicLrc" forIndexPath:indexPath];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
cell.backgroundColor = [UIColor clearColor];
cell.contentView.backgroundColor = [UIColor clearColor];
cell.lrcModel = [GLMusicPlayer defaultPlayer].musicLRCArray[indexPath.row];
if (indexPath.row == self.currentLcrIndex) {
[cell reloadCellForSelect:YES];
}else{
[cell reloadCellForSelect:NO];
}
return cell;
}
這里面唯一比較麻煩的可能就是更新歌詞了,在上面的定時(shí)器中,我們也通過代理來更新了進(jìn)度條,所以我也將更新歌詞的部分放在了代理中,這樣可以達(dá)到實(shí)時(shí)更新的目的,下面看看方法
//逐行更新歌詞
- (void)updateMusicLrcForRowWithCurrentTime:(NSTimeInterval)currentTime
{
for (int i = 0; i < [GLMusicPlayer defaultPlayer].musicLRCArray.count; i ++) {
GLMusicLRCModel *model = [GLMusicPlayer defaultPlayer].musicLRCArray[i];
NSInteger next = i + 1;
GLMusicLRCModel *nextLrcModel = nil;
if (next < [GLMusicPlayer defaultPlayer].musicLRCArray.count) {
nextLrcModel = [GLMusicPlayer defaultPlayer].musicLRCArray[next];
}
if (self.currentLcrIndex != i && currentTime >= model.time)
{
BOOL show = NO;
if (nextLrcModel) {
if (currentTime < nextLrcModel.time) {
show = YES;
}
}else{
show = YES;
}
if (show) {
NSIndexPath *currentIndexPath = [NSIndexPath indexPathForRow:i inSection:0];
NSIndexPath *previousIndexPath = [NSIndexPath indexPathForRow:self.currentLcrIndex inSection:0];
self.currentLcrIndex = i;
MusicLRCTableViewCell *currentCell = [self.lrcTableView cellForRowAtIndexPath:currentIndexPath];
MusicLRCTableViewCell *previousCell = [self.lrcTableView cellForRowAtIndexPath:previousIndexPath];
//設(shè)置當(dāng)前行的狀態(tài)
[currentCell reloadCellForSelect:YES];
//取消上一行的選中狀態(tài)
[previousCell reloadCellForSelect:NO];
if (!self.isDrag) {
[self.lrcTableView scrollToRowAtIndexPath:currentIndexPath atScrollPosition:UITableViewScrollPositionMiddle animated:YES];
}
}
}
if (self.currentLcrIndex == i) {
MusicLRCTableViewCell *cell = [self.lrcTableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:i inSection:0]];
CGFloat totalTime = 0;
if (nextLrcModel) {
totalTime = nextLrcModel.time - model.time;
}else{
totalTime = [GLMusicPlayer defaultPlayer].duration.minute * 60 + [GLMusicPlayer defaultPlayer].duration.second - model.time;
}
CGFloat progressTime = currentTime - model.time;
cell.lrcLable.progress = progressTime / totalTime;
}
}
}
到此為止,我們一個(gè)簡單的播放器就差不多實(shí)現(xiàn)了,但是這...并沒有完,相比QQ音樂而言,它還差一個(gè)播放順序切換的功能和鎖屏播放功能
4、切換播放順序
這個(gè)比較簡單,只是需要注意在切換的時(shí)候,注意數(shù)組的越界和不同模式的處理
這里,我定義了如下幾種模式
typedef NS_ENUM(NSInteger,GLLoopState){
GLSingleLoop = 0,//單曲循環(huán)
GLForeverLoop,//重復(fù)循環(huán)
GLRandomLoop,//隨機(jī)播放
GLOnceLoop//列表一次順序播放
};
切換代碼
//不同狀態(tài)下 播放歌曲
- (void)playMusicForState
{
switch (self.loopState) {
case GLSingleLoop:
{
[self playMusicAtIndex:self.currentIndex];
}
break;
case GLForeverLoop:
{
if (self.currentIndex == self.musicListArray.count-1) {
[self playMusicAtIndex:0];
}else{
[self playMusicAtIndex:self.currentIndex + 1];
}
}
break;
case GLRandomLoop:
{
//取隨機(jī)值
int index = arc4random() % self.musicListArray.count;
[self playMusicAtIndex:index];
}
break;
case GLOnceLoop:
{
if (self.currentIndex == self.musicListArray.count-1) {
[self stop];
}else{
[self playMusicAtIndex:self.currentIndex + 1];
}
}
break;
default:
break;
}
}
5、鎖屏播放
就如上圖2中那樣,由于在iOS 11中好像不能支持背景圖片和歌詞展示,可能是為了界面更加簡潔吧,所以我這里也就沒有加該功功能,只是簡答的有個(gè)播放界面和幾個(gè)控制按鈕
首先需要在工程中這樣設(shè)置,保證在后臺(tái)播放
然后就是在appdelegate中添加如下代碼
AVAudioSession *session = [AVAudioSession sharedInstance]; // [session setActive:YES error:nil]; [session setActive:YES withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:nil]; [session setCategory:AVAudioSessionCategoryPlayback error:nil]; [[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
并且添加控制事件
#pragma mark == event response
-(void)remoteControlReceivedWithEvent:(UIEvent *)event{
NSLog(@"%ld",event.subtype);
if (event.type == UIEventTypeRemoteControl) {
switch (event.subtype) {
case UIEventSubtypeRemoteControlPlay:
{
//點(diǎn)擊播放按鈕或者耳機(jī)線控中間那個(gè)按鈕
[[GLMusicPlayer defaultPlayer] pause];
}
break;
case UIEventSubtypeRemoteControlPause:
{
//點(diǎn)擊暫停按鈕
[[GLMusicPlayer defaultPlayer] pause];
}
break;
case UIEventSubtypeRemoteControlStop :
{
//點(diǎn)擊停止按鈕
[[GLMusicPlayer defaultPlayer] stop];
}
break;
case UIEventSubtypeRemoteControlTogglePlayPause:
{
//點(diǎn)擊播放與暫停開關(guān)按鈕(iphone抽屜中使用這個(gè))
[[GLMusicPlayer defaultPlayer] pause];
}
break;
case UIEventSubtypeRemoteControlNextTrack:
{
//點(diǎn)擊下一曲按鈕或者耳機(jī)中間按鈕兩下
[[GLMusicPlayer defaultPlayer] playNext];
}
break;
case UIEventSubtypeRemoteControlPreviousTrack:
{
//點(diǎn)擊上一曲按鈕或者耳機(jī)中間按鈕三下
[[GLMusicPlayer defaultPlayer] playFont];
}
break;
case UIEventSubtypeRemoteControlBeginSeekingBackward:
{
//快退開始 點(diǎn)擊耳機(jī)中間按鈕三下不放開
}
break;
case UIEventSubtypeRemoteControlEndSeekingBackward:
{
//快退結(jié)束 耳機(jī)快退控制松開后
}
break;
case UIEventSubtypeRemoteControlBeginSeekingForward:
{
//開始快進(jìn) 耳機(jī)中間按鈕兩下不放開
}
break;
case UIEventSubtypeRemoteControlEndSeekingForward:
{
//快進(jìn)結(jié)束 耳機(jī)快進(jìn)操作松開后
}
break;
default:
break;
}
}
}
beginReceivingRemoteControlEvents為允許傳遞遠(yuǎn)程控制事件,remoteControlReceivedWithEvent為接收一個(gè)遠(yuǎn)程控制事件,關(guān)于控制事件的類型,在代碼中,已經(jīng)注釋過,這里就不再說了。
控制事件搞定了,剩下的就是界面的展示了,主要是歌曲信息的展示,通過如下的代碼就能實(shí)現(xiàn)
NSMutableDictionary *musicInfoDict = [[NSMutableDictionary alloc] init];
//設(shè)置歌曲題目
[musicInfoDict setObject:self.currentTitle forKey:MPMediaItemPropertyTitle];
//設(shè)置歌手名
[musicInfoDict setObject:@"" forKey:MPMediaItemPropertyArtist];
//設(shè)置專輯名
[musicInfoDict setObject:@"" forKey:MPMediaItemPropertyAlbumTitle];
//設(shè)置歌曲時(shí)長
[musicInfoDict setObject:[NSNumber numberWithFloat:totalTime]
forKey:MPMediaItemPropertyPlaybackDuration];
//設(shè)置已經(jīng)播放時(shí)長
[musicInfoDict setObject:[NSNumber numberWithFloat:currentTime]
forKey:MPNowPlayingInfoPropertyElapsedPlaybackTime];
[[MPNowPlayingInfoCenter defaultCenter] setNowPlayingInfo:musicInfoDict];
關(guān)于歌曲信息的設(shè)置,可以不按照我這樣,定時(shí)器中時(shí)刻進(jìn)行刷新,只需要在播放、暫停、快進(jìn)快退這些時(shí)間有變化的地方傳入當(dāng)前歌曲的關(guān)鍵信息就可以,系統(tǒng)會(huì)自動(dòng)去根據(jù)播放情況去更新鎖屏界面上的進(jìn)度條,而不需要我們時(shí)刻傳入當(dāng)前播放時(shí)間。這里我為了偷懶,就加在里面了。為了防止頻繁操作,我采取了個(gè)方法,在其他地方看到的,就是監(jiān)聽鎖屏情況
//監(jiān)聽鎖屏狀態(tài) lock=1則為鎖屏狀態(tài)
uint64_t locked;
__block int token = 0;
notify_register_dispatch("com.apple.springboard.lockstate",&token,dispatch_get_main_queue(),^(int t){
});
notify_get_state(token, &locked);
//監(jiān)聽屏幕點(diǎn)亮狀態(tài) screenLight = 1則為變暗關(guān)閉狀態(tài)
uint64_t screenLight;
__block int lightToken = 0;
notify_register_dispatch("com.apple.springboard.hasBlankedScreen",&lightToken,dispatch_get_main_queue(),^(int t){
});
notify_get_state(lightToken, &screenLight);
通過該情況來設(shè)置。
在上面鎖屏播放的過程中,出現(xiàn)一個(gè)問題,就是當(dāng)我切換歌曲的時(shí)候,不管是在鎖屏情況下,還是在app內(nèi)
通過各種查找,大概找到問題,首先在appdelegate中將[session setActive:YES error:nil]改成了[session setActive:YES withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:nil],然后再播放的地方加了一個(gè)[self stop],先停止播放
- (void)playFromURL:(NSURL *)url
{
//根據(jù)地址 在本地找歌詞
NSMutableDictionary *dic = [NSMutableDictionary dictionaryWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"musiclist" ofType:@"plist"]];
for (NSString *playStringKey in dic.allKeys) {
if ([[dic valueForKey:playStringKey] isEqualToString:url.absoluteString]) {
self.currentTitle = playStringKey;
break;
}
}
[self stop];
if (![url.absoluteString isEqualToString:self.url.absoluteString]) {
[super playFromURL:url];
}else{
[self play];
}
到此為止,一個(gè)簡單的播放器就差不多了,由于時(shí)間關(guān)系,可能還有些bug,希望大家能多多提出來,我好進(jìn)行修正。下面還是附上 demo ,后續(xù)我還將加一個(gè)功能,因?yàn)檫@兩天公司有個(gè)很老的項(xiàng)目,有個(gè)下載問題,有點(diǎn)蛋疼,所以準(zhǔn)備些一個(gè)隊(duì)列下載,然后順便加到播放器上。
說說遇到的坑
第一個(gè)就是我們項(xiàng)目中也有用到科大訊飛的語音.和錄音的功能這些東西都需要對AVAudioSession進(jìn)行操作.在切換使用AVAudioSession的時(shí)候就會(huì)報(bào)[AVAudioSession setActive:withOptions:error:]: Deactivating an audio session that has running I/O. All I/O should be stopped or paused prior to deactivating the audio session. 這樣的錯(cuò)誤,這個(gè)錯(cuò)會(huì)導(dǎo)致音頻在播放但是沒有聲音. 我的解決辦法是搜索框架中所有的setActive:NO,把NO改成YES,這個(gè)問題就完美的解決了.
第二個(gè)坑就是當(dāng)剛開始緩存但是沒有出聲音的時(shí)候這個(gè)時(shí)候調(diào)暫停的方法是沒用的,即使調(diào)用了暫停的方法.但是音頻還是會(huì)播放.我剛開始的解決辦法是在監(jiān)聽FSAudioStreamState的kFsAudioStreamPlaying狀態(tài).在playFromURL:的時(shí)候設(shè)置了一個(gè)屬性Buffering置為YES,在調(diào)用kFsAudioStreamPlaying的置為NO,這樣在暫停方法里這樣寫
- (void)suspentFM {
if (self.isSuspendFM==YES) return;
if (self.Buffering ==YES) {
[_audioStream stop];
}else {
[_audioStream pause];
}
self.isSuspendFM = YES;
_suspentBtn.hidden = NO;
}
就解決了這個(gè)問題.但是解決的并不完美.kFsAudioStreamPlaying這個(gè)狀態(tài)會(huì)調(diào)用很多次.這樣在少數(shù)情況下還是會(huì)有問題,具體情況已經(jīng)忘了.于是乎我就放出了終極大招在定時(shí)器里監(jiān)聽進(jìn)度
if ( progressView.progress<0.007) {
self.Buffering = YES;
}else {
self.Buffering = NO;
}
到這里才完美的解決這個(gè)問題
在接下來說使用小技巧吧.就是緩存的進(jìn)度和播放的進(jìn)度
FSStreamPosition cur = self.audioStream.currentTimePlayed;
self.playbackTime =cur.playbackTimeInSeconds/1;
self.ProgressView.progress = cur.position;//播放進(jìn)度
self.progress = cur.position;
float prebuffer = (float)self.audioStream.prebufferedByteCount;
float contentlength = (float)self.audioStream.contentLength;
if (contentlength>0) {
self.ProgressView.cacheProgress = prebuffer /contentlength;//緩存進(jìn)度
}
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- ios 流媒體播放器實(shí)現(xiàn)流程及FreeStreamer的使用的示例
- 實(shí)例講解iOS音樂播放器DOUAudioStreamer用法
- 運(yùn)用iOS教你輕松制作音樂播放器
- ios開發(fā):一個(gè)音樂播放器的設(shè)計(jì)與實(shí)現(xiàn)案例
- iOS中視頻播放器的簡單封裝詳解
- iOS中的音頻服務(wù)和音頻AVAudioPlayer音頻播放器使用指南
- 實(shí)例解析iOS中音樂播放器應(yīng)用開發(fā)的基本要點(diǎn)
- iOS開發(fā)中音頻工具類的封裝以及音樂播放器的細(xì)節(jié)控制
- iOS音樂播放器實(shí)現(xiàn)代碼完整版
相關(guān)文章
Flutter?GetPageRoute實(shí)現(xiàn)嵌套導(dǎo)航學(xué)習(xí)
這篇文章主要為大家介紹了Flutter?GetPageRoute實(shí)現(xiàn)嵌套導(dǎo)航的示例學(xué)習(xí),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08
iOS中設(shè)置父視圖透明但內(nèi)容不透明的方法
設(shè)置一定的背景透明會(huì)讓用戶的體驗(yàn)非常不錯(cuò),下面這篇文章就主要跟大家分享了iOS中設(shè)置父視圖透明但內(nèi)容不透明的方法,文中給出了詳細(xì)的示例代碼,需要的朋友們下面來一起看看吧。2017-05-05
iOS實(shí)現(xiàn)漸變按鈕Gradient Button的方法示例
這篇文章主要給大家介紹了關(guān)于iOS實(shí)現(xiàn)漸變按鈕Gradient Button的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對各位iOS開發(fā)者們具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-08-08
iOS實(shí)現(xiàn)左右拖動(dòng)抽屜效果
這篇文章主要介紹了iOS實(shí)現(xiàn)左右拖動(dòng)抽屜效果,理解ios平臺(tái)類似于QQ主頁面,利用觸摸事件滑動(dòng)touchesMoved實(shí)現(xiàn)的效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-02-02

