iOS之基于FreeStreamer的簡(jiǎn)單音樂(lè)播放器

天道酬勤

前言

作為一名iOS開(kāi)發(fā)者,每當(dāng)使用APP的時(shí)候莫矗,總難免會(huì)情不自禁的去想想飒硅,這個(gè)怎么做的?該怎么實(shí)現(xiàn)呢作谚?很久之前三娩,就想寫一個(gè)關(guān)于音樂(lè)方面的播放器,最近剛好得空妹懒,就趁機(jī)摸索著寫了下雀监,寫的不好,還望多多指教眨唬。

前提準(zhǔn)備

為了能夠有明確的思路來(lái)做這個(gè)demo会前,我下載了QQ音樂(lè)網(wǎng)易云音樂(lè),然后分別對(duì)比匾竿,最終選擇了QQ音樂(lè)來(lái)參照瓦宜,先是獲取了其中的所有資源文件(如果有不知道怎么提取資源文件的,可以參考iOS提取APP中的圖片資源
)搂橙,在這之后就是研究使用技術(shù)歉提,這里我選擇了FreeStreamer笛坦,雖然系統(tǒng)也有区转,但是該框架可能更好用點(diǎn)苔巨。

實(shí)現(xiàn)部分

在這之前,先來(lái)看看大概效果圖吧


IMG_6210.PNG
IMG_6211.PNG
list.png

再看完效果圖之后废离,我們就來(lái)看看這其中涉及到的幾個(gè)難點(diǎn)吧(在我看開(kāi)~)

  • 1侄泽、先讓播放器跑起來(lái)
    這里我使用的是pods來(lái)管理三方庫(kù),代碼如下
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

