iOS微信聊天界面朋友圈多个小视频同时播放不卡顿
我的簡書地址http://www.jianshu.com/p/10206ed63e0d
之前有個需求是實現如微信朋友圈動態列表小視頻播放的效果,最近有空整理下給同樣有需要的同學。
我們都知道微信朋友圈列表允許多個小視頻同時無聲播放,并且不會有絲毫卡頓問題,點擊了才放大有聲播放。
照著視頻播放相關技術,我們可以實現通過AVPlayer來播放視頻。但是如果在UITableView列表上通過AVPlayer來播放cell上的視頻,要是視頻一多,列表滾動就卡的不要不要的,嚴重的影響用戶體驗。至于單個cell上視頻點擊放大播放,就沒有關系了,完全可以寫一個節目用AVPlayer播放,這里我們只講列表cell上的視頻播放效果。
通過查找相關資料,知道因為AVPlayer的新能局限性,AVPlayer只能同時播放16個視頻(具體怎么得出的,我也不懂,反正大佬說是就是了),再多久卡頓嚴重。最終采用AVAssetReader+AVAssetReaderTrackOutput的方式來實現多個視頻同時播放。
先來看個最終的體驗效果:
達到了非常流暢的效果,同時看下性能消耗:
妥妥的有沒有!!!!!
下面來分析下最終的實現步驟。
這里先說下在實現過程中查找了不少資料,也試了好幾個第三方代碼,最終在不知道哪個地方找到了這一份代碼,
反正現在也不知道出處了,在這里感謝下這位大佬。本文就是在分析大佬的實現方式。
同時在查找學習過程中,也翻到了這篇文章http://www.jianshu.com/p/3d5ccbde0de1,實現思路是一樣,具體這個需求完成挺長時間了,也記不清是先看到這文章還是先看到這份代碼,或者這就是一個人,反正就是感慨大佬就是大佬。
下面進入正題,總得來說,既然AVPlayer有性能局限,那我們可以通過截取視頻的每一幀,轉換成圖片,賦給View來顯示,這樣就能實現無聲的視頻播放了。
我們通過使用NSOperation和NSOperationQueue多線程的API來并發實現多個視頻同時播放,實現思路如下:
(1)將每一個cell上的視頻播放操作封裝到一個NSOperation對象中,這個操作內部就實現抽取每一幀轉換為圖片,通過回調返回給View的layer來顯示。
(2)然后將NSOperation對象添加到NSOperationQueue隊列中,同時搞一個NSMutableDictionary管理這所有NSOperation操作,key為視頻地址url。
(3)提供取消單個視頻播放任務、所有視頻播放任務。
下面是梳理并且copy了一份大佬的代碼:
1、自定義NSOperation的子類NSBlockOperation(其他的子類實現也行)定義如下的方法:
.m里面實現- (void)videoPlayTask:(NSString *)videoFilePath;方法.
初始化AVUrlAsset獲取對應視頻的詳細信息(AVAsset具有多種有用的方法和屬性,比如時長,創建日期和元數據等)
創建一個讀取媒體數據的閱讀器AVAssetReader
NSError *error; AVAssetReader *assetReader = [[AVAssetReader alloc] initWithAsset:asset error:&error];獲取視頻的軌跡AVAssetTrack
NSArray *videoTracks = [asset tracksWithMediaType:AVMediaTypeVideo]; //如果AVAssetTrack信息為空,直接返回 if (!videoTracks.count) {return; } AVAssetTrack *videoTrack = [videoTracks objectAtIndex:0];獲取視頻圖像方向
UIImageOrientation orientation = [self orientationFromAVAssetTrack:videoTrack];這里看到很多人都說視頻方向怎么不對,應該是沒有正確的設置圖像方向吧。
- (UIImageOrientation)orientationFromAVAssetTrack:(AVAssetTrack *)videoTrack {UIImageOrientation orientation = UIImageOrientationUp;CGAffineTransform t = videoTrack.preferredTransform;if (t.a == 0 && t.b == 1.0 && t.c == -1.0 && t.d == 0){orientation = UIImageOrientationRight;}else if (t.a == 0 && t.b == -1.0 && t.c == 1.0 && t.d == 0){orientation = UIImageOrientationLeft;}else if (t.a == 1.0 && t.b == 0 && t.c == 0 && t.d == 1.0){orientation = UIImageOrientationUp;}else if (t.a == -1.0 && t.b == 0 && t.c == 0 && t.d == -1.0){orientation = UIImageOrientationDown;}return orientation; }為閱讀器AVAssetReader進行配置,如配置讀取的像素,視頻壓縮等等,得到我們的輸出端口AVAssetReaderTrackOutput軌跡,也就是我們的數據來源
/**摘自http://www.jianshu.com/p/6f55681122e4iOS系統定義了很多很多視頻格式,讓人眼花繚亂。不過一旦熟悉了它的命名規則,其實一眼就能看明白。kCVPixelFormatType_{長度|序列}{顏色空間}{Planar|BiPlanar}{VideoRange|FullRange}*///至于為啥設置這個,網上說是經驗//其他用途,如視頻壓縮 m_pixelFormatType = kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange;int m_pixelFormatType = kCVPixelFormatType_32BGRA;NSDictionary *options = [NSDictionary dictionaryWithObject:[NSNumber numberWithInt:(int)m_pixelFormatType] forKey:(id)kCVPixelBufferPixelFormatTypeKey];//獲取輸出端口AVAssetReaderTrackOutputAVAssetReaderTrackOutput *videoReaderTrackOptput = [[AVAssetReaderTrackOutput alloc] initWithTrack:videoTrack outputSettings:options];//添加輸出端口,開啟閱讀器[assetReader addOutput:videoReaderTrackOptput];[assetReader startReading];獲取每一幀的數據CMSampleBufferRef,并且通過回調返回給需要的類
//確保nominalFrameRate幀速率 > 0,碰到過坑爹的安卓拍出來0幀的視頻//同時確保當前Operation操作沒有取消while (assetReader.status == AVAssetReaderStatusReading && videoTrack.nominalFrameRate > 0 && !self.isCancelled) {//依次獲取每一幀視頻CMSampleBufferRef sampleBufferRef = [videoReaderTrackOptput copyNextSampleBuffer];if (!sampleBufferRef) {return;}//根據視頻圖像方向將CMSampleBufferRef每一幀轉換成CGImageRefCGImageRef imageRef = [ABListVideoOperation imageFromSampleBuffer:sampleBufferRef rotation:orientation];dispatch_async(dispatch_get_main_queue(), ^{if (self.videoDecodeBlock) {self.videoDecodeBlock(imageRef, videoFilePath);}//釋放內存if (sampleBufferRef) {CFRelease(sampleBufferRef);}if (imageRef) {CGImageRelease(imageRef);}});//根據需要休眠一段時間;比如上層播放視頻時每幀之間是有間隔的,這里設置0.035,本來應該根據視頻的minFrameDuration來設置,但是坑爹的又是安卓那邊,這里參數信息有問題,倒是每一幀展示的速度異常,所有已只好手動設置。(網上看到的資料有的設置0.001)//[NSThread sleepForTimeInterval:CMTimeGetSeconds(videoTrack.minFrameDuration)];[NSThread sleepForTimeInterval:0.035];}//結束閱讀器[assetReader cancelReading];捕捉視頻幀,轉換成CGImageRef,不用UIImage的原因是因為創建CGImageRef不會做圖片數據的內存拷貝,它只會當 Core Animation執行 Transaction::commit() 觸發layer -display時,才把圖片數據拷貝到 layer buffer里。簡單點的意思就是說不會消耗太多的內存!
+ (CGImageRef)imageFromSampleBuffer:(CMSampleBufferRef)sampleBuffer rotation:(UIImageOrientation)orientation {CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);// Lock the base address of the pixel bufferCVPixelBufferLockBaseAddress(imageBuffer, 0);// Get the number of bytes per row for the pixel buffersize_t bytesPerRow = CVPixelBufferGetBytesPerRow(imageBuffer);// Get the pixel buffer width and heightsize_t width = CVPixelBufferGetWidth(imageBuffer);size_t height = CVPixelBufferGetHeight(imageBuffer);//Generate image to editunsigned char *pixel = (unsigned char *)CVPixelBufferGetBaseAddress(imageBuffer);CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();CGContextRef context = CGBitmapContextCreate(pixel, width, height, 8, bytesPerRow, colorSpace, kCGBitmapByteOrder32Little|kCGImageAlphaPremultipliedFirst);CGImageRef image = CGBitmapContextCreateImage(context);CGContextRelease(context);CGColorSpaceRelease(colorSpace);CVPixelBufferUnlockBaseAddress(imageBuffer, 0);UIGraphicsEndImageContext();return image; }以上是一個視頻從載入到播放的步驟,通過開辟線程NSBlockOperation來處理。
下面是視頻播放管理工具,控制這所有的視頻NSBlockOperation線程操作。具體的就看代碼,注釋很清晰,先看.h文件:
再看.m
#import "ABListVideoPlayer.h"@implementation ABListVideoPlayerstatic ABListVideoPlayer *_instance = nil;+ (instancetype)sharedPlayer {static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{_instance = [[self alloc] init];//初始化一個視頻操作緩存字典_instance.videoOperationDict = [NSMutableDictionary dictionary];//初始化一個視頻播放操作隊列,并設置最大并發數(隨意)_instance.videoOperationQueue = [[NSOperationQueue alloc] init];_instance.videoOperationQueue.maxConcurrentOperationCount = 10;});return _instance; }- (void)startPlayVideo:(NSString *)filePath withVideoDecode:(VideoDecode)videoDecode {[self checkVideoPath:filePath withBlock:videoDecode]; }- (ABListVideoOperation *)checkVideoPath:(NSString *)filePath withBlock:(VideoDecode)videoBlock {//視頻播放操作Operation隊列,就初始化隊列,if (!self.videoOperationQueue) {self.videoOperationQueue = [[NSOperationQueue alloc] init];self.videoOperationQueue.maxConcurrentOperationCount = 1000;}//視頻播放操作Operation存放字典,初始化視頻操作緩存字典if (!self.videoOperationDict) {self.videoOperationDict = [NSMutableDictionary dictionary];}//初始化了一個自定義的NSBlockOperation對象,它是用一個Block來封裝需要執行的操作ABListVideoOperation *videoOperation;//如果這個視頻已經在播放,就先取消它,再次進行播放[self cancelVideo:filePath];videoOperation = [[ABListVideoOperation alloc] init];__weak ABListVideoOperation *weakVideoOperation = videoOperation;videoOperation.videoDecodeBlock = videoBlock;//并發執行一個視頻操作任務[videoOperation addExecutionBlock:^{[weakVideoOperation videoPlayTask:filePath];}];//執行完畢后停止操作[videoOperation setCompletionBlock:^{//從視頻操作字典里面異常這個Operation[self.videoOperationDict removeObjectForKey:filePath];//屬性停止回調if (weakVideoOperation.videoStopBlock) {weakVideoOperation.videoStopBlock(filePath);}}];//將這個Operation操作加入到視頻操作字典內[self.videoOperationDict setObject:videoOperation forKey:filePath];//add之后就執行操作[self.videoOperationQueue addOperation:videoOperation];return videoOperation; }- (void)reloadVideoPlay:(VideoStop)videoStop withFilePath:(NSString *)filePath {ABListVideoOperation *videoOperation;if (self.videoOperationDict[filePath]) {videoOperation = self.videoOperationDict[filePath];videoOperation.videoStopBlock = videoStop;} }-(void)cancelVideo:(NSString *)filePath {ABListVideoOperation *videoOperation;//如果所有視頻操作字典內存在這個視頻操作,取出這個操作if (self.videoOperationDict[filePath]) {videoOperation = self.videoOperationDict[filePath];//如果這個操作已經是取消狀態,就返回。if (videoOperation.isCancelled) {return;}//操作完不做任何事[videoOperation setCompletionBlock:nil];videoOperation.videoStopBlock = nil;videoOperation.videoDecodeBlock = nil;//取消這個操作[videoOperation cancel];if (videoOperation.isCancelled) {//從視頻操作字典里面異常這個Operation[self.videoOperationDict removeObjectForKey:filePath];}} }-(void)cancelAllVideo {if (self.videoOperationQueue) {//根據視頻地址這個key來取消所有OperationNSMutableDictionary *tempDict = [NSMutableDictionary dictionaryWithDictionary:self.videoOperationDict];for (NSString *key in tempDict) {[self cancelVideo:key];}[self.videoOperationDict removeAllObjects];[self.videoOperationQueue cancelAllOperations];} }實際項目中運用看如下代碼:
#import "ABVideoCell.h" #import "ABVideoModel.h" #import "ABListVideoPlayer.h"@interface ABVideoCell () @property (weak, nonatomic) IBOutlet UILabel *nameLabel; @property (weak, nonatomic) IBOutlet UIImageView *videoView; @end@implementation ABVideoCell+ (instancetype)cellWithTableView:(UITableView *)tableView {static NSString *ID = @"ABVideoCell";ABVideoCell *cell = [tableView dequeueReusableCellWithIdentifier:ID];if (!cell) {cell = [[[NSBundle mainBundle] loadNibNamed:ID owner:self options:nil] objectAtIndex:0];cell.accessoryType = UITableViewCellAccessoryNone;}return cell; }- (void)setModel:(ABVideoModel *)model {_model = model;self.nameLabel.text = model.videoFilePath.lastPathComponent;[self playVideo:model.videoFilePath]; }- (void)playVideo:(NSString *)theVideoFilePath {__weak typeof(self) weakSelf = self;[[ABListVideoPlayer sharedPlayer] startPlayVideo:theVideoFilePath withVideoDecode:^(CGImageRef videoImageRef, NSString *videoFilePath) {weakSelf.videoView.layer.contents = (__bridge id _Nullable)(videoImageRef);}];[[ABListVideoPlayer sharedPlayer] reloadVideoPlay:^(NSString *videoFilePath) {[weakSelf playVideo:theVideoFilePath];} withFilePath:theVideoFilePath]; }@end有個細節,最好在UITableView的-(void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath代理方法里面這么處理下
-(void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {ABVideoModel *model = self.videos[indexPath.row];[[ABListVideoPlayer sharedPlayer] cancelVideo:model.videoFilePath]; }到現在才總結這玩意,主要還是懶,以后要多總結了!
總結
以上是生活随笔為你收集整理的iOS微信聊天界面朋友圈多个小视频同时播放不卡顿的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 资产负债表java_2.资产负债表的基本
- 下一篇: 多时态的被动语态