【AVFoundation】AVPlayer视频播放

1,803 阅读3分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第5天,点击查看活动详情

AVFoundation 是Apple iOS和OS X系统中用于处理基于时间的媒体数据的高级框架,通过开发所需的工具提供了强大的功能集,让开发者能够基于苹果平台创建当下最先进的媒体应用程序,其针对64位处理器设计,充分利用了多核硬件优势,会自动提供硬件加速操作,确保大部分设备能以最佳性能运行,是iOS开发接触音视频开发必学的框架之一

参与掘金日新计划,持续记录AVFoundation学习,Demo学习地址,这篇文章主要讲述AVPlayer视频播放,其他类的相关用法可查看我的其他文章。

AVPlayer

AVFoundation的播放都围绕AVPlayer展开,AVPlayer是一个用来播放基于时间的视听媒体的控制器对象。支持播放从本地、分布下载或通过HTTP Live Streaing协议得到的流媒体,并在多种播放场景中播放这些资源。

AVPlayer是一个不可见的组件,要将视频资源展现给用户,需要使用AVPlayerLayer

AVPlayerLayer

AVPlayerLayer 构建于CoreAnimation之上,继承于CALayer,创建AVPlayerLayer需要一个AVPlayer实例,这就将图层和播放器紧密的绑定在一起,保证了当播放器基于时间的方法出现时使二者保持同步。AVPlayerLayer与其他CALayer类一样,可以设置为UIView的备用层,或者手动添加到一个已有层的继承关系中。

可以通过更改videoGravity来改变显示比例

  • 默认resizeAspect,会在承载范围内缩放视频大小保持原始宽高比
  • resizeAspectFill会保持视频的宽高比,并使其通过缩放填满层的范围区域,通常会导致图像被部分裁剪
  • resize会将视频内容拉伸来匹配承载层的范围,这种情况最不常用,因为他通常会导致图片扭曲而导致的funhouse effect效应
let videoUrl: URL = Bundle.main.url(forResource: "hubblecast2", withExtension: "m4v")!
playerItem = AVPlayerItem(url: videoUrl)
player = AVPlayer(playerItem: playerItem)
playerLayer = AVPlayerLayer(player: player)
// 显示比例,默认resizeAspect,会在承载范围内缩放视频大小保持原始宽高比
// resizeAspectFill会保持视频的宽高比,并使其通过缩放填满层的范围区域,通常会导致图像被部分裁剪
// resize会将视频内容拉伸来匹配承载层的范围,这种情况最不常用,因为他通常会导致图片扭曲而导致的funhouse effect效应
playerLayer.videoGravity = .resize
playerLayer.frame = CGRect(x: 0, y: 100, width: SCREEN_WIDTH, height: 300)
view.layer.addSublayer(playerLayer)
playerItem.addObserver(self, forKeyPath: "status", options: .new, context: nil)

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        if keyPath == "status" {
            if playerItem.status == .readyToPlay {
                CQLog("可以播放了")
            }
        }
    }
    
@objc private func playAction() {
    player.play()
}

@objc private func pauseAction() {
    player.pause()
}

当设置好播放器之后,等playerItem.status为readyToPlay就可以播放了,如果直接播放,视频过大或者是视频流,可能就会出现没有反应的等待情况。

监听当前的播放时间

  • forInterval 监听间隔
  • queue 监听回调队列
  • 回调的time为当前播放时长
  • 注意需要将timeObserver强引用
  • 可以在回调里显示时间、调整进度条
private func addPlayerItemTimeObserver() {
        timeObserver = player.addPeriodicTimeObserver(forInterval: CMTimeMake(value: 10, timescale: 600), queue: DispatchQueue.main) {  [weak self] (time) in
            guard let `self` = self else { return }
            let currentTime: TimeInterval = CMTimeGetSeconds(time)
            self.timeSlider.value = Float(currentTime/CMTimeGetSeconds(self.playerItem.duration))*self.timeSlider.maximumValue
            CQLog("当前播放时间\(currentTime)")
        }
}

监听播放完成回调

  • 注意需要将endObserver强引用
private func addPlayerItemTimeEndObserver() {
        endObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: playerItem, queue: OperationQueue.main, using: { notification in
            CQLog("播放完成了")
        })
    }

seek

  • 拖动进度条seek,注意seek时也会走时间进度监听回调,由于在进度监听里做了进度条的调整,所以这里应该移除监听,或设置一个字段在拖动进度条时收到监听不做处理
@objc private func timeSliderEditingDidBegin(sender: UISlider) {
    lastRate = player.rate
    player.pause()
    player.removeTimeObserver(timeObserver!)
 }

@objc private func timeSliderActionEditingChanged(sender: UISlider) {
    playerItem.cancelPendingSeeks()
    let currTime: CMTime = CMTimeMultiplyByFloat64(playerItem.duration, multiplier: Float64(sender.value/sender.maximumValue))
    playerItem.seek(to: currTime) { isCompletion in
    }
}

   
@objc private func timeSliderEditingDidEnd(sender: UISlider) {
   addPlayerItemTimeObserver()
   player.rate = lastRate
}