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

写在前面

公司项目最近有个小视频功能,上传的视频最长只有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个时间点参数,这一步的很多逻辑都封装到第一步的一些方法中了,所以这一步比较简单。

3.拖动时暂停播放,松手后播放相应时间范围视频内容,可以循环播放。关于开始和结束指示图片的拖动状态可以用上面提到的panGR.state == UIGestureRecognizerStateEnded来判断,进入判断说明松手了,没有则还在拖动。而scrollView的拖动和停止直接调用它的代理就行了,这里不赘述,不明白可以在demo里面查看。这里遇到个坑是因为前面在视频预览页面拖动的时候需要有当前的视频帧画面用作预览,而开始是getCoverImage: atTime: isKeyImage:这个方法来获取帧图片的,当拖动时就显示图片图层,停止拖动就隐藏图片图层,进而显示下面的视频图层。结果这个方法比较消耗cpu,会导致卡顿情况,还会经常因为cpu过高直接crash。后来发现AVPlayer里面有一个seekToTime: toleranceBefore: toleranceAfter: completionHandler:方法,作用是让视频跳到某个时间点开始播放。我去,这么简单我却饶了好大一个弯,所以大家一定要在使用类的时候要养成多看原类文件的好习惯,可以少跳抗,囧!!!其实AVPlayer还有一个seekToTime:方法,我不使用它的原因是它有一个自己的最小时间单位(貌似是关键帧),用它不会实时改变播放器画面。

视频拖动时:

[_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;

大家如果下demo来看的话会发现我在这个方法调用时在回调里面多加了一个保存视频方法到里面,是由于我的项目需求。这个方法会新建一个以项目名称命名的相册,用来存放剪切后的视频,回调会传回一个PHAsset对象(项目需求),这个就是赠送节目了😁。

这里是demo下载地址,如果对你有用的话别忘了star一个😁。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容