Swift - Downloading Video with Downloadtaskwithurl

Swift - Downloading video with downloadTaskWithURL

Please check comments through the code:

Xcode 8 • Swift 3

import UIKit
import Photos

class ViewController: UIViewController {

func downloadVideoLinkAndCreateAsset(_ videoLink: String) {

// use guard to make sure you have a valid url
guard let videoURL = URL(string: videoLink) else { return }

guard let documentsDirectoryURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return }

// check if the file already exist at the destination folder if you don't want to download it twice
if !FileManager.default.fileExists(atPath: documentsDirectoryURL.appendingPathComponent(videoURL.lastPathComponent).path) {

// set up your download task
URLSession.shared.downloadTask(with: videoURL) { (location, response, error) -> Void in

// use guard to unwrap your optional url
guard let location = location else { return }

// create a deatination url with the server response suggested file name
let destinationURL = documentsDirectoryURL.appendingPathComponent(response?.suggestedFilename ?? videoURL.lastPathComponent)

do {

try FileManager.default.moveItem(at: location, to: destinationURL)

PHPhotoLibrary.requestAuthorization({ (authorizationStatus: PHAuthorizationStatus) -> Void in

// check if user authorized access photos for your app
if authorizationStatus == .authorized {
PHPhotoLibrary.shared().performChanges({
PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: destinationURL)}) { completed, error in
if completed {
print("Video asset created")
} else {
print(error)
}
}
}
})

} catch { print(error) }

}.resume()

} else {
print("File already exists at destination url")
}

}

override func viewDidLoad() {
super.viewDidLoad()
downloadVideoLinkAndCreateAsset("https://www.yourdomain.com/yourmovie.mp4")
}

}

try to download video locall on iOS swift

There are several things wrong with your code.

First the way you use dispatch queues.
The right way for a background queue is:

DispatchQueue.global(qos: .default).async {
// Code ...
}

or for the main queue:

DispatchQueue.main.async {
// Code ...
}

Second, in order to use the Photos framework, you need to import it. Add import Photos at the top of your file.

Third, there are several API changes in Swift 3, but the compiler should offer to auto-fix them (for example you should use URL and Data everywhere, not NSURL or NSData). Alternatively, you can convert a Swift 2 project into a Swift 3 project by going to Edit -> Convert -> To Current Swift Syntax

And last, the way you download your video is wrong, it will be entirely loaded in memory before being saved to disk. If the video is big this will cause issues. Look into the URLSession API for a way to download a ressource directly into a file.

Swift - Download a video from distant URL and save it in an photo album

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!")
}
}
}
}
}

How do I get the NSURLResponse before the downloadTaskWithURL finishes?

Do not use Shared session

Keep a session property,use this function to init.

 init(configuration configuration: NSURLSessionConfiguration?,
delegate delegate: NSURLSessionDelegate?,
delegateQueue queue: NSOperationQueue?) -> NSURLSession

Then use dataTask to download image

In this delegate method you can get Response

Then change the dataTask to downlaodTask

optional func URLSession(_ session: NSURLSession,
dataTask dataTask: NSURLSessionDataTask,
didReceiveResponse response: NSURLResponse,
completionHandler completionHandler: (NSURLSessionResponseDisposition) -> Void)

Example code:

 import UIKit
class ViewController: UIViewController,NSURLSessionDelegate,NSURLSessionDataDelegate,NSURLSessionDownloadDelegate{
var session:NSURLSession?
var dataTask:NSURLSessionDataTask?
let url = NSURL(string:"http://www.zastavki.com/pictures/originals/2013/Photoshop_Image_of_the_horse_053857_.jpg")!
var infoDic = NSMutableDictionary()
override func viewDidLoad() {
super.viewDidLoad()
let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
let manqueue = NSOperationQueue.mainQueue()
session = NSURLSession(configuration: configuration, delegate:self, delegateQueue: manqueue)
dataTask = session?.dataTaskWithRequest(NSURLRequest(URL: url))
dataTask?.resume()

// Do any additional setup after loading the view, typically from a nib.
}
func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveResponse response: NSURLResponse, completionHandler: (NSURLSessionResponseDisposition) -> Void) {
NSLog("%@",response.description)
completionHandler(NSURLSessionResponseDisposition.BecomeDownload)
}
func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didBecomeDownloadTask downloadTask: NSURLSessionDownloadTask) {
downloadTask.resume()
}
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didFinishDownloadingToURL location: NSURL) {
NSLog("%@",location);
//Get response
NSLog("%@", downloadTask.response!.description)

}
}

