Last active
November 2, 2023 15:05
-
-
Save acj/b8c5f8eafe0605a38692 to your computer and use it in GitHub Desktop.
Trim video using AVFoundation in Swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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") | |
} | |
} |
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
Swift 5 version + SwiftLint