基于AVPlayer封装的音频播放器XTPlayer

XTPlayer

XTPlayer是一款简单易用的基于AVPlayer封装的纯Swift音频播放器,满足对音频文件播放的基本需求。
github地址

功能:

  • [x] 支持对音频资源的权限管理
  • [x] 支持播放的同时缓存文件
  • [x] 支持播放时记录播放进度
  • [x] 支持对历史进度的跳转
  • [x] 支持对播放状态改变的监听
  • [x] 处理了弱网环境的播放问题
  • [x] 支持实时更新UI
  • [x] 倒计时暂停播放
  • [x] 支持对音频资源的播放进度进行查询

要求

  • iOS 9.0+
  • Xcode 10.1+
  • Swift 4.2+

依赖库

XTPlayer依赖Alamofire完成对音频资源的下载;依赖WCDB完成对播放历史的记录及查询。

安装

CocoaPods

可以通过CocoaPods方式对XTPlayer进行集成,在你工程的Podfile文件中写上如下代码

pod 'XTPlayer'

示例工程

你可以从这里将示例工程下载下来,运行Example文件夹中的XTPlayer.xcworkspace

XTPlayer使用.2019-04-10 14_21_13.gif

示例工程中只展示了XTPlayer的几个功能,还有一些功能并未展示出来,XTPlayer实现了数据源的访问权限管理、进度跳转、切换数据源、改变播放速率、缓存音频数据源、区别用户记录播放记录、统计日播放时长、倒计时暂停播放、后台播放、区别用户的历史播放进度跳转等功能。

使用方式

XTPlayer内置了一个全局的单例对象,可以使用xt_player访问该对象,在使用播放器前需要调用实例方法active()来对XTPlayer进行初始化,这里同时对计时器进行了初始化操作,如果不需要对播放中的音频进行播放记录同时也不需要设置倒计时关闭播放,这里可以不用初始化计时器。

func application(_ application: UIApplication, 
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
  initConfig()
  return true
}
    
/** 初始化配置*/
func initConfig() {
  /** 激活播放器*/
  xt_player.active()
  /** 计时器初始化配置*/
  xt_countdown.initConfig()
}

基本用法

首先配置播放数据源模型,自建一个自定义对象,需要遵守XTPlayerDataSource协议,并实现协议要求的3个属性,分别为xt_playURL(对应播放源地址)和xt_sourceName(对应播放源名称)以及xt_sourceType(对应播放源访问权限)。
其中,播放源访问权限有3种权限可以设置,分别为

// MARK: —————————— 播放器数据源类型 ——————————
public enum XTPlayerSourceType {
    /** 没有权限*/
    case noPermission
    /** 完整播放*/
    case full
    /** 部分播放,仅可以播放前length秒的长度*/
    case partly(length: UInt)
}

开发者可以在自定义模型类中实现这3个属性的get方法。

import UIKit
import XTPlayer

class CustomAudioModel: XTPlayerDataSource {
    /** 需要具体描述音频数据源的播放类型,共有3种类型,
     /** 没有权限*/
     case noPermission
     /** 完整播放*/
     case full
     /** 部分播放,参数为允许播放时长*/
     case partly(length: UInt)
     */
    var xt_sourceType: XTPlayerSourceType {
        get {
            if isFree == 1 {
                return .full
            } else if let length = freeTime {
                return .partly(length: length)
            } else {
                return .noPermission
            }
        }
    }
    
    var xt_playURL: String? {
        get {
            return audioUrl
        }
    }
    
    var xt_sourceName: String? {
        get {
            return audioTitle
        }
    }
    
    //以下内容开发者可以根据实际情况自定义属性 
    /** 音频地址*/
    var audioUrl: String?
    /** 音频标题*/
    var audioTitle: String?
    /** 是否可以完整播放*/
    var isFree: Int?
    /** 不可以完整播放时能播放的秒数*/
    var freeTime: UInt?
}

待数据源模型配置好后,我们可以准备调用XTPlayer的相关API了。XTPlayer提供的公开API有播放、暂停、切换音频、恢复播放、跳转进度。

