該文章引用自:http://www.jianshu.com/p/3d5ccbde0de1

IOS 微信聊天傳送小視訊的祕密(AVAssetReader+AVAssetReaderTrackOutput播放視訊)

對於播放視訊,大家應該一開始就想到比較方便快捷使用簡單的MPMoviePlayerController類,確實用這個蘋果官方為我們包裝好了的 API 確實有很多事情都不用我們煩心,我們可以很快的做出一個視訊播放器,但是很遺憾,高度封裝的東西,就證明了可自定義性越受限制,而MPMoviePlayerController卻正正證明了這一點。所以大家又相對的想起了AVPlayer,是的,AVPlayer是一個很好的自定義播放器,但是,AVPlayer卻有著效能限制,微信團隊也證實這一點,AVPlayer只能同事播放16個視訊,之後建立一個視訊,對可滾動的聊天介面來說,是一個非常致命的效能限制了。

AVAssetReader+AVAssetReaderTrackOutput

那麼既然AVPlayer有著效能限制,我們就做一個屬於我們的播放器吧,AVAssetReader可以從原始資料裡獲取解碼後的音視訊資料。結合AVAssetReaderTrackOutput ,能讀取一幀幀的CMSampleBufferRef 。CMSampleBufferRef 可以轉化成CGImageRef 。為此,我們可以建立一個ABSMovieDecoder 的一個類來負責視訊解碼,把讀出的每一個CMSampleBufferRef 傳遞給上層。

那麼用ABSMovieDecoder- (void)transformViedoPathToSampBufferRef:(NSString *)videoPath方法利用AVAssetReader+AVAssetReaderTrackOutput解碼的步驟如下:
1.獲取媒體檔案的資源AVURLAsset

// 獲取媒體檔案路徑的 URL,必須用 fileURLWithPath: 來獲取檔案 URL
NSURL *fileUrl = [NSURL fileURLWithPath:videoPath];
AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:fileUrl options:nil];
NSError *error = nil;
AVAssetReader *reader = [[AVAssetReader alloc] initWithAsset:asset error:&error];

2.建立一個讀取媒體資料的閱讀器AVAssetReader

AVAssetReader *reader = [[AVAssetReader alloc] initWithAsset:asset error:&error];

3.獲取視訊的軌跡AVAssetTrack其實就是我們的視訊來源

NSArray *videoTracks = [asset tracksWithMediaType:AVMediaTypeVideo];
AVAssetTrack *videoTrack =[videoTracks objectAtIndex:0];

4.為我們的閱讀器AVAssetReader進行配置,如配置讀取的畫素,視訊壓縮等等,得到我們的輸出埠videoReaderOutput軌跡,也就是我們的資料來源

 int m_pixelFormatType;
// 視訊播放時,
m_pixelFormatType = kCVPixelFormatType_32BGRA;
// 其他用途,如視訊壓縮
// m_pixelFormatType = kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange; NSMutableDictionary *options = [NSMutableDictionary dictionary];
[options setObject:@(m_pixelFormatType) forKey:(id)kCVPixelBufferPixelFormatTypeKey];
AVAssetReaderTrackOutput *videoReaderOutput = [[AVAssetReaderTrackOutput alloc] initWithTrack:videoTrack outputSettings:options];

5.為閱讀器新增輸出埠,並開啟閱讀器

[reader addOutput:videoReaderOutput];
[reader startReading];

6.獲取閱讀器輸出的資料來源 CMSampleBufferRef

// 要確保nominalFrameRate>0,之前出現過android拍的0幀視訊
while ([reader status] == AVAssetReaderStatusReading && videoTrack.nominalFrameRate > 0) {
// 讀取 video sample
CMSampleBufferRef videoBuffer = [videoReaderOutput copyNextSampleBuffer];
[self.delegate mMoveDecoder:self onNewVideoFrameReady:videoBuffer]; // 根據需要休眠一段時間;比如上層播放視訊時每幀之間是有間隔的,這裡的 sampleInternal 我設定為0.001秒
[NSThread sleepForTimeInterval:sampleInternal];
}

7.通過代理告訴上層解碼結束

// 告訴上層視訊解碼結束
[self.delegate mMoveDecoderOnDecoderFinished:self];

至此,我們就能獲取視訊的每一幀的元素CMSampleBufferRef,但是我們要把它轉換成對我們有用的東西,例如圖片

// AVFoundation 捕捉視訊幀,很多時候都需要把某一幀轉換成 image
+ (CGImageRef)imageFromSampleBufferRef:(CMSampleBufferRef)sampleBufferRef
{
// 為媒體資料設定一個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); // 釋放 context 和顏色空間
CGContextRelease(context);
CGColorSpaceRelease(colorSpace);
// 用 Quzetz image 建立一個 UIImage 物件
// UIImage *image = [UIImage imageWithCGImage:quartzImage]; // 釋放 Quartz image 物件
// CGImageRelease(quartzImage); return quartzImage; }

從上面大家可以可得出,獲取圖片圖片的最直接有效的是 UIImage 了,但是為什麼我不需要 UIImage 卻要了個撇足的 CGImageRef 呢? 那是因為建立CGImageRef不會做圖片資料的記憶體拷貝,它只會當 Core Animation執行 Transaction::commit() 觸發 layer -display時,才把圖片資料拷貝到 layer buffer裡。簡單點的意思就是說不會消耗太多的記憶體!


接下來我們需要把所有得到的CGImageRef元素都合成視訊了。當然在這之前應該把所有的 CGImageRef 當做物件放在一個數組中。那麼知道CGImageRef為 C 語言的結構體,這時候我們要用到橋接來將CGImageRef轉換成我們能用的物件了

CGImageRef cgimage = [UIImage imageFromSampleBufferRef:videoBuffer];
if (!(__bridge id)(cgimage)) { return; }
[images addObject:((__bridge id)(cgimage))];
CGImageRelease(cgimage);

- (void)mMoveDecoderOnDecoderFinished:(TransformVideo *)transformVideo
{
NSLog(@"視訊解檔完成");
// 得到媒體的資源
AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:[NSURL fileURLWithPath:filePath] options:nil];
// 通過動畫來播放我們的圖片
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"contents"];
// asset.duration.value/asset.duration.timescale 得到視訊的真實時間
animation.duration = asset.duration.value/asset.duration.timescale;
animation.values = images;
animation.repeatCount = MAXFLOAT;
[self.preView.layer addAnimation:animation forKey:nil];
// 確保記憶體能及時釋放掉
[images enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if (obj) {
obj = nil;
}
}];
}

@end