总结一下音视频开发中遇到过的坑和一些优化点。
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也可以读取音频数据,但怎么播放目前还不知道!!!
参考: