Problem:
When I record a video in my UIImagePickerController
with allowsEditing set to YES, and afterwards trim the video by using the trim-interface that comes after video capture, I get returned the original video, instead of the trimmed one.
Setup:
I am using a UIImagePickerController
for video capture, with the allowsEditing
property set to YES. In the delegate method didFinishPickingMediaWithInfo
, I use UIImagePickerControllerMediaURL
from the info NSDictionary
to get the path URL. The official Apple docs don't mention any Edited video URL unfortunately.
Code:
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
{
NSString *mediaType = [info objectForKey: UIImagePickerControllerMediaType];
if (CFStringCompare ((__bridge CFStringRef) mediaType, kUTTypeMovie, 0)
== kCFCompareEqualTo) {
self.tempVideoPath = [[info objectForKey:
UIImagePickerControllerMediaURL] path];
}
}
I realise this question is similar to other ones posted here on SO, but there was no definitive answer why it doesn't work or why the option is even there. If it is intended like this, I don't understand why there is an 'allowsEditing' property for the picker.
EDIT: In the info dictionary I got back are the following keys:
info: {
UIImagePickerControllerMediaType = "public.movie";
UIImagePickerControllerMediaURL = "file://localhost/private/var/mobile/Applications/F12E4608-FE5A-4EE3-B4E2-8F7D2508C4C8/tmp/capture-T0x21d810.tmp.wabFCC/capturedvideo.MOV";
"_UIImagePickerControllerVideoEditingEnd" = "5.498333333333333";
"_UIImagePickerControllerVideoEditingStart" = "4.273402690887451";
}
Does this mean we have to trim it ourselves with this data? Then the Apple documentation isn't very clear about this. If so, do you know a good practice for this?
Here's a quick and dirty Swift 5 example of how to trim the video from UIImagePickerController
extension ViewController: UIImagePickerControllerDelegate {
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
let mediaType = info[.mediaType] as! String
dismiss(animated: true) { [weak self] in
// Handle a movie capture
if mediaType == kUTTypeMovie as String {
guard let videoURL = info[.mediaURL] as? URL else {
SwiftyBeaver.error("Could not get URL for movie")
return
}
let editingEnd = UIImagePickerController.InfoKey(rawValue: "_UIImagePickerControllerVideoEditingEnd")
let editingStart = UIImagePickerController.InfoKey(rawValue: "_UIImagePickerControllerVideoEditingStart")
let startMilliseconds: Double?
let endMilliseconds: Double?
if let start = info[editingStart] as? Double, let end = info[editingEnd] as? Double {
startMilliseconds = start
endMilliseconds = end
} else {
startMilliseconds = nil
endMilliseconds = nil
}
let alert = UIAlertController(title: "Creating", message: "File is being processed", preferredStyle: .alert)
self?.present(alert, animated: true)
self?.process(srcVideoURL: videoURL, startSeconds: startMilliseconds, endSeconds: endMilliseconds) { (error) in
DispatchQueue.main.async {
if let error = error {
alert.title = "Whoops"
alert.message = "\(error)"
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { _ in
self?.dismiss(animated: true, completion: nil)
}))
return
}
self?.dismiss(animated: true, completion: nil)
}
}
}
}
}
}
enum VideoError: Error {
case error(message: String)
}
extension ViewController {
func process(srcVideoURL: URL, startSeconds: Double?, endSeconds: Double?, completed: @escaping (_ error: Error?) -> ()) {
DispatchQueue.global(qos: .userInitiated).async {
let dstVideoURL: URL // some URL for the destination
do {
try self.handleNewVideo(srcVideoURL: srcVideoURL, dstVideoURL: dstVideoURL, startSeconds: startSeconds, endSeconds: endSeconds)
completed(nil)
} catch {
completed(error)
}
}
}
func handleNewVideo(srcVideoURL: URL, dstVideoURL: URL, startSeconds: Double?, endSeconds: Double?) throws {
guard let start = startSeconds, let end = endSeconds else {
print("No video editing information. Copying file.")
try FileManager.default.moveItem(at: srcVideoURL, to: dstVideoURL)
return
}
print("Video editing information. Processing start \(start) end \(end).")
let videoAsset = AVURLAsset(url: srcVideoURL)
let exportSession = AVAssetExportSession(asset: videoAsset, presetName: AVAssetExportPresetHighestQuality)!
exportSession.outputURL = dstVideoURL
exportSession.outputFileType = AVFileType.mov
let timeRange = CMTimeRange(start: CMTime(seconds: start, preferredTimescale: 1000), duration: CMTime(seconds: end - start, preferredTimescale: 1000))
exportSession.timeRange = timeRange
var error: Error? = nil
let dispatchGroup = DispatchGroup()
dispatchGroup.enter()
exportSession.exportAsynchronously(completionHandler: {
switch exportSession.status {
case .completed:
break
case .failed:
error = exportSession.error ?? VideoError.error(message: "Unknown failed error")
case .cancelled:
error = exportSession.error ?? VideoError.error(message: "Video Cancelled")
case .exporting:
error = exportSession.error ?? VideoError.error(message: "Video still exporting")
case .unknown:
error = exportSession.error ?? VideoError.error(message: "Unknown unknown error")
case .waiting:
error = exportSession.error ?? VideoError.error(message: "Waiting error")
@unknown default:
error = exportSession.error ?? VideoError.error(message: "Future error")
}
dispatchGroup.leave()
})
dispatchGroup.wait()
if let error = error {
throw error
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With