I triggered a background download of an image. It succeeds - location is a path to the actual image. I get no error message, but also the image does NOT show up in the photos app. I have set the NSPhotoLibraryUsageDescription Info.plist key. The app has rights to access the photos. I know, this code triggers another background thread, but that shouldn't be a problem, because the "location" file is still there after "didFinishDownloadingTo" finished. Is there anything else to take care of when storing JPG files to the camera roll?
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
PHPhotoLibrary.shared().performChanges({
print("starting")
let req = PHAssetCreationRequest.forAsset()
req.isFavorite = true;
let options = PHAssetResourceCreationOptions();
options.shouldMoveFile = true;
req.addResource(with: PHAssetResourceType.photo, fileURL: location, options: options)
})
}
You're using addResource incorrectly. What you have is not a resource; it's the image. So first, load the data from the URL and turn it into an image:
if let url = location, let d = try? Data(contentsOf:url) {
let im = UIImage(data:d)
}
Now just add the image as an asset:
PHPhotoLibrary.shared().performChanges({
PHAssetChangeRequest.creationRequestForAsset(from: im!)
})
One you've persuaded yourself that that works, you can start dressing it up.
Related
i have a problem described in title. you may see source code in my repository (https://github.com/Hudayberdyyev/custom_download_manager) . i will try to briefly explain the problem. I am trying to write a download manager based on this repo (https://github.com/r-plus/HLSion). and basically it consists of 3 parts:
SessionManager (Which managed all of sessions)
HLSData (HLSData model which initialized same as the code below. it is like an intermediary between the session manager )
public convenience init(url: URL, options: [String: Any]? = nil, name: String) {
let urlAsset = AVURLAsset(url: url, options: options)
self.init(asset: urlAsset, description: name)
}
AssetStore (It's managed HLSData.plist file. Which contain name and path of each download session).
this is how the start of downloads is implemented:
var sources = [HLSData]()
#objc func startDownloadButtonTapped() {
print(#function)
let hlsData = sources[0]
switch hlsData.state {
case .notDownloaded:
hlsData.download { (percent) in
DispatchQueue.main.async {
print("percent = \(percent)")
self.percentLabel.text = "\(percent)"
}
}.finish { (relativePath) in
DispatchQueue.main.async {
print("download completed relative path = \(relativePath)")
}
}.onError { (error) in
print("Error finish. \(error)")
}
case .downloading:
print("State is downloading")
break
case .downloaded:
print(hlsData.localUrl ?? "localURL is nil")
}
}
Before tapping state is notDownloaded. respectively app is start download when the button tapped and state is changed to downloading.
Everything is works fine and progress tracked well. But when i go to the background and return back to app, state is still keep of downloading, but progress closure doesn't work anymore. How can i restore or reset this closures for tracking progress. Thanks in advance.
On doing some tests, I feel there is a bug in iOS 12 and below with the AVAssetDownloadDelegate
When doing some tests, I noticed the following when trying to download media over HLS using AVAssetDownloadTask:
iOS 13 and above
When going into the background, the download continues
When coming into the foreground from the background, the AVAssetDownloadDelegate still triggers assetDownloadTask didLoad totalTimeRangesLoaded and the progress can be updated
After suspending or quitting an app, reinitializing an AVAssetDownloadURLSession with the same URLSessionConfiguration identifier, the download resumes automatically from where it last left off
iOS 12 and below
Everything still almost holds true except point 2, for some reason the assetDownloadTask didLoad totalTimeRangesLoaded no longer gets triggered when coming into the foreground from the background and so the progress no longer gets updated.
One workaround I got was from this answer https://stackoverflow.com/a/55847387/1619193 was that in the past, downloads had to be resumed manually after the app was suspended for AVAssetDownloadTask by providing it a location to the partially downloaded file on disk.
As per the documentation:
AVAssetDownloadTask provides the ability to resume previously stopped
downloads under certain circumstances. To do so, simply instantiate a
new AVAssetDownloadTask with an AVURLAsset instantiated with a file
NSURL pointing to the partially downloaded bundle with the desired
download options, and the download will continue restoring any
previously downloaded data.
Interestingly, you cannot find this on the official documentation anymore and also it seems like setting the destinationURL has been deprecated so it seems like there has been some refactoring in how things work.
My solution:
Subscribe to the UIApplication.willEnterForegroundNotification notification
In the call back for the UIApplication.willEnterForegroundNotification, check if the device is running iOS 12 and below
If it does, cancel the current AVAssetDownloadTask
This should trigger the AVAssetDownloadDelegate callback assetDownloadTask didFinishDownloadingTo which will give you the location of the partially downloaded file
Reconfigure the AVAssetDownloadTask but do not configure it with the HLS url, instead configure it with the URL to the partially downloaded asset
Resume the download and the progress AVAssetDownloadDelegate will seem to start firing again
You can download an example of this here
Here are some small snippets of the above steps:
private let downloadButton = UIButton(type: .system)
private let downloadTaskIdentifier = "com.mindhyve.HLSDOWNLOADER"
private var backgroundConfiguration: URLSessionConfiguration?
private var assetDownloadURLSession: AVAssetDownloadURLSession!
private var downloadTask: AVAssetDownloadTask!
override func viewDidLoad()
{
super.viewDidLoad()
// UI configuration left out intentionally
subscribeToNotifications()
initializeDownloadSession()
}
private func initializeDownloadSession()
{
// This will create a new configuration if the identifier does not exist
// Otherwise, it will reuse the existing identifier which is how a download
// task resumes
backgroundConfiguration
= URLSessionConfiguration.background(withIdentifier: downloadTaskIdentifier)
// Resume will happen automatically when this configuration is made
assetDownloadURLSession
= AVAssetDownloadURLSession(configuration: backgroundConfiguration!,
assetDownloadDelegate: self,
delegateQueue: OperationQueue.main)
}
private func resumeDownloadTask()
{
var sourceURL = getHLSSourceURL(.large)
// Now Check if we have any previous download tasks to resume
if let destinationURL = destinationURL
{
sourceURL = destinationURL
}
if let sourceURL = sourceURL
{
let urlAsset = AVURLAsset(url: sourceURL)
downloadTask = assetDownloadURLSession.makeAssetDownloadTask(asset: urlAsset,
assetTitle: "Movie",
assetArtworkData: nil,
options: nil)
downloadTask.resume()
}
}
func cancelDownloadTask()
{
downloadTask.cancel()
}
private func getHLSSourceURL(_ size: HLSSampleSize) -> URL?
{
if size == .large
{
return URL(string: "https://video.film.belet.me/45505/480/ff27c84a-6a13-4429-b830-02385592698b.m3u8")
}
return URL(string: "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_4x3/bipbop_4x3_variant.m3u8")
}
// MARK: INTENTS
#objc
private func downloadButtonTapped()
{
print("\(downloadButton.titleLabel!.text!) tapped")
if downloadTask != nil,
downloadTask.state == .running
{
cancelDownloadTask()
}
else
{
resumeDownloadTask()
}
}
#objc
private func didEnterForeground()
{
if #available(iOS 13.0, *) { return }
// In iOS 12 and below, there seems to be a bug with AVAssetDownloadDelegate.
// It will not give you progress when coming from the background so we cancel
// the task and resume it and you should see the progress in maybe 5-8 seconds
if let downloadTask = downloadTask
{
downloadTask.cancel()
initializeDownloadSession()
resumeDownloadTask()
}
}
private func subscribeToNotifications()
{
NotificationCenter.default.addObserver(self,
selector: #selector(didEnterForeground),
name: UIApplication.willEnterForegroundNotification,
object: nil)
}
// MARK: AVAssetDownloadDelegate
func urlSession(_ session: URLSession,
task: URLSessionTask,
didCompleteWithError error: Error?)
{
guard error != nil else
{
// download complete, do what you want
return
}
// something went wrong, handle errors
}
func urlSession(_ session: URLSession,
assetDownloadTask: AVAssetDownloadTask,
didFinishDownloadingTo location: URL)
{
// Save the download path of the task to resume downloads
destinationURL = location
}
If something seems out of place, I recommend checking out the full working example here
I am following Apple's documentation on caching HLS (.m3u8) video.
https://developer.apple.com/library/archive/documentation/AudioVideo/Conceptual/MediaPlaybackGuide/Contents/Resources/en.lproj/HTTPLiveStreaming/HTTPLiveStreaming.html
Under Playing Offline Assets in the documentation, it is instructed to use AVAssetDownloadTask's asset to simultaneously start playing.
func downloadAndPlayAsset(_ asset: AVURLAsset) {
// Create new AVAssetDownloadTask for the desired asset
// Passing a nil options value indicates the highest available bitrate should be downloaded
let downloadTask = downloadSession.makeAssetDownloadTask(asset: asset,
assetTitle: assetTitle,
assetArtworkData: nil,
options: nil)!
// Start task
downloadTask.resume()
// Create standard playback items and begin playback
let playerItem = AVPlayerItem(asset: downloadTask.urlAsset)
player = AVPlayer(playerItem: playerItem)
player.play()
}
The issue is that the same asset is downloaded twice.
Right after AVPlayer is initialized it starts to buffer the asset. Initially, I assumed that the data from the buffer must be used to create cache but AVAssetDownloadTask doesn't start to download the data for caching until AVPlayer finishes playing the asset. The buffered data is basically discarded.
I used KVO on currentItem.loadedTimeRanges to check state of buffer.
playerTimeRangesObserver = currentPlayer.observe(\.currentItem?.loadedTimeRanges, options: [.new, .old]) { (player, change) in
let time = self.currentPlayer.currentItem?.loadedTimeRanges.firs.
if let t = time {
print(t.timeRangeValue.duration.seconds)
}
}
Below method to check the downloading status of AVAssetDownloadTask.
/// Method to adopt to subscribe to progress updates of an AVAssetDownloadTask.
func urlSession(_ session: URLSession, assetDownloadTask: AVAssetDownloadTask, didLoad timeRange: CMTimeRange, totalTimeRangesLoaded loadedTimeRanges: [NSValue], timeRangeExpectedToLoad: CMTimeRange) {
// This delegate callback should be used to provide download progress for your AVAssetDownloadTask.
guard let asset = activeDownloadsMap[assetDownloadTask] else { return }
var percentComplete = 0.0
for value in loadedTimeRanges {
let loadedTimeRange: CMTimeRange = value.timeRangeValue
percentComplete +=
loadedTimeRange.duration.seconds / timeRangeExpectedToLoad.duration.seconds
}
print("PercentComplete for \(asset.stream.name) = \(percentComplete)")
}
Is this the right behaviour or am I doing something wrong?
I want to be able to use the video data that is being cached (AVAssetDownloadTask downloading is in progress) to play in AVPlayer.
Your AVAssetDownloadTask must be configured to download differing HLS variants than your AVPlayerItem is requesting.
If you already have some data downloaded by AVAssetDownloadTask, your AVPlayerItem will subsequently use it.
But if you already have some data downloaded by AVPlayerItem, your AVAssetDownloadTask may ignore it, as it needs to satisfy the requirements of your download configuration.
I have a music app which downloads audio from an url and play it inside my app and background and everything works great. Now, I'm working on a feature where my app can play next song after current song is finished and my app works perfectly except when my device is in background or lockscreen. This is how to call AVPlayer to play audio from url.
guard let urlLink = googleDriveUrl else {return}
let urlReformatted = removePartOfGoogleDriveUrl(url: urlLink)
guard let url = URL(string: urlReformatted) else {return}
audioPlayer = AVPlayer(url: url)
audioPlayer?.play()
I couldn't figure out why then I use
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64,
to check downloading process with background fetch allowed and use the same audio url and found out that my app did download complete file even in background or lockscreen but AVPlayer won't play. It's so weird cause everything works great inside my app. Also, if I go back to the app from background and click play button, it plays the file. Any suggestions?
In Project->Select Targets->Capabilities and set as below.
var backgroundIdentifier = UIBackgroundTaskInvalid
func configureBackgroundAudioTask() {
backgroundIdentifier = UIApplication.shared.beginBackgroundTask (expirationHandler: { () -> Void in
UIApplication.shared.endBackgroundTask(self.backgroundIdentifier)
self.backgroundIdentifier = UIBackgroundTaskInvalid
})
}
I am a newbie to swift and tvOS.I want to download a video from the url and display it in the ViewController. while i try with the ios Devices the video is downloaded and works fine.But with the tvOS the Video is not downloaded.
why is it so..?
How can i download and play the video in the Apple TV.
Here is my Code
let backgroundSessionConfiguration = URLSessionConfiguration.background(withIdentifier: "backgroundSession")
backgroundSession = Foundation.URLSession(configuration: backgroundSessionConfiguration, delegate: self, delegateQueue: OperationQueue.main)
let url = URL(string: "myURl")!
downloadTask = backgroundSession.downloadTask(with: url)
downloadTask.resume()
func urlSession(_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didFinishDownloadingTo location: URL){
let path = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)
let documentDirectoryPath:String = path[0]
let fileManager = FileManager()
let destinationURLForFile = URL(fileURLWithPath: documentDirectoryPath.appendingFormat("/file.mp4"))
if fileManager.fileExists(atPath: destinationURLForFile.path){
showFileWithPath(path: destinationURLForFile.path)
}
else{
do {
try fileManager.moveItem(at: location, to: destinationURLForFile)
// show file
showFileWithPath(path: destinationURLForFile.path)
}catch{
print("An error occurred while moving file to destination url")
}
}
}
There are some restrictions in tvOS regarding Local storage
Your app can only access 500 KB of persistent storage that is local to the device (using the NSUserDefaults class). Outside of this limited local storage, all other data must be purgeable by the operating system when space is low...
You can take a look to this documentation for further details:
https://developer.apple.com/library/content/documentation/General/Conceptual/AppleTV_PG/
In an app I'm writing, I need to download a PDF from a server and display it in a UIWebView. To this end, I've got a bit of code that retrieves the PDF from an endpoint (it's not a URL, and on a desktop computer, opens up a dialogue for saving as in a browser) and loads it onto the device by grabbing the data in a class called PDFGrabber:
func getPDF(completionHandler:#escaping (URL) -> Void)
{
let theURL:String = "https://mywebsite.com/Endpoint"
let fileURL:URL = URL(string: theURL)!
var request = URLRequest(url: fileURL, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 10.0)
let session = URLSession.shared
let task = session.dataTask(with: request, completionHandler: {data, response, error -> Void in
var documentURL = (FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)).last
documentURL = documentURL?.appendingPathComponent("MyDocument.pdf")
do
{
try data?.write(to: documentURL!, options: .atomic)
completionHandler(documentURL!)
}
catch
{
print(error)
}
})
task.resume()
}
Then, in the table view controller showing the PDFs (let's call it PDFTableView, I can use the documentURL from the PDFGrabber when a PDF is requested (by tapping a cell in the table):
PDFGrabber.getPDF(){fileURL -> () in
DispatchQueue.main.async
{
if let resultController = self.storyboard!.instantiateViewController(withIdentifier: "PDFViewer") as? PDFViewer
{
resultController.thePDFPath = stringURL
self.present(resultController, animated: true, completion: nil)
}
}
Finally, I have another View Controller with a UIWebView called "WebView" and the attribute "thePDFPath" as a string in the PDFViewer view controller. In the viewDidLoad() method, I can say:
override func viewDidLoad()
{
let pathToPDF:URL = URL(string: thePDFPath)!
WebView.loadRequest(URLRequest(url: pathToPDF))
}
Together, this loads the PDF into the web view. However, the loading times can be a little slow, and I'd like to be able to calculate how much of the PDF has been loaded onto a user's device using a progress bar and a string. From other questions, I gather than I'd need to have my PDFGrabber class extend URLSessionDownloadDelegate, and then implement the functions. To get the amount of bytes downloaded is straightforward, since I can simply go:
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten writ: Int64, totalBytesExpectedToWrite exp: Int64)
{
percentDownloaded = (Float(writ)/Float(exp)) * 100
print("Progress: " + String(describing: percentDownloaded))
}
But I'd also like to be able to open the PDFView only after the PDF is wholly downloaded after displaying its progress as a percent (I've got an overlay view that does this). Before, I could use a completion handler and wait until the PDF finished downloading, then open it.
However, this method does not allow me to access the amount of bytes downloaded; would I go about opening the view from the
urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL)
function, or is there something else I need to do?
Suggestions would be much appreciated; thanks!
import WebKit
here you can load your pdf in your application
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
if let pdfURL = Bundle.main.url(forResource: "food-and-drink-menu-trafalgar-tavern", withExtension: "pdf", subdirectory: nil, localization: nil) {
do {
let data = try Data(contentsOf: pdfURL)
let webView = WKWebView(frame: CGRect(x:0,y:NavigationView.frame.size.height,width:view.frame.size.width, height:view.frame.size.height-NavigationView.frame.size.height))
webView.load(data, mimeType: "application/pdf", characterEncodingName:"", baseURL: pdfURL.deletingLastPathComponent())
view.addSubview(webView)
}
catch {
// catch errors here
}
}
}