对于播放视频,大家应该一开始就想到比较方便快捷使用简单的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: 来获取文件 URLNSURL *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
之前写了的文章大家反应也算不错,这就是让我坚持写作的一方面,我们尽量让大家都能看完我的文章后都有所收获, 如果你遇到文章上有某些位置不太明白的,可以私信我,如果想看源码的朋友,也可以私信我, 我会尽我所能帮大家解决问题的。
心如止水,奋力前行
作者:葱神大大
链接:https://www.jianshu.com/p/3d5ccbde0de1