var allModels: [CustomAudioModel] {
        get {
            let model1 = CustomAudioModel()
            model1.audioUrl = "http://sc1.111ttt.cn/2018/1/03/13/396131229550.mp3"
            model1.isFree = 1
            model1.freeTime = 0
            model1.audioTitle = "音频1"
            
            let model2 = CustomAudioModel()
            model2.audioUrl = "http://sc1.111ttt.cn/2018/1/03/13/396131232171.mp3"
            model2.isFree = 0
            model2.freeTime = 100
            model2.audioTitle = "音频2"
            
            let model3 = CustomAudioModel()
            model3.audioUrl = "http://sc1.111ttt.cn/2018/1/03/13/396131228287.mp3"
            model3.isFree = 0
            model3.freeTime = 0
            model3.audioTitle = "音频3"

            /**
             音频1:可以播放完整音频
             音频2:可以播放100秒
             音频3:不可以播放
             */
            return [model1, model2, model3]
        }
    }
    // 跳转进度
    @IBAction func changeProgress(_ sender: Any) {
        guard let slider = sender as? UISlider else { return }
        xt_player.prepareForSeek(to: slider.value)
    }
    // 上一条音频
    @IBAction func playLastBtnClicked(_ sender: Any) {
        try? xt_player.playLast()
    }
    // 播放/暂停
    @IBAction func playBtnClicked(_ sender: Any) {
        do {
            try xt_player.play(index: 0)
        } catch let error {
            debugPrint(error)
        }
    }
    // 下一条音频
    @IBAction func playNextBtnClicked(_ sender: Any) {
        try? xt_player.playNext()
    }
    // XTPlayer加载原始数据源
    @IBAction func loadDataBtnClicked(_ sender: Any) {
        xt_player.allOriginalModels = allModels
    }
    // 切换播放速率
    @IBAction func changeRateBtnClicked(_ sender: Any) {
        
        if xt_player.settings.rate >= 2 {
            xt_player.settings.rate = 0.25
        } else {
            xt_player.settings.rate += 0.25
        }
    }
    // 销毁播放器
    @IBAction func destoryPlayerBtnClicked(_ sender: Any) {
        xt_player.allOriginalModels = nil
        xt_player.destroyPlayer()
    }

要对播放器的功能进行设置或对各种状态进行监听,需要遵守XTPlayerDelegate协议,并设置XTPlayer的代理。

xt_player.delegate = self

实现协议方法,其中configePlayer方法为必须要实现的,其它方法是可选的。
configePlayer方法中,我们需要完成对播放器功能的配置,播放器功能包括如下几种

// MARK: —————————— 播放器功能 ——————————
public struct XTPlayerFunction : OptionSet {
    /** 默认*/
    public static let `default` = XTPlayerFunction(rawValue: 1 << 0)
    /** 缓存*/
    public static let cache = XTPlayerFunction(rawValue: 1 << 1)
    /** 播放信息记录数据库*/
    public static let database = XTPlayerFunction(rawValue: 1 << 2)
    /** 跳转数据库记录的历史进度,如果需要跳转历史进度,必须拥有database功能*/
    public static let seekToHistory = XTPlayerFunction(rawValue: 1 << 3)
    /** 允许蜂窝网络加载*/
    public static let allowWWAN = XTPlayerFunction(rawValue: 1 << 4)
}

其中cache功能为在播放的同时,对数据源进行缓存;database功能为在播放的同时,记录播放记录,这里播放记录支持对不同的用户进行记录,通过对databaseID属性进行设置即可;seekToHistory功能支持对历史播放进度的跳转,如果需要跳转音频的历史进度,必须设置功能包括databaseallowWWAN功能为允许使用蜂窝网络加载网络音频资源。

extension ViewController: XTPlayerDelegate {
    
    func configePlayer() {
        xt_player.function = [.cache, .database, .seekToHistory]
        xt_player.databaseID = "customUID"
    }
    
    func playDataSourceWillChange(now: XTPlayerDataSource?, new: XTPlayerDataSource?) {
        debugPrint("设置上一个数据源,说明要切换音频了,当前是\(now?.xt_sourceName!),即将播放的是\(new?.xt_sourceName!)")
    }
    