針對(duì)FreeStreamer我簡(jiǎn)單進(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<NSObject>

/**
 *
 實(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 <GLMusicLRCModel*>*musicLRCArray;

/**
 *
 當(dāng)前播放
 *
 **/
@property (nonatomic,assign,readonly) NSUInteger currentIndex;

/**
 *
 當(dāng)前播放的音樂(lè)的標(biāo)題
 *
 **/
@property (nonatomic,strong) NSString *currentTitle;


/**
 是否是暫停狀態(tài)
 */
@property (nonatomic,assign) BOOL isPause;

@property (nonatomic,weak) id<GLMusicPlayerDelegate>glPlayerDelegate;

//默認(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)簡(jiǎn)單的播放,則可以不用重寫該方法俯画,直接通過(guò)playFromURL就可以實(shí)現(xiàn)我們的播放功能析桥。

  • 2、更新UI
    這里的UI暫不包括歌詞的更新艰垂,而只是進(jìn)度條的更新泡仗,要更新進(jìn)度條,比不可少的是定時(shí)器猜憎,這里我沒(méi)有選擇NSTimer,而是選擇了CADisplayLink娩怎,至于為什么,我想大家應(yīng)該都比較了解胰柑,可以這么來(lái)對(duì)比截亦,下面引用一段其他博客的對(duì)比:
    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使用場(chǎng)合相對(duì)專一,適合做UI的不停重繪蛋逾,比如自定義動(dòng)畫引擎或者視頻播放的渲染集晚。NSTimer的使用范圍要廣泛的多,各種需要單次或者循環(huán)定時(shí)處理的任務(wù)都可以使用区匣。在UI相關(guān)的動(dòng)畫或者顯示內(nèi)容使用 CADisplayLink比起用NSTimer的好處就是我們不需要在格外關(guān)心屏幕的刷新頻率了偷拔,因?yàn)樗旧砭褪歉聊凰⑿峦降?br> 使用方法
    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è)屬性:currentTimePlayedduration,分別保存著當(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;

我們?cè)诟?code>UI的時(shí)候莲绰,主要可以根據(jù)其中的minutesecond來(lái)欺旧,如果播放了90s,那么minute就為1蛤签,而second30辞友,所以我們?cè)谟?jì)算的時(shí)候,應(yīng)該是這樣的(self.currentTimePlayed.second + self.currentTimePlayed.minute * 60)
當(dāng)然在更新進(jìn)度條的時(shí)候震肮,我們也可以通過(guò)position直接來(lái)給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控件沒(méi)有用系統(tǒng)的戳晌,而是簡(jiǎn)單的寫了一個(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疫向、更新歌詞界面
    說(shuō)到歌詞界面,我們看到QQ音樂(lè)的效果是這樣的扛施,逐行逐字進(jìn)行更新鸿捧,注意不是逐行更新「碓考慮到逐字進(jìn)行更新匙奴,那么我們必須要對(duì)lable進(jìn)行干點(diǎn)什么,這里對(duì)其進(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)行漸變的效果
逐字的問(wèn)題解決了泼菌,那么就剩下逐行問(wèn)題了,逐行的問(wèn)題應(yīng)該不難啦租,是的哗伯。我們只需要在指定的時(shí)間內(nèi)將其滾動(dòng)就行,如下

[self.lrcTableView scrollToRowAtIndexPath:currentIndexPath atScrollPosition:UITableViewScrollPositionMiddle animated:YES]

但是這中要注意一個(gè)問(wèn)題篷角,那就是必須做到焊刹,在下一行進(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ì)改變
在大問(wèn)題已經(jīng)解決的情況下,我們就需要關(guān)心另一個(gè)重要的問(wèn)題了嘉蕾,那就是歌詞贺奠。這里先介紹一個(gè)網(wǎng)站,可以獲取歌曲名和歌詞的
(找了好久....)歌曲歌詞獲取错忱,不過(guò)好多好聽(tīng)的歌曲居然播放不了儡率,你懂得挂据,大天朝版權(quán)問(wèn)題....找一首歌,播放就能看到看到歌詞了儿普。關(guān)于歌詞崎逃,有許多格式,這里我用的是lrc格式箕肃,應(yīng)該還算比較主流婚脱,格式大概如下

[ti:老人與海]
[ar:海鳴威 ]
[al:單曲]
[by:www.5nd.com From 那時(shí)花開(kāi)]
[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]消失不見(jiàn) 愛(ài)的碎片
[00:57.87]Rap:
[00:59.32]翻開(kāi)塵封的相片
[01:00.87]想起和你看過(guò) 的那些老舊默片
[01:02.50]老人與海的情節(jié)
[01:04.23]畫面中你卻依稀 在浮現(xiàn)

在有了格式后今魔,我們就需要一個(gè)模型勺像,來(lái)分離歌曲信息了,下面是我建的模型

#import <Foundation/Foundation.h>


@interface GLMusicLRCModel : NSObject

//該段歌詞對(duì)應(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 <GLMusicLRCModel *>*)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 <GLMusicLRCModel *>*)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,通過(guò)每一個(gè)cell來(lái)加載不同的歌詞涩维,然后通過(guò)歌詞的時(shí)間信息來(lái)更新和滾動(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í)器中,我們也通過(guò)代理來(lái)更新了進(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è)簡(jiǎn)單的播放器就差不多實(shí)現(xiàn)了控硼,但是這...并沒(méi)有完,相比QQ音樂(lè)而言戳杀,它還差一個(gè)播放順序切換的功能和鎖屏播放功能

  • 4该面、切換播放順序
    這個(gè)比較簡(jiǎn)單,只是需要注意在切換的時(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中好像不能支持背景圖片和歌詞展示猾瘸,可能是為了界面更加簡(jiǎn)潔吧,所以我這里也就沒(méi)有加該功功能丢习,只是簡(jiǎn)答的有個(gè)播放界面和幾個(gè)控制按鈕
    首先需要在工程中這樣設(shè)置牵触,保證在后臺(tái)播放
    setting.png

    然后就是在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)擊播放與暫停開(kāi)關(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:
            {
                //快退開(kāi)始 點(diǎn)擊耳機(jī)中間按鈕三下不放開(kāi)
            }
                break;
            case UIEventSubtypeRemoteControlEndSeekingBackward:
            {
                //快退結(jié)束 耳機(jī)快退控制松開(kāi)后
            }
                break;
            case UIEventSubtypeRemoteControlBeginSeekingForward:
            {
                //開(kāi)始快進(jìn) 耳機(jī)中間按鈕兩下不放開(kāi)
            }
                break;
            case UIEventSubtypeRemoteControlEndSeekingForward:
            {
                //快進(jìn)結(jié)束 耳機(jī)快進(jìn)操作松開(kāi)后
            }
                break;
                
            default:
                break;
        }
        
    }
}

beginReceivingRemoteControlEvents為允許傳遞遠(yuǎn)程控制事件,remoteControlReceivedWithEvent為接收一個(gè)遠(yuǎn)程控制事件泛领,關(guān)于控制事件的類型荒吏,在代碼中,已經(jīng)注釋過(guò)渊鞋,這里就不再說(shuō)了绰更。
控制事件搞定了瞧挤,剩下的就是界面的展示了,主要是歌曲信息的展示儡湾,通過(guò)如下的代碼就能實(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í)長(zhǎng)
        [musicInfoDict setObject:[NSNumber numberWithFloat:totalTime]
                          forKey:MPMediaItemPropertyPlaybackDuration];
        //設(shè)置已經(jīng)播放時(shí)長(zhǎng)
        [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)聽(tīng)鎖屏情況

    //監(jiān)聽(tīng)鎖屏狀態(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)聽(tīng)屏幕點(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);

通過(guò)該情況來(lái)設(shè)置。

在上面鎖屏播放的過(guò)程中盹兢,出現(xiàn)一個(gè)問(wèn)題邻梆,就是當(dāng)我切換歌曲的時(shí)候,不管是在鎖屏情況下绎秒,還是在app內(nèi)

bug.jpg

通過(guò)各種查找浦妄,大概找到問(wèn)題,首先在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è)簡(jiǎn)單的播放器就差不多了辆童,由于時(shí)間關(guān)系宜咒,可能還有些bug,希望大家能多多提出來(lái)把鉴,我好進(jìn)行修正故黑。下面還是附上demo,后續(xù)我還將加一個(gè)功能庭砍,因?yàn)檫@兩天公司有個(gè)很老的項(xiàng)目场晶,有個(gè)下載問(wèn)題,有點(diǎn)蛋疼怠缸,所以準(zhǔn)備些一個(gè)隊(duì)列下載诗轻,然后順便加到播放器上。

2018-07-03 bug 修改

針對(duì)有位朋友提出的bug
偶爾切換下一首時(shí)會(huì)出現(xiàn) 播放1s然后停止播放了揭北,再次點(diǎn)擊播放按鈕又可以接著播放了 下邊打印這樣的日志 Deactivating an audio session that has running I/O. All I/O should be stopped or paused prior to deactivating the audio session
在仔細(xì)排查問(wèn)題及搜尋答案后扳炬,大概找到問(wèn)題所在吏颖,修改如下

bug2.png

FreeStreamer中增加了一個(gè)category來(lái)強(qiáng)制修改返回狀態(tài),因?yàn)?code>FreeStreamer中會(huì)去修改該狀態(tài)恨樟。所以將該category引入到FSAudioStream類中
修改.jpg

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末半醉,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子劝术,更是在濱河造成了極大的恐慌缩多,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,839評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件养晋,死亡現(xiàn)場(chǎng)離奇詭異衬吆,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)匙握,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門咆槽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人圈纺,你說(shuō)我怎么就攤上這事÷笊洌” “怎么了蛾娶?”我有些...
    開(kāi)封第一講書人閱讀 153,116評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)潜秋。 經(jīng)常有香客問(wèn)我蛔琅,道長(zhǎng),這世上最難降的妖魔是什么峻呛? 我笑而不...
    開(kāi)封第一講書人閱讀 55,371評(píng)論 1 279
  • 正文 為了忘掉前任罗售,我火速辦了婚禮,結(jié)果婚禮上钩述,老公的妹妹穿的比我還像新娘寨躁。我一直安慰自己,他們只是感情好牙勘,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布职恳。 她就那樣靜靜地躺著,像睡著了一般方面。 火紅的嫁衣襯著肌膚如雪放钦。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 49,111評(píng)論 1 285
  • 那天恭金,我揣著相機(jī)與錄音操禀,去河邊找鬼。 笑死横腿,一個(gè)胖子當(dāng)著我的面吹牛颓屑,可吹牛的內(nèi)容都是我干的辙培。 我是一名探鬼主播,決...
    沈念sama閱讀 38,416評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼邢锯,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼扬蕊!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起丹擎,我...
    開(kāi)封第一講書人閱讀 37,053評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤尾抑,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后蒂培,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體再愈,經(jīng)...
    沈念sama閱讀 43,558評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評(píng)論 2 325
  • 正文 我和宋清朗相戀三年护戳,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了翎冲。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,117評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡媳荒,死狀恐怖抗悍,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情钳枕,我是刑警寧澤缴渊,帶...
    沈念sama閱讀 33,756評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站鱼炒,受9級(jí)特大地震影響衔沼,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜昔瞧,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評(píng)論 3 307
  • 文/蒙蒙 一指蚁、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧自晰,春花似錦凝化、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,315評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至袜蚕,卻和暖如春糟把,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背牲剃。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 31,539評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工遣疯, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人凿傅。 一個(gè)月前我還...
    沈念sama閱讀 45,578評(píng)論 2 355
  • 正文 我出身青樓缠犀,卻偏偏與公主長(zhǎng)得像数苫,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子辨液,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容