iOS - AVPlayer播放本地和网络视频

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

AVplayer

An AVPlayer is a controller object used to manage the playback and timing of a media asset. It provides the interface to control the player’s transport behavior such as its ability to play, pause, change the playback rate, and seek to various points in time within the media’s timeline. You can use an AVPlayer to play local and remote file-based media, such as QuickTime movies and MP3 audio files, as well as audiovisual media served using HTTP Live Streaming.

AVPlayer是一个控制对象用于管理媒体asset的播放,它提供了相关的接口控制播放器的行为,比如:播放、暂停、改变播放的速率、跳转到媒体时间轴上的某一个点(简单理解就是实现拖动功能显示对应的视频位置内容)。我们能够使用AVPlayer播放本地和远程的媒体文件(使用 HTTP Live Streaming),比如: QuickTime movies 和 MP3 audio files,所以AVPlayer可以满足音视频播放的基本需求。

常用属性方法

// 播放速率
open var rate: Float

// 播放视频
open func play()

// 停止播放
open func pause()

// 播放状态
open var status: AVPlayer.Status { get }
  • rate

如果rate为0说明是停止状态,1是则是正常播放状态。

注意:

1:AVPlayer继承NSObject,所以单独使用AVPlayer时无法显示视频的,必须将视频图层添加到AVPlayerLayer中方能显示视频。

2:AVPlayer一次只能播放单一的媒体资源(asset),但是player实例对象能够被重复用于播放其它媒体资源,可以调用replaceCurrentItem(with:)方法更新当前播放资源。如果想播放多个资源,我们可以使用AVPlayer的子类AVQueuePlayer,该类能够创建和管理多个媒体资源列队,

显示视频方式

AVplayerAVPlayerItem都是不可见对象,这意味着它们是不能够呈现视频在屏幕上的,我们有两个基本方法在屏幕上显示视频:

  • AVKit

The best way to present your video content is by using the AVKit framework’s AVPlayerViewController class in iOS and tvOS or the AVPlayerView class in macOS. These classes present the video content, along with playback controls and other media features giving you a full-featured playback experience.

使用AVKit,这是最好的方式呈现视频内容,我们只需要使用AVKit框架的AVPlayerViewController类,该类能够播放视频内容,并且带有相应的播放控件和一些其他的媒体特征,能够进行全屏播放。

  • AVPlayerLayer

If you are building a custom interface for your player, you use a Core Animation CALayer subclass provided by AVFoundation called AVPlayerLayer. The player layer can be set as a view’s backing layer or can be added directly to the layer hierarchy. Unlike AVPlayerView and AVPlayerViewController, a player layer doesn’t present any playback controls, but simply presents the visual content on screen. It is up to you to build the playback transport controls to play, pause, and seek through the media.

使用AVPlayerLayer,如果是为播放器创建自定义界面,我们能够使用核心动画CALayer的子类AVPlayerLayer来播放。该播放layer能够被设置为视图的backing layer或者直接添加到layer层级中。AVPlayerLayer只是简单的呈现视频内容,并不像AVPlayerViewAVPlayerViewController,是没有提供播放控件的。对于播放界面需要什么样的播放控件,完全取决于我们自己自定义界面实现播放、暂停、调整等一系列功能

AVPlayerLayer

AVPlayerLayer视频播放图层对象,它是需要添加到当前视图的图层上 。

AVPlayerLayer构建于Core Animation之上,是AV Foundation中能找到为数不多的可见组件。Core Animation是Mac和iOS平台上负责图形渲染于动画的基础框架,主要用于这些平台资源的美化和动画流畅度提升。Core Animation本身具有基于时间的属性,并且由于它基于OpenGL,所以具有很好的性能,能够非常好地满足AV Foundation的各种需要

AVPlayerLayer扩展了Core AnimationCALayer类,并通过框架在屏幕上显示视频内容。这一图层并不提供任何可视化控件或其他附件,但是它用作视频内容的渲染面。创建AVPlayerLayer需要一个指向AVPlayer实例的指针,这就将图层和播放器紧密绑在一起,保证了当播放器基于时间的方法出现时时二者保持同步。AVPlayerLayer与其他CALayer一样,设置为UIView的备用层,或者手动添加到一个已有的层继承关系中作为子layer

