iOS開(kāi)發(fā)-相冊(cè)視頻編輯裁剪

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

UIVideoEditorController打開(kāi)效果.jpeg

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

UIImagePickerController開(kāi)的視頻效果.jpeg

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

微信朋友圈視頻編輯.jpeg

實(shí)現(xiàn)邏輯主要分5步:

  • 1.AVPlayer循環(huán)播放視頻
    如果是整段視頻循環(huán)播放,有兩種實(shí)現(xiàn)方式,一種是KVO監(jiān)聽(tīng)AVPlayertimeControlStatus屬性,
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)截圖

官方文檔截圖.png

文檔解釋:每一步的大小取決于接收機(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)):

展示效果.gif

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)步!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容