AVplayer实现播放本地和网络视频(Swift3.0)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/longshihua/article/details/53909733
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可以满足音视频播放的基本需求。

注意:
1:AVPlayer继承NSObject,所以单独使用AVPlayer时无法显示视频的,必须将视频图层添加到AVPlayerLayer中方能显示视频。
2:AVPlayer一次只能播放单一的媒体资源(asset),但是player实例对象能够被重复用于播放其它媒体资源,可以调用replaceCurrentItem(with:)方法更新当前播放资源。如果想播放多个资源,我们可以使用AVPlayer的子类AVQueuePlayer,该类能够创建和管理多个媒体资源列队,

AVPlayer是一个动态对象,它的状态不断改变,有两种方式我们能够观察播放器的状态:

1:General State Observations: You can use Key-value observing (KVO) to observe state changes to many of the player’s dynamic properties, such as its currentItem or its playback rate. You should register and unregister for KVO change notifications on the main thread. This avoids the possibility of receiving a partial notification if a change is being made on another thread. AVFoundation invokes observeValue(forKeyPath:of:change:context:) on the main thread, even if the change operation is made on another thread.

基本状态观察者:你能够使用KVO来观察player动态属性的状态改变,比如像: currentItem 或者它的播放速度。我们应该在主线程注册和去除KVO,这能够避免如果在其它线程发送改变而导致接收局部通知,当发生通知,AVFoundation将在主线程调用observeValue(forKeyPath:of:change:context:) 方法,即使是在其他线程发生。

2:Timed State Observations: KVO works well for general state observations, but isn’t intended for observing continuously changing state like the player’s time. AVPlayer provides two methods to observe time changes:addPeriodicTimeObserver(forInterval:queue:using:)addBoundaryTimeObserver(forTimes:queue:using:)
These methods let you observe time changes either periodically or by boundary, respectively. As changes occur, the callback block or closure you supply to these methods is invoked giving you the opportunity to take some action such as updating the state of your player’s user interface.

时间状态观察者:KVO能够很好的观察生成的状态,但是并不能够观察播放时间的改变,所以AVPlayer提供了两个方法来观察时间的改变,addPeriodicTimeObserver(forInterval:queue:using:)addBoundaryTimeObserver(forTimes:queue:using:),这两个方法能够让我们周期性或者边界方法进行观察,当改变发生,我们所提供的回调block或者闭包将会被触发,在闭包中我们将有机会更新播放器的状态和相关UI界面。

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

1: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类,该类能够播放视频内容,并且带有相应的播放控件和一些其他的媒体特征,能够进行全屏播放。

2: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只是简单的呈现视频内容,并不像AVPlayerView和AVPlayerViewController,是没有提供播放控件的。对于播放界面需要什么样的播放控件,完全取决于我们自己自定义界面实现播放、暂停、调整等一系列功能。

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的媒体资源将会立马准备用于播放。

AVPlayerItem属性解析:
 
loadedTimeRanges 该属性是一个数组,主要是包含已经下载的媒体数据,所提供的范围可能不连续。
playbackBufferEmpty:是否播放已经消耗了所有的缓存媒体数据,并且播放将结束
playbackLikelyToKeepUp:是否播放将继续不会停止
 
open var status:AVPlayerItemStatus{ get }该值是一个可观察属性,用于确定是否接受者能够播放,当值为failed时,接受者将不能够用于进行播放,需要创建一个新的实例取代。
 
AVPlayerItemStatus枚举值表面是否AVPlayerItem能够成功播放。
public enum  AVPlayerItemStatus :Int {
 case unknown    未知状态
 
case readyToPlay准备播放状态,表示player item准备被播放
 
case failed     失败状态
 }
 
open var error:Error? { get }如果接受者的状态是AVPlayerItemStatusFailed,该属性值可以描述失败的原因。
open var forwardPlaybackEndTime:CMTime跳到结束位置
open var reversePlaybackEndTime:CMTime跳到开始位置
open func seek(to time:CMTime)跳到指定位置

AVPlayerLayer

AVPlayerLayer:视频播放图层对象,它是需要添加到当前视图的图层上 。AVPlayerLayerCALayer的子类,使用AVPlayer对象创建AVPlayerLayer用于播放视频,注意:开发中,单纯使用AVPlayer类是无法显示视频的,要将视频层添加至AVPlayerLayer中,这样才能将视频显示出来,我们能够使用下面代码理解:

