IOS 微信聊天发送小视频的秘密(AVAssetReader+AVAssetReaderTrackOutput播放视频)

对于播放视频,大家应该一开始就想到比较方便快捷使用简单的MPMoviePlayerController类,确实用这个苹果官方为我们包装好了的 API 确实有很多事情都不用我们烦心,我们可以很快的做出一个视频播放器,但是很遗憾,高度封装的东西,就证明了可自定义性越受限制,而MPMoviePlayerController却正正证明了这一点。所以大家又相对的想起了AVPlayer,是的,AVPlayer是一个很好的自定义播放器,但是,AVPlayer却有着性能限制,微信团队也证实这一点,AVPlayer只能同事播放16个视频,之后创建一个视频,对可滚动的聊天界面来说,是一个非常致命的性能限制了。

AVAssetReader+AVAssetReaderTrackOutput

那么既然AVPlayer有着性能限制,我们就做一个属于我们的播放器吧,AVAssetReader 可以从原始数据里获取解码后的音视频数据。结合AVAssetReaderTrackOutput ,能读取一帧帧的CMSampleBufferRefCMSampleBufferRef 可以转化成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

之前写了的文章大家反应也算不错,这就是让我坚持写作的一方面,我们尽量让大家都能看完我的文章后都有所收获,
如果你遇到文章上有某些位置不太明白的,可以私信我,如果想看源码的朋友,也可以私信我,
我会尽我所能帮大家解决问题的。

心如止水,奋力前行

你可能感兴趣的:(IOS 微信聊天发送小视频的秘密(AVAssetReader+AVAssetReaderTrackOutput播放视频))