AVPlayerLayer是一个相对简单的类,使用起来很简单。在这一层可以自定义领域只有video gravity。总共可为video gravity属性定义三个不同的gravity值,用来确定在承载层的范围内视频可以拉伸或缩放的程度。

extension AVLayerVideoGravity {
    // 按原视频比例显示,是竖屏的就显示出竖屏的,两边留黑
    public static let resizeAspect: AVLayerVideoGravity
    // 以原比例拉伸视频,直到两边屏幕都占满,但视频内容有部分就被切割了
    public static let resizeAspectFill: AVLayerVideoGravity
    // 是拉伸视频内容达到边框占满,但不按原比例拉伸,这里明显可以看出宽度被拉伸了
    public static let resize: AVLayerVideoGravity
}

AVPlayerItem

AVPlayerItem是代表一个AVAsset状态,可以使用它实时的观察到视频播放状态。管理着视频的一些基本信息和状态,一个AVPlayerItem对应着一个视频资源。

AVPlayerItem存储了AVAsset对象的引用,它代表被播放的媒体,如果你需要访问asset的信息,在它进入播放列队之前,我们能够使用AVAsynchronousKeyValueLoading协议中的方法来加载我们所需要的值。

也有可选的方法,那就是AVPlayerItem对象能够自动根据你传递给构造器方法init(asset:automaticallyLoadedAssetKeys:) 的参数来加载需要的asset数据,当AVPlayerItem准备好进行播放,asset相关的属性都将被加载用于播放。

AVPlayerItem是动态对象,除了能够被改变的属性值之外,其它的可读属性值,会在AVPlayer播放期间发生改变。我们能够使用 Key-value observing来观察这些属性的改变,对于AVPlayerItem最重要的一个属性就是status。该属性指示是否playerItem已经准备好用于播放。

事实上,当我们第一次创建playerItem的时候,该status属性是指为unknown,这意味着媒体并没有加载完成,还没有准备好播放。当为AVplayer关联该playerItem对象,那么playerItem的媒体资源将会立马准备用于播放。如

func prepareToPlay() {
    let url = <#Asset URL#>
    // Create asset to be played
    asset = AVAsset(url: url)
    
    let assetKeys = [
        "playable",
        "hasProtectedContent"
    ]
    // Create a new AVPlayerItem with the asset and an
    // array of asset keys to be automatically loaded
    playerItem = AVPlayerItem(asset: asset,
                              automaticallyLoadedAssetKeys: assetKeys)
    
    // Register as an observer of the player item's status property
    playerItem.addObserver(self,
                           forKeyPath: #keyPath(AVPlayerItem.status),
                           options: [.old, .new],
                           context: &playerItemContext)
    
    // Associate the player item with the player
    player = AVPlayer(playerItem: playerItem)
}

为了处理属性的改变,我们需要重写通知方法observeValue(forKeyPath:of:change:context:)方法

override func observeValue(forKeyPath keyPath: String?,
                           of object: Any?,
                           change: [NSKeyValueChangeKey : Any]?,
                           context: UnsafeMutableRawPointer?) {
    // Only handle observations for the playerItemContext
    guard context == &playerItemContext else {
        super.observeValue(forKeyPath: keyPath,
                           of: object,
                           change: change,
                           context: context)
        return
    }
    
    if keyPath == #keyPath(AVPlayerItem.status) {
        let status: AVPlayerItemStatus
        
        // Get the status change from the change dictionary
        if let statusNumber = change?[.newKey] as? NSNumber {
            status = AVPlayerItemStatus(rawValue: statusNumber.intValue)!
        } else {
            status = .unknown
        }
        
        // Switch over the status
        switch status {
        case .readyToPlay:
        // Player item is ready to play.
        case .failed:
        // Player item failed. See error.
        case .unknown:
            // Player item is not yet ready.
        }
    }
}

AVPlayerItem常用属性方法

监控PlayerItem

open var status: AVPlayerItem.Status { get }
open var loadedTimeRanges: [NSValue] { get }
open var duration: CMTime { get } // 视频总时间
  • status

