開始實(shí)現(xiàn)之前,先介紹一下 AVFoundation用到的類!
AVAsset
一個(gè)統(tǒng)一多媒體文件類,不局限于音頻視頻,我們就可以通過這個(gè)類獲取到它的多媒體文件各種屬性比如類型時(shí)長每秒幀數(shù)等等AVURLAsset
AVAsset子類,用來本地或者遠(yuǎn)程創(chuàng)建AVAssetAVAssetTrack
多媒體文件軌道:AVMediaTypeVideo/AVMediaTypeAudio/AVMediaTypeText等等
一般來說視頻有兩個(gè)軌道,一個(gè)是播放聲音一個(gè)播放畫面,所以說我們需要取播放畫面的軌道的話:
self.assetTrack = self.asset?.tracksWithMediaType(AVMediaTypeVideo)[0]
- AVAssetReader
我們可以通過這個(gè)類獲取asset的媒體數(shù)據(jù)(會(huì)拋出異常,所以放在do-catch里面或者直接try!)
self.assetReader = try AVAssetReader(asset: self.asset!)
- AVAssetReaderTrackOutput
能從AVAssetReader對(duì)象中讀取同一類型媒體數(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)]
這個(gè)直接使用網(wǎng)上的這個(gè),但是看到stackoverflow有說,除非需要特別的format,不然可以outputSettings為nil,說是AVFoundation會(huì)選擇最優(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ù)需要休眠一段時(shí)間;比如上層播放視頻時(shí)每幀之間是有間隔的
NSThread.sleepForTimeInterval(0.001)
}
})
}
// 完成回調(diào)
guard self.delegate != nil else {
print("代理沒設(shè)置")
return
}
self.delegate?.movieDecoderFinishCallBack(self)
}
- CMSampleBuffer--> CGImageRef的方法我是在Objective-C里面處理的,因?yàn)閟wift的autoreleasepool里面不能return
-(CGImageRef)cgImageFromSampleBufferRef:(CMSampleBufferRef)sampleBufferRef {
@autoreleasepool {
// 為媒體數(shù)據(jù)設(shè)置一個(gè)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)建一個(gè)依賴于設(shè)備的 RGB 顏色空間
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
// 用抽樣緩存的數(shù)據(jù)創(chuàng)建一個(gè)位圖格式的圖形上下文(graphic context)對(duì)象
CGContextRef context = CGBitmapContextCreate(baseAddress, width, height, 8, bytesPerRow, colorSpace, kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedFirst);
//根據(jù)這個(gè)位圖 context 中的像素創(chuàng)建一個(gè) Quartz image 對(duì)象
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下一個(gè)CMSampleBuffer。每一個(gè)CMSampleBuffer在緩沖區(qū)有一個(gè)單獨(dú)的樣本幀(視頻樣本幀)相關(guān)聯(lián)。CVImageBufferRef
有了CMSampleBuffer,我們就可以在圖像緩沖區(qū)創(chuàng)建CVImageBufferRef(Pixel buffers 像素緩沖區(qū)屬于CVImageBufferRef派生類),有了這個(gè)圖像緩沖區(qū),我們就可以對(duì)它做像素級(jí)別的操作。CGBitmapContextCreate創(chuàng)建位圖對(duì)象
- data: 指向要渲染的繪制內(nèi)存的地址。這個(gè)內(nèi)存塊的大小至少是(bytesPerRow*height)個(gè)字節(jié)
- width: bitmap的寬度,單位為像素
- height: bitmap的高度,單位為像素
- bitsPerComponent: 內(nèi)存中像素的每個(gè)組件的位數(shù).例如,對(duì)于32位像素格式和RGB 顏色空間,你應(yīng)該將這個(gè)值設(shè)為8.
- bytesPerRow: bitmap的每一行在內(nèi)存所占的比特?cái)?shù)
- colorspace: bitmap上下文使用的顏色空間。
- bitmapInfo: 指定bitmap是否包含alpha通道,像素中alpha通道的相對(duì)位置,像素組件是整形還是浮點(diǎn)型等信息的字符串。
第六步和第七步都使用了autoreleasepool為了及時(shí)釋放掉內(nèi)存,網(wǎng)上看到很多網(wǎng)友說用這個(gè)方法都內(nèi)存爆掉了,我加了這兩個(gè)之后雖然還是在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)無論怎么滾動(dòng),內(nèi)存都穩(wěn)定在50M以下,比以前動(dòng)輒幾百M(fèi)的好多了,不會(huì)出現(xiàn)crash的情況,不過依然會(huì)出現(xiàn)內(nèi)存泄露的問題。
最后在網(wǎng)上找到一個(gè)感覺像是處理內(nèi)存泄露的方法,具體我沒考究,不過應(yīng)該可以提供些許思路:
把一個(gè)視頻拆分成多個(gè)AVAssetTrack,這樣做的原因是因?yàn)椋褂肁VAssetReader讀取每一幀SampleBuffer的數(shù)據(jù)是需要把數(shù)據(jù)加載到內(nèi)存里面去的,如果直接把整個(gè)視頻的SampleBuffer加載到內(nèi)存,會(huì)造成閃退
鏈接:GitHub:KayWong/VideoReverse 使用AVFoundation實(shí)現(xiàn)視頻倒序
以上代碼參考鏈接:
IOS 微信聊天發(fā)送小視頻的秘密(AVAssetReader+AVAssetReaderTrackOutput播放視頻)
國慶期間我會(huì)寫一個(gè)demo出來,現(xiàn)在暫時(shí)沒時(shí)間。