音视频四、AVPlayer使用中的坑和一些优化点

总结一下音视频开发中遇到过的坑和一些优化点。

1. 开始播放时黑屏一下。

原因:下载数据不够就开始播放,导致卡顿和解码异常,影响播放体验

通常调用AVPlayer的play方法,不会立即播放,因为AVPlayer需要加载资源(解封装、解码等)。可以监听player的状态,当状态变为readyToPlay时进行播放,最好多缓存1到2秒的数据后再播放。

2. 部分老设备(第一二代iPad)无法播放视频,播放时白屏(原因未知)。

3. AVAssetResourceLoaderDelegate协议方法不调用。

只有url的协议头为自定义协议头时,才会调用AVAssetResourceLoaderDelegate协议方法。因此做离线缓存时,都需要修改原来的协议,代理方法拦截时在用原有协议请求数据。

4. dataRequest记得finishLoading()不然极有可能没有继续的请求了。重复请求的时候也要取消掉上一次请求。

5. 使用URLSession的dataTask(with: request)方法创建URLSessionDataTask时,URLSessionDelegate方法不调用.

原因是遵守了URLSessionDelegate协议而不是URLSessionDataTaskDelegate。推荐使用dataTask(with:, completionHandler:)方法创建URLSessionDataTask,简单直接。

6. 如果网络不好,首帧出图之后,如何优化使后续播放不卡顿?

a.采用硬件解码

对于清晰度较高的视频,对于解码的性能消耗也会较大,如果设备的性能不足,则可能会造成解码速度赶不上播放速度,从而造成卡顿。在实际情况中,可以尽量选择使用硬解,通过 GPU 来进行解码。常用的FFMpeg是软件解码、iOS AudioToolBox/VideoToolBox是硬件解码。

b.码率或者说是码流切换

720P、1080P 等清晰度较高的码流,对于网速的要求以及设备性能的要求都会相对较高,在发生卡顿的情况下,可以考虑将播放的视频切换到较低清晰度的码流,从而优化网络加载速度,降低对设备性能的消耗,优化视频播放的卡顿。

c.优化缓冲策略

在点播场景下,为了减少播放过程中的卡顿,可以在缓冲一定的数据后再解码播放。但是这样,就会影响视频的首屏播放速度。

增大播放器的缓冲区,使得每次下载时能够加载足够的数据再进行播放,能够降低播放过程中卡顿的频次,但是这样也会延长首屏播放速度以及每次卡顿后恢复播放的速度。所以对于缓冲区的大小的设置,需要考虑卡顿和快速开播两个因素,尽量取得平衡。

在iOS平台上,使用系统的 AVPlayer 时,属性automaticallyWaitsToMinimizeStalling 就是控制播放器缓冲策略的。当该值为 YES 时,AVPlayer 会努力尝试延迟开始播放,加载足够的数据来保证整个播放过程中尽量卡顿最少。这个接口在 iOS 10 及以上版本才开放,在 iOS 10 之前的版本,在播放 HLS 这种流媒体视频时,效果如同 automaticallyWaitsToMinimizeStalling 为 YES,播放基于文件的视频资源,包括通过网络传输的网络视频文件,则效果如同 automaticallyWaitsToMinimizeStalling 为 NO。

实际使用中,automaticallyWaitsToMinimizeStalling = false好像没啥用!!!

7. AVPlayer播放单个视频并没有太大的性能问题,但在tableView中滚动时,自动切换和播放视频会造成滑动卡顿,该如何优化?

AVPlayer的replaceCurrentItemWithPlayerItem(用来切换视频的)方法在切换视频时底层会调用信号量等待,然后导致当前线程卡顿,如果在UI线程调用,会导致UI线程卡顿。但放在子线程又会导致播放无法及时响应。下面是可行的方案。

a. 使用AVQueuePlayer的advanceToNextItem方法进行item的切换,这个类还是很好使的,可以在不卡顿主线程的情况下流畅切换视频。

b. 使用 AVAssetReader+AVAssetReaderTrackOutput 获取视频每一帧,然后转换成CGImage给Layer显示。这种方法可以顺滑地同时静音播放多个视频并且视频要是本地视频,因为AVAssetReader初始化的时候必须是本地url.

