該文章引用自: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