如何使用 AVFoundation 修剪视频

Posted

技术标签:

【中文标题】如何使用 AVFoundation 修剪视频【英文标题】:How to trim the video using AVFoundation 【发布时间】:2010-12-14 13:35:07 【问题描述】:

我可以使用 AVFoundation 或 UIImagePickerController 录制视频。但是我无法将视频从某一秒修剪到另一个特定的持续时间/时间。谁能帮帮我。

谢谢, 湿婆克里希纳。

【问题讨论】:

在寻找修剪现有视频的内容时发现了这个问题。修剪捕获的视频相对简单。但是让那个修剪窗口出现似乎让我望而却步。希望我的回答对您有所帮助。 【参考方案1】:

您可以让 UIImagePickerController 启用修剪

UIImagePickerController *videoRecorder = [[UIImagePickerController alloc]init];         
        NSArray *sourceTypes = [UIImagePickerController availableMediaTypesForSourceType:videoRecorder.sourceType];
        NSLog(@"Available types for source as camera = %@", sourceTypes);
        if (![sourceTypes containsObject:(NSString*)kUTTypeMovie] ) 
            UIAlertView *alert = [[UIAlertView alloc] initWithTitle:nil 
                                                            message:@"Device Not Supported for video Recording."                                                                       delegate:self 
                                                  cancelButtonTitle:@"Yes" 
                                                  otherButtonTitles:@"No",nil];
            [alert show];
            [alert release];
            return;
        
        videoRecorder.allowsEditing = YES;

不幸的是,当你从 imagePickerController 回来后,你不得不手动转换视频。

