这是一款音乐播放器,本地,网络音乐都可以播放,做这个主要原因:一个是为了毕业设计,一个是为了考验一下自己编码的能力。毕业设计题目是“基于iOS平台的网络音乐播放器的设计与实现”
项目要求
功能要求:
- 基本的音乐播放功能(播放,暂停,继续等)。
- 实现本地或者网络音乐的播放。
- 实现基本的播放模式(顺序播放,随机播放等)。
- 实现歌曲搜索(根据歌手进行搜索,根据歌名进行搜索)。
- 收藏歌曲,将喜欢的歌曲进行收藏到指定的文件夹下。
- 进行歌曲的下载和缓存,实现离线播放已经缓存好的或者已经下载好的音乐。
- 实现歌词同步显示。
- 清除缓存功能,删除已经下载的音乐。
后台:
总体设计
播放器设计
- ❌
AVAudioPlayer
只支持本地播放。
Q: Does the AVAudioPlayer provide support for streaming audio content?
A: The AVAudioPlayer class does not provide support for streaming audio based on HTTP URL’s. The URL used with initWithContentsOfURL: must be a File URL (file://). That is, a local path.
As mentioned in Core Audio Essentials, the AVAudioPlayer class is ideal for applications that require a simple Objective-C interface for audio playback, are not concerned about audio positioning or precise synchronization, and are not playing audio from a network stream.
- ✅ 自己使用
Audio File Stream Services
和Audio Queue Services
来解析音频流。
To play streamed audio, connect to a network stream using the CFNetwork interfaces from Core Foundation, such as those in CFHTTPMessage. Parse the network packets into audio packets using Audio File Stream Services (AudioToolbox/AudioFileStream.h) and play the audio packets using Audio Queue Services (AudioToolbox/AudioQueue.h). You may also use Audio File Stream Services to parse audio packets from an on-disk file.
- ✅
AVPlayer
支持任何流媒体播放,包括视频,音频。
AVPlayer (available in iOS 4.0 and later) may also be used to stream audio content.
播放器-实现方案
采用
AVFoundation
库:AVQueuePlayer
,AVPlayerItem
,AVURLAsset
,AVPlayer
。Apple 提供的高级API接口,但是坑比较多,实现变播放边缓存比较麻烦。
采用第三方流媒体库:
StreamingKit
,DOUAudioStreamer
,FreeStreamer
。第三方流媒体框架。比较老旧,很多已经几年没有更新,稳定性高。
采用
AudioToolbox
库:AudioQueue
,AudioFileStream
。接近于底层的API接口,提供更多的控制,需要自己实现音频流的解析。
播放器-实现方案测试
AudioToolbox > Audio Queue Services & Audio File Stream Services
Overview
Using Self In C Block
⚠️:
Audio Queue Services
和Audio File Stream Services
提供的回调都是C的回调,所以在Swift中使用这些回调有两个选择,一个是Block,一个是全局变量如果在Block中使用self会出现:
A C function pointer cannot be formed from a closure that captures context
。一般这样的函数都会有一个参数是传递当前的实例,比如下面函数中的
inClientData: UnsafeMutableRawPointer
。传递参数的时候将自身实例转换成指针传递
unsafeBitCast(self, to: UnsafeMutableRawPointer.self)
在回调中
let mySelf = Unmanaged<YourClass>.fromOpaque(inClientData).takeUnretainedValue()
再将指针转换成YourClass类型的指针。
下面给出这个播放器整体运行的流程图:
Thread in AudioQueue and AudioFileStream
这里主要说的是AudioQueue和AudioFileStream中线程的问题
在AudioQueueNewOutput
创建的时候有一个参数inCallbackRunLoop
,官方是这样讲解这个参数的:
The event loop on which the callback function pointed to by the inCallbackProc parameter is to be called. If you specify NULL, the callback is invoked on one of the audio queue’s internal threads.
总结来说就是播放音频的线程需要开启
Runloop
,如果给nil
,那么就会使用自己内部的线程。由于主线程的
Runloop
是开启的,所以你的操作如果全部放在主线程,那么依旧可以播放,只不过性能不好。如果你的音频播放在没有开启
Runloop
的线程中,那么就会出现播放不出来,或者播放一小段时间就自动停止的情况,例如DispatchQueue.global()
创建的线程
是否可以播放 | AudioQueueNewOutput | AudioFileStreamParseBytes | inCallbackRunLoop | 详解 |
---|---|---|---|---|
✅ | AnyThread | AnyThread | nil | 推荐,AnyThread可以是自己创建的线程,也可以是非主线程,例如DispatchQueue.global() 创建的线程 |
✅ | AnyThread | AnyThread | CFRunLoopGetMain() | 不建议将解析数据和播放音频放在主队列中进行 ,虽然这是没有问题的 |
✨✨✨✨✨最佳方案: 在DispatchQueue
串行队列(保证数据连续性)中调用AudioFileStreamParseBytes
解析数据,inCallbackRunLoop
传递nil。
Thanks for: Why might my AudioQueueOutputCallback not be called?
Memory leak in AudioQueue
在播放完成之后要调用AudioQueueDispose
,否则会出现之前播放的数据不被释放,内存不断的上涨。
When you’re finished playing a file, dispose of the audio queue, close the audio file, and free any remaining resources.
The AudioQueueDispose function disposes of the audio queue and all of its resources, including its buffers.
AudioFileStream音频文件流
AudioFileStreamOpen打开一个音频文件流
1 | func AudioFileStreamOpen(_ inClientData: UnsafeMutableRawPointer?, _ inPropertyListenerProc: @escaping AudioFileStream_PropertyListenerProc, _ inPacketsProc: @escaping AudioFileStream_PacketsProc, _ inFileTypeHint: AudioFileTypeID, _ outAudioFileStream: UnsafeMutablePointer<AudioFileStreamID?>) -> OSStatus |
参数 | 意义 |
---|---|
inPropertyListenerProc | 歌曲信息解析的回调(一首歌曲只会回调一次) |
inPacketsProc | 分离帧的回调(每解析一部分数据就会回调一次) |
inFileTypeHint | MP3音频文件类型:kAudioFileMP3Type , 不知道文件类型就传0 |
outAudioFileStream | 成功返回音频流解析器ID: AudioFileStreamID |
AudioFileStream_PropertyListenerProc音频属性解析回调
1 | typealias AudioFileStream_PropertyListenerProc = (UnsafeMutableRawPointer, AudioFileStreamID, AudioFileStreamPropertyID, UnsafeMutablePointer<AudioFileStreamPropertyFlags>) -> Void |
参数 | 意义 |
---|---|
inAudioFileStream | 音频流解析器ID |
inPropertyID | 属性ID,表示当前哪个属性被解析到。 |
ioFlags | 表示这个property是否需要被缓存 |
当我们在这里解析到inPropertyID
为kAudioFileStreamProperty_DataFormat
的时候表示解析到了音频格式的数据,这个时候就可以建立输出队列,并开始解析音频数据,控制播放了。
AudioFileStream全局变量名 | 值 |
---|---|
kAudioFileStreamProperty_ReadyToProducePackets | 1919247481 |
kAudioFileStreamProperty_FileFormat | 1717988724 |
kAudioFileStreamProperty_DataFormat | 1684434292 |
kAudioFileStreamProperty_FormatList | 1718383476 |
kAudioFileStreamProperty_MagicCookieData | 1835493731 |
kAudioFileStreamProperty_AudioDataByteCount | 1650683508 |
kAudioFileStreamProperty_AudioDataPacketCount | 1885564532 |
kAudioFileStreamProperty_MaximumPacketSize | 1886616165 |
kAudioFileStreamProperty_DataOffset | 1685022310 |
kAudioFileStreamProperty_ChannelLayout | 1668112752 |
kAudioFileStreamProperty_PacketToFrame | 1886086770 |
kAudioFileStreamProperty_FrameToPacket | 1718775915 |
kAudioFileStreamProperty_PacketToByte | 1886085753 |
kAudioFileStreamProperty_ByteToPacket | 1652125803 |
kAudioFileStreamProperty_PacketTableInfo | 1886283375 |
kAudioFileStreamProperty_PacketSizeUpperBound | 1886090594 |
kAudioFileStreamProperty_AverageBytesPerPacket | 1633841264 |
kAudioFileStreamProperty_BitRate | 1651663220 |
kAudioFileStreamProperty_InfoDictionary | 1768842863 |
下面是我解析一个mp3文件得到的文件信息:
ID对应的实际值 | ID名称 | 表示内容 |
---|---|---|
1651663220 | kAudioFileStreamProperty_BitRate | 码率 |
1650683508 | kAudioFileStreamProperty_AudioDataByteCount | 音频数据总量 |
1885564532 | kAudioFileStreamProperty_AudioDataPacketCount | 音频帧总量 |
1717988724 | kAudioFileStreamProperty_FileFormat | 文件格式 |
1684434292 | kAudioFileStreamProperty_DataFormat | 数据格式 |
1685022310 | kAudioFileStreamProperty_DataOffset | 数据偏移量 |
1919247481 | kAudioFileStreamProperty_ReadyToProducePackets | 标志着接下来就是分离帧的步骤 |
AudioFileStream_PacketsProc帧分离回调
1 | typealias AudioFileStream_PacketsProc = (UnsafeMutableRawPointer, UInt32, UInt32, UnsafeRawPointer, UnsafeMutablePointer<AudioStreamPacketDescription>) -> Void |
参数 | 意义 |
---|---|
inNumberBytes | 音频的字节 |
inNumberPackets | 音频中包含的帧数 |
inInputData | 解析的音频数据 |
inPacketDescriptions | 是一个AudioStreamPacketDescription 类型的数组,通过mStartOffset 和mDataByteSize ,结合inInputData 就可以将分离的帧保存起来 |
AudioFileStreamParseBytes音频解析
1 | func AudioFileStreamParseBytes(_ inAudioFileStream: AudioFileStreamID, _ inDataByteSize: UInt32, _ inData: UnsafeRawPointer, _ inFlags: AudioFileStreamParseFlags) -> OSStatus |
参数 | 意义 |
---|---|
inAudioFileStream | 音频流解析器ID |
inDataByteSize | 解析的数据总量 |
inData | 解析的数据 |
inFlags | 解析的数据是否和上一个有关联 |
在哪个线程中调用这个方法,就在哪个线程中解析数据,建议在非主线程中调用这个方法。
AudioQueue音频队列
This table lists result codes defined for Audio Queue Services.
AudioQueue全局变量 | 值 |
---|---|
kAudioQueueErr_InvalidBuffer | -66687 |
kAudioQueueErr_BufferEmpty | -66686 |
kAudioQueueErr_DisposalPending | -66685 |
kAudioQueueErr_InvalidProperty | -66684 |
kAudioQueueErr_InvalidPropertySize | -66683 |
kAudioQueueErr_InvalidParameter | -66682 |
kAudioQueueErr_CannotStart | -66681 |
kAudioQueueErr_InvalidDevice | -66680 |
kAudioQueueErr_BufferInQueue | -66679 |
kAudioQueueErr_InvalidRunState | -66678 |
kAudioQueueErr_InvalidQueueType | -66677 |
kAudioQueueErr_Permissions | -66676 |
kAudioQueueErr_InvalidPropertyValue | -66675 |
kAudioQueueErr_PrimeTimedOut | -66674 |
kAudioQueueErr_CodecNotFound | -66673 |
kAudioQueueErr_InvalidCodecAccess | -66672 |
kAudioQueueErr_QueueInvalidated | -66671 |
kAudioQueueErr_TooManyTaps | -66670 |
kAudioQueueErr_InvalidTapContext | -66669 |
kAudioQueueErr_RecordUnderrun | -66668 |
kAudioQueueErr_InvalidTapType | -66667 |
kAudioQueueErr_BufferEnqueuedTwice | -66666 |
kAudioQueueErr_CannotStartYet | -66665 |
kAudioQueueErr_EnqueueDuringReset | -66632 |
kAudioQueueErr_InvalidOfflineMode | -66626 |
AudioQueueNewOutput创建音频输出队列
1 | func AudioQueueNewOutput(_ inFormat: UnsafePointer<AudioStreamBasicDescription>, _ inCallbackProc: @escaping AudioQueueOutputCallback, _ inUserData: UnsafeMutableRawPointer?, _ inCallbackRunLoop: CFRunLoop?, _ inCallbackRunLoopMode: CFString?, _ inFlags: UInt32, _ outAQ: UnsafeMutablePointer<AudioQueueRef?>) -> OSStatus |
参数 | 意义 |
---|---|
inFormat | AudioStreamBasicDescription ,要播放的音频格式 |
inCallbackProc | 当音频队列中的的buffer已经播放完成并且能够重利用的时候 |
inCallbackRunLoop | 回调的RunLoop,nil表示采用音频队列自己的RunLoop进行回调 |
inCallbackRunLoopMode | RunLoopMode,nil表示采用kCFRunLoopCommonModes |
inFlags | 预留,必须传0 |
outAQ | 成功返回音频队列实例: AudioQueueRef |
AudioQueueOutputCallback播放音频完成回调
1 | typealias AudioQueueOutputCallback = (UnsafeMutableRawPointer?, AudioQueueRef, AudioQueueBufferRef) -> Void |
参数 | 意义 |
---|---|
inAQ | 音频回调队列 |
inBuffer | Buffer已经播放完成,可以重新利用 |
下面是官方图解:(这个Buffer是可以重新利用的)
但是在代码中是直接销毁重新创建,由于时间关系,留在以后再优化。
下面来一个动图帮助理解(Thanks for: Streaming Audio to Multiple Listeners via iOS’ Multipeer Connectivity)
AudioQueueAddPropertyListener添加音频队列属性监听
1 | func AudioQueueAddPropertyListener(_ inAQ: AudioQueueRef, _ inID: AudioQueuePropertyID, _ inProc: @escaping AudioQueuePropertyListenerProc, _ inUserData: UnsafeMutableRawPointer?) -> OSStatus |
参数 | 意义 |
---|---|
inAQ | 想要添加监听的音频队列 |
inID | 想要监听的音频队列属性AudioQueuePropertyID |
inProc | 当监听的属性改变的时候的回调 |
kAudioQueueProperty_IsRunning
可以监听当前队列是否正在运行。
AudioQueueAllocateBuffer为音频队列分配空间
1 | func AudioQueueAllocateBuffer(_ inAQ: AudioQueueRef, _ inBufferByteSize: UInt32, _ outBuffer: UnsafeMutablePointer<AudioQueueBufferRef?>) -> OSStatus |
参数 | 意义 |
---|---|
inAQ | 想要为哪个音频队列分配空间 |
inBufferByteSize | 分配空间大小 |
outBuffer | 成功之后分配空间的地址 AudioQueueBufferRef |
在这里我们向新的空间填充音频数据
1 | /// New buffer |
AudioQueueEnqueueBuffer将Buffer加入到音频队列中
1 | func AudioQueueEnqueueBuffer(_ inAQ: AudioQueueRef, _ inBuffer: AudioQueueBufferRef, _ inNumPacketDescs: UInt32, _ inPacketDescs: UnsafePointer<AudioStreamPacketDescription>?) -> OSStatus |
参数 | 意义 |
---|---|
inAQ | 音频队列 |
inBuffer | 分配新Buffer空间 |
inNumPacketDescs | 帧总信息数量 |
inPacketDescs | 帧信息AudioStreamPacketDescription 数组 |
⚠️ 在短时间内多次调用AudioQueueStop
会导致返回kAudioQueueErr_EnqueueDuringReset
,但是依旧可以正常播放。常见于seek中,先AudioQueueStop
再设置偏移量就会出现。暂时不知道如何更好的解决办法。
播放器测试结果:
✅ AudioToolbox
是否可以 | 内容 | 方法 | 详解 |
---|---|---|---|
✅ | 播放 | AudioQueueStart | 开始解析音频数据 |
✅ | 暂停 | AudioQueuePause | 再次点击播放,继续从上次的地方进行解析。 |
✅ | 停止 | AudioQueueStop | 再次点击播放,继续从上次的地方进行解析。 |
✅ | 快进,快退 | AudioQueueEnqueueBuffer | 向队列中压入指定的数据来实现快进快退 |
✅ | 当前时间 | AudioQueueGetCurrentTime | 获取播放时间后,通过时间偏移量来计算当前时间 |
✅ | 总时间 | AudioFileStream_PropertyListenerProc | 回调中解析到kAudioFileStreamProperty_BitRate 和kAudioFileStreamProperty_AudioDataByteCount 之后通过计算得到 |
API选择
✅ 采用源网易云音乐API
比较复杂,需要自己实现加密算法等,耗时比较长。
✅ 采用网易云音乐NodeJS版API
简单,不需要考虑加密等算法。
❌ 自己实现音乐API
没有音乐资源
实际项目开发
主要界面搭建
播放器界面
核心代码
后台音频播放及控制
后台音频播放
AppDelegate
中我们要添加下面这个来设置音频行为:
1 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { |
Setting the audio session category ensures that the application has the audio behavior expected of a media playback app.(设置
Audio Session
目的就是为了配置音频的行为)
An audio session acts as an intermediary between your app and the operating system.(
Audio Session
是作为App
和操作系统
的中间人)
默认有下列行为配置:
- Audio playback is supported, but audio recording is disallowed (audio recording applies only to iOS).(录音只适用于iOS)
- In iOS, setting the Ring/Silent switch to silent mode silences any audio being played by the app.(静音按钮对所有App奏效)
- In iOS, when the device is locked, the app’s audio is silenced.(默认没有后台播放)
- When the app plays audio, any other background audio is silenced.(默认单个App播放)
而我们之前设置的AVAudioSessionCategoryPlayback
就是其中的一个行为:
When using this category, your app audio continues with the Silent switch set to silent or when the screen locks.(按钮和锁屏都会使App不能后台播放)To continue playing audio when your app transitions to the background (for example, when the screen locks), add the audio value to the UIBackgroundModes key in your information property list file.(要在后台继续播放,需要配置
Info.plist
文件)
配置方式:
Select your app’s target in Xcode and select the Capabilities tab. Under the Capabilities tab, set the Background Modes switch to ON and select the “Audio, AirPlay, and Picture in Picture” option under the list of available modes.
后台音频播放控制
这个时候我们需要用到MediaPlayer
库中的MPRemoteCommandCenter
和MPNowPlayingInfoCenter
,两个类都是单例
添加播放,暂停,下一首,上一首控制。
1 | MPRemoteCommandCenter.shared().playCommand.addTarget(musicPlayerViewController, action: #selector(musicPlayerViewController.playCommand)) |
设置音乐名称,封面图,当前时间,总共时间,需要自己设置Timer来刷新。
1 | private func updateRemoteControl(_ time: TimeInterval) { |
多线程
DispatchGroup
在缓存音乐的时候需要保证音乐数据已经全部请求完成,而且音乐信息已经全部请求完成,这就涉及到多个异步线程完成之后统一回调的问题,所以采用DispatchGroup
的两个方法enter()
, leave()
来实现。
1 | /// 请求前enter |
DispatchQoS
A quality of service (QoS) class categorizes work to be performed on a DispatchQueue. By specifying a QoS to work, you indicate its importance, and the system prioritizes it and schedules it accordingly.
Because higher priority work is performed more quickly and with more resources than lower priority work, it typically requires more energy than lower priority work. Accurately specifying appropriate QoS classes for the work your app performs ensures that your app is responsive and energy efficient.
描述了服务质量,高优先级的执行越快。
Manager
MusicResourceManager
MusicFileManager
MusicDataBaseManager
数据库设计
数据库采用SQLite。
表 | 内容 |
---|---|
Cache | 存储已经缓存的文件ID |
Download | 存储已经下载的文件ID |
Resources | 存储音乐信息 |
CollectionList | 保存文件夹信息,主要用于离线使用 |
ListDetail | 保存文件夹下具体信息,主要用于离线使用 |
LeastResources | 程序退出前保存最后播放的信息(暂时未使用) |
Cache:1
2
3CREATE TABLE IF NOT EXISTS Cache (
id text PRIMARY KEY NOT NULL
);
Download:1
2
3CREATE TABLE IF NOT EXISTS Download (
id text PRIMARY KEY NOT NULL
);
Resources:1
2
3
4
5
6
7
8
9
10CREATE TABLE IF NOT EXISTS Resources (
id text PRIMARY KEY NOT NULL,
name text NOT NULL,
duration real NOT NULL,
lyric blob NOT NULL,
artist blob NOT NULL,
album blob NOT NULL,
info blob NOT NULL,
status integer NOT NULL
);
CollectionList:1
2
3
4CREATE TABLE IF NOT EXISTS CollectionList (
id text PRIMARY KEY NOT NULL,
playList blob NOT NULL
);
ListDetail:1
2
3
4CREATE TABLE IF NOT EXISTS ListDetail (
id text PRIMARY KEY NOT NULL,
playList blob NOT NULL
);
LeastResources:1
2
3
4
5
6
7
8
9
10CREATE TABLE IF NOT EXISTS LeastResources (
id text PRIMARY KEY NOT NULL,
name text NOT NULL,
duration real NOT NULL,
lyric blob NOT NULL,
artist blob NOT NULL,
album blob NOT NULL,
info blob NOT NULL,
status integer NOT NULL
);
在这里并没有直接存储各个字段,而是存储成blob类型,实际就是获取的JSON,这样从网络获取的数据和从数据库获取的数据可以采用同一个解析方式。
总结
- 字节计算,用于缓存计算,缺点是只能是整数。
1 | ByteCountFormatter.string(fromByteCount: 1024, countStyle: .binary) |
- 图片旋转
1 | UIImage(cgImage: <#T##CGImage#>, scale: UIScreen.main.scale, orientation: <#T##UIImageOrientation#>) |
- StatusBar
在UINavigationController
或者其子类中重写下面属性。
1 | override var preferredStatusBarStyle: UIStatusBarStyle { return .lightContent } |
Thanks for: How to change Status Bar text color in iOS 7
- 第三方库的使用
由于Swift是以文件为单位进行管理,所以每次使用第三方库的时候都需要
import
,非常的麻烦。所以如果这个库需要在很多文件中使用,那么使用下面的方式好很多。
如果这个库需要再次进行封装,或者只是在一个文件中使用,那么在单个文件中
import
比较好。
在这里只需要重命名类型(class, struct, tyealias, enum….)就可以。类似于SnapKit
,由于是extension
则不需要,只要在一个地方import
过就可以在整个项目中使用。
1 | //MARK: - Alamofire |
- 关于UIApplicationDelegate
基本的流程大概是这样的。
项目踩坑
StoryBoard
- 约束相对于的对象写错
push页面的时候因为TabBar被动态隐藏,所以页面会跳动一下。
内容 | 描述 |
---|---|
Top Layout Guide | 设置的约束相对于StatusBar 和NavigationBar 。 |
Bottom Layout Guide | 设置的约束相对于TabBar 。 |
Super View | 设置的约束相对于父视图。 |
- 之前采用StoryBoard写播放器界面
在下面代码中,第一次初始化播放器的时候,并且页面还没有push出来。这个如果使用里面的控件,都会是nil。所以只能放弃,使用代码进行布局。
1 | extension UIViewController { |
UITabBar
是否解决成功 | 问题描述 | 如何解决 |
---|---|---|
✅ | 透明背景 | 设置tabBar.backgroundImage 为一张透明背景图。 |
✅ | 文字颜色,图片颜色修改 |
全局设定TabBar的字体大小1
2
3
4let attributesTabBarNormal = [NSForegroundColorAttributeName: UIColor.white, NSFontAttributeName: UIFont.font10]
let attributesTabBarSelected = [NSForegroundColorAttributeName: UIColor.white, NSFontAttributeName: UIFont.font10]
UITabBarItem.appearance().setTitleTextAttributes(attributesTabBarNormal, for: .normal)
UITabBarItem.appearance().setTitleTextAttributes(attributesTabBarSelected, for: .selected)
修改选中图片颜色1
tabBar.tintColor = .white
是否解决成功 | 问题描述 | 如何解决 |
---|---|---|
✅ | push的时候出现跳动问题 | 约束应该依照当前的View进行设置。而不是Bottom Layout Guide设置,否则push时候隐藏TabBar会导致视图跳动。 |
JSON
JSON序列化
⚠️ 序列化的条件是JSONSerialization.isValidJSONObject(<#T##obj: Any##Any#>)
返回true
1 | /* |
文档中这样描述这个方法的意义,明确指出了能够序列化的类型
- Key必须是
NSString
类型 - 最高层必须是
NSArray
或者NSDictionary
,意味着在Swift中元组,闭包等等都不可以。 - 对象必须是
NSString
,NSNumber
,NSArray
,NSDictionary
, 或者NSNull
NSNumber
必须有意义
在Swift中
Bool
也是可以的,但是URL
却不符合,只能使用absoluteString
转换成String
进行存储由于这种持久化方式过于麻烦,所以采用SQLite数据库。
播放器资源设计
在进行编码的时候一直都存在着一些难点
- 播放器播放的时候需要哪些资源。
- 其他模块如何给定这些资源。
- 音乐请求之后需要缓存,如何实现。
- 点击下载之后怎们办。
ResourceManager
其他模块在传递的时候只告诉播放器音乐ID,播放去请求ResourceManager
,ResourceManager
决定资源从哪来。
其他模块需要先reset资源,
ResourceManager
从资源中找到对应的ID并判断资源来源,有没有缓存或者下载,之后再请求网络,或者从本地读取数据给播放器。
缺点很明显:需要兼顾网络和本地的管理,接口变得复杂,内部变的臃肿,资源包含的主体内容不明确。
暂时无法解决的Bug
赞赏功能无法使用
使用
URLSession
的dataTask
,包括使用Alamofire
返回的code都是301。但是使用
浏览器
或者Postman
请求同一个URL没有任何问题。Swift好像暂时没有修复这个Bug。URLSession dataTask doesn’t follow redirects
参考
⚠️ 未明确说明,英文说明都来自于Apple官方文档
参考博客
参考博客地址 | 参考内容 |
---|---|
码农人生 | 音频流解析,播放 |
Play And Record With CoreAudio on iOS | 音频流解析,播放 |
参考代码
第三方库
第三方库地址 | 使用内容 |
---|---|
SnapKit | 约束布局 |
Kingfisher | 图片缓存 |
Alamofire | 网络请求(使用了其中的编码功能) |
SwiftyJSON | JSON解析 |
MJRefresh | 刷新组件(OC) |
SQLite.swift | SQLite数据库工具 |
Toast-Swift | Toast提示组件 |
自定义库
自定义库地址 | 使用内容 |
---|---|
PageKit | 简易的分页库 |
Wave | 基于AudioToolBox简易流媒体播放器 |
Log | 简易Log工具 |
MusicAPI | 封装网易云音乐NodeJS版API 提供的API |
Lotus | 简易网络请求库 |