iOS开发--AVPlayer实现音乐播放器

本文是一篇教学博客,通过构建一个音乐播放器应用,讲解如何使用AVPlayer实现音乐播放功能,涉及单例、block、多线程、代理、通知等多个知识点。文章介绍了产品原型、功能模块划分、工具类、数据模型、页面布局和控制器的实现,并对AVPlayer的特点和使用进行了详细说明。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

这是一篇教学Blog. 重点不完全在播放器上, 目的是通过这个过程掌握以下知识点:

  • 单例
  • block传值
  • 多线程
  • 代理传值
  • 通知
  • 观察者
  • 网络请求
  • 数据解析
  • 多控件布局
  • 开发模式和框架设计

今天敲一个音乐播放器, 音乐源我就不共享了, 涉及到版权保护, 别问我的源是哪儿来的. 不告诉你们

这篇博客是一篇教学Blog, Demo不能直接用作生产, 但其中的逻辑是经得起推敲, UI部分美化美化一下即可. 要做到举一反三.

开始敲之前, 我们先看看当前可供使用的多媒体播放框架有哪些

file-list


简单介绍一下:

  • AudioToolbox.framework的音频播放时间不能超过30s,数据必须是PCM或者IMA4格式,音频文件必须打包成.caf、.aif、.wav中的一种(注意这是官方文档的说法,实际测试发现一些.mp3也可以播放. 它的主要用途可以用作app的音效(不是背景音).
  • MediaPlayer.framework框架下有两个常用的系统封装好的播放器:
    MPMoviePlayController 和 MPMoviePlayViewController, 二者的区别在于, 后者的视频图像需要一张View视图作为载体, 你可以自己创建这个View, 那么也可以自由的控制它. 最明显的例子就是你可以用它做个浮窗播放器.
  • AVFoundation.framework 目前被AVKit框架替代了, 但是我没有跟进, 我就用它:
    AVAudioRecorder播放器, 提供录音, 录音的的代码加起来没你jj长.
    AVPlayer播放器, 一个能播放网络和本地视频/音频的播放器, 和MediaPlayer.framework框架下的两个播放器不同, 系统并未提供它的UI界面, 我们需要自己实现, 往好听了说: 这是一个可以高度自定义的播放器.
    AVAudioPlayer与 AVPlayer播放器的区别在于, 这货只能播放本地音乐.

另附一张表格, 里面登记了大多数播放器的优缺点, 图片来源网络:
file-list

下面开始我们的音乐播放器之旅.

一. 产品原型图

当我们在实际生产过程中, 作为App前端开发工程师, 我们会拿到产品模型(原型图), 这个模型可能使用墨刀为你精准绘制, 也可能某个页面使用草纸为你勾勒, 不管怎样, 你肯定能拿到下面的东西, 这些图片,描绘了你要做的app大概长成什么样子.

  • 歌曲列表
    file-list

  • 播放界面
    file-list

  • 播放界面滑动CD还有歌词呢
    file-list

我们要做的就是上面样式的播放器, 如果您觉得太low, 请左右上角.


二. 功能模块划分:
括号后面的字母作为标记, 后面实现方法中会有这个字母, 您可以根据标记本节来查看当前代码属于哪一模块.
  • View层:两个界面

    1.歌曲列表: 第一个界面是一个TableView界面.(A)
    2.播放界面: 一个自定义界面, 需要我们布局.(B)

  • Controller层:

    上述两个View的控制器:
    1.歌曲列表TableView的控制器.(C)
    2.播放界面的控制器.(D)

  • Model层:两个模型

    1.歌曲信息模型, 存放每首歌曲的名称, 时长, url, 缩略图, 封面, 歌词等信息.(E)
    2.歌词模型, 歌词的基本格式, 这里是 [00:01]我大声说我爱的就是我 字符串格式.(F)

很多时候, 我们将一些功能模块单独独立起来, 做一次封装, 封装的好处, 找个机会开篇blog.

  • Tools 工具封装:

    1.一个从网络中获取歌曲信息的方法.(G)
    2.能将MP3文件播放出声音的类(对AVPlayer的的封装).(H)

我们先按照上面的思路进行, 这个模块划分的原则因人而异, 当前项目比较简单, 无论怎么划分都不会产生太大差异性, 不过如果项目足够大, 一个有经验的开发者和新手之间的差距就体现出来了.

按照上述模块划分, 我们需要8个类, 每个类完成自己独特的功能, 所谓”工欲善其事, 必先利其器”, 我们打算从工具类Tools开始, 深入浅出, 然后深入, 深入, 再深入.

file-list



三.工具类Tools
3.1 数据请求(G)

在这个工具类里面, 我们将这个类设置成为一个单例类

将数据请求封装成单例的好处是显而易见,

  • 首先做到了Model和Controllerc层的完全剥离, 从C层中调用数据请求的方法, 将请求回来的数据存放在单例类中, 也可以回传给C层做进一步的处理使用.
  • 其次, 如果歌曲清单没有改变, 那么一次请求的数据应该贯穿应用程序的整个声明周期. 这样, APP运行的任何时刻, 我们都能获取到这个歌曲信息.
  • 最后, 所有的页面(我们的APP只有两个页面)都可能使用某首歌曲的信息, 将数据存放到单例的另一个好处就是, 数据伴随单例, 扩大了作用域范围, 与上一条连用, 我们做到了任何页面任何时刻, 都可以随意的访问数据内容.
新建一个类, 继承NSObject, 名称为: GetDataTools

GetDataTools.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#import <Foundation/Foundation.h>
// 定义block
typedef void (^PassValue)(NSArray * array);

@interface GetDataTools : NSObject
// 作为单例的属性,这个数组可以在任何位置,任何时间被访问.
@property(nonatomic,strong)NSMutableArray * dataArray;

// 单例方法
+(instancetype)shareGetData;

// 根据传入的URL,通过Block返回一个数组.
-(void)getDataWithURL:(NSString *)URL PassValue:(PassValue)passValue;

// 根据传入的Index,返回一个"歌曲信息的模型",这个模型来自上面的属性数组.
-(MusicInfoModel *)getModelWithIndex:(NSInteger)index;
@end

GetDataTools.m

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
#import "GetDataTools.h"

static GetDataTools * gd = nil;

@implementation GetDataTools
// 单例方法, 这个单例方法是不完全的, 如果C层开发者使用了[alloc init]的方式创建对象, 仍不为单例, 正确的封闭其他所有init方法, 或者重写调用我们当前的方法返回对象.
+(instancetype)shareGetData
{
      
    if (gd == nil) {
      
        static dispatch_once_t once_token;
        dispatch_once(&once_token, ^{
      
            gd = [[GetDataTools alloc] init];
        });
    }
    return gd;
}

// 传入URL, 通过Block返回歌曲信息列表队列.
-(void)getDataWithURL:(NSString *)URL PassValue:(PassValue)passValue
{
      
    // 这里为什么要用子线程?
    // 因为,这里请求数据时:arrayWithContentsOfURL方法是同步请求(请求不结束,主线程什么也干不了)
    // 所以,为了规避这种现象,我们将请求的动作放到子线程中.
   
    // 创建线程队列(全局), 改天写个多线程的blog.
    dispatch_queue_t globl_t = dispatch_get_global_queue(0, 0);
   
    // 定义子线程的内容.
    dispatch_async(globl_t, ^{
      
        // 在这对花括号内的所有操作都不会阻塞主线程了哦
       
        // 请求数据
        NSArray * array =[NSArray arrayWithContentsOfURL:[NSURL URLWithString:URL]];
       
        // 解析,将解析好的"歌曲信息模型", 加入我们的属性数组, 以便外界能随时访问.
        for (NSDictionary * dict in array) {
      
            MusicInfoModel * model = [[MusicInfoModel alloc] init];
            [model setValuesForKeysWithDictionary:dict];
            [self.dataArray addObject:model];
        }
        // !!!Block回传值
        passValue(self.dataArray);
    });
}

// 属性数组的懒加载(并不是必须用懒加载, 懒加载有懒加载的好处)
-(NSMutableArray *)dataArray
{
      
    if (_dataArray == nil) {
      
        _dataArray = [NSMutableArray array];
    }
    return _dataArray;
}

// 根据传入的index返回一个"歌曲信息模型"
-(MusicInfoModel *)getModelWithIndex:(NSInteger)index
{
      
    return self.dataArray[index];
}

@end

在这个类中, 我们定义了三个方法:

  • shareGetData; 单例方法, 单例的好处不在此处赘述. 单例很重要, 一定要熟练掌握.
  • getDataWithURL: PassValue:; 这个方法是本类的核心功能了, 从网络中请求数据(异步), 通过block将数组返回. 注意: 这个类本身有一个成员变量_dataArray, 它里面存放了所有的歌曲信息, 其他页面可以通过单例.dataArray方法获取到, 但是我们这里仍然封装其返回一个新的数组的方法. 不为别的, 就是因为这种方式太重要了 – 为了使用而使用.
  • getModelWithIndex; 您可能不明为为什么要写一个这么个方法,这个方法的产生是有后面的逻辑背景的, 这里因为blog书写不便, 就直接写在这里了.

以上是我们的数据请求类(GetDataTools).


3.2 播放器工具类(H)

在封装AVPlayer之前, 我们先了解一下AVPlayer有什么特点.
AVPlayer存在于AVFoundation中, 它更加接近于底层, 所以灵活性也更强,AVPlayer本身并不能显示视频,而且它也不像MPMoviePlayerController有一个view属性。如果AVPlayer要显示必须创建一个播放器层AVPlayerLayer用于展示,播放器层继承于CALayer,有了AVPlayerLayer之添加到控制器视图的layer中即可。要使用AVPlayer首先了解一下几个常用的类和它的属性/方法(本段源自网络):

  • AVAsset: 属性, 主要用于获取多媒体信息,是一个抽象类,不能直接使用。
  • AVURLAsset: 属性, AVAsset的子类,可以根据一个URL路径创建一个包含媒体信息的AVURLAsset对象。
  • AVPlayerItem: 属性,一个媒体资源管理对象,管理者视音频的一些基本信息和状态,一个AVPlayerItem对应着一个视音频资源.
  • replaceCurrentItemWithPlayerItem: 替换AVPlayer的当前Item.
  • play: 方法, 播放媒体.
  • pause: 方法, 暂停.
  • seekToTime:completionHandler: 方法, 播放跳转, 调整播放进度.

下面我们解释一下AVPlayerItem:

  • AVPlayerItem
    我们不妨做一次角色扮演游戏, 我是老板, 你是一位员工小张, 主要负责向客户推销一款产品.
    今天早上我对你下达了这样命令:
    小张, 隔壁的桌子上有一款新开发的产品, 你现在拿着它去给展厅的客户们介绍一下.
    上面的命令透露出两个信息:
    1.这个款产品是新产品, 你从来没有听说过, 你不知道它的任何参数.
    2.你需要立刻完成这件事.
    你不傻掉才怪. 这样的命令式不合逻辑, 不合设计思路的.
    对比下面的命令:
    小张, 这里有份资料, 里面记录着隔壁桌子上新产品的详细信息, 你拿去研究一下, 等到你完全掌握并且准备好时, 你告诉我, 我给你安排一个展厅向客户介绍它.
    这条命令透露出的信息:
    1.这个产品有说明书
    2.你别着急, 慢慢研究, 研究好了你告诉我, 这个时间我(老板)先干点别的.
    很明显下面的方式要好于上面.
    同样, 你对AVPlayer下达命令也不能采用第一种方式, 你要告诉它, 你要播放的歌曲是什么名字, 有多长时间, 文件在什么位置, 歌曲的图片是什么, 这些东西你要给它写一份详细的说明书.
    这个说明书就是AVPlayerItem.
    每一个AVPlayer对象, 都有一个自己的AVPlayerItem属性, 名字叫做:currentItem, 我们可以通过
    replaceCurrentItemWithPlayerItem: 方法来替换当前的Item, 将准备好的Item, 交给Player.
    这个过程我们使用观察者模式模式来监视AVPlayerItem的准备情况. 一旦准备完毕, 会修改自身的status属性为AVPlayerItemStatusReadyToPlay枚举值, 一旦观察到这种状态, 我们就开始真正的播放.

  • 方法: play 和 pause
    这个两个是AVPlayer的播放控制方法, 我们在控制界面有个按钮, 点一下就播放, 再点一下就暂停, 反复重复. 貌似没有什么, 但是这里有个棘手的问题, AVPlayer的对象成员变量中, 居然没有来标识当前播放状态的! 也就是说, 你永远也不可能直接的获得当前AVPlayer正在播放中或者暂停了.
    通常情况下, 我们通过AVPlayer的一个rate(播放速率)来间接得到播放状态, rate==0则暂停, 不为0则正在播放中.

  • 切换歌曲
    AVPlayer并没有直接提供下一曲和上一曲的的功能, 但是我们可以通过上面的replaceCurrentItemWithPlayerItem:方法, 将AVPlayer对象的Item替换掉, 之后让它播放, 就可以达到这个效果.

新建一个类, 继承NSObject, 名称为: MusicPlayTools

MusicPlayTools.h

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
#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>

// !!! 与block回传值作比较.
// 定义协议. 通过代理方法返回当前歌曲的播放进度.
// 如果外界想使用本播放器,必须遵循和实现协议中的两个方法.
@protocol MusicPlayToolsDelegate <NSObject>
// 外界实现这个方法的同时, 也将参数的值拿走了, 这样我们起到了"通过代理方法向外界传递值"的功能.
-(void)getCurTiem:(NSString *)curTime Totle:(NSString *)totleTime Progress:(CGFloat)progress;
// 播放结束之后, 如何操作由外部决定.
-(void)endOfPlayAction;
@end

@interface MusicPlayTools : NSObject
// 本类中的播放器指针.
@property(nonatomic,strong)AVPlayer * player;
// 本类中的,播放中的"歌曲信息模型"
@property(nonatomic,strong)MusicInfoModel * model;
// 代理
@property(nonatomic,weak)id<MusicPlayToolsDelegate> delegate;

// 单例方法
+(instancetype)shareMusicPlay;
// 播放音乐
-(void)musicPlay;
// 暂停音乐
-(void)musicPause;
// 准备播放
-(void)musicPrePlay;
// 跳转
-(void)seekToTimeWithValue:(CGFloat)value;
// 返回一个歌词数组
-(NSMutableArray *)getMusicLyricArray;
// 根据当前播放时间,返回 对应歌词 在 数组 中的位置.
-(NSInteger)getIndexWithCurTime;
@end

MusicPlayTools.m

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值