Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Trim video using AVFoundation in Swift
//
// TrimVideo.swift
// VideoLab
//
// Created by Adam Jensen on 3/28/15.
// Updated for Swift 5 (tested with Xcode 10.3) on 7/30/19.
// MIT license
//
import AVFoundation
import Foundation
typealias TrimCompletion = (Error?) -> ()
typealias TrimPoints = [(CMTime, CMTime)]
func verifyPresetForAsset(preset: String, asset: AVAsset) -> Bool {
let compatiblePresets = AVAssetExportSession.exportPresets(compatibleWith: asset)
let filteredPresets = compatiblePresets.filter { $0 == preset }
return filteredPresets.count > 0 || preset == AVAssetExportPresetHighestQuality
}
func removeFileAtURLIfExists(url: NSURL) {
if let filePath = url.path {
let fileManager = FileManager.default
if fileManager.fileExists(atPath: filePath) {
do {
try fileManager.removeItem(atPath: filePath)
}
catch {
print("Couldn't remove existing destination file: \(error)")
}
}
}
}
func trimVideo(sourceURL: URL, destinationURL: URL, trimPoints: TrimPoints, completion: TrimCompletion?) {
assert(sourceURL.isFileURL)
assert(destinationURL.isFileURL)
let options = [ AVURLAssetPreferPreciseDurationAndTimingKey: true ]
let asset = AVURLAsset(url: sourceURL, options: options)
let preferredPreset = AVAssetExportPresetHighestQuality
if verifyPresetForAsset(preset: preferredPreset, asset: asset) {
let composition = AVMutableComposition()
guard
let videoCompTrack = composition.addMutableTrack(withMediaType: AVMediaType.video, preferredTrackID: CMPersistentTrackID()),
let audioCompTrack = composition.addMutableTrack(withMediaType: AVMediaType.audio, preferredTrackID: CMPersistentTrackID())
else {
let error = NSError(domain: "org.linuxguy.VideoLab", code: -1, userInfo: [NSLocalizedDescriptionKey: "Couldn't add mutable tracks"])
completion?(error)
return
}
guard
let assetVideoTrack = asset.tracks(withMediaType: AVMediaType.video).first,
let assetAudioTrack = asset.tracks(withMediaType: AVMediaType.audio).first
else {
let error = NSError(domain: "org.linuxguy.VideoLab", code: -1, userInfo: [NSLocalizedDescriptionKey: "Couldn't find video or audio track in source asset"])
completion?(error)
return
}
// Preserve the orientation of the source asset
videoCompTrack.preferredTransform = assetVideoTrack.preferredTransform
var accumulatedTime = CMTime.zero
for (startTimeForCurrentSlice, endTimeForCurrentSlice) in trimPoints {
let durationOfCurrentSlice = CMTimeSubtract(endTimeForCurrentSlice, startTimeForCurrentSlice)
let timeRangeForCurrentSlice = CMTimeRangeMake(start: startTimeForCurrentSlice, duration: durationOfCurrentSlice)
do {
try videoCompTrack.insertTimeRange(timeRangeForCurrentSlice, of: assetVideoTrack, at: accumulatedTime)
try audioCompTrack.insertTimeRange(timeRangeForCurrentSlice, of: assetAudioTrack, at: accumulatedTime)
}
catch {
let error = NSError(domain: "org.linuxguy.VideoLab", code: -1, userInfo: [NSLocalizedDescriptionKey: "Couldn't insert time ranges: \(error)"])
completion?(error)
return
}
accumulatedTime = CMTimeAdd(accumulatedTime, durationOfCurrentSlice)
}
guard let exportSession = AVAssetExportSession(asset: composition, presetName: preferredPreset) else {
let error = NSError(domain: "org.linuxguy.VideoLab", code: -1, userInfo: [NSLocalizedDescriptionKey: "Couldn't create export session"])
completion?(error)
return
}
exportSession.outputURL = destinationURL
exportSession.outputFileType = AVFileType.mp4
exportSession.shouldOptimizeForNetworkUse = true
removeFileAtURLIfExists(url: destinationURL as NSURL)
exportSession.exportAsynchronously(completionHandler: {
completion?(exportSession.error)
})
} else {
let error = NSError(domain: "org.linuxguy.VideoLab", code: -1, userInfo: [NSLocalizedDescriptionKey: "Could not find a suitable export preset for the input video"])
if let completion = completion {
completion(error)
return
}
}
}
//
// Example usage from a Swift playground
//
import PlaygroundSupport
let sourceURL = playgroundSharedDataDirectory.appendingPathComponent("TestVideo.mov")
let destinationURL = playgroundSharedDataDirectory.appendingPathComponent("TrimmedVideo.mp4")
let timeScale: Int32 = 1000
let trimPoints = [(CMTimeMake(value: 2000, timescale: timeScale), CMTimeMake(value: 5000, timescale: timeScale)),
(CMTimeMake(value: 20500, timescale: timeScale), CMTimeMake(value: 23000, timescale: timeScale)),
(CMTimeMake(value: 60000, timescale: timeScale), CMTimeMake(value: 65000, timescale: timeScale))]
trimVideo(sourceURL: sourceURL, destinationURL: destinationURL, trimPoints: trimPoints) { error in
if let error = error {
print("Failure: \(error)")
} else {
print("Success")
}
}
@felipericieri
Copy link