-(void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info 

    if ([self.popoverLibraryBrowser isPopoverVisible])
    
        [self.popoverLibraryBrowser dismissPopoverAnimated:YES];
    
    NSString *type = [info objectForKey:UIImagePickerControllerMediaType];
    if ([type isEqualToString:(NSString *)kUTTypeVideo] || 
        [type isEqualToString:(NSString *)kUTTypeMovie])  // movie != video
        NSURL *videoURL = [info objectForKey:UIImagePickerControllerMediaURL];


        NSNumber *start = [info objectForKey:@"_UIImagePickerControllerVideoEditingStart"];
        NSNumber *end = [info objectForKey:@"_UIImagePickerControllerVideoEditingEnd"];

        // if start and end are nil then clipping was not used.
        // You should use the entire video.


        int startMilliseconds = ([start doubleValue] * 1000);
        int endMilliseconds = ([end doubleValue] * 1000);

        NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
        NSString *documentsDirectory = [paths objectAtIndex:0];

        NSFileManager *manager = [NSFileManager defaultManager];

        NSString *outputURL = [documentsDirectory stringByAppendingPathComponent:@"output"] ;
        [manager createDirectoryAtPath:outputURL withIntermediateDirectories:YES attributes:nil error:nil];

        outputURL = [outputURL stringByAppendingPathComponent:@"output.mp4"];
        // Remove Existing File
        [manager removeItemAtPath:outputURL error:nil];


        //[self loadAssetFromFile:videoURL];

        [self.recorder dismissModalViewControllerAnimated:YES];

        AVURLAsset *videoAsset = [AVURLAsset URLAssetWithURL:videoURL options:nil]; 


        AVAssetExportSession *exportSession = [[AVAssetExportSession alloc] initWithAsset:videoAsset presetName:AVAssetExportPresetHighestQuality];
        exportSession.outputURL = [NSURL fileURLWithPath:outputURL];
        exportSession.outputFileType = AVFileTypeQuickTimeMovie;
        CMTimeRange timeRange = CMTimeRangeMake(CMTimeMake(startMilliseconds, 1000), CMTimeMake(endMilliseconds - startMilliseconds, 1000));
        exportSession.timeRange = timeRange;

        [exportSession exportAsynchronouslyWithCompletionHandler:^
            switch (exportSession.status) 
                case AVAssetExportSessionStatusCompleted:
                    // Custom method to import the Exported Video
                    [self loadAssetFromFile:exportSession.outputURL];
                    break;
                case AVAssetExportSessionStatusFailed:
                    //
                    NSLog(@"Failed:%@",exportSession.error);
                    break;
                case AVAssetExportSessionStatusCancelled:
                    //
                    NSLog(@"Canceled:%@",exportSession.error);
                    break;
                default:
                    break;
            
        ];



        //NSData *videoData = [NSData dataWithContentsOfURL:videoURL];
        //NSString *videoStoragePath;//Set your video storage path to this variable
        //[videoData writeToFile:videoStoragePath atomically:YES];
        //You can store the path of the saved video file in sqlite/coredata here.
    

【讨论】:

感谢来源。当我使用“*sourceTypes”作为“UIImagePickerControllerSourceTypeCamera”时,我得到“*开始和*结束时间”。但是当我使用“*sourceTypes”作为“UIImagePickerControllerSourceTypeSavedPhotosAlbum”时,我没有得到“开始和结束”时间。问题是什么?如何获得“开始和结束”时间? 您是否让他们能够“剪辑”视频?填充编辑开始和结束的部分是剪辑。如果您不剪辑视频,则这些值将保留为 nil,这反过来意味着应该跳过剪辑并使用整个视频。 请参考这里的日志pastebin.com/uMU1k61T。我允许用户从缩略图中选择帧。没问题,如果我编辑“相机录制的视频”并且当我尝试从相册加载的视频中选择帧时会出现问题。 @karthi 使用UIImagePickerControllerSourceTypeSavedPhotosAlbum 时,您从[info objectForKey:UIImagePickerControllerMediaURL]; 获得的url 引用了修剪后的视频文件,因此不需要开始和结束时间。我知道,因为我从[info objectForKey:UIImagePickerControllerMediaURL]; 获得了 url 的持续时间并将其与原始视频持续时间进行比较。 How to get the duration of video 您应该检查类型是否符合电影:UTTypeConformsTo((__bridge CFStringRef)mediaType, kUTTypeMovie) != 0,而不是检查电影视频。电影和视频也是如此。【参考方案2】:

上述的 Swift 版本

import UIKit
import AVFoundation
import MobileCoreServices

func pickVideo()
    if UIImagePickerController.isSourceTypeAvailable(.Camera) 
        let videoRecorder = UIImagePickerController()
        videoRecorder.sourceType = .Camera
        videoRecorder.mediaTypes = [kUTTypeMovie as String]
        videoRecorder.allowsEditing = true
        videoRecorder.delegate = self

        presentViewController(videoRecorder, animated: true, completion: nil)
    



func imagePickerController(picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : AnyObject]) 
    picker.dismissViewControllerAnimated(true, completion: nil)
    let manager = NSFileManager.defaultManager()

    guard let documentDirectory = try? manager.URLForDirectory(.DocumentDirectory, inDomain: .UserDomainMask, appropriateForURL: nil, create: true) else return
    guard let mediaType = info[UIImagePickerControllerMediaType] as? String else return
    guard let url = info[UIImagePickerControllerMediaURL] as? NSURL else return

    if mediaType == kUTTypeMovie as String || mediaType == kUTTypeVideo as String 
        let asset = AVAsset(URL: url)
        let length = Float(asset.duration.value) / Float(asset.duration.timescale)
        print("video length: \(length) seconds")

        let start = info["_UIImagePickerControllerVideoEditingStart"] as? Float
        let end = info["_UIImagePickerControllerVideoEditingEnd"] as? Float


        var outputURL = documentDirectory.URLByAppendingPathComponent("output")


        do 
            try manager.createDirectoryAtURL(outputURL, withIntermediateDirectories: true, attributes: nil)
            outputURL = outputURL.URLByAppendingPathComponent("output.mp4")
        catch let error 
            print(error)
        

        //Remove existing file
         _ = try? manager.removeItemAtURL(outputURL)


        guard let exportSession = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetHighestQuality) else return
        exportSession.outputURL = outputURL
        exportSession.outputFileType = AVFileTypeMPEG4

        let startTime = CMTime(seconds: Double(start ?? 0), preferredTimescale: 1000)
        let endTime = CMTime(seconds: Double(end ?? length), preferredTimescale: 1000)
        let timeRange = CMTimeRange(start: startTime, end: endTime)

        exportSession.timeRange = timeRange
        exportSession.exportAsynchronouslyWithCompletionHandler
            switch exportSession.status 
            case .Completed:
                print("exported at \(outputURL)")

            case .Failed:
                print("failed \(exportSession.error)")

            case .Cancelled:
                print("cancelled \(exportSession.error)")

            default: break
            
        
    