该值是一个可观察属性,用于确定是否接受者能够播放,当值为failed时,接受者将不能够用于进行播放,需要创建一个新的实例取代。

public enum  AVPlayerItemStatus :Int {
    case unknown    // 未知状态
    case readyToPlay // 准备播放状态,表示player item准备被播放
    case failed     // 失败状态
 }
  • loadedTimeRanges

该属性是一个数组,主要是包含已经下载的媒体数据,所提供的范围可能不连续。

移动播放头(Moving the Playhead)

open func seek(to time: CMTime, completionHandler: ((Bool) -> Void)? = nil) 跳到指定位置

获取播放信息(Getting Information About Playback)

open var isPlaybackLikelyToKeepUp: Bool { get }
open var isPlaybackBufferEmpty: Bool { get }
  • isPlaybackBufferEmpty

是否播放已经消耗了所有的缓存媒体数据,并且播放将结束

  • isPlaybackLikelyToKeepUp

确定是否播放将继续不会停止

获取时间信息(Getting Timing Information)

open var forwardPlaybackEndTime: CMTime  // 跳到结束位置
open var reversePlaybackEndTime: CMTime  // 跳到开始位置

AVAsset

AVAsset代表一个抽象的媒体,包括标题,文件大小等等,不关联任何格式,每个AVAsset由多个track组成,每个track可以是一个音频通道或者视频通道,经常使用AVAsset的子类AVURLAsset初始化asset,传入URL,该URL引用了视听媒体的资源,比如:stream(包括:HTTP live streams), QuickTime电影文件,MP3文件,和其它格式的文件,我们也可以使用其它具体的子类来初始化asset,具体子类扩大了视听媒体有用方式的基本模型,比如:AVComposition用于处理临时的编辑

由于音视频资源的实时性,当asset初始化成功之后,keys中对应的有些value可能并不会立马可能获得。key所对应的value可以在任何时候获取,并且是同步返回的,所以可能阻塞调用线程。为了避免阻塞情况,我们可以注册对应key的kvo通知,更多信息AVAsynchronousKeyValueLoading.

为了播放AVAsset实例,需要初始化AVPlayerItem,使用player item来建立AVAsset的呈现状态(比如:是否在被播放的时候仅仅只限制asset的缓存范围),而且为AVplayer对象提供该player item对象用于播放,或者组合多个player item。

为了收集一个或者多个资源asset的视听数据结构,我们可以插入AVAsset对象到AVMutableComposition。

处理时间

AVPlayerAVPlayerItem都时基于时间的对象,但是在我们使用它们的功能前,需要了解在AVFoundation框架中出现时间的方式。AVFoundation使用一种可靠的方法来表示时间信息,那就是CMTime数据结构

CMTime

CMTime为时间的正确表示给出来一种结构,即分数值的方式,具体定义如下:

public struct CMTime {

    public var value: CMTimeValue 
    public var timescale: CMTimeScale 
    public var flags: CMTimeFlags
    public var epoch: CMTimeEpoch 

    public init()
    public init(value: CMTimeValue, timescale: CMTimeScale, flags: CMTimeFlags, epoch: CMTimeEpoch)
}

这个结构最关键的两个值是value和timescale。value是64位整数,timescale是一个32位整数,在时间呈现样式中分别作为分子和分母。 value/timescale = seconds.

如何使用CMTimeMake函数创建时间

// 0.5 seconds
CMTime halfSeconds = CMTimeMake(1, 2);
// 5 seconds
CMTime fiveSeconds = CMTimeMake(5, 1);
// one sample from a 44.1kHz audio file
CMTime oneSample = CMTimeMake(1, 44100);

// zero time value
CMTime zeroTime = kCMTimeZero;

时间监听

KVO对于常见的状态监控表现得非常出色,并且可以监听AVPlayerItemAVPlayer的许多属性。不过KVO也有不能胜任的场景,比如:AVPlayer的时间变化。这些监听类型都是自身具有明显的动态特性并需要非常高的精确度,这一点要比标准的键值监听要求高。为满足这一需求AVPlayer提供了两种基于时间的监听方法,让应用程序可以对时间变化进行精确的监听

