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
})
}
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'm actually implementing a download functionality in an application.
I'm facing a very weird bug with AVAssetDownloadTask.
Indeed at the beginning of the day the implementation for downloading a asset was working.
When I called the resume function on my AssetDownloadTask, newly created, the download started instantly and the AVAssetDownloadDelegate function
func urlSession(
_ session: URLSession,
assetDownloadTask: AVAssetDownloadTask,
didLoad timeRange: CMTimeRange,
totalTimeRangesLoaded loadedTimeRanges: [NSValue],
timeRangeExpectedToLoad: CMTimeRange
)
was called and showed progress for the active downloading task.
But at one point in the day the process stopped working. Now when I try to start/resume a AssetDownloadTask nothing happens.
No AVAssetDownloadDelegate function is called for the progression of the download. It is like nothing is happening. I don't even get an error.
The weird thing is if I cancelled this AssetDownloadTask the
func urlSession(
_ session: URLSession,
assetDownloadTask: AVAssetDownloadTask,
didFinishDownloadingTo location: URL
)
and
func urlSession(
_ session: URLSession,
task: URLSessionTask,
didCompleteWithError error: Error?
)
are correctly called showing that the process seems to be on hold somehow.
I don't seem to have any of this problems on devices that are running on IOS 11.
I was wondering if any of you had an idea that could explain why my AssetDownloadTask doesn't start/resume on IOS 10.3 devices.
You will find below a few parts of my download manager.
let backgroundConfiguration = URLSessionConfiguration.background(withIdentifier: "\(Bundle.main.bundleIdentifier!).background")
// Create the AVAssetDownloadURLSession using the configuration.
assetDownloadURLSession = AVAssetDownloadURLSession(configuration: backgroundConfiguration, assetDownloadDelegate: self, delegateQueue: OperationQueue.main)
private var activeDownloadsMap = [AVAssetDownloadTask: XXXXXXX]()
func downloadAsset(for asset: XXXXX) {
// Get the default media selections for the asset's media selection groups.
let preferredMediaSelection = asset.geturlAsset().preferredMediaSelection
if #available(iOS 10.0, *) {
guard let task = assetDownloadURLSession.makeAssetDownloadTask(asset: asset.geturlAsset(),
assetTitle: asset.title,
assetArtworkData: nil,
options: nil) else { return }
// To better track the AVAssetDownloadTask we set the taskDescription to something unique for our sample.
task.taskDescription = asset.title
activeDownloadsMap[task] = asset
task.resume()
} else {
return
}
}
extension DownloadManager: AVAssetDownloadDelegate {
public 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 +=
CMTimeGetSeconds(loadedTimeRange.duration) / CMTimeGetSeconds(timeRangeExpectedToLoad.duration)
}
debugPrint("DOWNLOAD: Progress \( assetDownloadTask) \(percentComplete)")
}
}
Thank you in advance for any help you could give me to try and figure this out.
Cant figure out if it is coming from the device / code / OS version
Best regards,
Martin
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.
I have background downloading zip file:
if let url = NSURL(string: urlstring)
{
let config = NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier((NSUUID().UUIDString))
let session = NSURLSession(configuration: config, delegate: self, delegateQueue: nil)
let task = session.downloadTaskWithURL(url)
session.sessionDescription = filepath
if let sessionId = session.configuration.identifier
{
print("start zip session: " + sessionId)
}
task.resume()
}
}
it works cool if you have internet connection but if you lose it during downloading app just wait and URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError error: NSError?) will not be called never.
How it can be handle?
Something like time for response from server
The question is old, but still unanswered, so here is one workaround.
Some clarifications first
In general there is a property of URLSessionConfiguration called waitsForConnectivity which can be set to false where URLSessionTask will then directly fail on connection lost. However if true then URLSessionTaskDelegate will receive a callback on the urlSession(_:taskIsWaitingForConnectivity:) method.
However
Background tasks such as DownloadTask always wait for connectivity and ignore waitsForConnectivity property of URLSessionConfiguration. They also DON'T trigger urlSession(_:taskIsWaitingForConnectivity:) callback, so there is no official way to listen for connectivity drop-out on download task.
Workaround
If you listening for the download progress you'll notice that a call to the method is done few times in a second. Therefore we can conclude that if the progress callback is not called for more than 5 seconds, then there could be a connectivity drop issue. So the workaround is to make additional property to the URLSessionDownloadDelegate delegate and store the last update of the progress. Then have interval function to periodically check whether this property were not updated soon.
Something like:
class Downloader: NSObject, URLSessionTaskDelegate, URLSessionDownloadDelegate {
var lastUpdate: Date;
var downloadTask: URLSessionDownloadTask?;
public var session : URLSession {
get {
let config = URLSessionConfiguration.background(
withIdentifier: "\(Bundle.main.bundleIdentifier!).downloader");
config.isDiscretionary = true;
return URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue());
}
}
override init() {
self.lastUpdate = Date();
super.init();
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
// Handle download completition
// ...
}
func urlSession(
_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didWriteData bytesWritten: Int64,
totalBytesWritten writ: Int64,
totalBytesExpectedToWrite exp: Int64)
{
let progress = 100 * writ / exp;
// Do something with the progress
// ...
self.lastUpdate = Date();
}
}
var downloader = Downloader();
let url = "http://...";
var request = URLRequest(url: URL(string: url)!);
currentWorker.downloadTask = downloader.session.downloadTask(with: request);
currentWorker.downloadTask!.resume();
// Schedule timer for every 5 secs
var timer = NSTimer.scheduledTimerWithTimeInterval(5.0, target: self, selector: "CheckInternetDrop", userInfo: nil, repeats: true);
func CheckInternetDrop(){
let interval = Date().timeIntervalSinceDate(downloader.lastUpdate);
if (interval > 5) {
print("connection dropped");
}
}
Very simple example...
According to apple docs on pausing and resuming downloads :
Also, downloads that use a background configuration will handle resumption automatically, so manual resuming is only needed for non-background downloads.
Besides, the reason waitsForConnectivity is being ignored is stated in the downloading files in the background :
If your app is in the background, the system may suspend your app while the download is performed in another process.
This is why even if you were to build a timer in a similar fashion to Dimitar Atanasov it will not work once the app goes in the background since it will be suspended.
I've tested the background downloading myself with network failures and the system resumes without fail, hence no extra code is required.
You can set timeouts for your requests:
config.timeoutIntervalForRequest = <desired value>
config.timeoutIntervalForResource = <desired value>
Documentation:
timeoutIntervalForRequest
timeoutIntervalForResource