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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,016评论 4 62
  • 上周践行情况 1、每天早起冥想(7/7) 2、运动跑步(3/7) 3、参加班级周会、8组小组会, 4、积极参与8组...
    心随乐动_ef61阅读 140评论 0 0
  • 盛夏快要结束,仍残留些蝉鸣 二十二岁仍碌碌无为的我 不知该如何去走自己的路 儿时的朋友各奔东西寻找出路 回头望去只...
    萧与南歌阅读 177评论 0 1
  • 又开始连续几天的失眠。 症状又出现反复,还是不稳定状态。 心中没有爱溢出的感觉,还是觉得空空的。 迷茫不知未来在哪...
    元谋人在涂鸦阅读 149评论 0 0
  • 在我的生命里给我伤害的东西从来不会善待我。 比如你不爱我,比如十九。 我以为昨晚的不欢而散已经足够给我利刃,没想到...
    阿羊ai阅读 276评论 2 0