How To Download Multiple Files Sequentially using NSURLSession downloadTask in Swift

Your code won't work because URLSessionDownloadTask runs asynchronously. Thus the BlockOperation completes before the download is done and therefore while the operations fire off sequentially, the download tasks will continue asynchronously and in parallel.

While there are work-arounds one can contemplate (e.g., recursive patterns initiating one request after the prior one finishes, non-zero semaphore pattern on background thread, etc.), the elegant solution is one of the proven asynchronous frameworks.

In iOS 15 and later, we would use async-await method download(from:delegate:), e.g.

func downloadFiles() async throws {
let folder = try! FileManager.default
.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true)

for url in urls {
let (source, _) = try await URLSession.shared.download(from: url)
let destination = folder.appendingPathComponent(url.lastPathComponent)
try FileManager.default.moveItem(at: source, to: destination)
}
}

Where

override func viewDidLoad() {
super.viewDidLoad()

Task {
do {
try await downloadFiles()
} catch {
print(error)
}
}
}

That only works in iOS 15 and later. But Xcode 13.2 and later actually lets you use async-await in iOS 13, but you just have to write your own async rendition of download:

extension URLSession {
@available(iOS, deprecated: 15, message: "Use `download(from:delegate:)` instead")
func download(with url: URL) async throws -> (URL, URLResponse) {
try await download(with: URLRequest(url: url))
}

@available(iOS, deprecated: 15, message: "Use `download(for:delegate:)` instead")
func download(with request: URLRequest) async throws -> (URL, URLResponse) {
let sessionTask = URLSessionTaskActor()

return try await withTaskCancellationHandler {
Task { await sessionTask.cancel() }
} operation: {
try await withCheckedThrowingContinuation { continuation in
Task {
await sessionTask.start(downloadTask(with: request) { location, response, error in
guard let location = location, let response = response else {
continuation.resume(throwing: error ?? URLError(.badServerResponse))
return
}

// since continuation can happen later, let's figure out where to store it ...

let tempURL = URL(fileURLWithPath: NSTemporaryDirectory())
.appendingPathComponent(UUID().uuidString)
.appendingPathExtension(request.url!.pathExtension)

// ... and move it to there

do {
try FileManager.default.moveItem(at: location, to: tempURL)
} catch {
continuation.resume(throwing: error)
return
}

continuation.resume(returning: (tempURL, response))
})
}
}
}
}
}

private extension URLSession {
actor URLSessionTaskActor {
weak var task: URLSessionTask?

func start(_ task: URLSessionTask) {
self.task = task
task.resume()
}

func cancel() {
task?.cancel()
}
}
}

And you would then call this rendition for iOS 13 and later:

func downloadFiles() async throws {
let folder = try! FileManager.default
.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true)

for url in urls {
let (source, _) = try await URLSession.shared.download(with: url)
let destination = folder.appendingPathComponent(url.lastPathComponent)
try FileManager.default.moveItem(at: source, to: destination)
}
}

In iOS versions prior to 13, if you wanted to control the degree of concurrency of a series of asynchronous tasks, we would reach for an asynchronous Operation subclass.

Or, in iOS 13 and later, you might also consider Combine. (There are other third-party asynchronous programming frameworks, but I will restrict myself to Apple-provided approaches.)

Both of these are described below in my original answer.


Operation

To address this, you can wrap the requests in asynchronous Operation subclass. See Configuring Operations for Concurrent Execution in the Concurrency Programming Guide for more information.

But before I illustrate how to do this in your situation (the delegate-based URLSession), let me first show you the simpler solution when using the completion handler rendition. We'll later build upon this for your more complicated question. So, in Swift 3 and later:

