Video disappears from URL when generating preview image after recompiling app - ios

I'm creating a video app where a user records a video and adds some additional information. The user can then the video and its information on a separate screen.
On the information screen I'm trying to display a still from the video. This works if I open the information screen within that session, but when I recompile and run the application the screen shots no longer appear. I get the following error:
NSUnderlyingError=0x162b9350 "The operation couldn’t be completed. No such file or directory", NSLocalizedDescription=The requested URL was not found on this server.
The video is on the device somewhere as I can view it via the built in 'Photos' app.
Im using the following code to perform the saving of the url string and for generating the preview image.
Saving the video url
let videoURL = info[UIImagePickerControllerMediaURL] as! NSURL
let videoData = NSData(contentsOfURL: videoURL)
let path = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0] as! String
let dataPath = path.stringByAppendingPathComponent("-cached.MOV")
videoData.writeToFile(dataPath, atomically: false)
NSUserDefaults.standardUserDefaults().setObject(dataPath, forKey: "dataPath")
videoNote.url = dataPath
Creating preview image
// filePathLocal == videoNote.url
func videoSnapshot(filePathLocal: NSString) -> UIImage? {
let vidURL = NSURL(fileURLWithPath:filePathLocal as String)
let asset = AVURLAsset(URL: vidURL, options: nil)
let generator = AVAssetImageGenerator(asset: asset)
generator.appliesPreferredTrackTransform = true
let timestamp: CMTime = asset.duration
var error: NSError?
if let imageRef = generator.copyCGImageAtTime(timestamp, actualTime: nil, error: &error){
return UIImage(CGImage: imageRef)
} else {
print("Image generation failed with error \(error)")
return nil
}
}
Any help much appreciated

Although I never managed to get the code from the videoSnapshot() method to work, I found that I could use the following to generate a preview shot. The downside is there is not control over the timestamp that the image is taken from.
ALAssetsLibrary().assetForURL(nsURL, resultBlock: { (asset) -> Void in
if let ast = asset {
return image = UIImage(CGImage: ast.defaultRepresentation().fullResolutionImage().takeUnretainedValue())
}
})

Related

"The file couldn’t be opened because you don’t have permission to view it." for iCloud Videos