【讨论】:

此解决方案对某些视频失败。有什么建议吗? 作为程序员,您应该知道“失败”不是描述错误/问题的恰当方式 云视频失败了,我调试并弄清楚了。我说失败了,因为我无法得到一个场景,所以想问你,如果你遇到了这样的奇怪问题。无论如何,我已经解决了这个问题。下次请和任何开发者好好谈谈。【参考方案3】:

swift 4 的最佳解决方案,我找到了there。我确实根据自己的需要对其进行了修复,但它确实非常清晰和方便。

代码:

import AVFoundation
import Foundation

extension FileManager 
    func removeFileIfNecessary(at url: URL) throws 
        guard fileExists(atPath: url.path) else 
            return
        

        do 
            try removeItem(at: url)
        
        catch let error 
            throw TrimError("Couldn't remove existing destination file: \(error)")
        
    


struct TrimError: Error 
    let description: String
    let underlyingError: Error?

    init(_ description: String, underlyingError: Error? = nil) 
        self.description = "TrimVideo: " + description
        self.underlyingError = underlyingError
    


extension AVMutableComposition 
    convenience init(asset: AVAsset) 
        self.init()

        for track in asset.tracks 
            addMutableTrack(withMediaType: track.mediaType, preferredTrackID: track.trackID)
        
    

    func trim(timeOffStart: Double) 
        let duration = CMTime(seconds: timeOffStart, preferredTimescale: 1)
        let timeRange = CMTimeRange(start: kCMTimeZero, duration: duration)

        for track in tracks 
            track.removeTimeRange(timeRange)
        

        removeTimeRange(timeRange)
    


extension AVAsset 
    func assetByTrimming(timeOffStart: Double) throws -> AVAsset 
        let duration = CMTime(seconds: timeOffStart, preferredTimescale: 1)
        let timeRange = CMTimeRange(start: kCMTimeZero, duration: duration)

        let composition = AVMutableComposition()

        do 
            for track in tracks 
                let compositionTrack = composition.addMutableTrack(withMediaType: track.mediaType, preferredTrackID: track.trackID)
                try compositionTrack?.insertTimeRange(timeRange, of: track, at: kCMTimeZero)
            
         catch let error 
            throw TrimError("error during composition", underlyingError: error)
        

        return composition
    

    func export(to destination: URL) throws 
        guard let exportSession = AVAssetExportSession(asset: self, presetName: AVAssetExportPresetPassthrough) else 
            throw TrimError("Could not create an export session")
        

        exportSession.outputURL = destination
        exportSession.outputFileType = AVFileType.m4v
        exportSession.shouldOptimizeForNetworkUse = true

        let group = DispatchGroup()

        group.enter()

        try FileManager.default.removeFileIfNecessary(at: destination)

        exportSession.exportAsynchronously 
            group.leave()
        

        group.wait()

        if let error = exportSession.error 
            throw TrimError("error during export", underlyingError: error)
        
    


func time(_ operation: () throws -> ()) rethrows 
    let start = Date()

    try operation()

    let end = Date().timeIntervalSince(start)
    print(end)

let sourceURL =  URL(fileURLWithPath: CommandLine.arguments[1])
let destinationURL = URL(fileURLWithPath: CommandLine.arguments[2])

do 
    try time 
        let asset = AVURLAsset(url: sourceURL)
        let trimmedAsset = try asset.assetByTrimming(timeOffStart: 1.0)
        try trimmedAsset.export(to: destinationURL)
    
 catch let error 
    print("? \(error)")


【讨论】:

【参考方案4】:

您应该在 setMediaTypes 数组中添加 kUTTypeMovie,它会起作用。

【讨论】:

以上是关于如何使用 AVFoundation 修剪视频的主要内容,如果未能解决你的问题,请参考以下文章

如何在 CarPlay 上播放视频?

AVFoundation学习笔记:视频播放相关

如何使用 AVFoundation 为您的视频添加不同图像和不同 CMTimes 的水印

如何防止使用 AVFoundation 录制视频中断当前正在播放的任何全局音频(Swift)?

Swift 3:如何在使用 AVFoundation 录制视频期间将麦克风静音/取消静音

使用 AVFoundation / QTKit 一次录制多个视频