核心代码:

private func readData(path: String, displayView: UIView) {
    // AVAssetReader必须本地资源
    // Error Domain=AVFoundationErrorDomain Code=-11838 "Cannot initialize an instance of AVAssetReader with an asset at non-local URL 'http://localhost:3000/video?name=123'" UserInfo={NSDebugDescription=Cannot initialize an instance of AVAssetReader with an asset at non-local URL 'http://localhost:3000/video?name=123', NSLocalizedDescription=Operation Stopped, NSLocalizedFailureReason=The operation is not supported for this media.}
//        let url = URL(string: "http://localhost:3000/video?name=123")!
        guard let url = Bundle.main.url(forResource: path, withExtension: nil) else {
            return
    }
        let asset = AVURLAsset(url: url)

    do {
        let assetReader = try AVAssetReader(asset: asset)
        let videoTracks = asset.tracks(withMediaType: .video)
        if videoTracks.count <= 0 {
            return
        }
        let videoTrack = videoTracks.first!
        let videoReaderTrackOutput = AVAssetReaderTrackOutput(track: videoTrack, outputSettings: [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA])
        assetReader.add(videoReaderTrackOutput)
        assetReader.startReading()
        while assetReader.status == .reading && videoTrack.nominalFrameRate > 0 {
            if let sampleBuffer = videoReaderTrackOutput.copyNextSampleBuffer() {
                if let cgImage = imageFromSampleBuffer(sampleBuffer, rotation: orientationFromAVAssetTrack(videoTrack)) {
                    DispatchQueue.main.async {
                        displayView.layer.contents = cgImage
                    }
                }
                //根据需要休眠一段时间, 视频播放的时候每一帧都是有间隔的
                Thread.sleep(forTimeInterval: 0.035)
            }
        }
        assetReader.cancelReading()
    } catch  {
        print(error)
    }
}

//MARK: 捕捉视频帧,转换成CGImage
private func orientationFromAVAssetTrack(_ videoTrack: AVAssetTrack) -> UIImage.Orientation {
    var orientation = UIImage.Orientation.up
    let t = videoTrack.preferredTransform
    if (t.a == 0 && t.b == 1.0 && t.c == -1.0 && t.d == 0) {
        orientation = .right
    }
    else if (t.a == 0 && t.b == -1.0 && t.c == 1.0 && t.d == 0) {
        orientation = .left
    }
    else if (t.a == 1.0 && t.b == 0 && t.c == 0 && t.d == 1.0) {
        orientation = .up
    }
    else if (t.a == -1.0 && t.b == 0 && t.c == 0 && t.d == -1.0) {
        orientation = .down
    }
    return orientation
}

private func imageFromSampleBuffer(_ sampleBuffer: CMSampleBuffer, rotation: UIImage.Orientation) -> CGImage?
{
    guard let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
        return nil
    }
    // Lock the base address of the pixel buffer
    CVPixelBufferLockBaseAddress(imageBuffer, [])
    // Get the number of bytes per row for the pixel buffer
    let bytesPerRow = CVPixelBufferGetBytesPerRow(imageBuffer)
    // Get the pixel buffer width and height
    let width = CVPixelBufferGetWidth(imageBuffer)
    let height = CVPixelBufferGetHeight(imageBuffer)
    //Generate image to edit
    let pixel = CVPixelBufferGetBaseAddress(imageBuffer)
    let colorSpace = CGColorSpaceCreateDeviceRGB()

    if let context = CGContext(data: pixel, width: width, height: height, bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) {
        let image = context.makeImage()
        CVPixelBufferUnlockBaseAddress(imageBuffer, [])
        UIGraphicsEndImageContext()
        return image
    }
    return nil
}

AVAssetReader也可以读取音频数据,但怎么播放目前还不知道!!!

参考:

细数AVPlayer的那些坑

Qzone– AVPlayer那些坑

iOS微信小视频优化心得

AVAssetReader+AVAssetReaderTrackOutput