Error while implementing AVAssetDownloadURLSession to download HLS stream - ios

I'm trying to implement an offline mode to a streaming application.
The goal is to be able to download an HLS stream on the device of the user to make it possible to watch the stream even while the user is offline.
I have recently stumble on this tutorial.
It seems to answer the exact requirements of what I was trying to implement but I'm facing a problem while trying to make it work.
I've created a little DownloadManager to apply the logic of the tutorial.
Here is my singleton class:
import AVFoundation
class DownloadManager:NSObject {
static var shared = DownloadManager()
private var config: URLSessionConfiguration!
private var downloadSession: AVAssetDownloadURLSession!
override private init() {
super.init()
config = URLSessionConfiguration.background(withIdentifier: "\(Bundle.main.bundleIdentifier!).background")
downloadSession = AVAssetDownloadURLSession(configuration: config, assetDownloadDelegate: self, delegateQueue: OperationQueue.main)
}
func setupAssetDownload(_ url: URL) {
let options = [AVURLAssetAllowsCellularAccessKey: false]
let asset = AVURLAsset(url: url, options: options)
// Create new AVAssetDownloadTask for the desired asset
let downloadTask = downloadSession.makeAssetDownloadTask(asset: asset,
assetTitle: "Test Download",
assetArtworkData: nil,
options: nil)
// Start task and begin download
downloadTask?.resume()
}
func restorePendingDownloads() {
// Grab all the pending tasks associated with the downloadSession
downloadSession.getAllTasks { tasksArray in
// For each task, restore the state in the app
for task in tasksArray {
guard let downloadTask = task as? AVAssetDownloadTask else { break }
// Restore asset, progress indicators, state, etc...
let asset = downloadTask.urlAsset
downloadTask.resume()
}
}
}
func playOfflineAsset() -> AVURLAsset? {
guard let assetPath = UserDefaults.standard.value(forKey: "assetPath") as? String else {
// Present Error: No offline version of this asset available
return nil
}
let baseURL = URL(fileURLWithPath: NSHomeDirectory())
let assetURL = baseURL.appendingPathComponent(assetPath)
let asset = AVURLAsset(url: assetURL)
if let cache = asset.assetCache, cache.isPlayableOffline {
return asset
// Set up player item and player and begin playback
} else {
return nil
// Present Error: No playable version of this asset exists offline
}
}
func getPath() -> String {
return UserDefaults.standard.value(forKey: "assetPath") as? String ?? ""
}
func deleteOfflineAsset() {
do {
let userDefaults = UserDefaults.standard
if let assetPath = userDefaults.value(forKey: "assetPath") as? String {
let baseURL = URL(fileURLWithPath: NSHomeDirectory())
let assetURL = baseURL.appendingPathComponent(assetPath)
try FileManager.default.removeItem(at: assetURL)
userDefaults.removeObject(forKey: "assetPath")
}
} catch {
print("An error occured deleting offline asset: \(error)")
}
}
}
extension DownloadManager: AVAssetDownloadDelegate {
func urlSession(_ session: URLSession, assetDownloadTask: AVAssetDownloadTask, didLoad timeRange: CMTimeRange, totalTimeRangesLoaded loadedTimeRanges: [NSValue], timeRangeExpectedToLoad: CMTimeRange) {
var percentComplete = 0.0
// Iterate through the loaded time ranges
for value in loadedTimeRanges {
// Unwrap the CMTimeRange from the NSValue
let loadedTimeRange = value.timeRangeValue
// Calculate the percentage of the total expected asset duration
percentComplete += loadedTimeRange.duration.seconds / timeRangeExpectedToLoad.duration.seconds
}
percentComplete *= 100
debugPrint("Progress \( assetDownloadTask) \(percentComplete)")
let params = ["percent": percentComplete]
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "completion"), object: nil, userInfo: params)
// Update UI state: post notification, update KVO state, invoke callback, etc.
}
func urlSession(_ session: URLSession, assetDownloadTask: AVAssetDownloadTask, didFinishDownloadingTo location: URL) {
// Do not move the asset from the download location
UserDefaults.standard.set(location.relativePath, forKey: "assetPath")
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
debugPrint("Download finished: \(location)")
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
debugPrint("Task completed: \(task), error: \(String(describing: error))")
guard error == nil else { return }
guard let task = task as? AVAssetDownloadTask else { return }
print("DOWNLOAD: FINISHED")
}
}
My problem comes when I try to call my setupAssetDownload function.
Everytime time I try to resume a downloadTask I get an error message in the urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) delegate function.
The log of the message is:
Task completed: <__NSCFBackgroundAVAssetDownloadTask:
0x7ff57fc024a0>{ taskIdentifier: 1 }, error: Optional(Error
Domain=AVFoundationErrorDomain Code=-11800 \"The operation could not
be completed\" UserInfo={NSLocalizedFailureReason=An unknown error
occurred (-12780), NSLocalizedDescription=The operation could not be
completed})
To give you all the relevant information the URL I past to my setupAssetDownload function is of type
URL(string: "https://bitdash-a.akamaihd.net/content/MI201109210084_1/m3u8s/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.m3u8")!
I been looking for a cause and solution for this error but I don't seem to be able to find one for the time being.
I would be very grateful for any tips or any clues on how resolve this issue or any indication of errors in my singleton implementation that could explain this behaviour.
Thank you in advance.
Martin
EDIT:
It seems that this bug occurs on a simulator. I launch my app on a real device and the download started without any problem. Hope this helps. Still don't understand why I cannot try this behaviour on a simulator.

