之前有个需求是实现如微信朋友圈动态列表小视频播放的效果,最近有空整理下给同样有需要的同学。

我们都知道微信朋友圈列表允许多个小视频同时无声播放,并且不会有丝毫卡顿问题,点击了才放大有声播放。

照着视频播放相关技术,我们可以实现通过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];

获取视频图像方向

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/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 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 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];
}

到现在才总结这玩意,主要还是懒,以后要多总结了!