iOS 视频播放器开发(一)

最近工作是开发通用的视频播放器,给公司不同部门使用。主要产出是:

  • CachedPlayer
    • 封装AVPlayer,提供更友好API
    • 视频边播边缓存
    • 预加载
  • CachedPlayerView
    • 封面图
    • 加载状态 loadingView
  • FullScreenVideoBoxView
    • 视频播放UI集成
    • 轻松嵌入到UITableViewCellUICollectionViewCell
    • 自动处理进入和退出全屏
    • 手势拖拽退出全屏

系列文章目录:

  1. 《iOS 视频播放器开发》
  2. 《iOS 视频缓存与预加载》
  3. 《iOS 全屏播放动画与手势》

CachedPlayer

AVPlayer的功能十分强大,但是API并不友好。我们需要通过KVO监听AVPlayerAVPlayerItem的多个属性才能获得其确切状态以及播放进度。CachedPlayer提供更加简单直接的API,将AVPlayer的复杂性封装于内部。

状态

enum Status {
    case unknown        // 初始状态
    case buffering      // 加载中
    case playing        // 播放中
    case paused         // 暂停
    case end            // 播放到末尾
    case error          // 播放出错
}
private(set) var status = Status.unknown // 初始默认为unknown

AVPlayer并没有一个确切的status来让我们获取当前播放器状态,在使用过程中往往需要通过多个属性联合判断当前状态。CachedPlayer的首要任务就是对状态进行封装。CachedPlayer通过对AVPlayerAVPlayerItem的多个属性进行监听,然后调用updateStatus()统一改变当前状态。

private func updateStatus() {
    DispatchQueue.main.async {  // 在主线程改变状态,因为外部通常会监听status改变进行UI操作
        guard let currentItem = self.currentItem else {
            return
        }
        if self.player.error != nil || currentItem.error != nil {
            self.status = .error
            
            return
        }
        if #available(iOS 10, *) {
            switch self.player.timeControlStatus {
            case .playing:
                self.status = .playing
            case .paused:
                self.status = .paused
            case .waitingToPlayAtSpecifiedRate:
                self.status = .buffering
            }
        } else {
            if self.player.rate != 0 { // 期望速率不为0
                if currentItem.isPlaybackLikelyToKeepUp {
                    self.status = .playing
                } else {
                    self.status = .buffering
                }
            } else {
                self.status = .paused
            }
        }
    }
}

iOS 10 之后推出了timeControlStatus让我们可以得到当前播放器处于播放、缓冲还是暂停状态。并且当player.automaticallyWaitsToMinimizeStalling = false时,AVPlayer加载数据即立即播放,不会有waitingToPlayAtSpecifiedRate状态,只会在playingpaused之间切换。

属性监听

KVO

AVPlayer:

  • rate: 期望播放速率
  • status: 播放器状态【播放是否失败】
  • timeControlStatus: 当前播放状态【暂停、缓冲、播放】

AVPlayerItem:

  • status: 播放状态
  • playbackLikelyToKeepUp: 是否在播放
  • isPlaybackBufferEmpty: 缓冲区是否为空
  • isPlaybackBufferFull: 缓冲区是否已满

TimeObserver

AVPlayer可以添加定时的监听器获取其当前播放时间。


timeObserver = player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 0.1, preferredTimescale: CMTimeScale(NSEC_PER_SEC)), queue: DispatchQueue.main, using: { [unowned self] (time) in
    self.updateStatus()
    
    guard let total = self.currentItem?.duration.seconds else {
        return
    }
    if total.isNaN || total.isZero {
        return
    }
    self.duration = total
    self.playedDuration = time.seconds
})

timeObserver需要在deinit时从播放器移除。

Notification

目前仅对AVPlayerItemDidPlayToEndTime进行监听,当播放到结尾,则将status设置为end

API

对外提供基本播放方法:


func replace(with url: URL) {
    currentItem = AVPlayerItem(url: url)
    player.replaceCurrentItem(with: currentItem)
    addItemObservers()
}
func stop() {
    removeItemObservers()
    currentItem = nil
    player.replaceCurrentItem(with: nil)
    
    status = .unknown
}
func play() {
    player.play()
}
func pause() {
    player.pause()
}
func seek(to time: TimeInterval) {
    player.seek(to: CMTime(seconds: time, preferredTimescale: CMTimeScale(NSEC_PER_SEC)))
}

回调

var statusDidChangeHandler: ((Status) -> Void)?
var playedDurationDidChangeHandler: ((TimeInterval, TimeInterval) -> Void)?

private(set) var playedDuration: TimeInterval = 0 {
    didSet {
        playedDurationDidChangeHandler?(playedDuration, duration)
    }
}
private(set) var status = Status.unknown {
    didSet {
        guard status != oldValue else {
            return
        }
        statusDidChangeHandler?(status)
    }
}

外部通过这两个闭包来分别监听播放状态的改变以及播放进度的改变。

CachedPlayerView

CachedPlayerView是提供UIKit的API,将CachedPlayer集成在里面。在开发中,我们直接创建CachedPlayerView实例添加到View上。

class CachedPlayerView: UIView {
    private(set) var player = CachedPlayer()
    
    override class var layerClass: AnyClass {
        get {
            return AVPlayerLayer.self
        }
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        
        player.bind(to: layer as! AVPlayerLayer)
    }
}

CachedPlayer里面添加

func bind(to playerLayer: AVPlayerLayer) {
    playerLayer.player = player
}

那么使用起来就简单了,在ViewController里面只用简单几行代码,就能播放视频了

let playerView = CachedPlayerView()
playerView.player.statusDidChangeHandler = { status in
    print(status)
}
playerView.player.playedDurationDidChangeHandler = { (played, total) in
    print("\(played)/\(total)")
}
playerView.frame = view.bounds
playerView.player.replace(with: url)
playerView.player.play()

更多

到此播放器的基本封装已经完成,提供了简单的对外接口,已经统一了状态监听。
下一篇文章会讲如何通过AVAssetResourceLoaderDelegate实现边播边下载功能。之后会对CachedPlayer进行扩充,封装缓存逻辑调用。

源码地址

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

推荐阅读更多精彩内容