it has been a couple of days, I am struggling with this issue. I am taking a video from photo library and using it to create a thumbnail image. My code is working properly and I am getting the video data and thumbnail for local videos.
But when I select a video that is in iCloud, I am getting the video data but can't create thumbnail.
let img = try assetImgGenerate.copyCGImage(at: time, actualTime: nil).
returns error : The file couldn’t be opened because you don’t have permission to view it.
I have found similar questions here, like this one, but the solution was for it is to turn of sand box under capabilities(Which I didn't find in my xcode 12). Tried this one too, I got same error.
All other similar questions are for files outside app sandbox, but mine is a video from photo library. Shouldn't I be able to access it within my app?. Below is my code. please help.
let options = PHVideoRequestOptions()
options.isNetworkAccessAllowed = true
options.progressHandler = { (progress, error, stop, info) in
print("progress: \(progress)")
}
//phAsset is asset fetched from photo library
PHImageManager.default().requestAVAsset(forVideo: phAsset, options: options)
{(avAsset, mix, info) in
let myAsset = avAsset as? AVURLAsset
do {
if let url = myAsset?.url{
let videoData = try Data(contentsOf:url)
DispatchQueue.global(qos: .background).async {
let assetImgGenerate = AVAssetImageGenerator(asset: avAsset)
assetImgGenerate.appliesPreferredTrackTransform = true
let time = CMTime(seconds: 0.0, preferredTimescale: 600)
do {
let img = try assetImgGenerate.copyCGImage(at: time, actualTime: nil)
let thumbnail = UIImage(cgImage: img)
} catch {
//here prints : "The file couldn’t be opened because you don’t have permission to view it."
print(error.localizedDescription)
}
}
}
} catch {
print("exception catch at block - while uploading video")
}
}
I've been seeing this same error on iOS 14 when simply trying to play a video from iCloud in an AVPlayer. It seems like a bug in the Photos framework and the only "solution" that has worked for me has been to request the highest quality video.
options.deliveryMode = .highQualityFormat
See the comment from #SwiftRabbit on Request AVAsset using iCloud PHAsset returns an AVAsset with no VideoTracks

How to prevent constantly downloading an image from Firebase and show the image even if there is no internet connection?

Every time I show the profile picture, the UIImageView flashes to signify that the image was just downloaded from the Firebase Storage URL. This download speed differs based on the device type, some times it is unnoticeable while other times there is a significant delay.
I have attempted to cache the image with NSCache and the Kingfisher library but I still see the UIImageView flash rather than remain there every time I reopen the app.
My last attempt was to save the image to the document directory and then retrieve it from there but I still see the image flash. I would also like the profile picture to remain there even if the application is opened without any internet connection.
func saveImageDocumentDirectory(imgUrl: URL){
let fileManager = FileManager.default
let paths = (NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] as NSString).appendingPathComponent("proPic.png")
let data = (try? Data(contentsOf: imgUrl))
let image = UIImage(data: data!)
print("\n\(paths)\n")
let imageData = image!.pngData()
fileManager.createFile(atPath: paths as String, contents: imageData, attributes: nil)
}
func getDirectoryPath() -> String {
let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
let documentsDirectory = paths[0]
return documentsDirectory
}
func getImage(){
let fileManager = FileManager.default
let imagePAth = (self.getDirectoryPath() as NSString).appendingPathComponent("proPic.png")
if fileManager.fileExists(atPath: imagePAth){
self.profilePic.image = UIImage(contentsOfFile: imagePAth)
}else{
print("\nNo Image\n")
}
}
func createDirectory(){
let fileManager = FileManager.default
let paths = (NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] as NSString).appendingPathComponent("customDirectory")
if !fileManager.fileExists(atPath: paths){
try! fileManager.createDirectory(atPath: paths, withIntermediateDirectories: true, attributes: nil)
}else{
print("\nAlready dictionary created.\n")
}
}
And I would call the function by:
func getEmailPic(){
guard let uid = Auth.auth().currentUser?.uid else {return}
//receive the location of the profile pic
let storageRef = Storage.storage().reference().child(uid).child("profilePic.png");
//how to access the downloadURL
_ = storageRef.downloadURL(completion: { (URLe, error) in
if let error = error{
//error handling
print("\nCould not download user's profile image from url.
Error: \(error.localizedDescription)\n");
return;
}
self.createDirectory()
self.saveImageDocumentDirectory(imgUrl: URLe!)
print("\nThis is the URL: \(URLe)\n")
self.getImage()
})
}
in viewDidLoad.
Using kingfisher for image caching, Try this and feel free to ask if facing any issue
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
// set profile image if you have url saved in userdefaults
let imageUrl = getUrlImageFromUserDefaults()
let placeholderImage = UIImage(named: "placeholder")
profileImageView.kf.setImage(with: imageUrl, placeholder: placeholderImage)
getEmailPic()
}
func getUrlImageFromUserDefaults() -> URL?{
// save image URL to userdefault and fetch here
let userdefaults = UserDefaults.standard
return userdefaults.url(forKey: "profileURL")
}
func getEmailPic(){
guard let uid = Auth.auth().currentUser?.uid else {return}
//receive the location of the profile pic
let storageRef = Storage.storage().reference().child(uid).child("profilePic.png");
//how to access the downloadURL
_ = storageRef.downloadURL(completion: { (URLe, error) in
if let error = error{
//error handling
print("\nCould not download user's profile image from url.
Error: \(error.localizedDescription)\n");
return;
}
if URLe == getUrlImageFromUserDefaults() {
// if url is same no need to set again
}else{
// set profile image
let placeholderImage = UIImage(named: "placeholder")
profileImageView.kf.setImage(with: URLe, placeholder: placeholderImage)
// and again save this new URL to userdefaults
}
})
}

Persisting data in iOS documents directory

I have an app that saves a recorded video to the documents directory, as well as a Post object, and populates a collection view from the Post object. However upon restarting the app, the collection view is empty, so the videos being saved to the docs directory is not persisting (at least I think that's the problem).
This is the function that saves the video:
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
let mediaType = info[UIImagePickerControllerMediaType] as! NSString
dismiss(animated: true, completion: nil)
if mediaType == kUTTypeMovie {
var uniqueVideoID = ""
var videoURL:NSURL? = NSURL()
var uniqueID = ""
uniqueID = NSUUID().uuidString
// Get the path as URL and store the data in myVideoVarData
videoURL = info[UIImagePickerControllerMediaURL] as? URL as NSURL?
let myVideoVarData = try! Data(contentsOf: videoURL! as URL)
// Write data to temp diroctory
let tempPath = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)
let tempDocumentsDirectory: AnyObject = tempPath[0] as AnyObject
uniqueVideoID = uniqueID + "TEMPVIDEO.MOV"
let tempDataPath = tempDocumentsDirectory.appendingPathComponent(uniqueVideoID) as String
try? myVideoVarData.write(to: URL(fileURLWithPath: tempDataPath), options: [])
// Get the time value of the video
let fileURL = URL(fileURLWithPath: tempDataPath)
let asset = AVAsset(url: fileURL)
let duration : CMTime = asset.duration
// Remove the data from the temp Document Diroctory.
do{
let fileManager = FileManager.default
try fileManager.removeItem(atPath: tempDataPath)
} catch {
//Do nothing
}
// Check to see if video is under the 18500 (:30 seconds)
if duration.value <= 18500 {
// Write the data to the Document Directory
let docPaths = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)
let documentsDirectory: AnyObject = docPaths[0] as AnyObject
uniqueVideoID = uniqueID + "VIDEO.MOV"
let docDataPath = documentsDirectory.appendingPathComponent(uniqueVideoID) as String
try? myVideoVarData.write(to: URL(fileURLWithPath: docDataPath), options: [])
print("docDataPath under picker ",docDataPath)
print("Video saved to documents directory")
//Create a thumbnail image from the video
let assetImageGenerate = AVAssetImageGenerator(asset: asset)
assetImageGenerate.appliesPreferredTrackTransform = true
let time = CMTimeMake(asset.duration.value / 3, asset.duration.timescale)
if let videoImage = try? assetImageGenerate.copyCGImage(at: time, actualTime: nil) {
//Add thumbnail & video path to Post object
let video = Post(pathToVideo: URL(fileURLWithPath: docDataPath), thumbnail: UIImage(cgImage: videoImage))
posts.append(video)
print("Video saved to Post object")
}
}else{
print("Video not saved")
}
}
}
Specifically, this is where the video path and thumbnail are added to my object:
//Add thumbnail & video path to Post object
if let videoImage = try? assetImageGenerate.copyCGImage(at: time, actualTime: nil) {
let video = Post(pathToVideo: URL(fileURLWithPath: docDataPath), thumbnail: UIImage(cgImage: videoImage))
posts.append(video)
So I do give it the path to the video in the documents directory; how can I ensure that the data persists there?
EDIT:
To verify if the videos are being saved on the device, connect the device to Xcode and navigate to Window->Devices in Xcode. Then select your device on the left and find your app in the Installed Apps list. Select your app and click on the gear icon at the bottom of the list and press 'Show Containter'. Wait for a few seconds and you should see all the folders in your app.
Secondly, not sure why you are writing the video and deleting it and writing it back again and also why use 'try?' instead of actually catching any exceptions thrown during the file write?

How to get URL from UIImage?

I have an iOS app in which there are 2 ways the user can get a picture:
Select it from photos library (UIImagePickerController)
Click it from a custom made camera
Here is my code for clicking the image from a custom camera (this is within a custom class called Camera, which is a subclass of UIView)
func clickPicture(completion:#escaping (UIImage) -> Void) {
guard let videoConnection = stillImageOutput?.connection(withMediaType: AVMediaTypeVideo) else { return }
videoConnection.videoOrientation = .portrait
stillImageOutput?.captureStillImageAsynchronously(from: videoConnection, completionHandler: { (sampleBuffer, error) -> Void in
guard let buffer = sampleBuffer else { return }
let imageData = AVCaptureStillImageOutput.jpegStillImageNSDataRepresentation(buffer)
let dataProvider = CGDataProvider(data: imageData! as CFData)
let cgImageRef = CGImage(jpegDataProviderSource: dataProvider!, decode: nil, shouldInterpolate: true, intent: .defaultIntent)
let image = UIImage(cgImage: cgImageRef!, scale: 1, orientation: .right)
completion(image)
})
}
Here is how I click the image within the ViewController:
#IBAction func clickImage(_ sender: AnyObject) {
cameraView.clickPicture { (image) in
//use "image" variable
}
}
Later, I attempt to upload this picture to the user's iCloud account using CloudKit. However I receive an error saying the record is too large. I then came across this SO post, which says to use a CKAsset. However, the only constructor for a CKAsset requires a URL.
Is there a generic way I can get a URL from any UIImage? Otherwise, how can get a URL from the image I clicked using my custom camera (I have seen other posts about getting a url from a UIImagePickerController)? Thanks!
CKAsset represents some external file (image, video, binary data and etc). This is why it requires URL as init parameter.
In your case I would recommend to use following steps to upload large image to CloudKit:
Save UIImage to local storage (e.g. documents directory).
Initialize CKAsset with path to image in local storage.
Upload asset to Cloud.
Delete image from local storage when uploading completed.
Here is some code:
// Save image.
let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first!
let filePath = "\(path)/MyImageName.jpg"
UIImageJPEGRepresentation(image, 1)!.writeToFile(filePath, atomically: true)
let asset = CKAsset(fileURL: NSURL(fileURLWithPath: filePath)!)
// Upload asset here.
// Delete image.
do {
try FileManager.default.removeItem(atPath: filePath)
} catch {
print(error)
}