class DownloadOperation : AsynchronousOperation {
var task: URLSessionTask!

init(session: URLSession, url: URL) {
super.init()

task = session.downloadTask(with: url) { temporaryURL, response, error in
defer { self.finish() }

guard
let httpResponse = response as? HTTPURLResponse,
200..<300 ~= httpResponse.statusCode
else {
// handle invalid return codes however you'd like
return
}

guard let temporaryURL = temporaryURL, error == nil else {
print(error ?? "Unknown error")
return
}

do {
let manager = FileManager.default
let destinationURL = try manager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
.appendingPathComponent(url.lastPathComponent)
try? manager.removeItem(at: destinationURL) // remove the old one, if any
try manager.moveItem(at: temporaryURL, to: destinationURL) // move new one there
} catch let moveError {
print("\(moveError)")
}
}
}

override func cancel() {
task.cancel()
super.cancel()
}

override func main() {
task.resume()
}

}

Where

/// Asynchronous operation base class
///
/// This is abstract to class emits all of the necessary KVO notifications of `isFinished`
/// and `isExecuting` for a concurrent `Operation` subclass. You can subclass this and
/// implement asynchronous operations. All you must do is:
///
/// - override `main()` with the tasks that initiate the asynchronous task;
///
/// - call `completeOperation()` function when the asynchronous task is done;
///
/// - optionally, periodically check `self.cancelled` status, performing any clean-up
/// necessary and then ensuring that `finish()` is called; or
/// override `cancel` method, calling `super.cancel()` and then cleaning-up
/// and ensuring `finish()` is called.

class AsynchronousOperation: Operation {

/// State for this operation.

@objc private enum OperationState: Int {
case ready
case executing
case finished
}

/// Concurrent queue for synchronizing access to `state`.

private let stateQueue = DispatchQueue(label: Bundle.main.bundleIdentifier! + ".rw.state", attributes: .concurrent)

/// Private backing stored property for `state`.

private var rawState: OperationState = .ready

/// The state of the operation

@objc private dynamic var state: OperationState {
get { return stateQueue.sync { rawState } }
set { stateQueue.sync(flags: .barrier) { rawState = newValue } }
}

// MARK: - Various `Operation` properties

open override var isReady: Bool { return state == .ready && super.isReady }
public final override var isExecuting: Bool { return state == .executing }
public final override var isFinished: Bool { return state == .finished }

// KVO for dependent properties

open override class func keyPathsForValuesAffectingValue(forKey key: String) -> Set<String> {
if ["isReady", "isFinished", "isExecuting"].contains(key) {
return [#keyPath(state)]
}

return super.keyPathsForValuesAffectingValue(forKey: key)
}

// Start

public final override func start() {
if isCancelled {
finish()
return
}

state = .executing

main()
}

/// Subclasses must implement this to perform their work and they must not call `super`. The default implementation of this function throws an exception.

open override func main() {
fatalError("Subclasses must implement `main`.")
}

/// Call this function to finish an operation that is currently executing

public final func finish() {
if !isFinished { state = .finished }
}
}

Then you can do:

for url in urls {
queue.addOperation(DownloadOperation(session: session, url: url))
}

So that's one very easy way to wrap asynchronous URLSession/NSURLSession requests in asynchronous Operation/NSOperation subclass. More generally, this is a useful pattern, using AsynchronousOperation to wrap up some asynchronous task in an Operation/NSOperation object.

Unfortunately, in your question, you wanted to use delegate-based URLSession/NSURLSession so you could monitor the progress of the downloads. This is more complicated.

This is because the "task complete" NSURLSession delegate methods are called at the session object's delegate. This is an infuriating design feature of NSURLSession (but Apple did it to simplify background sessions, which isn't relevant here, but we're stuck with that design limitation).

But we have to asynchronously complete the operations as the tasks finish. So we need some way for the session to figure out which operation to complete when didCompleteWithError is called. Now you could have each operation have its own NSURLSession object, but it turns out that this is pretty inefficient.

So, to handle that, I maintain a dictionary, keyed by the task's taskIdentifier, which identifies the appropriate operation. That way, when the download finishes, you can "complete" the correct asynchronous operation. Thus:

/// Manager of asynchronous download `Operation` objects

class DownloadManager: NSObject {

/// Dictionary of operations, keyed by the `taskIdentifier` of the `URLSessionTask`

fileprivate var operations = [Int: DownloadOperation]()

/// Serial OperationQueue for downloads

private let queue: OperationQueue = {
let _queue = OperationQueue()
_queue.name = "download"
_queue.maxConcurrentOperationCount = 1 // I'd usually use values like 3 or 4 for performance reasons, but OP asked about downloading one at a time

return _queue
}()

/// Delegate-based `URLSession` for DownloadManager

lazy var session: URLSession = {
let configuration = URLSessionConfiguration.default
return URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
}()

/// Add download
///
/// - parameter URL: The URL of the file to be downloaded
///
/// - returns: The DownloadOperation of the operation that was queued

@discardableResult
func queueDownload(_ url: URL) -> DownloadOperation {
let operation = DownloadOperation(session: session, url: url)
operations[operation.task.taskIdentifier] = operation
queue.addOperation(operation)
return operation
}

/// Cancel all queued operations

func cancelAll() {
queue.cancelAllOperations()
}

}

// MARK: URLSessionDownloadDelegate methods

extension DownloadManager: URLSessionDownloadDelegate {

func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
operations[downloadTask.taskIdentifier]?.urlSession(session, downloadTask: downloadTask, didFinishDownloadingTo: location)
}

func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
operations[downloadTask.taskIdentifier]?.urlSession(session, downloadTask: downloadTask, didWriteData: bytesWritten, totalBytesWritten: totalBytesWritten, totalBytesExpectedToWrite: totalBytesExpectedToWrite)
}
}

// MARK: URLSessionTaskDelegate methods

extension DownloadManager: URLSessionTaskDelegate {

func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
let key = task.taskIdentifier
operations[key]?.urlSession(session, task: task, didCompleteWithError: error)
operations.removeValue(forKey: key)
}

}

/// Asynchronous Operation subclass for downloading

class DownloadOperation : AsynchronousOperation {
let task: URLSessionTask

init(session: URLSession, url: URL) {
task = session.downloadTask(with: url)
super.init()
}

override func cancel() {
task.cancel()
super.cancel()
}

override func main() {
task.resume()
}
}

// MARK: NSURLSessionDownloadDelegate methods

extension DownloadOperation: URLSessionDownloadDelegate {

func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
guard
let httpResponse = downloadTask.response as? HTTPURLResponse,
200..<300 ~= httpResponse.statusCode
else {
// handle invalid return codes however you'd like
return
}

do {
let manager = FileManager.default
let destinationURL = try manager
.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
.appendingPathComponent(downloadTask.originalRequest!.url!.lastPathComponent)
try? manager.removeItem(at: destinationURL)
try manager.moveItem(at: location, to: destinationURL)
} catch {
print(error)
}
}

func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
let progress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
print("\(downloadTask.originalRequest!.url!.absoluteString) \(progress)")
}
}

// MARK: URLSessionTaskDelegate methods

extension DownloadOperation: URLSessionTaskDelegate {

func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
defer { finish() }

if let error = error {
print(error)
return
}

// do whatever you want upon success
}

}

And then use it like so:

let downloadManager = DownloadManager()

override func viewDidLoad() {
super.viewDidLoad()

let urlStrings = [
"http://spaceflight.nasa.gov/gallery/images/apollo/apollo17/hires/s72-55482.jpg",
"http://spaceflight.nasa.gov/gallery/images/apollo/apollo10/hires/as10-34-5162.jpg",
"http://spaceflight.nasa.gov/gallery/images/apollo-soyuz/apollo-soyuz/hires/s75-33375.jpg",
"http://spaceflight.nasa.gov/gallery/images/apollo/apollo17/hires/as17-134-20380.jpg",
"http://spaceflight.nasa.gov/gallery/images/apollo/apollo17/hires/as17-140-21497.jpg",
"http://spaceflight.nasa.gov/gallery/images/apollo/apollo17/hires/as17-148-22727.jpg"
]
let urls = urlStrings.compactMap { URL(string: $0) }

let completion = BlockOperation {
print("all done")
}

for url in urls {
let operation = downloadManager.queueDownload(url)
completion.addDependency(operation)
}

OperationQueue.main.addOperation(completion)
}

See revision history for Swift 2 implementation.


Combine

For Combine, the idea would be to create a Publisher for URLSessionDownloadTask. Then you can do something like:

var downloadRequests: AnyCancellable?

/// Download a series of assets

