iOS相冊(cè)視頻編輯有兩種方式,一種是使用系統(tǒng)自帶的控制器UIVideoEditorController但是該類只提供了基礎(chǔ)的視頻編輯功能,接口十分有限,界面樣式?jīng)]法修改,效果如下圖。

UIVideoEditorController和UIImagePickerController的視頻顯示界面十分相似,區(qū)別就是前者可以編輯,后者不能。

第二種方式就是利用強(qiáng)大的
AVFoudation框架自己動(dòng)手實(shí)現(xiàn)。

實(shí)現(xiàn)邏輯主要分5步:
- 1.AVPlayer循環(huán)播放視頻
如果是整段視頻循環(huán)播放,有兩種實(shí)現(xiàn)方式,一種是KVO監(jiān)聽(tīng)AVPlayer的timeControlStatus屬性,
typedef NS_ENUM(NSInteger, AVPlayerTimeControlStatus) {
AVPlayerTimeControlStatusPaused,
AVPlayerTimeControlStatusWaitingToPlayAtSpecifiedRate,
AVPlayerTimeControlStatusPlaying
} NS_ENUM_AVAILABLE(10_12, 10_0);
當(dāng)狀態(tài)為AVPlayerTimeControlStatusPaused的時(shí)候讓player回到起點(diǎn)并繼續(xù)播放。
[self.player seekToTime:CMTimeMake(0, 1)];
[self.player play];
第二種循環(huán)播放方式是,利用計(jì)時(shí)器設(shè)置要播放時(shí)長(zhǎng),并循環(huán)執(zhí)行計(jì)時(shí)器方法。
- (void)repeatPlay{
[self.player play];
CMTime start = CMTimeMakeWithSeconds(self.startTime, self.player.currentTime.timescale);
[self.player seekToTime:start toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero];
}
編輯視頻時(shí)的視頻段循環(huán)播放,顯然只能通過(guò)第二種方式實(shí)現(xiàn)。
- 2.以1秒為單位,獲取視頻幀圖像
編輯區(qū)域需要顯示視頻幀圖像,通過(guò)AVAssetImageGenerator這個(gè)類來(lái)獲取,在該類的獲取視頻幀圖像接口調(diào)用時(shí)需要傳入要獲取視頻幀圖像的時(shí)間節(jié)點(diǎn)。
- (void)generateCGImagesAsynchronouslyForTimes:(NSArray<NSValue *> *)requestedTimes completionHandler:(AVAssetImageGeneratorCompletionHandler)handler;
在視頻編輯功能中,一般的時(shí)間節(jié)點(diǎn)都是以1秒為單位獲取視頻幀圖像。在該接口回調(diào)中由于是異步執(zhí)行,所以需要在回調(diào)中直接顯示圖片,詳細(xì)代碼實(shí)現(xiàn)如下。
#pragma mark 讀取解析視頻幀
- (void)analysisVideoFrames{
//初始化asset對(duì)象
AVURLAsset *videoAsset = [[AVURLAsset alloc]initWithURL:self.videoUrl options:nil];
//獲取總視頻的長(zhǎng)度 = 總幀數(shù) / 每秒的幀數(shù)
long videoSumTime = videoAsset.duration.value / videoAsset.duration.timescale;
//創(chuàng)建AVAssetImageGenerator對(duì)象
AVAssetImageGenerator *generator = [[AVAssetImageGenerator alloc]initWithAsset:videoAsset];
generator.maximumSize = bottomView.frame.size;
generator.appliesPreferredTrackTransform = YES;
generator.requestedTimeToleranceBefore = kCMTimeZero;
generator.requestedTimeToleranceAfter = kCMTimeZero;
// 添加需要幀數(shù)的時(shí)間集合
self.framesArray = [NSMutableArray array];
for (int i = 0; i < videoSumTime; i++) {
CMTime time = CMTimeMake(i *videoAsset.duration.timescale , videoAsset.duration.timescale);
NSValue *value = [NSValue valueWithCMTime:time];
[self.framesArray addObject:value];
}
NSMutableArray *imgArray = [NSMutableArray array];
__block long count = 0;
[generator generateCGImagesAsynchronouslyForTimes:self.framesArray completionHandler:^(CMTime requestedTime, CGImageRef img, CMTime actualTime, AVAssetImageGeneratorResult result, NSError *error){
if (result == AVAssetImageGeneratorSucceeded) {
NSLog(@"%ld",count);
UIImageView *thumImgView = [[UIImageView alloc] initWithFrame:CGRectMake(50+count*self.IMG_Width, 0, self.IMG_Width, 70)];
thumImgView.image = [UIImage imageWithCGImage:img];
dispatch_async(dispatch_get_main_queue(), ^{
[editScrollView addSubview:thumImgView];
editScrollView.contentSize = CGSizeMake(100+count*self.IMG_Width, 0);
});
count++;
}
if (result == AVAssetImageGeneratorFailed) {
NSLog(@"Failed with error: %@", [error localizedDescription]);
}
if (result == AVAssetImageGeneratorCancelled) {
NSLog(@"AVAssetImageGeneratorCancelled");
}
}];
[editScrollView setContentOffset:CGPointMake(50, 0)];
}
3.添加編輯視圖,并控制AVPlayer循環(huán)播放該時(shí)間區(qū)域視頻段
視頻編輯框,我的思路是左右添加一個(gè)視圖,根據(jù)在其父視圖上的添加的拖拽手勢(shì),如果當(dāng)前觸點(diǎn)在編輯框視圖上,則根據(jù)其父視圖拖動(dòng)的距離調(diào)整編輯框位置,并調(diào)整播放視頻的起止時(shí)間。4.監(jiān)聽(tīng)編輯框和視頻幀滑動(dòng),并調(diào)整AVPlayer循環(huán)播放的視頻段
編輯框的位置移動(dòng)第3步已經(jīng)說(shuō)了,視頻幀圖像是放到UIScrollView上的,這里也可以用UICollectionView實(shí)現(xiàn),關(guān)于滑動(dòng)區(qū)域的位置監(jiān)聽(tīng)和編輯框移動(dòng)時(shí)視頻播放區(qū)間的調(diào)整的實(shí)現(xiàn)邏輯稍復(fù)雜些,稍后會(huì)附上Demo地址,大家可以詳細(xì)看代碼實(shí)現(xiàn),這里就貼手勢(shì)處理的部分代碼。
#pragma mark 編輯區(qū)域手勢(shì)拖動(dòng)
- (void)moveOverlayView:(UIPanGestureRecognizer *)gesture{
switch (gesture.state) {
case UIGestureRecognizerStateBegan:
{
[self stopTimer];
BOOL isRight = [rightDragView pointInsideImgView:[gesture locationInView:rightDragView]];
BOOL isLeft = [leftDragView pointInsideImgView:[gesture locationInView:leftDragView]];
_isDraggingRightOverlayView = NO;
_isDraggingLeftOverlayView = NO;
self.touchPointX = [gesture locationInView:bottomView].x;
if (isRight){
self.rightStartPoint = [gesture locationInView:bottomView];
_isDraggingRightOverlayView = YES;
_isDraggingLeftOverlayView = NO;
}
else if (isLeft){
self.leftStartPoint = [gesture locationInView:bottomView];
_isDraggingRightOverlayView = NO;
_isDraggingLeftOverlayView = YES;
}
}
break;
case UIGestureRecognizerStateChanged:
{
CGPoint point = [gesture locationInView:bottomView];
// Left
if (_isDraggingLeftOverlayView){
CGFloat deltaX = point.x - self.leftStartPoint.x;
CGPoint center = leftDragView.center;
center.x += deltaX;
CGFloat durationTime = (SCREEN_WIDTH-100)*2/10; // 最小范圍2秒
BOOL flag = (self.endPointX-point.x)>durationTime;
if (center.x >= (50-SCREEN_WIDTH/2) && flag) {
leftDragView.center = center;
self.leftStartPoint = point;
self.startTime = (point.x+editScrollView.contentOffset.x)/self.IMG_Width;
topBorder.frame = CGRectMake(self.boderX+=deltaX/2, 0, self.boderWidth-=deltaX/2, 2);
bottomBorder.frame = CGRectMake(self.boderX+=deltaX/2, 50-2, self.boderWidth-=deltaX/2, 2);
self.startPointX = point.x;
}
CMTime startTime = CMTimeMakeWithSeconds((point.x+editScrollView.contentOffset.x)/self.IMG_Width, self.player.currentTime.timescale);
// 只有視頻播放的時(shí)候才能夠快進(jìn)和快退1秒以內(nèi)
[self.player seekToTime:startTime toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero];
}
else if (_isDraggingRightOverlayView){ // Right
CGFloat deltaX = point.x - self.rightStartPoint.x;
CGPoint center = rightDragView.center;
center.x += deltaX;
CGFloat durationTime = (SCREEN_WIDTH-100)*2/10; // 最小范圍2秒
BOOL flag = (point.x-self.startPointX)>durationTime;
if (center.x <= (SCREEN_WIDTH-50+SCREEN_WIDTH/2) && flag) {
rightDragView.center = center;
self.rightStartPoint = point;
self.endTime = (point.x+editScrollView.contentOffset.x)/self.IMG_Width;
topBorder.frame = CGRectMake(self.boderX, 0, self.boderWidth+=deltaX/2, 2);
bottomBorder.frame = CGRectMake(self.boderX, 50-2, self.boderWidth+=deltaX/2, 2);
self.endPointX = point.x;
}
CMTime startTime = CMTimeMakeWithSeconds((point.x+editScrollView.contentOffset.x)/self.IMG_Width, self.player.currentTime.timescale);
// 只有視頻播放的時(shí)候才能夠快進(jìn)和快退1秒以內(nèi)
[self.player seekToTime:startTime toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero];
}
else { // 移動(dòng)scrollView
CGFloat deltaX = point.x - self.touchPointX;
CGFloat newOffset = editScrollView.contentOffset.x + deltaX;
CGPoint currentOffSet = CGPointMake(newOffset, 0);
if (currentOffSet.x >= 0 && currentOffSet.x <= (editScrollView.contentSize.width-SCREEN_WIDTH)) {
editScrollView.contentOffset = CGPointMake(newOffset, 0);
self.touchPointX = point.x;
}
}
}
break;
case UIGestureRecognizerStateEnded:
{
[self startTimer];
}
default:
break;
}
}
-
補(bǔ)充小細(xì)節(jié)
只有在視頻播放時(shí),調(diào)用AVPlayer的
- (void)seekToTime:(CMTime)time toleranceBefore:(CMTime)toleranceBefore toleranceAfter:(CMTime)toleranceAfter;
這個(gè)接口才能實(shí)現(xiàn)小于1秒以內(nèi)的快進(jìn)和快退,如果是暫停狀態(tài),不管如何傳時(shí)間值,都是以1秒為單位快進(jìn)和快退。在開(kāi)發(fā)過(guò)程中倒是發(fā)現(xiàn)了一個(gè)在暫停時(shí)可以以小于1秒的單位快進(jìn)和快退的接口,是AVPlayerItem這個(gè)類的這個(gè)接口,
- (void)stepByCount:(NSInteger)stepCount;
但是這個(gè)stepCount值官方文檔說(shuō)的很含糊,見(jiàn)截圖