    func playDataSourceDidChanged(last: XTPlayerDataSource?, now: XTPlayerDataSource) {
        debugPrint("设置新的数据源,说明已经切换音频了,原来是\(last?.xt_sourceName!),当前是\(now.xt_sourceName!)")
    }
    
    func didPlayToEnd(dataSource: XTPlayerDataSource, isTheEnd: Bool) {
        debugPrint("数据源\(dataSource.xt_sourceName!)已播放至结尾")
    }
    
    
    func noPermissionToPlayDataSource(dataSource: XTPlayerDataSource) {
        debugPrint("没有权限播放\(dataSource.xt_sourceName!)")
    }
    
    func didReadTotalTime(totalTime: UInt, formatTime: String) {
        //        debugPrint("已经读取到时长为duration = \(totalTime), format = \(formatTime)")
    }
    
    // 通过block方式询问当没有wlan网络时,是否允许通过wwan方式访问网络音频资源
    func askForWWANLoadPermission(confirmed: @escaping () -> ()) {
        let alert = UIAlertController.init(title: "网络环境确认", message: "当前非wifi环境,确定继续加载么", preferredStyle: .alert)
        let confirmAction = UIAlertAction.init(title: "确定", style: .default) {_ in
            confirmed()
        }
        alert.addAction(confirmAction)
        UIApplication.shared.keyWindow?.rootViewController?.present(alert, animated: true, completion: nil)
    }
    
   // 用于监听播放器状态改变
    func stateDidChanged(_ state: XTPlayerState) {
    }
    
    // 用于更新播放器界面UI
    func updateUI(dataSource: XTPlayerDataSource?, state: XTPlayerState, isPlaying: Bool, detailInfo: XTPlayerStateModel?) {
        
        playBtn.isSelected = isPlaying
        
        audioTitleLbl.text = dataSource?.xt_sourceName!
        guard let detail = detailInfo else { return }
        let currentTime = xt_playerTool.formatTime(seconds: detail.current)
        let durationTime = xt_playerTool.formatTime(seconds: detail.duration)
        audioDurationLbl.text = currentTime + "/" + durationTime
        bufferProgress.progress = detail.buffer
        audioProgressSlider.value = detail.progress
        
    }
    
    // 数据源发生改变时会调用此方法
    func dataSourceDidChange(lastOriginal: [XTPlayerDataSource]?, lastAvailable: [XTPlayerDataSource]?, nowOriginal: [XTPlayerDataSource]?, nowAvailable: [XTPlayerDataSource]?) {
    }
    
    func unifiedExceptionHandle(error: XTPlayerError) {
        debugPrint(error)
        
        let alert = UIAlertController.init(title: "Error", message: error.errorDescription, preferredStyle: .alert)
        let confirm = UIAlertAction.init(title: "ok", style: .default, handler: nil)
        alert.addAction(confirm)
        UIApplication.shared.keyWindow?.rootViewController?.present(alert, animated: true, completion: nil)
    }
}

XTPlayerError

如果XTPlayer在播放音频资源出错时,会将错误信息通过unifiedExceptionHandle代理方法传出来,并停止播放,错误基本分为四种类型。

  1. 数据源错误
  2. 网络错误
  3. 功能异常
  4. 播放器状态异常

具体错误原因请开发者查看XTPlayerError类详细了解。

XTPlayerStateModel

在播放过程中,如果需要更新界面信息,XTPlayer通过updateUI(dataSource: XTPlayerDataSource?, state: XTPlayerState, isPlaying: Bool, detailInfo: XTPlayerStateModel?)代理方法将可以更新的内容反馈出来,具体播放进度的详细音频信息存储在XTPlayerStateModel中。

public class XTPlayerStateModel {
    /** 播放状态*/
    public var state: XTPlayerState = .idle
    /** 播放进度*/
    public var progress: Float = 0
    /** 缓冲进度*/
    public var buffer: Float = 0
    /** 当前播放秒数*/
    public var current: UInt = 0
    /** 总时长*/
    public var duration: UInt = 0
}

XTPlayerRecord