felipericieri commented Sep 1, 2017

Thanks for sharing this @acj! Here's a Swift 3.1 version of it:

import AVFoundation
import Foundation
import UIKit

typealias TrimCompletion = (Error?) -> ()
typealias TrimPoints = [(CMTime, CMTime)]

func verifyPresetForAsset(preset: String, asset: AVAsset) -> Bool {
    let compatiblePresets = AVAssetExportSession.exportPresets(compatibleWith: asset) 
    let filteredPresets = compatiblePresets.filter { $0 == preset }
    return filteredPresets.count > 0 || preset == AVAssetExportPresetPassthrough
}

func removeFileAtURLIfExists(url: URL) {
    
    let fileManager = FileManager.default
    
    guard fileManager.fileExists(atPath: url.path) else { return }
    
    do {
        try fileManager.removeItem(at: url)
    }
    catch let error {
        print("TrimVideo - Couldn't remove existing destination file: \(String(describing: error))")
    }
}

func trimVideo(sourceURL: NSURL, destinationURL: NSURL, trimPoints: TrimPoints, completion: TrimCompletion?) {
    
    guard sourceURL.isFileURL else { return }
    guard destinationURL.isFileURL else { return }
    
    let options = [
        AVURLAssetPreferPreciseDurationAndTimingKey: true
    ]
    
    let asset = AVURLAsset(url: sourceURL as URL, options: options)
    let preferredPreset = AVAssetExportPresetPassthrough
    
    if  verifyPresetForAsset(preset: preferredPreset, asset: asset) {
        
        let composition = AVMutableComposition()
        let videoCompTrack = composition.addMutableTrack(withMediaType: AVMediaTypeVideo, preferredTrackID: CMPersistentTrackID())
        let audioCompTrack = composition.addMutableTrack(withMediaType: AVMediaTypeAudio, preferredTrackID: CMPersistentTrackID())
        
        guard let assetVideoTrack: AVAssetTrack = asset.tracks(withMediaType: AVMediaTypeVideo).first else { return }
        guard let assetAudioTrack: AVAssetTrack = asset.tracks(withMediaType: AVMediaTypeAudio).first else { return }
        
        var accumulatedTime = kCMTimeZero
        for (startTimeForCurrentSlice, endTimeForCurrentSlice) in trimPoints {
            let durationOfCurrentSlice = CMTimeSubtract(endTimeForCurrentSlice, startTimeForCurrentSlice)
            let timeRangeForCurrentSlice = CMTimeRangeMake(startTimeForCurrentSlice, durationOfCurrentSlice)
            
            do {
                try videoCompTrack.insertTimeRange(timeRangeForCurrentSlice, of: assetVideoTrack, at: accumulatedTime)
                try audioCompTrack.insertTimeRange(timeRangeForCurrentSlice, of: assetAudioTrack, at: accumulatedTime)
                accumulatedTime = CMTimeAdd(accumulatedTime, durationOfCurrentSlice)
            }
            catch let compError {
                print("TrimVideo: error during composition: \(compError)")
                completion?(compError)
            }
        }
        
        guard let exportSession = AVAssetExportSession(asset: composition, presetName: preferredPreset) else { return }
        
        exportSession.outputURL = destinationURL as URL
        exportSession.outputFileType = AVFileTypeAppleM4V
        exportSession.shouldOptimizeForNetworkUse = true
        
        removeFileAtURLIfExists(url: destinationURL as URL)
        
        exportSession.exportAsynchronously {
            completion?(exportSession.error)
        }
    }
    else {
        print("TrimVideo - Could not find a suitable export preset for the input video")
        let error = NSError(domain: "com.bighug.ios", code: -1, userInfo: nil)
        completion?(error)
    }
}

@aligungor
Copy link