文檔解釋:每一步的大小取決于接收機(jī)的功能avplayeritemtrack對(duì)象(參考[tracks
]),然而我打印了tracks,只有一個(gè)音頻軌跡一個(gè)視頻軌跡,實(shí)在沒(méi)找到有價(jià)值的東西,暫時(shí)放棄了這條路,好在視頻播放時(shí)時(shí)可以以小于1秒單位快進(jìn)和快退,微信朋友圈也是一直循環(huán)播放可能也有這個(gè)原因在里面。
- 5.完成時(shí),根據(jù)編輯區(qū)域截取視頻段存入相冊(cè)并獲取URL使用
最終完成視頻剪輯需要用到AVAssetExportSession這個(gè)類,通過(guò)起止時(shí)間和源文件,完成視頻的最終剪輯,代碼如下。
#pragma mark 視頻裁剪
- (void)notifyDelegateOfDidChange{
self.tempVideoPath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"tmpMov.mov"];
[self deleteTempFile];
AVAsset *asset = [AVAsset assetWithURL:self.videoUrl];
AVAssetExportSession *exportSession = [[AVAssetExportSession alloc]
initWithAsset:asset presetName:AVAssetExportPresetPassthrough];
NSURL *furl = [NSURL fileURLWithPath:self.tempVideoPath];
exportSession.outputURL = furl;
exportSession.outputFileType = AVFileTypeQuickTimeMovie;
CMTime start = CMTimeMakeWithSeconds(self.startTime, self.player.currentTime.timescale);
CMTime duration = CMTimeMakeWithSeconds(self.endTime - self.startTime, self.player.currentTime.timescale);;
CMTimeRange range = CMTimeRangeMake(start, duration);
exportSession.timeRange = range;
[exportSession exportAsynchronouslyWithCompletionHandler:^{
switch ([exportSession status]) {
case AVAssetExportSessionStatusFailed:
NSLog(@"Export failed: %@", [[exportSession error] localizedDescription]);
break;
case AVAssetExportSessionStatusCancelled:
NSLog(@"Export canceled");
break;
default:
NSLog(@"NONE");
NSURL *movieUrl = [NSURL fileURLWithPath:self.tempVideoPath];
dispatch_async(dispatch_get_main_queue(), ^{
UISaveVideoAtPathToSavedPhotosAlbum([movieUrl relativePath], self,@selector(video:didFinishSavingWithError:contextInfo:), nil);
NSLog(@"編輯后的視頻路徑: %@",self.tempVideoPath);
self.isEdited = YES;
[self invalidatePlayer];
[self initPlayerWithVideoUrl:movieUrl];
bottomView.hidden = YES;
});
break;
}
}];
}
- (void)video:(NSString*)videoPath didFinishSavingWithError:(NSError*)error contextInfo:(void*)contextInfo {
if (error) {
NSLog(@"保存到相冊(cè)失敗");
} else {
NSLog(@"保存到相冊(cè)成功");
}
}
- (void)deleteTempFile{
NSURL *url = [NSURL fileURLWithPath:self.tempVideoPath];
NSFileManager *fm = [NSFileManager defaultManager];
BOOL exist = [fm fileExistsAtPath:url.path];
NSError *err;
if (exist) {
[fm removeItemAtURL:url error:&err];
NSLog(@"file deleted");
if (err) {
NSLog(@"file remove error, %@", err.localizedDescription );
}
} else {
NSLog(@"no file by that name");
}
}
完成效果如下(手機(jī)屏幕投到電腦上的錄屏,所有手勢(shì)沒(méi)法顯示,大家可以下demo自己體驗(yàn)):

