寫在前面
公司項目最近有個小視頻功能丰涉,上傳的視頻最長只有15秒企垦,所以需要實現(xiàn)一個視頻剪輯的功能。發(fā)現(xiàn)微信有這個功能涯保,便準(zhǔn)備仿微信的交互寫一個诉濒,結(jié)果遇到不少坑,分享給大家讓大家少走彎路夕春。擼起袖子說干就干未荒。
分析需求
我們先看一看微信的界面
1.頁面下部拖動左邊和右邊的白色豎條控制剪切視頻的開始和結(jié)束時間,預(yù)覽界面跟隨拖動位置跳到視頻相應(yīng)幀畫面,控制視頻長度最長15秒撇他,最短5秒
2.拖動下部圖片預(yù)覽條茄猫,視頻預(yù)覽畫面跳轉(zhuǎn)到左邊白條停留處的幀畫面
3.下部操作區(qū)域拖動操作時,視頻暫停困肩,松手后視頻播放划纽,播放內(nèi)容為兩個白條之間的內(nèi)容,可以循環(huán)播放
4.界面的“取消”返回锌畸,“確定”后裁剪視頻輸出
先上一個我做完的效果截圖:
我自己設(shè)計的控制條跟微信略有不同勇劣,微信是最長時間時候左右兩個白色豎條離邊框都還有一點距離,我這里設(shè)計的是兩邊白條都貼邊框,返回按鈕和確定裁剪按鈕也不同比默。其實也沒差幻捏,要說微信那樣設(shè)計有特殊考慮的話,我只能說我不是交互和視覺設(shè)計師??
實現(xiàn)
1.我這里完整的拖動選擇視圖是封裝的一個view命咐,上面放一個scrollView來展示小的預(yù)覽圖片篡九,再上面放兩個image來做視頻截取范圍的開始和結(jié)束指示器。首先需要實現(xiàn)下面縮略圖排列以及它的左右滑動醋奠,首先需要找到方法獲取視頻的幀圖片榛臼。找了一下資料,很多窜司,基本都是同一個方法沛善,所以暫時選取了這個方法。為何說暫時塞祈,后面會解釋金刁。
#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:下面代碼控制時間點的取圖是否為關(guān)鍵幀圖片,系統(tǒng)為了性能是默認取關(guān)鍵幀圖片
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;
}
通常開發(fā)者認為時間的呈現(xiàn)格式應(yīng)該是浮點數(shù)據(jù)议薪,我們一般使用NSTimeInterval尤蛮,實際上它是簡單的雙精度double類型,只是typedef了一下笙蒙,但是由于浮點型數(shù)據(jù)計算很容易導(dǎo)致精度的丟失抵屿,在一些要求高精度的應(yīng)用場景顯然不適合,于是蘋果在Core Media框架中定義了CMTime數(shù)據(jù)類型作為時間的格式
? typedef struct{
CMTimeValue? ? value;
CMTimeScale? ? timescale;
CMTimeFlags? ? flags;
CMTimeEpoch? ? epoch;
} CMTime;
//? 顯然捅位,CMTime定義是一個C語言的結(jié)構(gòu)體轧葛,CMTime是以分數(shù)的形式表示時間,value表示分子艇搀,timescale表示分母尿扯,flags是位掩碼,表示時間的指定狀態(tài)焰雕。CMTimeMake(3, 1)結(jié)果為3衷笋。
我是默認一個完整屏幕寬度為15秒的截取長度,在視頻的每秒取一張幀圖片作為底部預(yù)覽小圖矩屁,起初我是用循環(huán)視頻時長秒數(shù)辟宗,每次用上面方法取一張圖片,再用UIImageView放置這張圖片吝秕,最后再計算imageView的位置添加到scrollView上泊脐。結(jié)果這是一個坑,視頻只有二三十秒還好烁峭,如果比較長則會創(chuàng)建很多個imageView容客,內(nèi)存暴漲秕铛,導(dǎo)致卡頓或者直接crash。后來想到了繪圖缩挑,這樣就不會請求內(nèi)存多次分配空間但两,從而解決內(nèi)存暴漲問題。
@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];
}
結(jié)果發(fā)現(xiàn)直接畫圖到scrollView在你拖動scrollView的時候它始終會只顯示前面15張圖片的效果供置,o(╯□╰)o=飨妗!芥丧!測試了一下悲关,滾動是有效果的,但是體驗不好啊娄柳。后來把上面的繼承類從UIScrollView改成了UIView,把圖片繪制到view上再加到scrollView上艘绍,設(shè)置好contentSize赤拒,問題解決。
@interface WZScrollView : UIView
接下來就是左右開始和結(jié)束的指示圖片了诱鞠,由于圖片太小會有可能接收不到點擊事件挎挖,所以我這里的切圖在開始處指示圖片的右邊和結(jié)束指示圖片的左邊多裁一部分透明范圍,這樣指示器的面積就比你看到的大了航夺,方便操作蕉朵。接下來就是它們的拖動操作,最開始我使用的是view的touchesMoved:withEvent:來讓圖片改變x值從而跟隨手指移動阳掐。結(jié)果發(fā)現(xiàn)始衅,手速稍快或者觸點稍微偏移就會導(dǎo)致圖片位置改變停止,體驗和性能都不行缭保。后來改用拖動手勢UIPanGestureRecognizer就完美解決了此問題汛闸,這里代碼多是邏輯處理問題,包括拖動范圍何時會讓相應(yīng)圖片進行位置改變的響應(yīng)艺骂,上下的白色線條位置和長度改變等等诸老。但這里需要注意三個問題:a.拖動手勢的回調(diào)方法里面的改變距離和原視圖位置的x值會指數(shù)相加,每次回調(diào)都應(yīng)該將視圖的translation置0钳恕。b.需要每次回調(diào)都計算開始和結(jié)束位置的時間點别伏,讓其有實時性。c.拖動結(jié)束時需要讓播放器循環(huán)播放兩個時間點間的視頻內(nèi)容忧额。
-(void)panAction:(UIPanGestureRecognizer *)panGR{
//偽代碼:根據(jù)需求改變開始和結(jié)束指示圖片的位置
if(panGR.state == UIGestureRecognizerStateChanged){
[panGR setTranslation:CGPointZero inView:self.superview];
}
[self calculateForTimeNodes];//實時計算裁剪時間
if (panGR.state == UIGestureRecognizerStateEnded) {
//偽代碼:指示播放器播放相應(yīng)視頻片段代碼
}
//計算開始結(jié)束時間點
-(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;//預(yù)覽時間點
if (_chooseType == imageTypeEnd) {
imageTime = _endTime;
}
if (self.getTimeRange) {
self.getTimeRange(_startTime,_endTime,imageTime);//控制預(yù)覽播放界面的當(dāng)前畫面(這里是一個播放頁傳過來的block的調(diào)用)
}
2.拖動scrollView時厘肮,默認是展示開始時間點的視頻幀畫面,在scrollViewDidScroll:方法中調(diào)用calculateForTimeNodes方法即可實時更新開始宙址、結(jié)束和預(yù)覽3個時間點參數(shù)轴脐,這一步的很多邏輯都封裝到第一步的一些方法中了,所以這一步比較簡單。
3.拖動時暫停播放大咱,松手后播放相應(yīng)時間范圍視頻內(nèi)容恬涧,可以循環(huán)播放。關(guān)于開始和結(jié)束指示圖片的拖動狀態(tài)可以用上面提到的panGR.state == UIGestureRecognizerStateEnded來判斷碴巾,進入判斷說明松手了溯捆,沒有則還在拖動。而scrollView的拖動和停止直接調(diào)用它的代理就行了厦瓢,這里不贅述提揍,不明白可以在demo里面查看。這里遇到個坑是因為前面在視頻預(yù)覽頁面拖動的時候需要有當(dāng)前的視頻幀畫面用作預(yù)覽煮仇,而開始是getCoverImage: atTime: isKeyImage:這個方法來獲取幀圖片的劳跃,當(dāng)拖動時就顯示圖片圖層,停止拖動就隱藏圖片圖層浙垫,進而顯示下面的視頻圖層刨仑。結(jié)果這個方法比較消耗cpu,會導(dǎo)致卡頓情況夹姥,還會經(jīng)常因為cpu過高直接crash杉武。后來發(fā)現(xiàn)AVPlayer里面有一個seekToTime: toleranceBefore: toleranceAfter: completionHandler:方法,作用是讓視頻跳到某個時間點開始播放辙售。我去,這么簡單我卻饒了好大一個彎轻抱,所以大家一定要在使用類的時候要養(yǎng)成多看原類文件的好習(xí)慣,可以少跳抗旦部,囧F硭选!士八!其實AVPlayer還有一個seekToTime:方法,我不使用它的原因是它有一個自己的最小時間單位(貌似是關(guān)鍵幀)夭问,用它不會實時改變播放器畫面。
視頻拖動時:
[_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.最后就是對視頻進行裁剪了曹铃,這里的這個方法不是我寫的缰趋,是網(wǎng)上找的別人的代碼,但是原代碼有個小問題陕见,就是輸出的視頻文件方向改變了秘血。在這里我用了下面3行代碼來保證輸出視頻的方向跟原視頻保持一致
AVURLAsset *asset = [AVURLAsset assetWithURL:videoUrl];
AVAssetTrack *assetVideoTrack = [[asset tracksWithMediaType:AVMediaTypeVideo]firstObject];
[compositionVideoTrack setPreferredTransform:assetVideoTrack.preferredTransform];
我這里視頻裁剪后的輸出視頻路徑是固定的,所以我封裝的方法里面的回調(diào)是沒有參數(shù)的评甜,碼友如果需要可以自行改裝:
+ (void)addBackgroundMiusicWithVideoUrlStr:(NSURL *)videoUrl audioUrl:(NSURL *)audioUrl andCaptureVideoWithRange:(TimeRange)videoRange completion:(void(^)(void))completionHandle灰粮;
大家如果下demo來看的話會發(fā)現(xiàn)我在這個方法調(diào)用時在回調(diào)里面多加了一個保存視頻方法到里面,是由于我的項目需求忍坷。這個方法會新建一個以項目名稱命名的相冊粘舟,用來存放剪切后的視頻熔脂,回調(diào)會傳回一個PHAsset對象(項目需求),這個就是贈送節(jié)目了??柑肴。
這里是demo下載地址霞揉,如果對你有用的話別忘了star一個??。
好久沒動swift了晰骑,本來想寫一個swift版練一練适秩,后面再說吧哈哈。硕舆。秽荞。