aligungor commented Jan 13, 2018

Swift 4 version:

import AVFoundation
import Foundation
import UIKit

class VideoTrimmer {
    typealias TrimCompletion = (Error?) -> ()
    typealias TrimPoints = [(CMTime, CMTime)]
    
    func verifyPresetForAsset(preset: String, asset: AVAsset) -> Bool {
        let compatiblePresets = AVAssetExportSession.exportPresets(compatibleWith: asset)
        let filteredPresets = compatiblePresets.filter { $0 == preset }
        return filteredPresets.count > 0 || preset == AVAssetExportPresetPassthrough
    }
    
    func removeFileAtURLIfExists(url: URL) {
        
        let fileManager = FileManager.default
        
        guard fileManager.fileExists(atPath: url.path) else { return }
        
        do {
            try fileManager.removeItem(at: url)
        }
        catch let error {
            print("TrimVideo - Couldn't remove existing destination file: \(String(describing: error))")
        }
    }
    
    func trimVideo(sourceURL: URL, destinationURL: URL, trimPoints: TrimPoints, completion: TrimCompletion?) {
        
        guard sourceURL.isFileURL else { return }
        guard destinationURL.isFileURL else { return }
        
        let options = [
            AVURLAssetPreferPreciseDurationAndTimingKey: true
        ]
        
        let asset = AVURLAsset(url: sourceURL, options: options)
        let preferredPreset = AVAssetExportPresetPassthrough
        
        if  verifyPresetForAsset(preset: preferredPreset, asset: asset) {
            
            let composition = AVMutableComposition()
            let videoCompTrack = composition.addMutableTrack(withMediaType: .video, preferredTrackID: CMPersistentTrackID())
            let audioCompTrack = composition.addMutableTrack(withMediaType: .audio, preferredTrackID: CMPersistentTrackID())
            
            guard let assetVideoTrack: AVAssetTrack = asset.tracks(withMediaType: .video).first else { return }
            guard let assetAudioTrack: AVAssetTrack = asset.tracks(withMediaType: .audio).first else { return }

            videoCompTrack!.preferredTransform = assetVideoTrack.preferredTransform
            
            var accumulatedTime = kCMTimeZero
            for (startTimeForCurrentSlice, endTimeForCurrentSlice) in trimPoints {
                let durationOfCurrentSlice = CMTimeSubtract(endTimeForCurrentSlice, startTimeForCurrentSlice)
                let timeRangeForCurrentSlice = CMTimeRangeMake(startTimeForCurrentSlice, durationOfCurrentSlice)
                
                do {
                    try videoCompTrack!.insertTimeRange(timeRangeForCurrentSlice, of: assetVideoTrack, at: accumulatedTime)
                    try audioCompTrack!.insertTimeRange(timeRangeForCurrentSlice, of: assetAudioTrack, at: accumulatedTime)
                    accumulatedTime = CMTimeAdd(accumulatedTime, durationOfCurrentSlice)
                }
                catch let compError {
                    print("TrimVideo: error during composition: \(compError)")
                    completion?(compError)
                }
            }
            
            guard let exportSession = AVAssetExportSession(asset: composition, presetName: preferredPreset) else { return }
            
            exportSession.outputURL = destinationURL as URL
            exportSession.outputFileType = AVFileType.m4v
            exportSession.shouldOptimizeForNetworkUse = true
            
            removeFileAtURLIfExists(url: destinationURL as URL)
            
            exportSession.exportAsynchronously {
                completion?(exportSession.error)
            }
        }
        else {
            print("TrimVideo - Could not find a suitable export preset for the input video")
            let error = NSError(domain: "com.bighug.ios", code: -1, userInfo: nil)
            completion?(error)
        }
    }
}

@lolthekiller
Copy link

lolthekiller commented Apr 4, 2018

Thanks for making and sharing this! How do you use this extension? (run it)

@omarojo
Copy link

omarojo commented Jun 23, 2018

I keep getting...

Error Domain=AVFoundationErrorDomain Code=-11800 "The operation could not be completed" UserInfo={NSLocalizedFailureReason=An unknown error occurred (-16976), NSLocalizedDescription=The operation could not be completed, NSUnderlyingError=0x1c405dd60 {Error Domain=NSOSStatusErrorDomain Code=-16976 "(null)"}}

in exportSession.exportAsynchronously

any ideas ?

@omarojo
Copy link

omarojo commented Jun 23, 2018

UPDATE:
I changed the presentName to HighestQuality... and it worked.
AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetHighestQuality)

@natanrolnik
Copy link

natanrolnik commented Jul 10, 2018