AVPlayer *player = <#A configured AVPlayer object#>;
CALayer *superlayer = <#Get a CALayer#>;
AVPlayerLayer *playerLayer = [AVPlayerLayer playerLayerWithPlayer:player];
[superlayer addSublayer:playerLayer];

常用属性:

videoGravity 的属性,默认设置了AVLayerVideoGravityResize,查看该属性以及相关的其他属性值发现有3种值可以设置
AVLayerVideoGravityResizeAspect:按原视频比例显示,是竖屏的就显示出竖屏的,两边留黑
AVLayerVideoGravityResizeAspectFill:以原比例拉伸视频,直到两边屏幕都占满,但视频内容有部分就被切割了
AVLayerVideoGravityResize:是拉伸视频内容达到边框占满,但不按原比例拉伸,这里明显可以看出宽度被拉伸了

AVAsset

AVAsset代表一个抽象的媒体,包括标题,文件大小等等,不关联任何格式,每个AVAsset由多个track组成,每个track可以是一个音频通道或者视频通道,经常使用AVAsset的子类AVURLAsset初始化asset,传入URL,该URL引用了视听媒体的资源,比如:stream(包括:HTTP live streams), QuickTime电影文件,MP3文件,和其它格式的文件,我们也可以使用其它具体的子类来初始化asset,具体子类扩大了视听媒体有用方式的基本模型,比如:AVComposition用于处理临时的编辑
 
为了播放AVAsset实例,需要初始化AVPlayerItem,使用player item来建立AVAsset的呈现状态(比如:是否在被播放的时候仅仅只限制asset的缓存范围),而且为AVplayer对象提供该player item对象用于播放,或者组合多个player item

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

使用AVplayer播放本地、网络视频

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

 1:播放/暂停
 
使用视频URL初始化一个AVPlayerItem,把AVPlayerItem设置为AVPlayercurrentItem,然后通过KVO监听AVPlayerItem的属性playerItem.addObserver(self, forKeyPath: "status", options: .new, context: nil),当属性变为AVPlayerStatusReadyToPlay时,通过AVPlayer调用play方法即可播放视频。

AVPlayerplaypause分别控制播放和暂停,根据AVPlayer的播放速度rate可以判断当前是否为播放状态,rate=0暂停,rate=1播放。视频播放完成后AVPlayerItem会发送AVPlayerItemDidPlayToEndTimeNotification通知。

 2:视频时间

视频时间包含两部分:视频总时间和视频当前播放时间。视频总时间通过CMTimeGetSeconds(player.currentItem.duration)获取,当前播放时间通过CMTimeGetSeconds(player.currentTime)获取。

获取到这两个时间,就可以展示视频播放进度。播放进度需要一秒更新一次,可以用定时器来更新,也可以用AVPlayer的方法 addPeriodicTimeObserver(forInterval interval:CMTime, queue:DispatchQueue?, using block:@escaping(CMTime) -> Swift.Void) -> Any在闭包里更新。建议使用这个方法更新时间,因为它更新时间更加准确,使用闭包的参数time获得准确的播放进度。


具体代码实现和效果


视图控制器中拥有如下几个属性,在viewDidLoad中创建UI布局

