使用AVPlayer时保持良好的滚动性能

Ant*_*nio 56 video avfoundation ios avplayer

我正在开发一个有集合视图的应用程序,集合视图的单元格可以包含视频.现在我正在用AVPlayer和显示视频AVPlayerLayer.不幸的是,滚动性能很糟糕.看起来AVPlayer,AVPlayerItemAVPlayerLayer做了很多工作的主线上.他们不断拿出锁,等待信号量等,这阻碍了主线程并导致严重的帧丢失.

有没有办法告诉AVPlayer停止在主线程上做这么多事情?到目前为止,我所尝试的一切都没有解决问题.

我也试过使用构建一个简单的视频播放器AVSampleBufferDisplayLayer.使用它我可以确保一切都发生在主线程之外,我可以在滚动和播放视频时达到~60fps.不幸的是,这种方法的级别要低得多,而且它不提供开箱即用的音频播放和时间擦除等功能.有没有办法获得类似的表现AVPlayer?我更愿意使用它.

编辑: 在进一步研究之后,看起来在使用时可能无法实现良好的滚动性能AVPlayer.创建一个AVPlayer并与一个AVPlayerItem实例关联开始了一大堆蹦床到主线程的工作,然后它在信号量上等待并尝试获取一堆锁.随着滚动视图中视频数量的增加,停止主线程的时间量会急剧增加.

AVPlayerdealloc似乎也是一个巨大的问题.Dealloc'ing an AVPlayer也尝试同步一堆东西.再次,当你创造更多的球员时,这会非常糟糕.

这非常令人沮丧,它AVPlayer几乎无法用于我正在尝试的事情.阻止像这样的主线程这样做是一件非常业余的事情,很难相信苹果工程师会犯这种错误.无论如何,希望他们能尽快解决这个问题.

dam*_*ian 21

AVPlayerItem尽可能在后台队列中构建(在主线程上必须执行一些操作,但是您可以执行设置操作并等待视频属性加载到后台队列 - 非常仔细地阅读文档).这涉及与KVO的伏都教舞蹈,真的不好玩.

AVPlayer等待AVPlayerItem状态变为时,打嗝发生AVPlayerItemStatusReadyToPlay.为了尽可能多地可以减少你想要做的打嗝的长度带来的AVPlayerItem更紧密AVPlayerItemStatusReadyToPlay其分配给之前在后台线程AVPlayer.

我实际上实现了这一段时间已经有一段时间了,但是IIRC主要的线程块是由于底层AVURLAsset的属性是延迟加载引起的,如果你自己不加载它们,它们会在主线程上忙于加载AVPlayer想玩.

查看AVAsset文档,尤其是周围的内容AVAsynchronousKeyValueLoading.我认为我们需要在使用资产时durationtracks之前加载值AVPlayer以最小化主线程块.有可能我们也必须遍历每个轨道并对AVAsynchronousKeyValueLoading每个段进行操作,但我不记得100%.

  • 非常感谢您的提示!你能详细说明"尽可能在后台队列上构建`AVPlayerItem`".我已尝试在后台线程上创建`AVPlayerItem`并加载其资产的轨道,但其状态始终为AVPlayerItemStatusUnknown.看起来它没有转换到AVPlayerItemStatusReadyToPlay,直到它与AVPlayer相关联,并且AVPlayer连接到AVPlayerLayer.我确定我在这里做错了什么. (5认同)

And*_*oes 14

不知道这是否会有所帮助 - 但是这里有一些我用来加载后台队列视频的代码,这肯定有助于主线程阻塞(道歉如果它不能编译1:1,我从更大的代码库中抽象出来我我正在努力:

func loadSource() {
    self.status = .Unknown

    let operation = NSBlockOperation()
    operation.addExecutionBlock { () -> Void in
    // create the asset
    let asset = AVURLAsset(URL: self.mediaUrl, options: nil)
    // load values for track keys
    let keys = ["tracks", "duration"]
    asset.loadValuesAsynchronouslyForKeys(keys, completionHandler: { () -> Void in
        // Loop through and check to make sure keys loaded
        var keyStatusError: NSError?
        for key in keys {
            var error: NSError?
            let keyStatus: AVKeyValueStatus = asset.statusOfValueForKey(key, error: &error)
            if keyStatus == .Failed {
                let userInfo = [NSUnderlyingErrorKey : key]
                keyStatusError = NSError(domain: MovieSourceErrorDomain, code: MovieSourceAssetFailedToLoadKeyValueErrorCode, userInfo: userInfo)
                println("Failed to load key: \(key), error: \(error)")
            }
            else if keyStatus != .Loaded {
                println("Warning: Ignoring key status: \(keyStatus), for key: \(key), error: \(error)")
            }
        }
        if keyStatusError == nil {
            if operation.cancelled == false {
                let composition = self.createCompositionFromAsset(asset)
                // register notifications
                let playerItem = AVPlayerItem(asset: composition)
                self.registerNotificationsForItem(playerItem)
                self.playerItem = playerItem
                // create the player
                let player = AVPlayer(playerItem: playerItem)
                self.player = player
            }
        }
        else {
            println("Failed to load asset: \(keyStatusError)")
        }
    })

    // add operation to the queue
    SomeBackgroundQueue.addOperation(operation)
}

func createCompositionFromAsset(asset: AVAsset, repeatCount: UInt8 = 16) -> AVMutableComposition {
     let composition = AVMutableComposition()
     let timescale = asset.duration.timescale
     let duration = asset.duration.value
     let editRange = CMTimeRangeMake(CMTimeMake(0, timescale), CMTimeMake(duration, timescale))
     var error: NSError?
     let success = composition.insertTimeRange(editRange, ofAsset: asset, atTime: composition.duration, error: &error)
     if success {
         for _ in 0 ..< repeatCount - 1 {
          composition.insertTimeRange(editRange, ofAsset: asset, atTime: composition.duration, error: &error)
         }
     }
     return composition
}
Run Code Online (Sandbox Code Playgroud)


Gre*_*egg 8

如果你看看Facebook的AsyncDisplayKit(Facebook和Instagram 提供商背后的引擎)你可以使用他们的AVideoNode在后台线程上渲染大部分视频.如果您将其子节点ASDisplayNode并将displayNode.view添加到您正在滚动的任何视图(表/集合/滚动),您可以实现完美平滑的滚动(只需确保创建节点和资产以及后台线程上的所有内容) .唯一的问题是更改视频项时,因为这会强制自己进入主线程.如果您在该特定视图上只有几个视频,则可以使用此方法!

        dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), {
            self.mainNode = ASDisplayNode()
            self.videoNode = ASVideoNode()
            self.videoNode!.asset = AVAsset(URL: self.videoUrl!)
            self.videoNode!.frame = CGRectMake(0.0, 0.0, self.bounds.width, self.bounds.height)
            self.videoNode!.gravity = AVLayerVideoGravityResizeAspectFill
            self.videoNode!.shouldAutoplay = true
            self.videoNode!.shouldAutorepeat = true
            self.videoNode!.muted = true
            self.videoNode!.playButton.hidden = true

            dispatch_async(dispatch_get_main_queue(), {
                self.mainNode!.addSubnode(self.videoNode!)
                self.addSubview(self.mainNode!.view)
            })
        })
Run Code Online (Sandbox Code Playgroud)

  • @Gregg你真的试过这个吗?`ASVideoNode`只是`AVPlayerLayer`的包装器.这不是魔术,它不会为视频播放带来任何滚动性能优势. (2认同)