開始實現(xiàn)之前瓜晤,先介紹一下 AVFoundation用到的類仑嗅!
AVAsset
一個統(tǒng)一多媒體文件類豆拨,不局限于音頻視頻浦旱,我們就可以通過這個類獲取到它的多媒體文件各種屬性比如類型時長每秒幀數(shù)等等AVURLAsset
AVAsset子類宇色,用來本地或者遠(yuǎn)程創(chuàng)建AVAssetAVAssetTrack
多媒體文件軌道:AVMediaTypeVideo/AVMediaTypeAudio/AVMediaTypeText等等
一般來說視頻有兩個軌道,一個是播放聲音一個播放畫面颁湖,所以說我們需要取播放畫面的軌道的話:
self.assetTrack = self.asset?.tracksWithMediaType(AVMediaTypeVideo)[0]
- AVAssetReader
我們可以通過這個類獲取asset的媒體數(shù)據(jù)(會拋出異常宣蠕,所以放在do-catch里面或者直接try!)
self.assetReader = try AVAssetReader(asset: self.asset!)
- AVAssetReaderTrackOutput
能從AVAssetReader對象中讀取同一類型媒體數(shù)據(jù)的樣品的集合,大概就是視頻輸出的意思甥捺,從AVAssetTrack獲取到某一通道的多媒體文件抢蚀,然后通過AVAssetReader.startReading()方法開始獲取視頻的每一幀。
關(guān)于AVAssetReaderTrackOutput采樣輸出屬性:
let m_pixelFormatType = kCVPixelFormatType_32BGRA //iOS在內(nèi)部進(jìn)行YUV至BGRA格式轉(zhuǎn)換
outputSettings:[String(kCVPixelBufferPixelFormatTypeKey) : Int(m_pixelFormatType)]
這個直接使用網(wǎng)上的這個镰禾,但是看到stackoverflow有說皿曲,除非需要特別的format,不然可以outputSettings為nil吴侦,說是AVFoundation會選擇最優(yōu)效率的format
Query for optimal pixel format when capturing video on iOS?
- while循環(huán)處理視頻幀樣本
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("代理沒設(shè)置")
return
}
self.delegate?.movieDecoderCallBack(self, cgImage: cgImage.takeRetainedValue())
// cgImage.release()
// 根據(jù)需要休眠一段時間谷饿;比如上層播放視頻時每幀之間是有間隔的
NSThread.sleepForTimeInterval(0.001)
}
})
}
// 完成回調(diào)
guard self.delegate != nil else {
print("代理沒設(shè)置")
return
}
self.delegate?.movieDecoderFinishCallBack(self)
}
- CMSampleBuffer--> CGImageRef的方法我是在Objective-C里面處理的,因為swift的autoreleasepool里面不能return
-(CGImageRef)cgImageFromSampleBufferRef:(CMSampleBufferRef)sampleBufferRef {
@autoreleasepool {
// 為媒體數(shù)據(jù)設(shè)置一個CMSampleBufferRef
CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBufferRef);
// 鎖定 pixel buffer 的基地址妈倔,保證在內(nèi)存中可用
CVPixelBufferLockBaseAddress(imageBuffer, 0);
// 得到 pixel buffer 的基地址
void *baseAddress = CVPixelBufferGetBaseAddress(imageBuffer);
// 得到 pixel buffer 的行字節(jié)數(shù)
size_t bytesPerRow = CVPixelBufferGetBytesPerRow(imageBuffer);
// 得到 pixel buffer 的寬和高
size_t width = CVPixelBufferGetWidth(imageBuffer);
size_t height = CVPixelBufferGetHeight(imageBuffer);
// 創(chuàng)建一個依賴于設(shè)備的 RGB 顏色空間
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
// 用抽樣緩存的數(shù)據(jù)創(chuàng)建一個位圖格式的圖形上下文(graphic context)對象
CGContextRef context = CGBitmapContextCreate(baseAddress, width, height, 8, bytesPerRow, colorSpace, kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst);
//根據(jù)這個位圖 context 中的像素創(chuàng)建一個 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在緩沖區(qū)有一個單獨的樣本幀(視頻樣本幀)相關(guān)聯(lián)。CVImageBufferRef
有了CMSampleBuffer盯蝴,我們就可以在圖像緩沖區(qū)創(chuàng)建CVImageBufferRef(Pixel buffers 像素緩沖區(qū)屬于CVImageBufferRef派生類)毅哗,有了這個圖像緩沖區(qū),我們就可以對它做像素級別的操作捧挺。CGBitmapContextCreate創(chuàng)建位圖對象
- data: 指向要渲染的繪制內(nèi)存的地址虑绵。這個內(nèi)存塊的大小至少是(bytesPerRow*height)個字節(jié)
- width: bitmap的寬度,單位為像素
- height: bitmap的高度,單位為像素
- bitsPerComponent: 內(nèi)存中像素的每個組件的位數(shù).例如,對于32位像素格式和RGB 顏色空間闽烙,你應(yīng)該將這個值設(shè)為8.
- bytesPerRow: bitmap的每一行在內(nèi)存所占的比特數(shù)
- colorspace: bitmap上下文使用的顏色空間翅睛。
- bitmapInfo: 指定bitmap是否包含alpha通道声搁,像素中alpha通道的相對位置,像素組件是整形還是浮點型等信息的字符串捕发。
第六步和第七步都使用了autoreleasepool為了及時釋放掉內(nèi)存疏旨,網(wǎng)上看到很多網(wǎng)友說用這個方法都內(nèi)存爆掉了,我加了這兩個之后雖然還是在Leaks看到有內(nèi)存泄露扎酷,但是沒有出現(xiàn)過內(nèi)存爆掉的情況
@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回調(diào)執(zhí)行self.images.removeAll()之外檐涝,我還在prepareForReuse方法里面將animation置成nil,然后發(fā)現(xiàn)無論怎么滾動法挨,內(nèi)存都穩(wěn)定在50M以下谁榜,比以前動輒幾百M的好多了,不會出現(xiàn)crash的情況凡纳,不過依然會出現(xiàn)內(nèi)存泄露的問題窃植。
最后在網(wǎng)上找到一個感覺像是處理內(nèi)存泄露的方法,具體我沒考究荐糜,不過應(yīng)該可以提供些許思路:
把一個視頻拆分成多個AVAssetTrack撕瞧,這樣做的原因是因為,使用AVAssetReader讀取每一幀SampleBuffer的數(shù)據(jù)是需要把數(shù)據(jù)加載到內(nèi)存里面去的狞尔,如果直接把整個視頻的SampleBuffer加載到內(nèi)存丛版,會造成閃退
鏈接:GitHub:KayWong/VideoReverse 使用AVFoundation實現(xiàn)視頻倒序
以上代碼參考鏈接:
IOS 微信聊天發(fā)送小視頻的秘密(AVAssetReader+AVAssetReaderTrackOutput播放視頻)
國慶期間我會寫一個demo出來,現(xiàn)在暫時沒時間偏序。