登录 立即注册
金钱:

Code4App-iOS开发-iOS 开源代码库-iOS代码实例搜索-iOS特效示例-iOS代码例子下载-Code4App.com

iOS之基于FreeStreamer的简单音乐播放器 [复制链接]

2017-11-17 10:50
BeatBeat 阅读:1525 评论:0 赞:1
Tag:  

前言

作为一名iOS开发者,每当使用APP的时候,总难免会情不自禁的去想想,这个怎么做的?该怎么实现呢?很久之前,就想写一个关于音乐方面的播放器,最近刚好得空,就趁机摸索着写了下,写的不好,还望多多指教。

实现部分

在这之前,先来看看大概效果图吧

1.png

2.png

3.png

list.png

再看完效果图之后,我们就来看看这其中涉及到的几个难点吧(在我看开~)

  • 1、先让播放器跑起来

这里我使用的是pods来管理三方库,代码如下

1
2
3
4
5
6
7
8
9
10
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我简单进行了封装下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#import "FSAudioStream.h"
@class GLMusicLRCModel;
typedef NS_ENUM(NSInteger,GLLoopState){
    GLSingleLoop = 0,//单曲循环
    GLForeverLoop,//重复循环
    GLRandomLoop,//随机播放
    GLOnceLoop//列表一次顺序播放
};
@protocol GLMusicPlayerDelegate/**
 *
 实时更新
 *
 **/
- (void)updateProgressWithCurrentPosition:(FSStreamPosition)currentPosition endPosition:(FSStreamPosition)endPosition;
- (void)updateMusicLrc;
@end
@interface GLMusicPlayer : FSAudioStream
/**
 *
 播放列表
 *
 **/
@property (nonatomic,strong) NSMutableArray *musicListArray;
/**
 当前播放歌曲的歌词
 */
@property (nonatomic,strong) NSMutableArray *musicLRCArray;
/**
 *
 当前播放
 *
 **/
@property (nonatomic,assign,readonly) NSUInteger currentIndex;
/**
 *
 当前播放的音乐的标题
 *
 **/
@property (nonatomic,strong) NSString *currentTitle;
/**
 是否是暂停状态
 */
@property (nonatomic,assign) BOOL isPause;
@property (nonatomic,weak) idglPlayerDelegate;
//默认 重复循环 GLForeverLoop
@property (nonatomic,assign) GLLoopState loopState;
/**
 *
 单例播放器
 *
 **/
+ (instancetype)defaultPlayer;
/**
 播放队列中的指定的文件 
 @param index 序号
 */
- (void)playMusicAtIndex:(NSUInteger)index;
/**
 播放前一首
 */
- (void)playFont;
/**
 播放下一首
 */
- (void)playNext;
@end

这里继承了FSAudioStream,并且采用了单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
+ (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) {
            //播放错误
            //有待解决
        };
        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;
            }
        };
        //设置音量
        [player setVolume:0.5];
        //设置播放速率
        [player setPlayRate:1];
        player.loopState = GLForeverLoop;
    });
    return player;
}

然后实现了播放方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
- (void)playFromURL:(NSURL *)url
{
    //根据地址 在本地找歌词
    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(@" 当前播放歌曲:%@",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界面,获取歌词等处理,如果要实现简单的播放,则可以不用重写该方法,直接通过playFromURL就可以实现我们的播放功能。

  • 2、更新UI

这里的UI暂不包括歌词的更新,而只是进度条的更新,要更新进度条,比不可少的是定时器,这里我没有选择NSTimer,而是选择了CADisplayLink,至于为什么,我想大家应该都比较了解,可以这么来对比,下面引用一段其他博客的对比:

iOS设备的屏幕刷新频率是固定的,CADisplayLink在正常情况下会在每次刷新结束都被调用,精确度相当高。

NSTimer的精确度就显得低了点,比如NSTimer的触发时间到的时候,runloop如果在阻塞状态,触发时间就会推迟到下一个runloop周期。并且 NSTimer新增了tolerance属性,让用户可以设置可以容忍的触发的时间的延迟范围。

CADisplayLink使用场合相对专一,适合做UI的不停重绘,比如自定义动画引擎或者视频播放的渲染。NSTimer的使用范围要广泛的多,各种需要单次或者循环定时处理的任务都可以使用。在UI相关的动画或者显示内容使用 CADisplayLink比起用NSTimer的好处就是我们不需要在格外关心屏幕的刷新频率了,因为它本身就是跟屏幕刷新同步的

使用方法

1
2
3
4
  if (!_progressTimer) {
        _progressTimer = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateProgress)];
        [_progressTimer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
    }

更新进度

1
2
3
4
5
6
7
8
9
- (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)];
}

在这里有两个属性:currentTimePlayed和duration,分别保存着当前播放时间和总时间,是如下的结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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的时候,主要可以根据其中的minute和second来,如果播放了90s,那么minute就为1,而second为30,所以我们在计算的时候,应该是这样的(self.currentTimePlayed.second + self.currentTimePlayed.minute * 60)

当然在更新进度条的时候,我们也可以通过position直接来给slider进行赋值,这表示当前播放的比例

1
2
3
4
5
6
7
8
9
10
11
12
13
#pragma mark == GLMusicPlayerDelegate
- (void)updateProgressWithCurrentPosition:(FSStreamPosition)currentPosition endPosition:(FSStreamPosition)endPosition
{
    //更新进度条
    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;
}

本项目中,slider控件没有用系统的,而是简单的写了一个,大概如下

1
2
3
4
5
6
7
8
9
10
11
12
@interface GLSlider : UIControl
//进度条颜色
@property (nonatomic,strong) UIColor *progressColor;
//缓存条颜色
@property (nonatomic,strong) UIColor *progressCacheColor;
//滑块颜色
@property (nonatomic,strong) UIColor *thumbColor;
//设置进度值 0-1
@property (nonatomic,assign) CGFloat value;
//设置缓存进度值 0-1
@property (nonatomic,assign) CGFloat cacheValue;
@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static CGFloat const kProgressHeight = 2;
static CGFloat const kProgressLeftPadding = 2;
static CGFloat const kThumbHeight = 16;
@interface GLSlider()
//滑块 默认
@property (nonatomic,strong) CALayer *thumbLayer;
//进度条
@property (nonatomic,strong) CALayer *progressLayer;
//缓存进度条
@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;
}