In case that might happen to others, I've had some orientation bugs.
It can be fixed adding the following line after creating videoCompTrack:

videoCompTrack!.preferredTransform = assetVideoTrack.preferredTransform

@aligungor would be good if you could add it, and also format your latest snippet. Thanks for sharing it!

@brend-creators
Copy link

brend-creators commented Jul 24, 2019

Thanks so much!

@AOFHBS
Copy link

AOFHBS commented Aug 6, 2019

Hi, anyone here is using ABVideoRangeSlider. The developer did not manage to code out part 2 which involve the video trimming part. I hope that someone may help out a bit more. I have managed to get the part 1 done and part 2 seem unsure in some area, thanks
https://medium.com/@AppsBoulevard/tutorial-how-to-trim-videos-in-ios-with-abvideorangeslider-part-1-of-2-fe51893270ff

@ahmedsafadii
Copy link

ahmedsafadii commented May 11, 2020

Swift 5 version + SwiftLint

import AVFoundation
import Foundation
import UIKit

class VideoTrimmer {
    typealias TrimCompletion = (Error?) -> Void
    typealias TrimPoints = [(CMTime, CMTime)]
    
    func verifyPresetForAsset(preset: String, asset: AVAsset) -> Bool {
        let compatiblePresets = AVAssetExportSession.exportPresets(compatibleWith: asset)
        let filteredPresets = compatiblePresets.filter { $0 == preset }
        return filteredPresets.count > 0 || preset == AVAssetExportPresetPassthrough
    }
    
    func removeFileAtURLIfExists(url: URL) {
        
        let fileManager = FileManager.default
        
        guard fileManager.fileExists(atPath: url.path) else { return }
        
        do {
            try fileManager.removeItem(at: url)
        } catch let error {
            print("TrimVideo - Couldn't remove existing destination file: \(String(describing: error))")
        }
    }
    
    func trimVideo(sourceURL: URL, destinationURL: URL, trimPoints: TrimPoints, completion: TrimCompletion?) {
        
        guard sourceURL.isFileURL else { return }
        guard destinationURL.isFileURL else { return }
        
        let options = [
            AVURLAssetPreferPreciseDurationAndTimingKey: true
        ]
        
        let asset = AVURLAsset(url: sourceURL, options: options)
        let preferredPreset = AVAssetExportPresetPassthrough
        
        if  verifyPresetForAsset(preset: preferredPreset, asset: asset) {
            
            let composition = AVMutableComposition()
            let videoCompTrack = composition.addMutableTrack(withMediaType: .video, preferredTrackID: CMPersistentTrackID())
            let audioCompTrack = composition.addMutableTrack(withMediaType: .audio, preferredTrackID: CMPersistentTrackID())
            
            guard let assetVideoTrack: AVAssetTrack = asset.tracks(withMediaType: .video).first else { return }
            guard let assetAudioTrack: AVAssetTrack = asset.tracks(withMediaType: .audio).first else { return }

            videoCompTrack!.preferredTransform = assetVideoTrack.preferredTransform
            
            var accumulatedTime = CMTime.zero
            for (startTimeForCurrentSlice, endTimeForCurrentSlice) in trimPoints {
                let durationOfCurrentSlice = CMTimeSubtract(endTimeForCurrentSlice, startTimeForCurrentSlice)
                let timeRangeForCurrentSlice = CMTimeRangeMake(start: startTimeForCurrentSlice, duration: durationOfCurrentSlice)
                
                do {
                    try videoCompTrack!.insertTimeRange(timeRangeForCurrentSlice, of: assetVideoTrack, at: accumulatedTime)
                    try audioCompTrack!.insertTimeRange(timeRangeForCurrentSlice, of: assetAudioTrack, at: accumulatedTime)
                    accumulatedTime = CMTimeAdd(accumulatedTime, durationOfCurrentSlice)
                } catch let compError {
                    print("TrimVideo: error during composition: \(compError)")
                    completion?(compError)
                }
            }
            
            guard let exportSession = AVAssetExportSession(asset: composition, presetName: preferredPreset) else { return }
            
            exportSession.outputURL = destinationURL as URL
            exportSession.outputFileType = AVFileType.m4v
            exportSession.shouldOptimizeForNetworkUse = true
            
            removeFileAtURLIfExists(url: destinationURL as URL)
            
            exportSession.exportAsynchronously {
                completion?(exportSession.error)
            }
        } else {
            print("TrimVideo - Could not find a suitable export preset for the input video")
            let error = NSError(domain: "com.bighug.ios", code: -1, userInfo: nil)
            completion?(error)
        }
    }
}

