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") | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Swift 5 + New
Result
type