19

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.

Melanie Journe
  • 1,249
  • 5
  • 16
  • 36
  • 1
    If you want to save a file to camera roll, take a look at PHPhotoLibrary classes and the method `creationRequestForAssetFromVideoAtFileURL` of `PHAssetChangeRequest` – Nimble Feb 18 '16 at 11:30
  • In the doc here : https://developer.apple.com/library/prerelease/ios/documentation/Photos/Reference/PHAssetChangeRequest_Class/index.html#//apple_ref/occ/clm/PHAssetChangeRequest/creationRequestForAssetFromVideoAtFileURL , I don't understand what fileURL is. – Melanie Journe Feb 18 '16 at 13:12
  • `fileURL` is a link to a video file. Did you try to use your `filePath` as a `fileURL`? It may look like this `PHAssetChangeRequest.creationRequestForAssetFromVideoAtFileURL(NSURL(fileURLWithPath: filePath))` – Nimble Feb 18 '16 at 13:18
  • by the way, you don't need to save a file manualy. Use NSURLSession downloadTaskWithURL to download a video file. There is a completion handler which returns `location:NSURL` which is the location of a temporary file where the server’s response is stored. Use this `location:NSURL` in `creationRequestForAssetFromVideoAtFileURL` – Nimble Feb 18 '16 at 13:21
  • I have my url in string, then i do let url = NSURL(string: fileURL); and I use this url in my second peace of code, but at execution Xcode stop on this ligne : albumChangeRequest!.addAssets([assetPlaceHolder!]) – Melanie Journe Feb 18 '16 at 13:41
  • http://stackoverflow.com/questions/35503723/swift-downloading-video-with-downloadtaskwithurl – Leo Dabus Sep 15 '16 at 09:04

4 Answers4

46

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!")
                }
            }
        }
    }
}
Tulleb
  • 8,919
  • 8
  • 27
  • 55
Nimble
  • 1,009
  • 1
  • 11
  • 11
  • 2
    Thanks a lot ! You're a life savior :) – Melanie Journe Feb 18 '16 at 16:42
  • @Nimble do you have any idea that can we store video directly to photo library without storing it to document folder first? – Urmi Sep 13 '17 at 16:07
  • 1
    You are saving the file in the documents directory but it is not being deleted afterwards (at least, in these code). You should also consider cleaning up. If you want automatic cleanup, why not save in the temp directory instead? Or, if you want it manually, do the clean up in the completion block. – yoninja Nov 07 '17 at 17:44
12

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!")
                }
            }
        }
    }
}
Yuval Tal
  • 645
  • 7
  • 12
  • 1
    do you have any idea that can we store video directly to photo library without storing it to document folder first? – Urmi Sep 13 '17 at 16:08
  • Not that I'm aware of. You should look at the photo library as a database, therefore downloading into a "database" does not make sense. – Yuval Tal Sep 13 '17 at 19:00
  • Okay. but you have any idea how much data (GB) we can store in for specific application. Just like WhatsApp stores all images and video to photo library. same time is it saving them to document folder? cause I am developing similar application download images and videos from sever and save it to document folder. – Urmi Sep 13 '17 at 19:13
  • I don't think there is a limit. Take a look at Apple's guidelines https://developer.apple.com/icloud/documentation/data-storage/index.html – Yuval Tal Sep 14 '17 at 17:27
1
PHPhotoLibrary.shared().performChanges({
     PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: video.url!)}) {
     saved, error in
     if saved {
          print("Save status SUCCESS")
     }
}
Jennis Vaishnav
  • 331
  • 7
  • 29
  • 5
    Please [format your code](https://meta.stackexchange.com/questions/22186/how-do-i-format-my-code-blocks) and explain what it does – Nino Filiu Mar 06 '19 at 13:02
0

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()

Mehdi S.
  • 471
  • 1
  • 4
  • 18
  • 1
    Receiving this error: `The operation couldn’t be completed. (PHPhotosErrorDomain error 3302.)` – Emm Oct 26 '22 at 20:56