MARK:新寫了Swift版,過(guò)程中遇到一些問(wèn)題也一起分享下
- Swift對(duì)于數(shù)學(xué)運(yùn)算的個(gè)別數(shù)值要求比較嚴(yán),不同數(shù)位需要轉(zhuǎn)換,比如調(diào)用方法
public func CMTimeMakeWithSeconds(_ seconds: Float64, _ preferredTimescale: Int32) -> CMTime
就需要把傳入的非Float64參數(shù)強(qiáng)轉(zhuǎn)
let startTim = CMTimeMakeWithSeconds(Float64(second), player.currentTime().timescale)
- swift需要拋出異常的函數(shù)方法調(diào)用時(shí)的格式
無(wú)參數(shù)格式:
do{ try session.setActive(true) }
catch{}
有參數(shù)格式:
do{try filem.removeItem(at: url)}
catch let err as NSError {
error = err
}
反正不管遇到什么問(wèn)題,就是多看官方文檔的相關(guān)說(shuō)明基本都可以解決掉。吐槽下:swift的編譯和自動(dòng)提示確實(shí)很慢,據(jù)說(shuō)Xcode9會(huì)有所改觀。
OC-Demo地址
Swift-Demo地址 GitHub給個(gè)Star噢!
喜歡就點(diǎn)個(gè)贊唄!
歡迎大家提出更好的改進(jìn)意見(jiàn)和建議,一起進(jìn)步!