-
-
Save acj/b8c5f8eafe0605a38692 to your computer and use it in GitHub Desktop.
// | |
// 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 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)
}
}
}
Thanks for making and sharing this! How do you use this extension? (run it)
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 ?
UPDATE:
I changed the presentName to HighestQuality... and it worked.
AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetHighestQuality)
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!
Thanks so much!
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
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)
}
}
}
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))
}
}
}
Thanks for sharing this @acj! Here's a Swift 3.1 version of it: