對于播放視頻,大家應(yīng)該一開始就想到比較方便快捷使用簡單的MPMoviePlayerController類,確實(shí)用這個蘋果官方為我們包裝好了的 API 確實(shí)有很多事情都不用我們煩心,我們可以很快的做出一個視頻播放器,但是很遺憾,高度封裝的東西,就證明了可自定義性越受限制,而MPMoviePlayerController卻正正證明了這一點(diǎn)。所以大家又相對的想起了AVPlayer,是的,AVPlayer是一個很好的自定義播放器,但是,AVPlayer卻有著性能限制,微信團(tuán)隊也證實(shí)這一點(diǎn),AVPlayer只能同事播放16個視頻,之后創(chuàng)建一個視頻,對可滾動的聊天界面來說,是一個非常致命的性能限制了。
AVAssetReader+AVAssetReaderTrackOutput
那么既然AVPlayer有著性能限制,我們就做一個屬于我們的播放器吧,AVAssetReader 可以從原始數(shù)據(jù)里獲取解碼后的音視頻數(shù)據(jù)。結(jié)合AVAssetReaderTrackOutput ,能讀取一幀幀的CMSampleBufferRef 。CMSampleBufferRef 可以轉(zhuǎn)化成CGImageRef 。為此,我們可以創(chuàng)建一個ABSMovieDecoder 的一個類來負(fù)責(zé)視頻解碼,把讀出的每一個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.創(chuàng)建一個讀取媒體數(shù)據(jù)的閱讀器AVAssetReader
AVAssetReader *reader = [[AVAssetReader alloc] initWithAsset:asset error:&error];
3.獲取視頻的軌跡AVAssetTrack其實(shí)就是我們的視頻來源
NSArray *videoTracks = [asset tracksWithMediaType:AVMediaTypeVideo];
AVAssetTrack *videoTrack =[videoTracks objectAtIndex:0];
4.為我們的閱讀器AVAssetReader進(jìn)行配置,如配置讀取的像素,視頻壓縮等等,得到我們的輸出端口videoReaderOutput軌跡,也就是我們的數(shù)據(jù)來源
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.獲取閱讀器輸出的數(shù)據(jù)源 CMSampleBufferRef
// 要確保nominalFrameRate>0,之前出現(xiàn)過android拍的0幀視頻
while ([reader status] == AVAssetReaderStatusReading && videoTrack.nominalFrameRate > 0) {
// 讀取 video sample
CMSampleBufferRef videoBuffer = [videoReaderOutput copyNextSampleBuffer];
[self.delegate mMoveDecoder:self onNewVideoFrameReady:videoBuffer];
// 根據(jù)需要休眠一段時間;比如上層播放視頻時每幀之間是有間隔的,這里的 sampleInternal 我設(shè)置為0.001秒
[NSThread sleepForTimeInterval:sampleInternal];
}
7.通過代理告訴上層解碼結(jié)束
// 告訴上層視頻解碼結(jié)束
[self.delegate mMoveDecoderOnDecoderFinished:self];
至此,我們就能獲取視頻的每一幀的元素CMSampleBufferRef,但是我們要把它轉(zhuǎn)換成對我們有用的東西,例如圖片
// AVFoundation 捕捉視頻幀,很多時候都需要把某一幀轉(zhuǎn)換成 image
+ (CGImageRef)imageFromSampleBufferRef:(CMSampleBufferRef)sampleBufferRef
{
// 為媒體數(shù)據(jù)設(shè)置一個CMSampleBufferRef
CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBufferRef);
// 鎖定 pixel buffer 的基地址
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);
// 釋放 context 和顏色空間
CGContextRelease(context);
CGColorSpaceRelease(colorSpace);
// 用 Quzetz image 創(chuàng)建一個 UIImage 對象
// UIImage *image = [UIImage imageWithCGImage:quartzImage];
// 釋放 Quartz image 對象
// CGImageRelease(quartzImage);
return quartzImage;
}
從上面大家可以可得出,獲取圖片圖片的最直接有效的是 UIImage 了,但是為什么我不需要 UIImage 卻要了個撇足的 CGImageRef 呢? 那是因?yàn)閯?chuàng)建CGImageRef不會做圖片數(shù)據(jù)的內(nèi)存拷貝,它只會當(dāng) Core Animation執(zhí)行 Transaction::commit() 觸發(fā) layer -display時,才把圖片數(shù)據(jù)拷貝到 layer buffer里。簡單點(diǎn)的意思就是說不會消耗太多的內(nèi)存!
接下來我們需要把所有得到的CGImageRef元素都合成視頻了。當(dāng)然在這之前應(yīng)該把所有的 CGImageRef 當(dāng)做對象放在一個數(shù)組中。那么知道CGImageRef為 C 語言的結(jié)構(gòu)體,這時候我們要用到橋接來將CGImageRef轉(zhuǎn)換成我們能用的對象了
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 得到視頻的真實(shí)時間
animation.duration = asset.duration.value/asset.duration.timescale;
animation.values = images;
animation.repeatCount = MAXFLOAT;
[self.preView.layer addAnimation:animation forKey:nil];
// 確保內(nèi)存能及時釋放掉
[images enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if (obj) {
obj = nil;
}
}];
}
@end
之前寫了的文章大家反應(yīng)也算不錯,這就是讓我堅持寫作的一方面,我們盡量讓大家都能看完我的文章后都有所收獲,
如果你遇到文章上有某些位置不太明白的,可以私信我,如果想看源碼的朋友,也可以私信我,
我會盡我所能幫大家解決問題的。