func downloadAssets() {
downloadRequests = downloadsPublisher(for: urls, maxConcurrent: 1).sink { completion in
switch completion {
case .finished:
print("done")

case .failure(let error):
print("failed", error)
}
} receiveValue: { destinationUrl in
print(destinationUrl)
}
}

/// Publisher for single download
///
/// Copy downloaded resource to caches folder.
///
/// - Parameter url: `URL` being downloaded.
/// - Returns: Publisher for the URL with final destination of the downloaded asset.

func downloadPublisher(for url: URL) -> AnyPublisher<URL, Error> {
URLSession.shared.downloadTaskPublisher(for: url)
.tryCompactMap {
let destination = try FileManager.default
.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
.appendingPathComponent(url.lastPathComponent)
try FileManager.default.moveItem(at: $0.location, to: destination)
return destination
}
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
}

/// Publisher for a series of downloads
///
/// This downloads not more than `maxConcurrent` assets at a given time.
///
/// - Parameters:
/// - urls: Array of `URL`s of assets to be downloaded.
/// - maxConcurrent: The maximum number of downloads to run at any given time (default 4).
/// - Returns: Publisher for the URLs with final destination of the downloaded assets.

func downloadsPublisher(for urls: [URL], maxConcurrent: Int = 4) -> AnyPublisher<URL, Error> {
Publishers.Sequence(sequence: urls.map { downloadPublisher(for: $0) })
.flatMap(maxPublishers: .max(maxConcurrent)) { $0 }
.eraseToAnyPublisher()
}

Now, unfortunately, Apple supplies a DataTaskPublisher (which loads the full asset into memory which is not acceptable solution for large assets), but one can refer to their source code and adapt it to create a DownloadTaskPublisher:

//  DownloadTaskPublisher.swift
//
// Created by Robert Ryan on 9/28/20.
//
// Adapted from Apple's `DataTaskPublisher` at:
// https://github.com/apple/swift/blob/88b093e9d77d6201935a2c2fb13f27d961836777/stdlib/public/Darwin/Foundation/Publishers%2BURLSession.swift

import Foundation
import Combine

// MARK: Download Tasks

@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
extension URLSession {
/// Returns a publisher that wraps a URL session download task for a given URL.
///
/// The publisher publishes temporary when the task completes, or terminates if the task fails with an error.
///
/// - Parameter url: The URL for which to create a download task.
/// - Returns: A publisher that wraps a download task for the URL.

public func downloadTaskPublisher(for url: URL) -> DownloadTaskPublisher {
let request = URLRequest(url: url)
return DownloadTaskPublisher(request: request, session: self)
}

/// Returns a publisher that wraps a URL session download task for a given URL request.
///
/// The publisher publishes download when the task completes, or terminates if the task fails with an error.
///
/// - Parameter request: The URL request for which to create a download task.
/// - Returns: A publisher that wraps a download task for the URL request.

public func downloadTaskPublisher(for request: URLRequest) -> DownloadTaskPublisher {
return DownloadTaskPublisher(request: request, session: self)
}

public struct DownloadTaskPublisher: Publisher {
public typealias Output = (location: URL, response: URLResponse)
public typealias Failure = URLError

public let request: URLRequest
public let session: URLSession

public init(request: URLRequest, session: URLSession) {
self.request = request
self.session = session
}

public func receive<S: Subscriber>(subscriber: S) where Failure == S.Failure, Output == S.Input {
subscriber.receive(subscription: Inner(self, subscriber))
}

private typealias Parent = DownloadTaskPublisher
private final class Inner<Downstream: Subscriber>: Subscription, CustomStringConvertible, CustomReflectable, CustomPlaygroundDisplayConvertible
where
Downstream.Input == Parent.Output,
Downstream.Failure == Parent.Failure
{
typealias Input = Downstream.Input
typealias Failure = Downstream.Failure

private let lock: NSLocking
private var parent: Parent? // GuardedBy(lock)
private var downstream: Downstream? // GuardedBy(lock)
private var demand: Subscribers.Demand // GuardedBy(lock)
private var task: URLSessionDownloadTask! // GuardedBy(lock)
var description: String { return "DownloadTaskPublisher" }
var customMirror: Mirror {
lock.lock()
defer { lock.unlock() }


Related Topics



Leave a reply



Submit