@Rashesh-Bosamiya
Copy link

Rashesh-Bosamiya commented Jun 22, 2020

Swift 5 + New Result type

import AVFoundation
import Foundation
import UIKit

class VideoTrimmer {
    
    typealias TrimPoints = [(CMTime, CMTime)]
    private static var trimError: Error {
        return NSError(domain: "com.bighug.ios", code: -1, userInfo: nil) as Error
    }
    
    func verifyPresetForAsset(preset: String, asset: AVAsset) -> Bool {
        let compatiblePresets = AVAssetExportSession.exportPresets(compatibleWith: asset)
        let filteredPresets = compatiblePresets.filter { $0 == preset }
        return filteredPresets.count > 0 || preset == AVAssetExportPresetPassthrough
    }
    
    func removeFileAtURLIfExists(url: URL) {
        
        let fileManager = FileManager.default
        
        guard fileManager.fileExists(atPath: url.path) else { return }
        
        do {
            try fileManager.removeItem(at: url)
        }
        catch let error {
            print("TrimVideo - Couldn't remove existing destination file: \(String(describing: error))")
        }
    }
    
    func trimVideo(sourceURL: URL, destinationURL: URL,
                   trimPoints: TrimPoints,
                   completion: @escaping (Result<URL, Error>) -> Void) {
        
        guard sourceURL.isFileURL, destinationURL.isFileURL else {
            completion(.failure(VideoTrimmer.trimError))
            return }
        
        let options = [
            AVURLAssetPreferPreciseDurationAndTimingKey: true
        ]
        let asset = AVURLAsset(url: sourceURL, options: options)
        let preferredPreset = AVAssetExportPresetPassthrough
        
        if  verifyPresetForAsset(preset: preferredPreset, asset: asset) {
            
            let composition = AVMutableComposition()
            guard let videoCompTrack = composition.addMutableTrack(withMediaType: .video,
                                                                   preferredTrackID: CMPersistentTrackID()),
                let audioCompTrack = composition.addMutableTrack(withMediaType: .audio,
                                                                 preferredTrackID: CMPersistentTrackID()),
                let assetVideoTrack: AVAssetTrack = asset.tracks(withMediaType: .video).first,
                let assetAudioTrack: AVAssetTrack = asset.tracks(withMediaType: .audio).first else {
                    completion(.failure(VideoTrimmer.trimError))
            return }

            videoCompTrack.preferredTransform = assetVideoTrack.preferredTransform
            
            var accumulatedTime = CMTime.zero
            for (startTimeForCurrentSlice, endTimeForCurrentSlice) in trimPoints {
                let durationOfCurrentSlice = CMTimeSubtract(endTimeForCurrentSlice, startTimeForCurrentSlice)
                let timeRangeForCurrentSlice = CMTimeRangeMake(start: startTimeForCurrentSlice,
                                                               duration: durationOfCurrentSlice)
                
                do {
                    try videoCompTrack.insertTimeRange(timeRangeForCurrentSlice,
                                                       of: assetVideoTrack,
                                                       at: accumulatedTime)
                    try audioCompTrack.insertTimeRange(timeRangeForCurrentSlice,
                                                       of: assetAudioTrack,
                                                       at: accumulatedTime)
                    accumulatedTime = CMTimeAdd(accumulatedTime, durationOfCurrentSlice)
                }
                catch let compError {
                    print("TrimVideo: error during composition: \(compError)")
                    completion(.failure(compError))
                }
            }
            
            guard let exportSession = AVAssetExportSession(asset: composition, presetName: preferredPreset) else {
                completion(.failure(VideoTrimmer.trimError))
                return }
            
            exportSession.outputURL = destinationURL
            exportSession.outputFileType = AVFileType.mp4
            exportSession.shouldOptimizeForNetworkUse = true
            
            removeFileAtURLIfExists(url: destinationURL as URL)
            
            exportSession.exportAsynchronously {
                
                switch exportSession.status {
                case .completed:
                    completion(.success(destinationURL))
                case .failed:
                    completion(.failure(exportSession.error!))
                    print("failed \(exportSession.error.debugDescription)")
                case .cancelled:
                    completion(.failure(exportSession.error!))
                    print("cancelled \(exportSession.error.debugDescription)")
                default:
                    if let err = exportSession.error {
                        completion(.failure(err))
                    }
                }
            }
        }
        else {
            print("TrimVideo - Could not find a suitable export preset for the input video")
            completion(.failure(VideoTrimmer.trimError))
        }
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment