您的当前位置:首页正文

仿微信朋友圈视频剪切功能

来源:图艺博知识网

写在前面

公司项目最近有个小视频功能,上传的视频最长只有15秒,所以需要实现一个视频剪辑的功能。发现微信有这个功能,便准备仿微信的交互写一个,结果遇到不少坑,分享给大家让大家少走弯路。撸起袖子说干就干。

分析需求

我们先看一看微信的界面

微信效果图

1.页面下部拖动左边和右边的白色竖条控制剪切视频的开始和结束时间,预览界面跟随拖动位置跳到视频相应帧画面,控制视频长度最长15秒,最短5秒

2.拖动下部图片预览条,视频预览画面跳转到左边白条停留处的帧画面

3.下部操作区域拖动操作时,视频暂停,松手后视频播放,播放内容为两个白条之间的内容,可以循环播放

4.界面的“取消”返回,“确定”后裁剪视频输出

先上一个我做完的效果截图:

仿写效果图

我自己设计的控制条跟微信略有不同,微信是最长时间时候左右两个白色竖条离边框都还有一点距离,我这里设计的是两边白条都贴边框,返回按钮和确定裁剪按钮也不同。其实也没差,要说微信那样设计有特殊考虑的话,我只能说我不是交互和视觉设计师😳

实现

1.我这里完整的拖动选择视图是封装的一个view,上面放一个scrollView来展示小的预览图片,再上面放两个image来做视频截取范围的开始和结束指示器。首先需要实现下面缩略图排列以及它的左右滑动,首先需要找到方法获取视频的帧图片。找了一下资料,很多,基本都是同一个方法,所以暂时选取了这个方法。为何说暂时,后面会解释。

#pragma 获取想要时间的帧视频图片

+(UIImage *)getCoverImage:(NSURL *)outMovieURL atTime:(CGFloat)time isKeyImage:(BOOL)isKeyImage{

AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:outMovieURL options:nil];

NSParameterAssert(asset);

AVAssetImageGenerator *assetImageGenerator = [[AVAssetImageGenerator alloc] initWithAsset:asset];

assetImageGenerator.appliesPreferredTrackTransform = YES;

assetImageGenerator.apertureMode = AVAssetImageGeneratorApertureModeEncodedPixels;

__block CGImageRef thumbnailImageRef = NULL;

NSError *thumbnailImageGenerationError = nil;

//tips:下面代码控制时间点的取图是否为关键帧图片,系统为了性能是默认取关键帧图片

CMTime myTime = CMTimeMake(time, 1);

if (!isKeyImage) {

assetImageGenerator.requestedTimeToleranceAfter = kCMTimeZero;

assetImageGenerator.requestedTimeToleranceBefore = kCMTimeZero;

CMTime duration = asset.duration;

myTime = CMTimeMake(time*30,30);

}

thumbnailImageRef = [assetImageGenerator copyCGImageAtTime:myTime actualTime:NULL error:nil];

if (!thumbnailImageRef){

NSLog(@"thumbnailImageGenerationError %@", thumbnailImageGenerationError);

}

UIImage *thumbnailImage = thumbnailImageRef ? [[UIImage alloc]

initWithCGImage:thumbnailImageRef] : nil;

CGImageRelease(thumbnailImageRef);

return thumbnailImage;

}

通常开发者认为时间的呈现格式应该是浮点数据,我们一般使用NSTimeInterval,实际上它是简单的双精度double类型,只是typedef了一下,但是由于浮点型数据计算很容易导致精度的丢失,在一些要求高精度的应用场景显然不适合,于是苹果在Core Media框架中定义了CMTime数据类型作为时间的格式

  typedef struct{

CMTimeValue    value;

CMTimeScale    timescale;

CMTimeFlags    flags;

CMTimeEpoch    epoch;

} CMTime;

//  显然,CMTime定义是一个C语言的结构体,CMTime是以分数的形式表示时间,value表示分子,timescale表示分母,flags是位掩码,表示时间的指定状态。CMTimeMake(3, 1)结果为3。

我是默认一个完整屏幕宽度为15秒的截取长度,在视频的每秒取一张帧图片作为底部预览小图,起初我是用循环视频时长秒数,每次用上面方法取一张图片,再用UIImageView放置这张图片,最后再计算imageView的位置添加到scrollView上。结果这是一个坑,视频只有二三十秒还好,如果比较长则会创建很多个imageView,内存暴涨,导致卡顿或者直接crash。后来想到了绘图,这样就不会请求内存多次分配空间,从而解决内存暴涨问题。

@interface WZScrollView : UIScrollView

@property (nonatomic, strong) UIImage *image;

