开始实现之前,先介绍一下 AVFoundation用到的类!
-
AVAsset
一个统一多媒体文件类,不局限于音频视频,我们就可以通过这个类获取到它的多媒体文件各种属性比如类型时长每秒帧数等等 -
AVURLAsset
AVAsset子类,用来本地或者远程创建AVAsset -
AVAssetTrack
多媒体文件轨道:AVMediaTypeVideo/AVMediaTypeAudio/AVMediaTypeText等等
一般来说视频有两个轨道,一个是播放声音一个播放画面,所以说我们需要取播放画面的轨道的话:
self.assetTrack = self.asset?.tracksWithMediaType(AVMediaTypeVideo)[0]
- AVAssetReader
我们可以通过这个类获取asset的媒体数据(会抛出异常,所以放在do-catch里面或者直接try!)
self.assetReader = try AVAssetReader(asset: self.asset!)
- AVAssetReaderTrackOutput
能从AVAssetReader对象中读取同一类型媒体数据的样品的集合,大概就是视频输出的意思,从AVAssetTrack获取到某一通道的多媒体文件,然后通过AVAssetReader.startReading()方法开始获取视频的每一帧。
关于AVAssetReaderTrackOutput采样输出属性:
let m_pixelFormatType = kCVPixelFormatType_32BGRA //iOS在内部进行YUV至BGRA格式转换
outputSettings:[String(kCVPixelBufferPixelFormatTypeKey) : Int(m_pixelFormatType)]
这个直接使用网上的这个,但是看到stackoverflow有说,除非需要特别的format,不然可以outputSettings为nil,说是AVFoundation会选择最优效率的format
Query for optimal pixel format when capturing video on iOS?
- while循环处理视频帧样本
assetReader?.startReading()
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
// 确保大于0帧
while self.assetReader?.status == .Reading && self.assetTrack?.nominalFrameRate > 0 {
// 读取视频
autoreleasepool({
let videoBuffer = self.videoReaderOutput?.copyNextSampleBuffer()
if videoBuffer != nil {
let cgImage = self.cgImageFromSampleBufferRef(videoBuffer!)
guard self.delegate != nil else {
print("代理没设置")
return
}
self.delegate?.movieDecoderCallBack(self, cgImage: cgImage.takeRetainedValue())
// cgImage.release()
// 根据需要休眠一段时间;比如上层播放视频时每帧之间是有间隔的
NSThread.sleepForTimeInterval(0.001)
}
})
}
// 完成回调
guard self.delegate != nil else {
print("代理没设置")
return
}
self.delegate?.movieDecoderFinishCallBack(self)
}
- CMSampleBuffer--> CGImageRef的方法我是在Objective-C里面处理的,因为swift的autoreleasepool里面不能return
-(CGImageRef)cgImageFromSampleBufferRef:(CMSampleBufferRef)sampleBufferRef {
@autoreleasepool {
// 为媒体数据设置一个CMSampleBufferRef
CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBufferRef);
// 锁定 pixel buffer 的基地址,保证在内存中可用
CVPixelBufferLockBaseAddress(imageBuffer, 0);
// 得到 pixel buffer 的基地址
void *baseAddress = CVPixelBufferGetBaseAddress(imageBuffer);
// 得到 pixel buffer 的行字节数
size_t bytesPerRow = CVPixelBufferGetBytesPerRow(imageBuffer);
// 得到 pixel buffer 的宽和高
size_t width = CVPixelBufferGetWidth(imageBuffer);
size_t height = CVPixelBufferGetHeight(imageBuffer);
// 创建一个依赖于设备的 RGB 颜色空间
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
// 用抽样缓存的数据创建一个位图格式的图形上下文(graphic context)对象
CGContextRef context = CGBitmapContextCreate(baseAddress, width, height, 8, bytesPerRow, colorSpace, kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst);
//根据这个位图 context 中的像素创建一个 Quartz image 对象
CGImageRef quartzImage = CGBitmapContextCreateImage(context);
// 解锁 pixel buffer
CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
// CVPixelBufferRelease(imageBuffer);
// Free up the context and color space
CGContextRelease(context);
CGColorSpaceRelease(colorSpace);
return quartzImage;
}
}
-
CMSampleBufferRef
AVAssetReaderTrackOutput.copyNextSampleBuffer()能同步copy下一个CMSampleBuffer。每一个CMSampleBuffer在缓冲区有一个单独的样本帧(视频样本帧)相关联。 -
CVImageBufferRef
有了CMSampleBuffer,我们就可以在图像缓冲区创建CVImageBufferRef(Pixel buffers 像素缓冲区属于CVImageBufferRef派生类),有了这个图像缓冲区,我们就可以对它做像素级别的操作。 -
CGBitmapContextCreate创建位图对象
- data: 指向要渲染的绘制内存的地址。这个内存块的大小至少是(bytesPerRow*height)个字节
- width: bitmap的宽度,单位为像素
- height: bitmap的高度,单位为像素
- bitsPerComponent: 内存中像素的每个组件的位数.例如,对于32位像素格式和RGB 颜色空间,你应该将这个值设为8.
- bytesPerRow: bitmap的每一行在内存所占的比特数
- colorspace: bitmap上下文使用的颜色空间。
- bitmapInfo: 指定bitmap是否包含alpha通道,像素中alpha通道的相对位置,像素组件是整形还是浮点型等信息的字符串。
第六步和第七步都使用了autoreleasepool为了及时释放掉内存,网上看到很多网友说用这个方法都内存爆掉了,我加了这两个之后虽然还是在Leaks看到有内存泄露,但是没有出现过内存爆掉的情况
@nonobjc private var images: Array<CGImageRef> = []
var animation: CAKeyframeAnimation?
override func prepareForReuse() {
animation = nil
images.removeAll()
videoView.layer.removeAllAnimations()
}
func movieDecoderFinishCallBack(movieDecoder: MovieDecoder) {
dispatch_async(dispatch_get_main_queue()) {
let videoPath = "\(CacheDirectory)/\(self.cellModel.mp4Id).mp4"
let fileUrl = NSURL(fileURLWithPath: videoPath)
let asset = AVURLAsset(URL: fileUrl)
self.animation = CAKeyframeAnimation.init(keyPath: "contents")
self.animation!.duration = Double(asset.duration.value)/Double(asset.duration.timescale);
self.animation!.values = self.images;
self.animation!.repeatCount = MAXFLOAT;
self.startAnimation()
// 清除缓存
self.images.removeAll()
}
}
除了在movieDecoderFinishCallBack回调执行self.images.removeAll()之外,我还在prepareForReuse方法里面将animation置成nil,然后发现无论怎么滚动,内存都稳定在50M以下,比以前动辄几百M的好多了,不会出现crash的情况,不过依然会出现内存泄露的问题。
最后在网上找到一个感觉像是处理内存泄露的方法,具体我没考究,不过应该可以提供些许思路:
把一个视频拆分成多个AVAssetTrack,这样做的原因是因为,使用AVAssetReader读取每一帧SampleBuffer的数据是需要把数据加载到内存里面去的,如果直接把整个视频的SampleBuffer加载到内存,会造成闪退
链接:GitHub:KayWong/VideoReverse 使用AVFoundation实现视频倒序
以上代码参考链接:
IOS 微信聊天发送小视频的秘密(AVAssetReader+AVAssetReaderTrackOutput播放视频)
国庆期间我会写一个demo出来,现在暂时没时间。
网友评论