iOS微信聊天界面朋友圈多个小视频同时播放不卡顿AVAssetR
之前有个需求是实现如微信朋友圈动态列表小视频播放的效果,最近有空整理下给同样有需要的同学。
我们都知道微信朋友圈列表允许多个小视频同时无声播放,并且不会有丝毫卡顿问题,点击了才放大有声播放。
照着视频播放相关技术,我们可以实现通过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
(其他的子类实现也行)定义如下的方法:
/**
视频文件解析回调
@param videoImageRef 视频每帧截图的CGImageRef图像信息
@param videoFilePath 视频路径地址
*/
typedef void(^VideoDecode)(CGImageRef videoImageRef, NSString *videoFilePath);
/**
视频停止播放
@param videoFilePath 视频路径地址
*/
typedef void(^VideoStop)(NSString *videoFilePath);
@interface ABListVideoOperation : NSBlockOperation
@property (nonatomic, copy) VideoDecode videoDecodeBlock;
@property (nonatomic, copy) VideoStop videoStopBlock;
- (void)videoPlayTask:(NSString *)videoFilePath;
@end
.m里面实现- (void)videoPlayTask:(NSString *)videoFilePath;
方法.
初始化AVUrlAsset
获取对应视频的详细信息(AVAsset
具有多种有用的方法和属性,比如时长,创建日期和元数据等)
NSURL *url = [NSURL fileURLWithPath:videoFilePath];
AVURLAsset *asset = [AVURLAsset URLAssetWithURL:url options:nil];
创建一个读取媒体数据的阅读器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];
为阅读器AVAssetReader
进行配置,如配置读取的像素,视频压缩等等,得到我们的输出端口AVAssetReaderTrackOutput
轨迹,也就是我们的数据来源
/**
摘自http://www.jianshu.com/p/6f55681122e4
iOS系统定义了很多很多视频格式,让人眼花缭乱。不过一旦熟悉了它的命名规则,其实一眼就能看明白。
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];
//获取输出端口AVAssetReaderTrackOutput
AVAssetReaderTrackOutput *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每一帧转换成CGImageRef
CGImageRef imageRef = [ABListVideoOperation imageFromSampleBuffer:sampleBufferRef];
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
{
CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
// Lock the base address of the pixel buffer
CVPixelBufferLockBaseAddress(imageBuffer, 0);
// Get the number of bytes per row for the pixel buffer
size_t bytesPerRow = CVPixelBufferGetBytesPerRow(imageBuffer);
// Get the pixel buffer width and height
size_t width = CVPixelBufferGetWidth(imageBuffer);
size_t height = CVPixelBufferGetHeight(imageBuffer);
//Generate image to edit
unsigned 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文件:
#import <Foundation/Foundation.h>
#import "ABListVideoOperation.h"
@interface ABListVideoPlayer : NSObject
/** 视频播放操作Operation存放字典 */
@property (nonatomic, strong) NSMutableDictionary *videoOperationDict;
/** 视频播放操作Operation队列 */
@property (nonatomic, strong) NSOperationQueue *videoOperationQueue;
/**
播放工具单例
*/
+ (instancetype)sharedPlayer;
/**
播放一个本地视频
@param filePath 视频路径
@param videoDecode 视频每一帧的图像信息回调
*/
- (void)startPlayVideo:(NSString *)filePath withVideoDecode:(VideoDecode)videoDecode;
/**
循环播放视频
@param videoStop 停止回调
@param filePath 视频路径
*/
- (void)reloadVideoPlay:(VideoStop)videoStop withFilePath:(NSString *)filePath;
/**
取消视频播放同时从视频播放队列缓存移除
@param filePath 视频路径
*/
-(void)cancelVideo:(NSString *)filePath;
/**
取消所有当前播放的视频
*/
-(void)cancelAllVideo;
@end
再看.m
#import "ABListVideoPlayer.h"
@implementation ABListVideoPlayer
static 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来取消所有Operation
NSMutableDictionary *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];
}
到现在才总结这玩意,主要还是懒,以后要多总结了!
补充demo,由于确实有点懒,没怎么搞github,直接上百度网盘:
http://pan.baidu.com/s/1eS5xr02