XTPlayerRecord类完成对用户播放进度的记录以及提供历史播放进度的查询功能。如果开启了database功能,XTPlayer在播放过程中,默认每5秒钟会自动记录一次播放进度,这个时间间隔可以通过recordInterval属性进行设置修改,另外在播放器暂停时也会记录一次数据库。
XTPlayerRecord的记录逻辑为,首先查找该用户在某个日期下是否有该条音频的播放记录,如果没有该条音频的播放记录,则会向数据库中插入一条记录;如果有,则会先读取该条音频的历史最大播放记录,以及读取该日期下累计播放时长,然后判断当前播放进度是否超过历史最大播放进度,并且将此次播放时长累加到同一日期下的累积播放时长中,然后对数据库表进行更新,这中间的所有操作均以事务方式进行。

XTCountdown

XTCountdown类实现了倒计时的功能,为XTPlayer的播放进度和倒计时暂停功能提供计时器相关功能的支持。
XTCountdown同样有一个xt_countdown的单例,通过initConfig函数对计时器进行初始化,其公开了如下几个函数:

// 初始化计时器
func initConfig()
// 检查是否存在以key为标记的未完成计时的计时器对象,并以block形式将该对象的详细信息传出来
func checkCountdown(key: String, progressHandler: ((inout CountdownDetailInfo) -> ())? = nil) -> (Bool, Bool)
// 移除以key为标识的计时器对象
func removeCountdown(key: String)
// 暂停或恢复计时
func pauseOrResumeCountdown(key: String, forcePause: Bool? = nil) -> CountdownDetailInfo?
// 开启或暂停计时器
func startCountdown(key: String, seconds: UInt, function: XTCountdownFunction = .default, onlyStart: Bool = false, progressHandler progress: ((inout CountdownDetailInfo) -> ())? = nil) -> CountdownDetailInfo?

XTCountdown也提供了一些可选功能,包括如下

// MARK: —————————— 定时器功能 ——————————
public struct XTCountdownFunction: OptionSet {
    
    /** 默认*/
    public static let `default` = XTCountdownFunction(rawValue: 1 << 0)
    /** 持久化,是否需要杀死程序后仍然开启计时*/
    public static let cache = XTCountdownFunction(rawValue: 1 << 3)
    /** 持久化时是否需要保持计时*/
    public static let remainTiming = XTCountdownFunction(rawValue: 1 << 4)
}

如果开发者设置了功能包括cache以及remainTiming,则应用程序在被杀死到下一次启动时的时间间隔也会影响倒计时剩余时间;如果功能只包括cache,则应用在被杀死时,会做持久化记录,下次启动时将继续进行上次未完成的计时;如果不需要对未完成计时设置持久化,则设置功能只包括default即可。
XTCountdown通过Block将未完成的计时器详细信息封装成一个CountdownDetailInfo类传出来,CountdownDetailInfo类还支持获取到某个计时器对象,使其执行开启计时、暂停计时、销毁操作,CountdownDetailInfo包括的计时器信息包括如下:

/** 计时器详细信息*/
public class CountdownDetailInfo {
    /** 功能状态值*/
    var state: Int = 0
    /** 已经运行时长*/
    var run: UInt = 0
    /** 剩余时长*/
    var left: UInt = 0
    /** 总计时长*/
    var total: UInt = 0
    /** 定时器对象*/
    var timer: Timer = Timer()
    /** 节点时间戳*/
    var checkpointStamp: Double = 0
    /** 结束时间戳*/
    var endStamp: Double = 0
    /** 唯一标识符*/
    var key: String = ""
    /** 本次运行时长*/
    var runThisTime: UInt = 0
    
    func start() {
        xt_countdown.pauseOrResumeCountdown(key: key, forcePause: false)
    }
    
    func pause() {
        xt_countdown.pauseOrResumeCountdown(key: key, forcePause: true)
    }
    
    func drop() {
        xt_countdown.removeCountdown(key: key)
    }
}

XTPlayerTool

作为XTPlayer的辅助工具类,XTPlayerTool提供一些工具方法,包括开启网络状态监听、设置音频播放进度时长输出格式、根据某个URL信息读取该音频资源总时长、下载音频资源等功能。

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