AVPlayer播放线上、本地音乐

前言

说到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地址

你可能感兴趣的:(AVPlayer播放线上、本地音乐)