定期监听

通常情况下,我们希望以一定的时间间隔获得通知。如果需要随着时间的变化移动播放头位置或更新时间显示,这非常重要。利用AVPlayeraddPeriodicTimeObserver(forInterval interval: CMTime, queue: DispatchQueue?, using block: @escaping (CMTime) -> Void) -> Any方法可以很容易地监听到此类变化。这个方法需要传递如下参数:

  • interval:一个用于指定通知周期间隔的CMTime值
  • queue:通知发送的顺序调度列队。大多数时候,我们希望这些通知发生在主列队,在如果没有明确指定的情况下默认为主列队。需要注意的是不可以使用并行调度列队,因为API没有处理并行列队的方法,否则会导致一些不可知的问题
  • block:一个在指定的时间间隔中将会在列队上调用的回调块。这个块传递的一个CMTime值用于指示播放器的当前时间
func addPeriodicTimeObserver() {
    // Invoke callback every half second
    let interval = CMTime(seconds: 0.5,
                          preferredTimescale: CMTimeScale(NSEC_PER_SEC))
    // Queue on which to invoke the callback
    let mainQueue = DispatchQueue.main
    // Add time observer
    timeObserverToken =
        player.addPeriodicTimeObserver(forInterval: interval, queue: mainQueue) {
            [weak self] time in
            // update player transport UI
    }
}

边界时间监听

AVPlayer还提供了一种更有针对性的方法来监听时间,应用程序可以得到播放器时间轴中多个边界点的遍历结果。这一方法主要用于同步用户界面变更或随着视频播放记录一些可视化数据。比如,可以定义25%、50%和75%边界的标记,以此判断用户播放进度。要使用这个功能,需要用到addBoundaryTimeObserver(forTimes times: [NSValue], queue: DispatchQueue?, using block: @escaping () -> Void) -> Any方法,并提供以下参数:

  • times:CMTime值组成的数组,定义了需要通知的边界点
  • queue:与定期类似,为方法提供一个用来发送通知的顺序调度列队,指定NULL等同于明确设置主列队
  • block:每当正常播放中跨越一个边界点时就会在列队中调用这个回调块。有趣的是,该块不提供遍历的CMTime值,所以开发者需要为此执行一些额外计算进行确定
func addBoundaryTimeObserver() {
    var times = [NSValue]()
    // Set initial time to zero
    var currentTime = kCMTimeZero
    // Divide the asset's duration into quarters.
    let interval = CMTimeMultiplyByFloat64(asset.duration, 0.25)
    
    // Build boundary times at 25%, 50%, 75%, 100%
    while currentTime < asset.duration {
        currentTime = currentTime + interval
        times.append(NSValue(time:currentTime))
    }
    // Queue on which to invoke the callback
    let mainQueue = DispatchQueue.main
    // Add time observer
    timeObserverToken =
        player.addBoundaryTimeObserver(forTimes: times, queue: mainQueue) {
            [weak self] time in
            // Update UI
    }
}

AVPlayer视频播放基本步骤

1:创建视频资源地址URL,可以是网络URL
2:通过URL创建视频内容对象AVPlayerItem,一个视频对应一个AVPlayerItem
3:创建AVPlayer视频播放器对象,需要一个AVPlayerItem进行初始化
4:创建AVPlayerLayer播放图层对象,添加到显示视图上
5:播放器播放play,播放器暂停pause
6:添加通知中心监听视频播放完成,使用KVO监听播放内容的属性变化
7:进度条监听是调用AVPlayer的对象方法:open func addPeriodicTimeObserver(forInterval interval:CMTime, queue:DispatchQueue?, using block:@escaping(CMTime) -> Swift.Void) ->Any

实战

使用AVPlayer自定义一个简单播放器播放视频

实现效果

2018-12-07 10-48-30.2018-12-07 10_49_28.gif

基本部分

import UIKit
import AVFoundation

class AVPlayerController: UIViewController {

    var containerView: UIView!   //播放器容器
    var playOrPauseButton: UIButton! //播放/暂停按钮
    var progress: UIProgressView!   //播放进度
    var timeLabel: UILabel!     //显示播放时间