class SHAVPlayerController: 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()
        createUI()
    }
    
}

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

   //UI布局
    func createUI(){
        containerView = UIView(frame: CGRect(x: 0, y: 100, width: self.view.width, height: 200))
        containerView.backgroundColor = UIColor.gray
        view.addSubview(containerView)
        
        playOrPauseButton = UIButton(type: .custom)
        playOrPauseButton.frame = CGRect(x: containerView.x + 10, y: containerView.bottom + 5, width: 16, height: 16)
        playOrPauseButton.setImage(UIImage(named:"player_play"), for: .normal)
        playOrPauseButton.addTarget(self, action: #selector(SHAVPlayerController.playOrPauseButtonClicked(button:)), for: .touchUpInside)
        view.addSubview(playOrPauseButton)
        
        progress = UIProgressView(frame: CGRect(x: playOrPauseButton.right + 5, y: playOrPauseButton.y, width: 220, height: 20))
        progress.centerY = playOrPauseButton.centerY;
        progress.progressTintColor = UIColor.blue
        progress.trackTintColor = UIColor.gray
        view.addSubview(progress)
        
        timeLabel = UILabel(frame: CGRect(x: progress.right + 5, y: playOrPauseButton.y, width: 60, height: 20))
        timeLabel.font = UIFont.systemFont(ofSize: 12.0)
        timeLabel.textColor = UIColor.red
        timeLabel.centerY = playOrPauseButton.centerY
        view.addSubview(timeLabel)
        
        addPlayerToAVPlayerLayer()
    }

获取本地资源,并添加观察者,等资源准备完毕,开始播放

    func addPlayerToAVPlayerLayer(){
        //获取本地视频资源
        guard let path = Bundle.main.path(forResource: "1", ofType: ".mp4") else {
            return
        }
        //播放本地视频
        let url = NSURL(fileURLWithPath: path)
        //播放网络视频
        // let url = NSURL(string: path)!
        playerItem = AVPlayerItem(url: url as URL)
        player = AVPlayer(playerItem: self.playerItem)
        
        //创建视频播放器图层对象
        let playerLayer = AVPlayerLayer(player: player)
        playerLayer.frame = CGRect(x: containerView.x, y: 0, width: containerView.width, height: containerView.height)
        playerLayer.videoGravity = AVLayerVideoGravityResizeAspect //视频填充模式
        containerView.layer.addSublayer(playerLayer)
        
        addProgressObserver()
        addObserver()
    }

AVPlayer提供了一种可以让我们周期性调用的一个block,在该block内我们可以实现进度的更新,block返回的值我们必须强引用,并且在不需要的时候去除时间观察者。第一个参数的调用的时间间隔,使用CMTime表示时间,不直接使用秒数,是考虑到播放的速率,value/timescale = seconds.第二个参数,我们需要传入串行列队,即主线程列队。CMTime(1,1); CMTime(2,2);均可以表示1秒,但是播放速率不一样。这里需要注意self的循环引用,所以使用[weakself]进行弱引用。

  //给播放器添加进度更新
    func addProgressObserver(){
    //这里设置每秒执行一次.
    timeObserver =  player?.addPeriodicTimeObserver(forInterval: CMTimeMake(Int64(1.0), Int32(1.0)), queue: DispatchQueue.main) { [weak self](time: CMTime) in
                //CMTimeGetSeconds函数是将CMTime转换为秒,如果CMTime无效,将返回NaN
                let currentTime = CMTimeGetSeconds(time)
                let totalTime = CMTimeGetSeconds(self!.playerItem!.duration)
                //更新显示的时间和进度条
                self!.timeLabel.text = self!.formatPlayTime(seconds: CMTimeGetSeconds(time))
                self!.progress.setProgress(Float(currentTime/totalTime), animated: true)
                print("当前已经播放\(self!.formatPlayTime(seconds: CMTimeGetSeconds(time)))")
        }
    }
    
    //给AVPlayerItem、AVPlayer添加监控
    func addObserver(){
        //为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(SHAVPlayerController.playerItemDidReachEnd(notification:)), name: Notification.Name.AVPlayerItemDidPlayToEndTime, object: playerItem)
    }

监听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 = avalableDurationWithplayerItem()
            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)
    }
    
    //计算当前的缓冲进度
    func avalableDurationWithplayerItem()->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 playerItemDidReachEnd(notification: Notification){
        player?.seek(to: kCMTimeZero, toleranceBefore: kCMTimeZero, toleranceAfter: kCMTimeZero)
        progress.progress = 0.0
        playOrPauseButton.setImage(UIImage(named:"player_play"), for: .normal)
    }

播放、暂停按钮响应事件

//点击播放/暂停按钮
    func playOrPauseButtonClicked(button: UIButton){
     if let player = player{
        if player.rate == 0{//点击时已暂停
           button.setImage(UIImage(named:"player_pause"), for: .normal)
           player.play()
        }else if player.rate == 1{//点击时正在播放
           player.pause()
           button.setImage(UIImage(named:"player_play"), for: .normal)
        }
      }
    }
最后,去除监听观察者

 //去除观察者
    func removeObserver(){
        playerItem?.removeObserver(self, forKeyPath: "status")
        playerItem?.removeObserver(self, forKeyPath: "loadedTimeRanges")
        player?.removeTimeObserver(timeObserver)
        NotificationCenter.default.removeObserver(self, name:  Notification.Name.AVPlayerItemDidPlayToEndTime, object: playerItem)
    }
    
    deinit {
        removeObserver()
    }


参考

阅读更多

没有更多推荐了,返回首页