前言
说到iOS 开发音乐播放,之前有自己简单写过demo,用的是AVAudioPlayer,是系统提供的专门播放音频、音效,觉得挺好用,但是不支持在线播放,这点很难将其应用到项目中去实现一个播放器的需求,除非先下载后播放。
当然也可以寻找三方帮忙解决,比较被大众认可的有FreeStreamer、AudioStreamer。FreeStreamer没有用过这里不发表看法。AudioStreamer自己有写demo应用过,整体感觉下来是不错,这里就简单说下缺点,首先AudioStreamer已经多久没人维护更新;再者只支持线上播放而不支持本地,这点也是很无奈;另外在获取音乐已播放时间和总时间上总感觉有点出入。
接下来说下苹果提供的AVPlayer,AVPlayer是一个可以播放任何格式的全功能影音播放器,适应于iPhone/iPod/iPad(摘自百度百科)。我个人开发的习惯是这样,出于业务需求要实现某个功能,苹果提供有相应的API,那么建议基于系统API自己去实现功能,而不是借助三方。一是可定制性高,可以随着产品需求而自己封装对应逻辑,也利于以后维护、更新;二则是对于我们开发者本身来说也是进步。文章结尾有我自己写的Demo链接,有兴趣的朋友可以下载玩一下。
代码实现
像播放音乐这种实现某一功能,一般建议封装一个工具类,然后提供出相应的接口即可(比如:播放、暂停、销毁)。
单例类统一控制音乐的播放、暂停、销毁
import UIKit
import AVFoundation
// MARK: - JYPlayer
class JYPlayerManager: NSObject {
/// 记录当前音乐链接
fileprivate var currentURLString: String?
fileprivate var player: AVPlayer?
fileprivate var playerItem: AVPlayerItem?
/// 记录是否正在播放
var isPlaying: Bool = false
/// 实例化对象单例方法
static let shareInstance: JYPlayerManager = {
return JYPlayerManager()
}()
// MARK: - lazy
// 缓存池:缓存当前播放的AVPlayer对象,以免暂停状态下再继续而重新创建播放对象
fileprivate lazy var playerDictionary: [String: AVPlayer] = {
return [String: AVPlayer]()
}()
提供相应操作接口
/**
播放
urlString: 音乐链接
isOnline: 是否是线上播放
*/
func play(urlString: String, isOnline: Bool) -> (AVPlayerItem?) {
// 先看缓存池中是否有player
player = playerDictionary[urlString]
if player != nil {// 缓存池中有
}else {// 缓存池中没有
var url: URL?
// 注意:在线播放和本地播放的主要区别就是创建URL的方法不同
if isOnline == true {// 在线播放
url = URL(string: urlString)
}else {// 本地播放
url = URL(fileURLWithPath: urlString)
}
guard let myURL = url else {
return nil
}
playerItem = AVPlayerItem(url: myURL)
player = AVPlayer(playerItem: playerItem)
// 将新创建的playerItem放入缓存池中
playerDictionary[urlString] = player
}
// 播放
player?.play()
isPlaying = true
// 记录当前音乐链接
currentURLString = urlString
return playerItem
}
/// 暂停
func pause() -> () {
guard let player = player else {
return
}
player.pause()
isPlaying = false
}
/// 销毁:一首曲子播放完毕,从缓存池中销毁player
func destroy() -> () {
player?.pause()
player = nil
playerItem = nil
playerDictionary.removeValue(forKey: currentURLString ?? "")
}
}
播放工具封装好,剩下的就是根据业务需要实现相应逻辑,这里简单写了一个播放界面,只实现了播放和暂停,至于上一曲、下一曲、这些业务逻辑需要单独另外写一个工具类来管理音乐数据源来控制;而进入曲目详情、播放列表则需要用到数据库把听过的曲目保存到本地,这些逻辑就不在这里叙述,也都不是难的事情,思路整理好就可以。
弹出播放界面方法
// 显示播放器
class func show(music: JYMusic, isOnline: Bool)
播放界面代码实现
import UIKit
import AVFoundation
class JYMusicPlayerView: UIView {
/// 歌曲名称
@IBOutlet fileprivate weak var musicNameLbl: UILabel!
/// 歌手名称
@IBOutlet fileprivate weak var singerNameLbl: UILabel!
/// 进度条视图左边距离
@IBOutlet fileprivate weak var progressContainerViewLeft: NSLayoutConstraint!
/// 进度条视图右边距离
@IBOutlet fileprivate weak var progressContainerViewRight: NSLayoutConstraint!
/// 播放进度圆点
@IBOutlet fileprivate weak var progressDotView: UIView!
/// 左边距离
@IBOutlet fileprivate weak var progressDotViewLeft: NSLayoutConstraint!
/// 宽度
@IBOutlet fileprivate weak var progressDotViewWidth: NSLayoutConstraint!
/// 当前播放时间
@IBOutlet fileprivate weak var currentTimeLbl: UILabel!
/// 总时长
@IBOutlet fileprivate weak var durationLbl: UILabel!
/// 播放、暂停按钮
@IBOutlet fileprivate weak var playOrPauseButton: UIButton!
fileprivate var urlString: String?
fileprivate var playerItem: AVPlayerItem?
/// 计时器:更新播放进度
fileprivate var progressTimer: Timer?
/// 显示
class func show(music: JYMusic, isOnline: Bool) {
guard let urlString = music.urlString else {
return
}
let playerView = Bundle.main.loadNibNamed("JYMusicPlayerView", owner: nil, options: nil)?.first as! JYMusicPlayerView
let window = UIApplication.shared.keyWindow!
window.isUserInteractionEnabled = false
window.addSubview(playerView)
playerView.frame = window.bounds
playerView.transform = CGAffineTransform(translationX: 0, y: window.height)
UIView.animate(withDuration: 0.25, animations: {
playerView.transform = CGAffineTransform.identity
}) { (_) in
window.isUserInteractionEnabled = true
// 1、停止之前播放
JYMusicPlayerManager.shareInstance.destroy()
// 2、开始现在播放
playerView.playerItem = JYMusicPlayerManager.shareInstance.play(urlString: urlString, isOnline: isOnline)
playerView.urlString = urlString
// 添加计时器
playerView.addProgressTimer()
// 歌曲名称
playerView.musicNameLbl.text = music.name
// 歌手名称
playerView.singerNameLbl.text = music.singerName
}
}
/// 消失
@IBAction fileprivate func dismissButtonDidClick() {
let window = UIApplication.shared.keyWindow!
window.isUserInteractionEnabled = false
UIView.animate(withDuration: 0.25, animations: {
self.y = window.height
}) {(_) in
self.removeFromSuperview()
window.isUserInteractionEnabled = true
}
}
override func awakeFromNib() {
super.awakeFromNib()
// 设置UI
setupUI()
}
/// 设置UI
fileprivate func setupUI() {
// 播放进度圆点添加滑动手势
let pan = UIPanGestureRecognizer(target: self, action: #selector(panProgressPointView(pan:)))
progressDotView.addGestureRecognizer(pan)
}
/// 滑动触发事件
@objc fileprivate func panProgressPointView(pan: UIPanGestureRecognizer) {
guard let playerItem = playerItem else {
return
}
// 获得移动距离
let point = pan.translation(in: pan.view)
// 将translation清空,避免重复叠加
pan.setTranslation(CGPoint.zero, in: pan.view)
// 最大移动距离
let maxValue = width - progressContainerViewLeft.constant - progressContainerViewRight.constant - progressDotViewWidth.constant
progressDotViewLeft.constant += point.x
if progressDotViewLeft.constant < 0 {
progressDotViewLeft.constant = 0;
}else if progressDotViewLeft.constant > maxValue {
progressDotViewLeft.constant = maxValue;
}
// 更新时间
let percent = progressDotViewLeft.constant / maxValue
if pan.state == UIGestureRecognizerState.began {// 开始滑动
// 移除计时器
removeProgressTimer()
}else if pan.state == UIGestureRecognizerState.ended {// 结束滑动
let expectedTime = CMTimeGetSeconds(playerItem.duration) * Float64(percent)
var time = playerItem.currentTime()
time.value = CMTimeValue(time.timescale) * CMTimeValue(expectedTime)
playerItem.seek(to: time)
// 添加计时器
addProgressTimer()
}
}
// 点击“上一首”按钮
@IBAction fileprivate func previousButtonDidClick() {
print("上一首")
}
// 点击“播放、暂停”按钮
@IBAction fileprivate func playOrPauseButtonDidClick() {
if JYMusicPlayerManager.shareInstance.isPlaying == true {
JYMusicPlayerManager.shareInstance.pause()
playOrPauseButton.setImage(UIImage(named: "Player_play"), for: .normal)
}else {
if let urlString = urlString {
playOrPauseButton.setImage(UIImage(named: "Player_pause"), for: .normal)
playerItem = JYMusicPlayerManager.shareInstance.play(urlString: urlString, isOnline: false)
}
}
}
// 点击“下一首”按钮
@IBAction fileprivate func nextButtonDidClick() {
print("下一首")
}
}
计时器逻辑:更新播放时间,进度条位置
// MARK: - 计时器逻辑
extension JYMusicPlayerView {
/// 添加计时器
fileprivate func addProgressTimer() {
removeProgressTimer()
progressTimer = Timer.scheduledTimer(timeInterval: 0.25, target: self, selector: #selector(updateProgress), userInfo: nil, repeats: true)
}
/// 计时器触发方法
@objc fileprivate func updateProgress() {
guard let playerItem = playerItem else {
return
}
let currentTime = CMTimeGetSeconds(playerItem.currentTime())
var duration = CMTimeGetSeconds(playerItem.duration)
if duration.isNaN == true {// 当分母为0时,结果为inf(inf表示无穷大)
duration = 0.001;
}
let percent = currentTime / duration
progressDotViewLeft.constant = CGFloat(percent) * (width - progressContainerViewLeft.constant - progressContainerViewRight.constant - progressDotViewWidth.constant)
currentTimeLbl.text = stringWithTime(time: currentTime)
durationLbl.text = stringWithTime(time: duration)
if currentTime == duration {
print("播放完毕")
// 移除计时器
removeProgressTimer()
// 可以在这里写自动播放下一首逻辑
}
}
/// 移除计时器
fileprivate func removeProgressTimer() {
progressTimer?.invalidate()
progressTimer = nil
}
/// 时间格式转换
fileprivate func stringWithTime(time: Float64) -> (String) {
let minute = Int(time / 60)
let second = Int(time) % 60
return String(format: "%02d:%02d", arguments: [minute, second])
}
}
业务逻辑上就不写那么全面,实现基本的播放操作,至于其他功能可以根据项目需求自己添加;如果有觉得写的有正确或不足之处、欢迎各位指正,期待共同进步...
Demo地址