Swift - Check if the file download from a URL is completed - swift

I am downloading mp4 clips from a M3U8 manifest which can have around 700 clips. Everything works fine to download them but what would be the best to check individual downloads are finished? When all the clips are downloaded, I merge them into one but I need to know when all my clips have been downloaded first.
This is the code snippet I use to download the video clip.
func download(video: String){
DispatchQueue.global(qos: .background).async {
if let url = URL(string: "http://SERVER/storage/sessions/SESSIONID/mp4_segments/\(video)"),
let urlData = NSData(contentsOf: url) {
let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0];
let fileName = video
let filePath = "\(documentsPath)/SegmentVideos/\(fileName)"
urlData.write(toFile: filePath, atomically: true)
}
}
}
This is the code snippet that reads the M3U8 file and splits it so I can grab the video clip's name.
func checkM3U8forClips(){
guard let url = url else {return}
do {
let contents = try String(contentsOf: url)
let splitContent = contents.components(separatedBy: "\n")
for split in splitContent {
if split.hasSuffix("mp4") {
download(video: split)
}
}
} catch {
print("error with mp4 segments: \(error.localizedDescription)")
}
}

One reason you are in a quandary is that this code is wrong:
if let url = URL(string: "http://SERVER/storage/sessions/SESSIONID/mp4_segments/\(video)"),
let urlData = NSData(contentsOf: url) {
You must never use NSData(contentsOf:) to do networking. If you want to network, then network: use URLSession and a proper data task or download task. Now you get the callbacks you need; if you do it in the full form you get a full set of delegate callbacks that tell you exactly when a download has succeeded and completed (or failed).
As for your overall question, i.e. how can I know when multiple asynchronous operations have all finished, that is what things like DispatchGroup, or operation dependencies, or the new Combine framework are for.

Related

How to save multi images from URLs to local storage in swift?

I am developing Restaurant app.
There are 340 foods data.
I am getting this data from backend that developed with Laravel.
App is working well in online.
But, although network is turned off, All foods data should be displayed in app.
So, I tried to save foods data to local.
It is good to save text data to local(exactly, UserDefaults and FileSystem).
But, when I try to save images to local from urls, It occur error and don't save to local exactly.
Have you ever seen such problems?
If yes, I appreciate your help.
Thanks
I don't know exactly what error you faced, but I think the answer to the link can help you.
How do I make JSON data persistent for offline use (Swift 4)
Additionally, image data would be good to be cached.
If you communicate based on URLSession, you can process caching as below.
Saving an Image to a Cache
Cache.imageCache.setObject(image, forKey: url.absoluteString as NSString)
Bring up cached images
let cacheImage = Cache.imageCache.object(forKey: url.absoluteString as NSString)
Code
class Cache {
static let imageCache = NSCache<NSString, UIImage>()
}
extension UIImageView {
func imageDownload(url: URL, contentMode mode: UIView.ContentMode = .scaleAspectFit) {
if let cacheImage = Cache.imageCache.object(forKey: url.absoluteString as NSString) {
DispatchQueue.main.async() { [weak self] in
self?.contentMode = mode
self?.image = cacheImage
}
}
else {
var request = URLRequest(url: url)
request.httpMethod = "GET"
URLSession.shared.dataTask(with: request) { data, response, error in
guard
let httpURLResponse = response as? HTTPURLResponse, httpURLResponse.statusCode == 200,
let mimeType = response?.mimeType, mimeType.hasPrefix("image"),
let data = data, error == nil,
let image = UIImage(data: data)
else {
print("Download image fail : \(url)")
return
}
DispatchQueue.main.async() { [weak self] in
print("Download image success \(url)")
Cache.imageCache.setObject(image, forKey: url.absoluteString as NSString)
self?.contentMode = mode
self?.image = image
}
}.resume()
}
}
}
To make caching more convenient, use a library called Kingfisher.
Kingfisher
You need to manage lazy loadings, you can use SDWebImages instead of manage it manually
SDWebImages will automatically manage your images caches and will also load them without internet here is a simple usage of it
add pod in podfile
pod 'SDWebImage'
usagae:
import SDWebImage
imageView.sd_setImage(with: URL(string: "http://www.example.com/path/to/image.jpg"), placeholderImage: UIImage(named: "placeholder.png"))

Swift: OperationQueue - wait for asynchronous calls to finish before starting next (have maximum of 2 URLRequests running at a time)

tl;dr I have an OperationQueue and I want to have two operations running at the time. Those operations download something asynchronously hence they all get triggered at once instead of running one after another.
I fill a table of very large images by doing doing the following for each of the images:
public func imageFromUrl(_ urlString: String) {
if let url = NSURL(string: urlString) {
let request = NSURLRequest(url: url as URL)
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
let task = session.dataTask(with: request as URLRequest, completionHandler: {(data, response, error) in
if let imageData = data as Data? {
DispatchQueue.main.async {
self.setImageData(imageData)
}
}
});
task.resume()
}
calling it like imageView.imageFromUrl(...).
On slower internet connections, the calls stack and it starts loading every image at once. The user then has to wait for the downloads to "fight" each other and is staring at a blank screen for a while before the images all appear at once (more or less). It would be a much better experience for the user if one image appeared after another.
I thought about queuing up the items, downloading the first of the list, drop it from the list and call the function recursively like this:
func doImageQueue(){
let queueItem = imageQueue[0]
if let url = NSURL(string: (queueItem.url)) {
print("if let url")
let request = NSURLRequest(url: url as URL)
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
let task = session.dataTask(with: request as URLRequest, completionHandler: {(data, response, error) in
print("in")
if let imageData = data as Data? {
print("if let data")
DispatchQueue.main.async {
queueItem.imageView.setImageData(imageData)
self.imageQueue.remove(at: 0)
if(self.imageQueue.count>0) {
self.doImageQueue()
}
}
}
});
task.resume()
}
}
This does load the images one after another, by I think it's a waste of time not to have at least 2 requests running at a time. Making my current implementation handle 2 images at the same time would result in big spaghetti code so I've looked into Swift's OperationQueue.
I would do
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 2
for (all images) {
queue.addOperation {
imageView.imageFromUrl(imageURL
}
}
But this also triggers all the calls at once, probably due to the fact that the requests run asynchronously and the method call ends before waiting for the image to be downloaded. How can I deal with that? The app will also run on watchOS, maybe there is a library for this already but I don't think this should be too hard to achieve without a library. Caching isn't a concern.
Your original code with the operation queue and your original iamgeFromUrl method are all you need if you make one small change to imageFromUrl. You need to add a couple lines of code to ensure that imageFromUrl doesn't return until the download is complete.
This can be done using a semaphore.
public func imageFromUrl(_ urlString: String) {
if let url = URL(string: urlString) {
let request = URLRequest(url: url)
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
let semaphore = DispatchSemaphore(value: 0)
let task = session.dataTask(with: request, completionHandler: {(data, response, error) in
semaphore.signal()
if let imageData = data as Data? {
DispatchQueue.main.async {
self.setImageData(imageData)
}
}
});
task.resume()
semaphore.wait()
}
}
As written now, the imageFromUrl will only return once the download completes. This now allows the operation queue to properly run the 2 desired concurrent operations.
Also note the code is modified to avoid using NSURL and NSURLRequest.

How can I play a sound from the asset catalog using AVAudioPlayerNode?

I'm trying to use an AVAudioPlayerNode to play sounds from the Assets.xcassets asset catalog, but I can't figure out how to do it.
I've been using AVAudioPlayer, which can be initialized with an NSDataAsset like this:
let sound = NSDataAsset(name: "beep")!
do {
let player = try AVAudioPlayer(data: sound.data, fileTypeHint: AVFileTypeWAVE)
player.prepareToPlay()
player.play()
} catch {
print("Failed to create AVAudioPlayer")
}
I want to use an AVAudioPlayerNode instead (for pitch shifting and other reasons). I can create the engine and hook up the node OK:
var engine = AVAudioEngine()
func playSound(named name: String) {
let mixer = engine.mainMixerNode
let playerNode = AVAudioPlayerNode()
engine.attach(playerNode)
engine.connect(playerNode, to: mixer, format: mixer.outputFormat(forBus: 0))
// play the file (this is what I don't know how to do)
}
It looks like the method to use for playing the file is playerNode.scheduleFile(). It takes an AVAudioFile, so I thought I'd try to make one. But the initializer for AVAudioFile wants a URL. As far as I can tell, assets in the asset catalog are not available by URL. I can get the data directly using NSDataAsset, but there doesn't seem to be any way to use it to populate an AVAudioFile.
Is it possible to play sounds from the asset catalog with an AVAudioPlayerNode? And if so, how?
OK so your problem is that you would like to get a URL from a file in your Asset catalog right?
I've looked around but only found this answer
As it says
It basically just gets image from assets, saves its data to disk and return file URL
You should probably change it to look for MP3 files (or WAV or whatever you prefer, maybe that could be an input parameter)
So you could end up with something like:
enum SoundType: String {
case mp3 = "mp3"
case wav = "wav"
}
class AssetExtractor {
static func createLocalUrl(forSoundNamed name: String, ofType type: SoundType = .mp3) -> URL? {
let fileManager = FileManager.default
let cacheDirectory = fileManager.urls(for: .cachesDirectory, in: .userDomainMask)[0]
let url = cacheDirectory.appendingPathComponent("\(name).\(type)")
guard fileManager.fileExists(atPath: url.path) else {
guard
let image = UIImage(named: name),
let data = UIImagePNGRepresentation(image)
else { return nil }
fileManager.createFile(atPath: url.path, contents: data, attributes: nil)
return url
}
return url
}
}
Maybe a bit far fetched but I haven't found any other options.
Hope that helps you.

Load temp video and play

I need to play continuously a video stored inside the temp directory.
func setVideo(url vid: String!) {
let directory = NSTemporaryDirectory()
let tempURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("tempMovie\(Date().timeIntervalSince1970)").appendingPathExtension("mp4")
let tempFile = NSURL.fileURL(withPathComponents: [directory])
let file = vid.components(separatedBy: ".")
guard let path = Bundle.main.path(forResource: "\(tempURL)", ofType:file[2]) else {
debugPrint( "\(file.joined(separator: ".")) not found with path: \(file[0] + "." + file[1])")
return
}
let player = AVPlayer(url: URL(fileURLWithPath: path))
let playerLayer = AVPlayerLayer(player: player)
playerLayer.frame = self.view.bounds
self.videoView.layer.addSublayer(playerLayer)
player.play()
NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: self.player.currentItem, queue: nil, using: { (_) in
DispatchQueue.main.async {
self.player?.seek(to: kCMTimeZero)
self.player?.play()
}
})
}
The video location and file url is:
file:///private/var/mobile/Containers/Data/Application/E7D12401-F147-4905-83BE-72909F91E004/tmp/tempMovie1501672791.33525.mp4
The continuous loop part works from a previous project however I can't seem to get the temp stored video to load inside the player. I've tried to get the temp directory manually but it didn't work.
Moving the video from tmp to Documents directory worked for me.
This is how you do it:
let documentsDirectoryURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let destinationURL = documentsDirectoryURL.appendingPathComponent("filename.fileExtension")
do {
try FileManager.default.moveItem(at: previousTempLocation, to: destinationURL)
} catch let error as NSError { print(error.localizedDescription)}
let player = AVPlayer(url: destinationURL)
Apple says this on /tmp directory
Use this directory to write temporary files that do not need to
persist between launches of your app. Your app should remove files
from this directory when they are no longer needed; however, the
system may purge this directory when your app is not running.
Apple also recommends this for user data
Put user data in Documents/. User data generally includes any files
you might want to expose to the user—anything you might want the user
to create, import, delete or edit. Video and audio apps may even include
files that the user has downloaded to watch or listen to later.

Swift Haneke: cache doesn't resolve

I'm trying to pre-load some images with the following code:
let thumbnailUrl = NSURL(string: urlString)
let fetcher = NetworkFetcher<UIImage>(URL: thumbnailUrl)
Shared.imageCache.fetch(fetcher) {
println("Finished")
}
But afterwards when I try to set it to the imageview, it downloads it again from the network instead of reading it from the cache. This is the code:
self.imageView.hnk_setImageFromURL(
NSURL(string: urlString)
success: { thumbnail in
println("Finished setting image")
}
)
Is this a bug or maybe I missunderstood the usage of imageCache.fetch()?
PD: I put breakpoints in the whole code, and I can guarantee the key for the cache (in this case, the url) is exactly the same, so I have no clue why the cache isn't resolving when used with .hnk_setImageFromURL()
I was stuck on this for 3 days. I hope this answer might help someone else in the future.
The issue was that I didn't specify a Format Name to the fetcher, while the UIImageView.hnk_setImageFromURL() does. The way to fix it is:
let thumbnailUrl = NSURL(string: urlString)
let fetcher = NetworkFetcher<UIImage>(URL: thumbnailUrl)
let cache = Shared.imageCache
let format = self.imageView.hnk_format
cache.fetch(fetcher, formatName: format.name) {
println("Finished")
}