Last active
August 23, 2016 02:38
-
-
Save bryan1anderson/c75afe6609b6189773e3f9dff79c5f2e to your computer and use it in GitHub Desktop.
Credit goes to Genady Okrain for his work on creating Live Photos from video assets. I was able to use a lot of his code to make sure all of the PHLivePhoto meta-data was working properly: https://github.com/genadyo/LivePhotoDemo
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
| // | |
| // LivePhotoCompressible.swift | |
| // Swop | |
| // | |
| // Created by Bryan on 5/18/16. | |
| // Copyright © 2016 Swop. All rights reserved. | |
| // | |
| import Foundation | |
| import UIKit | |
| import Photos | |
| import ImageIO | |
| import MobileCoreServices | |
| enum LivePhotoQuality: String { | |
| case Low | |
| case Medium | |
| case High | |
| case Highest | |
| var videoAssetQuality: String { | |
| switch self { | |
| case .Low: | |
| return AVAssetExportPresetMediumQuality | |
| case .Medium: | |
| return AVAssetExportPreset640x480 | |
| case .High: | |
| return AVAssetExportPreset960x540 | |
| case .Highest: | |
| return AVAssetExportPreset1280x720 | |
| } | |
| } | |
| var photoLossyCompressionQuality: Int { | |
| switch self { | |
| case .Low: | |
| return 1 | |
| case .Medium: | |
| return 3 | |
| case .High: | |
| return 5 | |
| case .Highest: | |
| return 7 | |
| } | |
| } | |
| var photoDPI: Int { | |
| switch self { | |
| case .Low: | |
| return 50 | |
| case .Medium: | |
| return 75 | |
| case .High: | |
| return 100 | |
| case .Highest: | |
| return 125 | |
| } | |
| } | |
| var photoPixelSize: Int { | |
| switch self { | |
| case .Low: | |
| return 300 | |
| case .Medium: | |
| return 500 | |
| case .High: | |
| return 800 | |
| case .Highest: | |
| return 1200 | |
| } | |
| } | |
| } | |
| protocol LivePhotoCompressible { | |
| func didCompressLivePhoto(quality: LivePhotoQuality, photoURL: NSURL, videoURL: NSURL) | |
| func didFailCompressingLivePhoto(error: String) | |
| } | |
| extension LivePhotoCompressible { | |
| @available(iOS 9.1, *) | |
| func compressLivePhoto(photo: PHLivePhoto, fileName: String, quality: LivePhotoQuality) { | |
| let assetResources = PHAssetResource.assetResourcesForLivePhoto(photo) | |
| guard let photoResource = assetResources.filter({$0.type == .Photo}).first, | |
| videoResource = assetResources.filter({$0.type == .PairedVideo}).first | |
| else { didFailCompressingLivePhoto("missing photo or video asset"); return } | |
| savePhotoAssetResource(photoResource, uniqueFilePathEndComponent: fileName + ".jpg", quality: quality, completion: { (photoURL, filePath) in | |
| self.saveVideoAssetResource(videoResource, quality: quality, uniqueFilePathEndComponent: fileName + ".mov", photoURL: photoURL, photoFilePath: filePath) | |
| }) | |
| } | |
| func savePhotoAssetResource(resource: PHAssetResource, uniqueFilePathEndComponent component: String, quality: LivePhotoQuality, completion: (photoURL: NSURL, filePath: String) -> ()) { | |
| let filenameTemp = getDocumentsDirectory().stringByAppendingPathComponent("tempphoto\(quality.rawValue + NSUUID().UUIDString).jpg") | |
| let url = NSURL(fileURLWithPath: filenameTemp) | |
| deleteFileAtPath(filenameTemp) | |
| PHAssetResourceManager.defaultManager().writeDataForAssetResource(resource, toFile: url, options: nil, completionHandler: { (error) in | |
| let filepath = self.getDocumentsDirectory().stringByAppendingPathComponent(component) | |
| self.deleteFileAtPath(filepath) | |
| NSUserDefaults.standardUserDefaults().setURL(url, forKey: "photoURL") | |
| NSUserDefaults.standardUserDefaults().setObject(filepath, forKey: "photoFilepath") | |
| return completion(photoURL: url, filePath: filepath) | |
| }) | |
| } | |
| func saveVideoAssetResource(resource: PHAssetResource, quality: LivePhotoQuality, uniqueFilePathEndComponent component: String, photoURL: NSURL, photoFilePath: String) { | |
| let filenameTemp = self.getDocumentsDirectory().stringByAppendingPathComponent("tempphoto.mov") | |
| let url = NSURL(fileURLWithPath: filenameTemp) | |
| deleteFileAtPath(filenameTemp) | |
| PHAssetResourceManager.defaultManager().writeDataForAssetResource(resource, toFile: url, options: nil, completionHandler: { (error) in | |
| let filepath = self.getDocumentsDirectory().stringByAppendingPathComponent(component) | |
| self.deleteFileAtPath(filepath) | |
| let asset = AVURLAsset(URL: url) | |
| guard let exportSession = AVAssetExportSession(asset: asset, presetName: quality.videoAssetQuality) else { return } | |
| exportSession.outputURL = NSURL(fileURLWithPath: filepath) | |
| exportSession.outputFileType = AVFileTypeQuickTimeMovie | |
| exportSession.shouldOptimizeForNetworkUse = true | |
| exportSession.exportAsynchronouslyWithCompletionHandler({ | |
| switch exportSession.status { | |
| // case .Cancelled: | |
| // print("cancelled!!!") | |
| // case .Failed: | |
| // print("Failed!!") | |
| // case .Unknown: | |
| // print("Unknown") | |
| case .Cancelled, .Failed, .Unknown: | |
| // print(quality.rawValue) | |
| break | |
| case .Completed: | |
| if let assetIdentifier = self.readAssetIdentifierFor(asset), | |
| jpg = JPEG(url: photoURL, quality: quality) { | |
| jpg.write(photoFilePath, assetIdentifier: assetIdentifier) | |
| self.didCompressLivePhoto(quality, photoURL: NSURL(fileURLWithPath: photoFilePath), videoURL: NSURL(fileURLWithPath: filepath)) | |
| } | |
| case .Exporting: | |
| break | |
| case .Waiting: | |
| break | |
| } | |
| }) | |
| }) | |
| } | |
| func getDocumentsDirectory() -> NSString { | |
| let paths = NSSearchPathForDirectoriesInDomains(.CachesDirectory, .UserDomainMask, true) | |
| let documentsDirectory = paths[0] | |
| let dataPath = documentsDirectory + "/LivePhotos/" | |
| do { | |
| try NSFileManager.defaultManager().createDirectoryAtPath(dataPath, withIntermediateDirectories: false, attributes: nil) | |
| return dataPath | |
| } catch let error as NSError { | |
| // print(error.localizedDescription); | |
| return dataPath | |
| } | |
| } | |
| func deleteFileAtPath(path: String) { | |
| do { | |
| try NSFileManager.defaultManager().removeItemAtPath(path) | |
| } catch { | |
| // print(error) | |
| } | |
| } | |
| func readAssetIdentifierFor(asset: AVAsset) -> String? { | |
| let kKeyContentIdentifier = "com.apple.quicktime.content.identifier" | |
| let kKeySpaceQuickTimeMetadata = "mdta" | |
| for item in asset.metadataForFormat(AVMetadataFormatQuickTimeMetadata) { | |
| if item.key as? String == kKeyContentIdentifier && | |
| item.keySpace == kKeySpaceQuickTimeMetadata { | |
| return item.value as? String | |
| } | |
| } | |
| return nil | |
| } | |
| } | |
| class JPEG { | |
| private let kFigAppleMakerNote_AssetIdentifier = "17" | |
| private let data: NSData | |
| private let quality: LivePhotoQuality | |
| init?(url : NSURL, quality: LivePhotoQuality = .Low) { | |
| if let udata = NSData(contentsOfURL: url) { | |
| self.data = udata | |
| self.quality = quality | |
| } else { | |
| return nil | |
| } | |
| } | |
| init(data: NSData, quality: LivePhotoQuality = .Low) { | |
| self.data = data | |
| self.quality = quality | |
| } | |
| func read() -> String? { | |
| guard let makerNote = metadata()?.objectForKey(kCGImagePropertyMakerAppleDictionary) as! NSDictionary? else { | |
| return nil } | |
| return makerNote.objectForKey(kFigAppleMakerNote_AssetIdentifier) as! String? | |
| } | |
| func write(dest : String, assetIdentifier : String) { | |
| guard let dest = CGImageDestinationCreateWithURL(NSURL(fileURLWithPath: dest), kUTTypeJPEG, 1, nil) | |
| else { return } | |
| defer { CGImageDestinationFinalize(dest) } | |
| guard let imageSource = self.imageSource else { return } | |
| guard let metadata = self.metadata()?.mutableCopy() as! NSMutableDictionary! else { return } | |
| let makerNote = NSMutableDictionary() | |
| makerNote.setObject(assetIdentifier, forKey: kFigAppleMakerNote_AssetIdentifier) | |
| metadata.setObject(makerNote, forKey: kCGImagePropertyMakerAppleDictionary as String) | |
| metadata.setValue(quality.photoLossyCompressionQuality, forKey: kCGImageDestinationLossyCompressionQuality as String) | |
| metadata.setValue(quality.photoDPI, forKey: kCGImagePropertyDPIWidth as String) | |
| metadata.setValue(quality.photoDPI, forKey: kCGImagePropertyDPIHeight as String) | |
| metadata.setValue(quality.photoPixelSize, forKey: kCGImageDestinationImageMaxPixelSize as String) | |
| // kCGImageSourceThumbnailMaxPixelSize | |
| CGImageDestinationAddImageFromSource(dest, imageSource, 0, metadata) | |
| } | |
| private func metadata() -> NSDictionary? { | |
| return self.imageSource.flatMap { | |
| CGImageSourceCopyPropertiesAtIndex($0, 0, nil) as NSDictionary? | |
| } | |
| } | |
| var imageSource: CGImageSource? { | |
| return CGImageSourceCreateWithData(data, nil) | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment