IOS 微信聊天發(fā)送小視頻的秘密(AVAssetReader+AVAssetReaderTrackOutput播放視頻)

對于播放視頻,大家應(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)也算不錯,這就是讓我堅持寫作的一方面,我們盡量讓大家都能看完我的文章后都有所收獲,
如果你遇到文章上有某些位置不太明白的,可以私信我,如果想看源碼的朋友,也可以私信我,
我會盡我所能幫大家解決問題的。

心如止水,奮力前行

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容