@property (nonatomic, assign) CGRect *rect;

-(void)drawImage:(UIImage *)image inRect:(CGRect)rect;

@end

@implementation WZScrollView

-(void)drawRect:(CGRect)rect{

[super drawRect:rect];

[_image drawInRect:rect];

}

-(void)drawImage:(UIImage *)image inRect:(CGRect)rect{

_image = image;

_rect = &rect

[self setNeedsDisplayInRect:rect];

}

结果发现直接画图到scrollView在你拖动scrollView的时候它始终会只显示前面15张图片的效果,o(╯□╰)o!!!测试了一下,滚动是有效果的,但是体验不好啊。后来把上面的继承类从UIScrollView改成了UIView,把图片绘制到view上再加到scrollView上,设置好contentSize,问题解决。

@interface WZScrollView : UIView

接下来就是左右开始和结束的指示图片了,由于图片太小会有可能接收不到点击事件,所以我这里的切图在开始处指示图片的右边和结束指示图片的左边多裁一部分透明范围,这样指示器的面积就比你看到的大了,方便操作。接下来就是它们的拖动操作,最开始我使用的是view的touchesMoved:withEvent:来让图片改变x值从而跟随手指移动。结果发现,手速稍快或者触点稍微偏移就会导致图片位置改变停止,体验和性能都不行。后来改用拖动手势UIPanGestureRecognizer就完美解决了此问题,这里代码多是逻辑处理问题,包括拖动范围何时会让相应图片进行位置改变的响应,上下的白色线条位置和长度改变等等。但这里需要注意三个问题:a.拖动手势的回调方法里面的改变距离和原视图位置的x值会指数相加,每次回调都应该将视图的translation置0。b.需要每次回调都计算开始和结束位置的时间点,让其有实时性。c.拖动结束时需要让播放器循环播放两个时间点间的视频内容。

-(void)panAction:(UIPanGestureRecognizer *)panGR{

//伪代码:根据需求改变开始和结束指示图片的位置

if(panGR.state == UIGestureRecognizerStateChanged){

[panGR setTranslation:CGPointZero inView:self.superview];

}

[self calculateForTimeNodes];//实时计算裁剪时间

if (panGR.state == UIGestureRecognizerStateEnded) {

//伪代码:指示播放器播放相应视频片段代码

}

//计算开始结束时间点

-(void)calculateForTimeNodes{

CGPoint offset = _scrollView.contentOffset;

_startTime =(offset.x+self.startView.frame.origin.x)*15*1.0f/self.bounds.size.width;

_endTime = (offset.x + self.endView.frame.origin.x + KendTimeButtonWidth) * 15 * 1.0f/self.bounds.size.width;

CGFloat imageTime = _startTime;//预览时间点

if (_chooseType == imageTypeEnd) {

imageTime = _endTime;

}

if (self.getTimeRange) {

self.getTimeRange(_startTime,_endTime,imageTime);//控制预览播放界面的当前画面(这里是一个播放页传过来的block的调用)

}

2.拖动scrollView时,默认是展示开始时间点的视频帧画面,在scrollViewDidScroll:方法中调用calculateForTimeNodes方法即可实时更新开始、结束和预览3个时间点参数,这一步的很多逻辑都封装到第一步的一些方法中了,所以这一步比较简单。

视频拖动时:

[_player pause];

[_player seekToTime:CMTimeMake(time*30, 30) toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero completionHandler:^(BOOL finished) {

}];

拖动停止时:

[_player seekToTime:CMTimeMake(_startTime*30, 30) toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero completionHandler:^(BOOL finished) {

[_player play];

}];

4.最后就是对视频进行裁剪了,这里的这个方法不是我写的,是网上找的别人的代码,但是原代码有个小问题,就是输出的视频文件方向改变了。在这里我用了下面3行代码来保证输出视频的方向跟原视频保持一致

AVURLAsset *asset = [AVURLAsset assetWithURL:videoUrl];

AVAssetTrack *assetVideoTrack = [[asset tracksWithMediaType:AVMediaTypeVideo]firstObject];

[compositionVideoTrack setPreferredTransform:assetVideoTrack.preferredTransform];

我这里视频裁剪后的输出视频路径是固定的,所以我封装的方法里面的回调是没有参数的,码友如果需要可以自行改装:

+ (void)addBackgroundMiusicWithVideoUrlStr:(NSURL *)videoUrl audioUrl:(NSURL *)audioUrl andCaptureVideoWithRange:(TimeRange)videoRange completion:(void(^)(void))completionHandle;

好久没动swift了,本来想写一个swift版练一练,后面再说吧哈哈。。。

Top