Related

Can't able to catch error when turn off network connection at the time of downloading a file

I am trying to download .mp3 file from server using using URLSession. It also works in background. Here is the variables i have declared.
var urlSession: URLSession?
var sessionTask: URLSessionDownloadTask?
var resumeData: Data?
This is how i initialised the URLSession.
let config = URLSessionConfiguration.background(withIdentifier: "com.example.app.background")
config.networkServiceType = .background
urlSession = URLSession(configuration: config, delegate: self, delegateQueue: nil)
To download i am using URLSessionDownloadTask. I have also implemented pause/resume functionality. Here is the code.
func startDownload(with url: String) {
sessionTask = urlSession?.downloadTask(with: URL(string: url)!)
sessionTask?.resume()
}
func cancelDownload() {
sessionTask?.cancel()
}
func pauseDownload() {
sessionTask?.cancel(byProducingResumeData: { (data) in
self.resumeData = data
})
}
func resumeDownload(url: String) {
if let resumeData = self.resumeData {
sessionTask = urlSession?.downloadTask(withResumeData: resumeData)
} else {
sessionTask = urlSession?.downloadTask(with: URL(string: url)!)
}
sessionTask?.resume()
}
As i have enabled download in background, to receive success, failure and progress of download i have confirmed URLSessionDownloadDelegate methods.
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL)
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?)
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64)
Everything seems fine when i download a file even in background. I have got the success, failure and progress of the download in delegate methods.
But the problem is if i turn off network connection at the middle of downloading a file, i get some error logs in console. But none of the above delegate methods are called. Here is the logs
Task <0B34AE9E-0A46-4E78-8C90-7353CDC34929>.<10> finished with error [-1020] Error Domain=NSURLErrorDomain Code=-1020 "A data connection is not currently allowed." UserInfo={_kCFStreamErrorCodeKey=50, NSUnderlyingError=0x2817b40c0 {Error Domain=kCFErrorDomainCFNetwork Code=-1020 "(null)" UserInfo={_kCFStreamErrorCodeKey=50, _kCFStreamErrorDomainKey=1}}, _NSURLErrorFailingURLSessionTaskErrorKey=LocalDataTask <0B34AE9E-0A46-4E78-8C90-7353CDC34929>.<10>, _NSURLErrorRelatedURLSessionTaskErrorKey=(
"LocalDataTask <0B34AE9E-0A46-4E78-8C90-7353CDC34929>.<10>"
), NSLocalizedDescription=A data connection is not currently allowed.
How can i get the error response to show the user that you have lost your network connection?
I have searched for it but didn't find the solution. Pardon me if it's a duplicate question. Thanks.
To get the error returned, you need to use the alternative functions of downloadTask that have a completionHandler`
self.urlSsn?.downloadTask(with: MyURLRequest, completionHandler: { data, response, error -> Void in
if let error = error {
print("\(error.localizedDescription)")
// This should print "A data connection is not currently allowed."
}
}
A similar is available for downloadTask(withResumeData) as well.
If you want to compare specific error codes (as error code -1020 in your example). You can do the following:
if let error = error {
let e = error! as NSError
switch e.code {
case NSURLErrorNotConnectedToInternet:
print("No internet")
default:
print("Some other error")
}
}
More info here and here

AVAssetDownloadTask method 'didFinishDownloadingTo' is not called in background mode with low disk space

I create a background session like this:
let backgroundConfiguration = URLSessionConfiguration.background(withIdentifier: backgroundSessionId)
backgroundConfiguration.isDiscretionary = false
backgroundConfiguration.sessionSendsLaunchEvents = true backgroundConfiguration.shouldUseExtendedBackgroundIdleMode = true
privateQueue = OperationQueue()
privateQueue.maxConcurrentOperationCount = 1
assetDownloadSession = AVAssetDownloadURLSession(configuration: backgroundConfiguration, assetDownloadDelegate: self, delegateQueue: privateQueue)
Also create and run a task:
let task = assetDownloadSession.makeAssetDownloadTask(asset: urlAsset, assetTitle: title, assetArtworkData: nil, options: nil)
task.resume()
But if I have less than 500MB of disk space on my real device the application is restarted and the following method is called:
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?)
But the next method is not called:
func urlSession(_ session: URLSession, assetDownloadTask: AVAssetDownloadTask, didFinishDownloadingTo location: URL)
The file is not deleted from the device, how can I find out where it is locally and how to delete it? The system does not do this automatically.
Checked many times on real iphone 6s, iOS 14.0.1 with logs and Mac OS console application.
If the memory is more than 500 MB, then everything works correctly and the method didCompleteWithError is called
You can use AVAggregateAssetDownloadTask to get a location of your downloading media in URLSession:aggregateAssetDownloadTask:willDownloadToURL:e.g.:
var assetDownloadURLSession: AVAssetDownloadURLSession!
var task = AVAggregateAssetDownloadTask?
var downloadURL: URL?
func download(asset: AVURLAsset)
let backgroundConfiguration = URLSessionConfiguration.background(withIdentifier: "AAPL-Identifier")
assetDownloadURLSession = AVAssetDownloadURLSession(configuration: backgroundConfiguration, assetDownloadDelegate: self, delegateQueue: OperationQueue.main)
task = assetDownloadURLSession.aggregateAssetDownloadTask(with: asset, ...)
task?.resume()
...
}
func urlSession(_ session: URLSession, aggregateAssetDownloadTask: AVAggregateAssetDownloadTask, willDownloadTo location: URL) {
downloadURL = location
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let url = downloadURL {
do {
try FileManager.default.removeItem(at: url)
}
catch {
print(error)
}
}
}
How to work with aggregate tasks you can look at Apple's sample project: https://developer.apple.com/documentation/avfoundation/media_playback_and_selection/using_avfoundation_to_play_and_persist_http_live_streams

download file with urlsesssion

I'm trying to download using urlsession background session this is my main function
func startfresh() {
session = URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue())
let url = URL(string: "https://nava.ir/wp-content/uploads/2018/08/Gholamreza-Sanatgar-Dorooghe-Sefid-128.mp3")
task = session.downloadTask(with: url!)
task.resume()
}
and my didcompletewitherror
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if error != nil {
let err = error as NSError?
let resumeData = err?.userInfo[NSURLSessionDownloadTaskResumeData] as? Data
print("anotherone")
let newtask = session.downloadTask(withResumeData: resumeData!)
newtask.resume()
}
else {
print("hichi")
}
}
but when I close the app when the download is still on progress and relaunch it again and press start download it starts 2 tasks resume previous one and start a new one I want to just resume the previous one with resume data what should I do to just trigger did complete with error method.
What you are seeing is kind of "expected" and you have to design your software to handle it. Actually, there are some more things you should consider. I've investigated and put down at the next as an answer. (NSURLSessionDownloadTask move temporary file)
A sample project is also available.

AVAssetDownloadTask doesn't start on IOS 10.3 device

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

AVAssetResourceLoaderDelegate implementation for an asset of unknown length

My iOS app uses AVPlayer to play streaming audio from my server and storing it on a device.
I implemented AVAssetResourceLoaderDelegate, so I could intercept the stream. I change my scheme (from http to a fake scheme, so that AVAssetResourceLoaderDelegate method gets called:
func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool
I followed this tutorial:
http://blog.jaredsinclair.com/post/149892449150/implementing-avassetresourceloaderdelegate-a
Over there, I put the original scheme back, and create a session for pulling audio from the server. Everything works perfectly when my server provides the Content-Length (size of the audio file in bytes) header for the streamed audio file.
But sometimes I stream audio files where I cannot provide their length ahead of time (let's say a live podcast stream). In this case, AVURLAsset sets length to -1 and fails with:
"Error Domain=AVFoundationErrorDomain Code=-11849 \"Operation Stopped\" UserInfo={NSUnderlyingError=0x61800004abc0 {Error Domain=NSOSStatusErrorDomain Code=-12873 \"(null)\"}, NSLocalizedFailureReason=This media may be damaged., NSLocalizedDescription=Operation Stopped}"
And I cannot bypass this error. I tried to go a hacky way, provide fake
Content-Length: 999999999, but in this case, once the entire audio stream is downloaded, my session fails with:
Loaded so far: 10349852 out of 99999999
The request timed out.
//Audio file got downloaded, its size is 10349852
//AVPlayer tries to get the next chunk and then fails with request times out
Have anyone ever faced this problem before?
P.S. If I keep original http scheme in AVURLAsset, AVPlayer knows how to handle this scheme, so it plays audio file just fine (even w/o Content-Length), I do not know how it does that w/o failing. Also, in this case, my AVAssetResourceLoaderDelegate is never used, so I cannot intercept and copy the content of the audio file to a local storage.
Here is the implementation:
import AVFoundation
#objc protocol CachingPlayerItemDelegate {
// called when file is fully downloaded
#objc optional func playerItem(playerItem: CachingPlayerItem, didFinishDownloadingData data: NSData)
// called every time new portion of data is received
#objc optional func playerItemDownloaded(playerItem: CachingPlayerItem, didDownloadBytesSoFar bytesDownloaded: Int, outOf bytesExpected: Int)
// called after prebuffering is finished, so the player item is ready to play. Called only once, after initial pre-buffering
#objc optional func playerItemReadyToPlay(playerItem: CachingPlayerItem)
// called when some media did not arrive in time to continue playback
#objc optional func playerItemDidStopPlayback(playerItem: CachingPlayerItem)
// called when deinit
#objc optional func playerItemWillDeinit(playerItem: CachingPlayerItem)
}
extension URL {
func urlWithCustomScheme(scheme: String) -> URL {
var components = URLComponents(url: self, resolvingAgainstBaseURL: false)
components?.scheme = scheme
return components!.url!
}
}
class CachingPlayerItem: AVPlayerItem {
class ResourceLoaderDelegate: NSObject, AVAssetResourceLoaderDelegate, URLSessionDelegate, URLSessionDataDelegate, URLSessionTaskDelegate {
var playingFromCache = false
var mimeType: String? // is used if we play from cache (with NSData)
var session: URLSession?
var songData: NSData?
var response: URLResponse?
var pendingRequests = Set<AVAssetResourceLoadingRequest>()
weak var owner: CachingPlayerItem?
//MARK: AVAssetResourceLoader delegate
func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool {
if playingFromCache { // if we're playing from cache
// nothing to do here
} else if session == nil { // if we're playing from url, we need to download the file
let interceptedURL = loadingRequest.request.url!.urlWithCustomScheme(scheme: owner!.scheme!).deletingLastPathComponent()
startDataRequest(withURL: interceptedURL)
}
pendingRequests.insert(loadingRequest)
processPendingRequests()
return true
}
func startDataRequest(withURL url: URL) {
let request = URLRequest(url: url)
let configuration = URLSessionConfiguration.default
configuration.requestCachePolicy = .reloadIgnoringLocalAndRemoteCacheData
configuration.timeoutIntervalForRequest = 60.0
configuration.timeoutIntervalForResource = 120.0
session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
let task = session?.dataTask(with: request)
task?.resume()
}
func resourceLoader(_ resourceLoader: AVAssetResourceLoader, didCancel loadingRequest: AVAssetResourceLoadingRequest) {
pendingRequests.remove(loadingRequest)
}
//MARK: URLSession delegate
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
(songData as! NSMutableData).append(data)
processPendingRequests()
owner?.delegate?.playerItemDownloaded?(playerItem: owner!, didDownloadBytesSoFar: songData!.length, outOf: Int(dataTask.countOfBytesExpectedToReceive))
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: #escaping (URLSession.ResponseDisposition) -> Void) {
completionHandler(URLSession.ResponseDisposition.allow)
songData = NSMutableData()
self.response = response
processPendingRequests()
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError err: Error?) {
if let error = err {
print(error.localizedDescription)
return
}
processPendingRequests()
owner?.delegate?.playerItem?(playerItem: owner!, didFinishDownloadingData: songData!)
}
//MARK:
func processPendingRequests() {
var requestsCompleted = Set<AVAssetResourceLoadingRequest>()
for loadingRequest in pendingRequests {
fillInContentInforation(contentInformationRequest: loadingRequest.contentInformationRequest)
let didRespondCompletely = respondWithDataForRequest(dataRequest: loadingRequest.dataRequest!)
if didRespondCompletely {
requestsCompleted.insert(loadingRequest)
loadingRequest.finishLoading()
}
}
for i in requestsCompleted {
pendingRequests.remove(i)
}
}
func fillInContentInforation(contentInformationRequest: AVAssetResourceLoadingContentInformationRequest?) {
// if we play from cache we make no URL requests, therefore we have no responses, so we need to fill in contentInformationRequest manually
if playingFromCache {
contentInformationRequest?.contentType = self.mimeType
contentInformationRequest?.contentLength = Int64(songData!.length)
contentInformationRequest?.isByteRangeAccessSupported = true
return
}
// have no response from the server yet
if response == nil {
return
}
let mimeType = response?.mimeType
contentInformationRequest?.contentType = mimeType
if response?.expectedContentLength != -1 {
contentInformationRequest?.contentLength = response!.expectedContentLength
contentInformationRequest?.isByteRangeAccessSupported = true
} else {
contentInformationRequest?.isByteRangeAccessSupported = false
}
}
func respondWithDataForRequest(dataRequest: AVAssetResourceLoadingDataRequest) -> Bool {
let requestedOffset = Int(dataRequest.requestedOffset)
let requestedLength = dataRequest.requestedLength
let startOffset = Int(dataRequest.currentOffset)
// Don't have any data at all for this request
if songData == nil || songData!.length < startOffset {
return false
}
// This is the total data we have from startOffset to whatever has been downloaded so far
let bytesUnread = songData!.length - Int(startOffset)
// Respond fully or whaterver is available if we can't satisfy the request fully yet
let bytesToRespond = min(bytesUnread, requestedLength + Int(requestedOffset))
dataRequest.respond(with: songData!.subdata(with: NSMakeRange(startOffset, bytesToRespond)))
let didRespondFully = songData!.length >= requestedLength + Int(requestedOffset)
return didRespondFully
}
deinit {
session?.invalidateAndCancel()
}
}
private var resourceLoaderDelegate = ResourceLoaderDelegate()
private var scheme: String?
private var url: URL!
weak var delegate: CachingPlayerItemDelegate?
// use this initializer to play remote files
init(url: URL) {
self.url = url
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)!
scheme = components.scheme
let asset = AVURLAsset(url: url.urlWithCustomScheme(scheme: "fakeScheme").appendingPathComponent("/test.mp3"))
asset.resourceLoader.setDelegate(resourceLoaderDelegate, queue: DispatchQueue.main)
super.init(asset: asset, automaticallyLoadedAssetKeys: nil)
resourceLoaderDelegate.owner = self
self.addObserver(self, forKeyPath: "status", options: NSKeyValueObservingOptions.new, context: nil)
NotificationCenter.default.addObserver(self, selector: #selector(didStopHandler), name:NSNotification.Name.AVPlayerItemPlaybackStalled, object: self)
}
// use this initializer to play local files
init(data: NSData, mimeType: String, fileExtension: String) {
self.url = URL(string: "whatever://whatever/file.\(fileExtension)")
resourceLoaderDelegate.songData = data
resourceLoaderDelegate.playingFromCache = true
resourceLoaderDelegate.mimeType = mimeType
let asset = AVURLAsset(url: url)
asset.resourceLoader.setDelegate(resourceLoaderDelegate, queue: DispatchQueue.main)
super.init(asset: asset, automaticallyLoadedAssetKeys: nil)
resourceLoaderDelegate.owner = self
self.addObserver(self, forKeyPath: "status", options: NSKeyValueObservingOptions.new, context: nil)
NotificationCenter.default.addObserver(self, selector: #selector(didStopHandler), name:NSNotification.Name.AVPlayerItemPlaybackStalled, object: self)
}
func download() {
if resourceLoaderDelegate.session == nil {
resourceLoaderDelegate.startDataRequest(withURL: url)
}
}
override init(asset: AVAsset, automaticallyLoadedAssetKeys: [String]?) {
fatalError("not implemented")
}
// MARK: KVO
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
delegate?.playerItemReadyToPlay?(playerItem: self)
}
// MARK: Notification handlers
func didStopHandler() {
delegate?.playerItemDidStopPlayback?(playerItem: self)
}
// MARK:
deinit {
NotificationCenter.default.removeObserver(self)
removeObserver(self, forKeyPath: "status")
resourceLoaderDelegate.session?.invalidateAndCancel()
delegate?.playerItemWillDeinit?(playerItem: self)
}
}
You can not handle this situation as for iOS this file is damaged because header is incorrect. System think that you are going to play regular audio file but it doesn't have all info about it. You don't know what audio duration will be, only if you have a live streaming. Live streaming on iOS is done using HTTP live streaming protocol.
Your iOS code is correct. You have to modify your backend and provide m3u8 playlist for live streaming audios, then iOS will accept it as a live stream and audio player will start tracks.
Some related info can be found here. As an iOS developer with good experience in streaming audio / video I can tell you that code to play live / VOD is the same.
But sometimes I stream audio files where I cannot provide their length ahead of time (let's say a live podcast stream). In this case, AVURLAsset sets length to -1 and fails with
In this scenario you should let the player re-request this data later on and set renewalDate property of contentInformationRequest for the given part to some point in future when this data will be available.
If it's just an inifinte live stream, you always provide the length of aquired portion, and set new renewDate for the next renewal cycle (according to my observation natively AVPlayer just updates this data with fixed period of time, say, every 4-6 seconds). The server usually provides such information with "Expires" http header. You can rely on this information yourself and implement something like this (borrowed from my own question on apple developers forum):
if let httpResonse = response as? HTTPURLResponse, let expirationValue = httpResonse.value(forHTTPHeaderField: "Expires") {
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss zzz"
if let expirationDate = dateFormatter.date(from: expirationValue) {
let renewDate = max(expirationDate, Date(timeIntervalSinceNow: 8))
contentInformationRequest.renewalDate = renewDate
}
}
This line let renewDate = max(expirationDate, Date(timeIntervalSinceNow: 8)) adds 8 seconds grace period for the
player to load videos. Otherwise it does not keep up with the pace of
renewals, and video loads in poor quality.
Or just update it periodically if you know in advance it's a live asset without fixed length and your server doesn't privde the required information:
contentInformationRequest.renewalDate = Date(timeIntervalSinceNow: 8)

Resources