Skip to content

Instantly share code, notes, and snippets.

@acj
Last active November 2, 2023 15:05
Show Gist options
  • Save acj/b8c5f8eafe0605a38692 to your computer and use it in GitHub Desktop.
Save acj/b8c5f8eafe0605a38692 to your computer and use it in GitHub Desktop.
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

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

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

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

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