    var player: AVPlayer? //播放器对象
    var playerItem: AVPlayerItem? //播放资源对象
    var timeObserver: Any! //时间观察者

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = UIColor.white
        setupUI()
        addPlayerToAVPlayerLayer()
        addPlayerItemObserver()
        addProgressObserver()
    }
}
  • 布局UI功能

containerView创建容器视图用于显示视频,playOrPauseButton播放、暂停按钮用于控制视频的播放和暂停;progress进度条显示视频当前的播放进度;时间timelabel显示当前的播放时间.

 func setupUI(){
     // 容器视图
     containerView = UIView(frame: CGRect(x: 0,
                                          y: 100,
                                          width: self.view.frame.width,
                                          height: 200))
     containerView.backgroundColor = UIColor.gray
     view.addSubview(containerView)

     // 播放暂停按钮
     playOrPauseButton = UIButton(type: .custom)
     playOrPauseButton.frame = CGRect(x: containerView.frame.minX + 10,
                                      y: containerView.frame.maxY + 5,
                                      width: 30,
                                      height: 30)
     playOrPauseButton.setImage(UIImage(named:"pause"), for: .normal)
     playOrPauseButton.addTarget(self,
                                 action: #selector(playOrPauseButtonClicked(button:)),
                                 for: .touchUpInside)
     view.addSubview(playOrPauseButton)

     // 进度条
     progress = UIProgressView(frame: CGRect(x: playOrPauseButton.frame.maxX + 5,
                                                y: playOrPauseButton.frame.minY,
                                                width: 220,
                                                height: 20))
     progress.center.y = playOrPauseButton.center.y;
     progress.progressTintColor = UIColor.blue
     progress.trackTintColor = UIColor.gray
     view.addSubview(progress)

     // 时间
     timeLabel = UILabel(frame: CGRect(x: progress.frame.maxX + 5,
                                       y: playOrPauseButton.frame.minY,
                                       width: 60,
                                       height: 20))
     timeLabel.font = UIFont.systemFont(ofSize: 12.0)
     timeLabel.textColor = UIColor.red
     timeLabel.center.y = playOrPauseButton.center.y
     view.addSubview(timeLabel)
 }
  • 获取本地资源

加载本地或网络资源,然后创建playerItem,通过playerItem创建player,最后由player创建playerLayer并添加到自定义的播放视图上,当视频资源加载完毕会播放视频

 func addPlayerToAVPlayerLayer(){
     // 获取本地视频资源
     guard let path = Bundle.main.path(forResource: "trailer", ofType: ".mp4") else { return }
     // 播放本地视频
     let url = URL(fileURLWithPath: path)
     // 播放网络视频
     // let url = URL(string: path)!
     playerItem = AVPlayerItem(url: url)
     player = AVPlayer(playerItem: self.playerItem)

     // 创建视频播放器图层对象
     let playerLayer = AVPlayerLayer(player: player)
     playerLayer.frame = CGRect(x: containerView.frame.minX,
                                y: 0,
                                width: containerView.frame.width,
                                height: containerView.frame.height)
     playerLayer.videoGravity = .resizeAspectFill //视频填充模式
     containerView.layer.addSublayer(playerLayer)
 }
  • 监听playerItem状态
 func addPlayerItemObserver(){
     // 为AVPlayerItem添加status属性观察,得到资源准备好,开始播放视频
     playerItem?.addObserver(self, forKeyPath: "status", options: .new, context: nil)
     // 监听AVPlayerItem的loadedTimeRanges属性来监听缓冲进度更新
     playerItem?.addObserver(self, forKeyPath: "loadedTimeRanges", options: .new, context: nil)
     NotificationCenter.default.addObserver(self,
                                            selector: #selector(playerItemDidReachEnd(notification:)),
                                            name: .AVPlayerItemDidPlayToEndTime, object: playerItem)
 }

重写监听方法,控制播放

 ///  通过KVO监控播放器状态
 ///
 /// - parameter keyPath: 监控属性
 /// - parameter object:  监视器
 /// - parameter change:  状态改变
 /// - parameter context: 上下文
 override func observeValue(forKeyPath keyPath: String?,
                             of object: Any?,
                             change: [NSKeyValueChangeKey : Any]?,
                             context: UnsafeMutableRawPointer?) {
      guard let object = object as? AVPlayerItem  else { return }
      guard let keyPath = keyPath else { return }
      if keyPath == "status" {
          if object.status == .readyToPlay { //当资源准备好播放,那么开始播放视频
              player?.play()
              print("正在播放...,视频总长度:\(formatPlayTime(seconds: CMTimeGetSeconds(object.duration)))")
          } else if object.status == .failed || object.status == .unknown {
              print("播放出错")
          }
      } else if keyPath == "loadedTimeRanges" {
          let loadedTime = availableDurationWithplayerItem()
          print("当前加载进度\(loadedTime)")
      }
 }

 // 将秒转成时间字符串的方法,因为我们将得到秒。
 func formatPlayTime(seconds: Float64) -> String {
     let min = Int(seconds / 60)
     let sec = Int(seconds.truncatingRemainder(dividingBy: 60))
     return String(format: "%02d:%02d", min, sec)
 }

播放结束,回到最开始位置,播放按钮显示带播放图标

@objc func playerItemDidReachEnd(notification: Notification) {
        player?.seek(to: CMTime.zero,
                     toleranceBefore: CMTime.zero,
                     toleranceAfter: CMTime.zero)
        progress.progress = 0.0
        playOrPauseButton.setImage(UIImage(named:"play"), for: .normal)
 }

获取当前加载进度

 func availableDurationWithplayerItem() -> TimeInterval {
      guard let loadedTimeRanges = player?.currentItem?.loadedTimeRanges,
            let first = loadedTimeRanges.first else {
                fatalError()
      }
      // 本次缓冲时间范围
      let timeRange = first.timeRangeValue
      let startSeconds = CMTimeGetSeconds(timeRange.start) // 本次缓冲起始时间
      let durationSecound = CMTimeGetSeconds(timeRange.duration)// 缓冲时间
      let result = startSeconds + durationSecound// 缓冲总长度
      return result
 }
  • 给播放器添加进度监听
 func addProgressObserver(){
     // 这里设置每秒执行一次.
     timeObserver =  player?.addPeriodicTimeObserver(forInterval: CMTimeMake(value: Int64(1.0),
                                                     timescale: Int32(1.0)),
                                                     queue: DispatchQueue.main) { [weak self] (time: CMTime) in
                                                         self?.updateProgress(time)
        }
    }

 func updateProgress(_ time: CMTime) {
     // CMTimeGetSeconds函数是将CMTime转换为秒,如果CMTime无效,将返回NaN
     guard let playerItem = playerItem else {
         return
     }
     let currentTime = CMTimeGetSeconds(time)
     let totalTime = CMTimeGetSeconds(playerItem.duration)
     // 更新显示的时间和进度条
     self.timeLabel.text = self.formatPlayTime(seconds: CMTimeGetSeconds(time))
     self.progress.setProgress(Float(currentTime/totalTime), animated: true)
     print("当前已经播放\(self.formatPlayTime(seconds: CMTimeGetSeconds(time)))")
 }
  • 播放、暂停
 @objc func playOrPauseButtonClicked(button: UIButton) {
       if let player = player {
           if player.rate == 0 { // 点击时已暂停
               button.setImage(UIImage(named:"pause"), for: .normal)
               player.play()
           } else if player.rate == 1 {// 点击时正在播放
               player.pause()
               button.setImage(UIImage(named:"play"), for: .normal)
           }
       }
 }

最后,记得去除监听观察者

 func removeObserver() {
     playerItem?.removeObserver(self, forKeyPath: "status")
     playerItem?.removeObserver(self, forKeyPath: "loadedTimeRanges")
     player?.removeTimeObserver(timeObserver)
     NotificationCenter.default.removeObserver(self,
                                               name:  .AVPlayerItemDidPlayToEndTime,
                                               object: playerItem)
 }

 deinit {
     removeObserver()
 }

参考

AVFoundation Programming Guide
AVPlayer
AVPlayerItem
AVAsset

你可能感兴趣的:(iOS - AVPlayer播放本地和网络视频)