ALAssetsLibrary is deprecated these days but practically all examples on SO are still making use of it. For my objective, I need to know the URL of the video that was added to the Photo Library so I could share a video to the Instagram app (that's the only way in which Instagram accepts videos). The URL should probably start with "assets-library://..."
This is simple with ALAssetsLibrary:
ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
[library writeVideoAtPathToSavedPhotosAlbum:videoFilePath completionBlock:^(NSURL *assetURL, NSError *error) { // ...
Above, the URL is passed as an argument to the completion block. But what about iOS 9 compatible way with PHPhotoLibrary class and performChanges method?
var videoAssetPlaceholder:PHObjectPlaceholder! // property
// ...
let videoURL = NSBundle.mainBundle().URLForResource("Video_.mp4", withExtension: nil)!
let photoLibrary = PHPhotoLibrary.sharedPhotoLibrary()
photoLibrary.performChanges({
let request = PHAssetChangeRequest.creationRequestForAssetFromVideoAtFileURL(videoURL)
self.videoAssetPlaceholder = request?.placeholderForCreatedAsset
},
completionHandler: { success, error in
if success
{
// The URL of the video asset?
}
})
Here's what I've ended up with:
let videoURL = NSBundle.mainBundle().URLForResource("Video_.mp4", withExtension: nil)!
let photoLibrary = PHPhotoLibrary.sharedPhotoLibrary()
var videoAssetPlaceholder:PHObjectPlaceholder!
photoLibrary.performChanges({
let request = PHAssetChangeRequest.creationRequestForAssetFromVideoAtFileURL(videoURL)
videoAssetPlaceholder = request!.placeholderForCreatedAsset
},
completionHandler: { success, error in
if success {
let localID = videoAssetPlaceholder.localIdentifier
let assetID =
localID.stringByReplacingOccurrencesOfString(
"/.*", withString: "",
options: NSStringCompareOptions.RegularExpressionSearch, range: nil)
let ext = "mp4"
let assetURLStr =
"assets-library://asset/asset.\(ext)?id=\(assetID)&ext=\(ext)"
// Do something with assetURLStr
}
})
Nice answer and pseudo, thanks Desmond.
Here is an updated answer :
Swift 3
var assetPlaceholder: PHObjectPlaceholder!
PHPhotoLibrary.shared().performChanges({
let assetRequest = PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: videoURL as URL!)
assetPlaceholder = assetRequest?.placeholderForCreatedAsset
}, completionHandler: { (success, error) in
if success {
let localID = assetPlaceholder.localIdentifier
let assetID = localID.replacingOccurrences(of: "/.*", with: "", options: NSString.CompareOptions.regularExpression, range: nil)
let ext = "mp4"
let assetURLStr = "assets-library://asset/asset.\(ext)?id=\(assetID)&ext=\(ext)"
// Do what you want with your assetURLStr
} else {
if let errorMessage = error?.localizedDescription {
print(errorMessage)
}
}
})
Going further
Although this works perfectly for the moment, would anyone know a "cleaner" way to get this "assets-library" URL, without this "manual" solution ?
I found this interesting thread, but it only gives a "file:///var/..." URL
Related
I wonder why the contentsOf returns nil for URL from AVURLAsset. After picking from the custom library with Photos framework, I tried to request the asset from PHAsset like the following:
PHCachingImageManager().requestAVAsset(forVideo: asset, options: nil) { (avAsset, _, _) in
DispatchQueue.main.async {
guard let asset = avAsset as? AVURLAsset else {
return
}
print(asset.url) // file:///var/mobile/Media/DCIM/101APPLE/IMG_1513.MP4
}
}
The video with the URL above can be displayed normally with AVPlayer. But when I try to get the data associated with the url using:
do {
let videoData = try Data(contentsOf: mediaURL!)
} catch (let error){
print(error.localizedDescription ?? "") // "The file “IMG_1490.MP4” couldn’t be opened because you don’t have permission to view it."
}
Please find working source code in which i have retrieved recent video from gallery then i have converted that video into AVURLAsset using that AVURLAsset i converted it's data.
Swift 4
override func viewDidLoad() {
super.viewDidLoad()
//For fetching Videos from Photo Library.
let phFetchOptions = PHFetchOptions()
phFetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate",
ascending: false)]
phFetchOptions.predicate = NSPredicate(format: "mediaType == %d",
PHAssetMediaType.video.rawValue)
phFetchOptions.fetchLimit = 2
let videoPhAssetResult = PHAsset.fetchAssets(with:phFetchOptions)
print(videoPhAssetResult.count)
let videoPHAsset = videoPhAssetResult.object(at: 0)
//For Fetching AvAsset from PHAsset and along with that getting Data from AvAsset
PHCachingImageManager().requestAVAsset(forVideo:videoPHAsset , options: nil) { (vidAvAsset, _, _) in
DispatchQueue.main.async {
if vidAvAsset != nil {
let assetURL = vidAvAsset as? AVURLAsset
print(assetURL?.url)
do {
let videoData = try Data(contentsOf:(assetURL?.url)!)
print(videoData)
} catch (let error){
print(error.localizedDescription)
}
}
}
}
}
(Note : Please don’t forget to add Privacy - Photo Library Usage Description permission for Accessing Photo Library of Device or Simulator)
In my App I want to make it possible, that the user sets an StarRating from 0 to 5 for any Image he has in his PhotoLibrary. My research shows, that there are a couple of ways to get this done:
Save the exif metadata using the new PHPhotoLibrary
Swift: Custom camera save modified metadata with image
Writing a Photo with Metadata using Photokit
Most of these Answers were creating a new Photo. My snippet now looks like this:
let options = PHContentEditingInputRequestOptions()
options.isNetworkAccessAllowed = true
self.requestContentEditingInput(with: options, completionHandler: {
(contentEditingInput, _) -> Void in
if contentEditingInput != nil {
if let url = contentEditingInput!.fullSizeImageURL {
if let nsurl = url as? NSURL {
if let imageSource = CGImageSourceCreateWithURL(nsurl, nil) {
var imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as Dictionary?
if imageProperties != nil {
imageProperties![kCGImagePropertyIPTCStarRating] = rating as AnyObject
let imageData = NSMutableData(contentsOf: url)
let image = UIImage(contentsOfFile: url.path)
let destination = CGImageDestinationCreateWithData(imageData!, CGImageSourceGetType(imageSource)!, 1, nil)
CGImageDestinationAddImage(destination!, image!.cgImage!, imageProperties! as CFDictionary)
var contentEditingOutput : PHContentEditingOutput? = nil
if CGImageDestinationFinalize(destination!) {
let archievedData = NSKeyedArchiver.archivedData(withRootObject: rating)
let identifier = "com.example.starrating"
let adjustmentData = PHAdjustmentData(formatIdentifier: identifier, formatVersion: "1.0", data: archievedData)
contentEditingOutput = PHContentEditingOutput(contentEditingInput: contentEditingInput!)
contentEditingOutput!.adjustmentData = adjustmentData
if imageData!.write(to: contentEditingOutput!.renderedContentURL, atomically: true) {
PHPhotoLibrary.shared().performChanges({
let request = PHAssetChangeRequest(for: self)
request.contentEditingOutput = contentEditingOutput
}, completionHandler: {
success, error in
if success && error == nil {
completion(true)
} else {
completion(false)
}
})
}
} else {
completion(false)
}
}
}
}
}
}
})
Now when I want to read the metadata from the PHAsset I request the ContentEditingInput again and do the following:
if let url = contentEditingInput!.fullSizeImageURL {
if let nsurl = url as? NSURL {
if let imageSource = CGImageSourceCreateWithURL(nsurl, nil) {
if let imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as Dictionary? {
if let starRating = imageProperties[kCGImagePropertyIPTCStarRating] as? Int {
rating = starRating
}
}
}
}
}
But I never get my rating because it says that the value of imageProperties[kCGImagePropertyIPTCStarRating] is nil.
I also tried the examples from the Answers I posted above, but I always get the same result.
I hope anybody knows, what I can do to change the Metadata.
Also, how can I change the Metadata from an PHAsset with the MediaType .video? I tried to achieve that through the AVAssetWriter and AVExportSession Objects, but in both cases it does not work. Here what I tried for Videos:
var exportSession = AVAssetExportSession(asset: asset!, presetName: AVAssetExportPresetPassthrough)
exportSession!.outputURL = outputURL
exportSession!.outputFileType = AVFileTypeQuickTimeMovie
exportSession!.timeRange = CMTimeRange(start: start, duration: duration)
var modifiedMetadata = asset!.metadata
let metadataItem = AVMutableMetadataItem()
metadataItem.keySpace = AVMetadataKeySpaceQuickTimeMetadata
metadataItem.key = AVMetadataQuickTimeMetadataKeyRatingUser as NSCopying & NSObjectProtocol
metadataItem.value = rating as NSCopying & NSObjectProtocol
modifiedMetadata.append(metadataItem)
exportSession!.metadata = modifiedMetadata
exportSession!.exportAsynchronously(completionHandler: {
let status = exportSession?.status
let success = status == AVAssetExportSessionStatus.completed
if success {
do {
let sourceURL = urlAsset.url
let manager = FileManager.default
_ = try manager.removeItem(at: sourceURL)
_ = try manager.moveItem(at: outputURL, to: sourceURL)
} catch {
LogError("\(error)")
completion(false)
}
} else {
LogError("\(exportSession!.error!)")
completion(false)
}
})
Sorry this isn't a full answer but it covers one part of your question. I noticed you are placing the StarRating in the wrong place. You need to place it in a IPTC dictionary. Also the properties data is stored as strings. Given you have the imageProperties you can add the star rating as follows and read it back using the following two functions
func setIPTCStarRating(imageProperties : NSMutableDictionary, rating : Int) {
if let iptc = imageProperties[kCGImagePropertyIPTCDictionary] as? NSMutableDictionary {
iptc[kCGImagePropertyIPTCStarRating] = String(rating)
} else {
let iptc = NSMutableDictionary()
iptc[kCGImagePropertyIPTCStarRating] = String(rating)
imageProperties[kCGImagePropertyIPTCDictionary] = iptc
}
}
func getIPTCStarRating(imageProperties : NSMutableDictionary) -> Int? {
if let iptc = imageProperties[kCGImagePropertyIPTCDictionary] as? NSDictionary {
if let starRating = iptc[kCGImagePropertyIPTCStarRating] as? String {
return Int(starRating)
}
}
return nil
}
As the imageProperties you get from the image are not mutable you need to create a mutable copy of these properties first before you can call the functions above. When you create your image to save use the mutable properties in your call to CGImageDestinationAddImage()
if let mutableProperties = imageProperties.mutableCopy() as? NSMutableDictionary {
setIPTCStarRating(imageProperties:mutableProperties, rating:rating)
}
One other point you are creating an unnecessary UIImage. If you use CGImageDestinationAddImageFromSource() instead of CGImageDestinationAddImage() you can use the imageSource you created earlier instead of loading the image data into a UIImage.
This is my code to accomplish the upload task:
let image = UIImage(named: "12.jpeg")
let fileManager = FileManager.default
let imageData = UIImageJPEGRepresentation(image!, 0.99)
let path = (NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] as NSString).appendingPathComponent("\(imageData!).jpeg")
fileManager.createFile(atPath: path as String, contents: imageData, attributes: nil)
let fileUrl = NSURL(fileURLWithPath: path)
uploadRequest?.bucket = "testrawdata"
uploadRequest?.key = "test/loodfd.jpeg"
uploadRequest?.contentType = "image/jpeg"
uploadRequest?.body = fileUrl as URL!
uploadRequest?.serverSideEncryption = AWSS3ServerSideEncryption.awsKms
uploadRequest?.uploadProgress = { (bytesSent, totalBytesSent, totalBytesExpectedToSend) -> Void in
DispatchQueue.main.async(execute: {
print("bytes sent \(bytesSent), total bytes sent \(totalBytesSent), of total \(totalBytesExpectedToSend)")
})
}
transferManager?.upload(uploadRequest).continue(with: AWSExecutor.mainThread(), withSuccessBlock: { (taskk: AWSTask) -> Any? in
if taskk.error != nil {
// Error.
} else {
// Do something with your result.
}
return nil
})
}
I know I don't need to apply it to image, but this is just an example, by default I'm going to send files like 100mb.
When I put my phone into airplane mode during the transfer then turn the network on again, it does not finish the upload task.
Docs are not saying explicitly what should I do to resume interrupted task.
Here is what I tried:
I put initialization of request and manager into viewDidLoad() to assure I'm not creating another request
class ViewController: UIViewController {
var uploadRequest:AWSS3TransferManagerUploadRequest!
var transferManager: AWSS3TransferManager!
override func viewDidLoad() {
super.viewDidLoad()
uploadRequest = AWSS3TransferManagerUploadRequest()
transferManager = AWSS3TransferManager.default()
}
and tried to call
func resumeTransfer() {
transferManager?.resumeAll(nil)
}
But it does not work.
Thanks in advance
It turns out that Transfer Utility is the right tool to accomplish this task
func uploadData(data: NSData) {
let expression = AWSS3TransferUtilityUploadExpression()
expression.progressBlock = progressBlock
let transferUtility = AWSS3TransferUtility.default()
transferUtility.uploadData(
data as Data,
bucket: "test",
key: "test/test.jpeg",
contentType: "image/jpeg",
expression: expression,
completionHander: completionHandler).continue(successBlock: { (task) -> AnyObject! in
if let error = task.error {
NSLog("Error: %#",error.localizedDescription);
}
if let exception = task.exception {
NSLog("Exception: %#",exception.description);
}
if let _ = task.result {
NSLog("Upload Starting!")
// Do something with uploadTask.
}
return nil;
})
}
This way all upload stuff happens in the background, I don't have to worry about app being killed by the iOS, about networks problem etc.
One can even specify
configuration?.allowsCellularAccess = false
in AWSServiceConfiguration
to resume the task only when wifi is available.
I'm trying to convert from ALAssetsLibrary to PHPhotoLibrary since it is deprecated. I need to grab the date from a chosen photo from the photo library but can't figure out the right way. Here is how I'm currently doing it.
mediaUrl = info[UIImagePickerControllerReferenceURL] as? NSURL
...
let assetsLibrary = ALAssetsLibrary()
assetsLibrary.assetForURL(mediaUrl, resultBlock: { (asset) -> Void in
guard let asset = asset else { return }
guard let date = asset.valueForProperty(ALAssetPropertyDate) as? NSDate else { return }
let dateString = dateFormatter.stringFromDate(date)
//---- use date string here
}) { (error) -> Void in
print(error)
}
Here is where I've gotten to:
let photoLibrary = PHPhotoLibrary.sharedPhotoLibrary()
var photoAssetPlaceholder: PHObjectPlaceholder!
photoLibrary.performChanges({
let request = PHAssetChangeRequest.creationRequestForAssetFromImage(image)
photoAssetPlaceholder = request.placeholderForCreatedAsset
}, completionHandler: { success, error in
if success {
print (photoAssetPlaceholder)
//How do I get date from PhotoAssetPlaceholder???
} else {
print(error?.localizedDescription)
}
})
I feel like I'm close. Any help would be appreciated!
The model objects that represents the photos and videos themselves are of type PHAsset.
A PHAsset contains metadata such as the asset’s media type and its creation date.
I'm currently displaying a video in my app and I want the user to be able to save it to its device gallery/album photo/camera roll.
Here it's what I'm doing but the video is not saved in the album :/
func downloadVideo(videoImageUrl:String)
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
//All stuff here
print("downloadVideo");
let url=NSURL(string: videoImageUrl);
let urlData=NSData(contentsOfURL: url!);
if((urlData) != nil)
{
let documentsPath = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0];
let fileName = videoImageUrl; //.stringByDeletingPathExtension
let filePath="\(documentsPath)/\(fileName)";
//saving is done on main thread
dispatch_async(dispatch_get_main_queue(), { () -> Void in
urlData?.writeToFile(filePath, atomically: true);
print("videoSaved");
})
}
})
}
I'va also look into this :
let url:NSURL = NSURL(string: fileURL)!;
PHPhotoLibrary.sharedPhotoLibrary().performChanges({
let assetChangeRequest = PHAssetChangeRequest.creationRequestForAssetFromVideoAtFileURL(url);
let assetPlaceHolder = assetChangeRequest!.placeholderForCreatedAsset;
let albumChangeRequest = PHAssetCollectionChangeRequest(forAssetCollection: self.assetCollection)
albumChangeRequest!.addAssets([assetPlaceHolder!])
}, completionHandler: saveVideoCallBack)
But I have the error "Unable to create data from file (null)". My "assetChangeRequest" is nil. I don't understand as my url is valid and when I go to it with a browser, it download a quick time file.
If anyone can help me, it would be appreciated ! I'm using Swift and targeting iOS 8.0 min.
Update
Wanted to update the answer for Swift 3 using URLSession and figured out that the answer already exists in related topic here. Use it.
Original Answer
The code below saves a video file to Camera Roll. I reused your code with a minor change - I removed let fileName = videoImageUrl; because it leads to incorrect file path.
I tested this code and it saved the asset into camera roll. You asked what to place into creationRequestForAssetFromVideoAtFileURL - put a link to downloaded video file as in the example below.
let videoImageUrl = "http://www.sample-videos.com/video/mp4/720/big_buck_bunny_720p_1mb.mp4"
DispatchQueue.global(qos: .background).async {
if let url = URL(string: urlString),
let urlData = NSData(contentsOf: url) {
let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0];
let filePath="\(documentsPath)/tempFile.mp4"
DispatchQueue.main.async {
urlData.write(toFile: filePath, atomically: true)
PHPhotoLibrary.shared().performChanges({
PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: URL(fileURLWithPath: filePath))
}) { completed, error in
if completed {
print("Video is saved!")
}
}
}
}
}
Swift 3 version of the code from #Nimble:
DispatchQueue.global(qos: .background).async {
if let url = URL(string: urlString),
let urlData = NSData(contentsOf: url)
{
let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0];
let filePath="\(documentsPath)/tempFile.mp4"
DispatchQueue.main.async {
urlData.write(toFile: filePath, atomically: true)
PHPhotoLibrary.shared().performChanges({
PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: URL(fileURLWithPath: filePath))
}) { completed, error in
if completed {
print("Video is saved!")
}
}
}
}
}
PHPhotoLibrary.shared().performChanges({
PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: video.url!)}) {
saved, error in
if saved {
print("Save status SUCCESS")
}
}
following #Nimble and #Yuval Tal solution, it is much more preferable to use the URLSession dataTask(with:completionHandler:) method to download a file before writing it as stated in the warning section of NSData(contentsOf:) Apple documentation
Important
Don't use this synchronous initializer to request network-based URLs.
For network-based URLs, this method can block the current thread for
tens of seconds on a slow network, resulting in a poor user
experience, and in iOS, may cause your app to be terminated.
Instead, for non-file URLs, consider using the
dataTask(with:completionHandler:) method of the URLSession
a correct implementation could be :
let defaultSession = URLSession(configuration: .default)
var dataTask: URLSessionDataTask? = nil
func downloadAndSaveVideoToGallery(videoURL: String, id: String = "default") {
DispatchQueue.global(qos: .background).async {
if let url = URL(string: videoURL) {
let filePath = FileManager.default.temporaryDirectory.appendingPathComponent("\(id).mp4")
print("work started")
self.dataTask = self.defaultSession.dataTask(with: url, completionHandler: { [weak self] data, res, err in
DispatchQueue.main.async {
do {
try data?.write(to: filePath)
PHPhotoLibrary.shared().performChanges({
PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: filePath)
}) { completed, error in
if completed {
print("Saved to gallery !")
} else if let error = error {
print(error.localizedDescription)
}
}
} catch {
print(error.localizedDescription)
}
}
self?.dataTask = nil
})
self.dataTask?.resume()
}
}
}
One more advantage is that you can pause, resume and terminate your download by calling the corresponding method on dataTask: URLSessionDataTask .resume() .suspend() .cancel()