Getting empty result when trying to fetch the output of AVAssetExportSession from PHAsset

I'm attempting to build a small toy iOS app that merges two video assets from the user's photos library. I've gotten to the point where I have merged the videos using an AVMutableComposition instance and now I need to export the composition. I am doing so with the following code:
func saveEditedComposition(_ composition: AVMutableComposition) {
let documentDirectory = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
let savePath = (documentDirectory as NSString).strings(byAppendingPaths: ["mergeVideo.mov"])[0]
let url = NSURL(fileURLWithPath: savePath)
// Set up exporter
guard let exporter = AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetHighestQuality) else { return }
exporter.outputURL = url as URL
exporter.outputFileType = AVFileTypeQuickTimeMovie
exporter.shouldOptimizeForNetworkUse = true
// Perform the export
exporter.exportAsynchronously(completionHandler: { () -> Void in
// Upon completion of the export,
DispatchQueue.global().async {
if exporter.status == .completed {
let fetchResult = PHAsset.fetchAssets(withALAssetURLs: [exporter.outputURL!], options: nil))
let phAsset = fetchResult.firstObject! // Crashes here, returning nil.
}
}
})
}
The problem is, when the completionHandler is run, I am able to see that my exporter has completed without error (if exporter.status == .completed {), but when I try to access the asset at exporter.outputURL, it returns an empty PHFetchResult<PHAsset>. Can anyone see what I'm doing wrong here?
You're exporting the video to the file system (the Documents directory), yet fetching from the camera roll/ALAssetsLibrary.
It depends on what you need to do with the exported file. If it needs to be in the ALAssetsLibrary, you can use
ALAssetsLibrary().writeVideoAtPath(toSavedPhotosAlbum: exporter.outputURL!) { alAssetURL, error in
}
However ALAssetsLibrary is now deprecated and you are supposed to use PHPhotoLibrary. I don't know the Photos framework, but I think it works like this:
var placeHolder: PHObjectPlaceholder?
PHPhotoLibrary.shared().performChanges({
let changeRequest = PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: exporter.outputURL!)
if let changeRequest = changeRequest {
// maybe set date, location & favouriteness here?
placeHolder = changeRequest.placeholderForCreatedAsset
}
}) { success, error in
placeHolder?.localIdentifier // should identify asset from now on?
}
If you don't need the video to live in the camera roll, then you can work directly with the file in exporter.outputURL!.

Resources