美文网首页程序员iOS Developer@IT·互联网
AVPlayer播放线上、本地音乐

AVPlayer播放线上、本地音乐

作者: 蓝色达风 | 来源:发表于2017-03-17 10:49 被阅读188次
    前言

    说到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播放线上、本地音乐

        本文链接:https://www.haomeiwen.com/subject/tjodnttx.html