Unable to play video in avplayer with custom request - ios

Hi am trying to send custom header with avplayer request so I had added a custom scheme like
let asset = AVURLAsset(url: URL(string: "mycustomscheme://tungsten.aaplimg.com/VOD")!, options: nil)
because of this request fails and delegate method shouldWaitForLoadingOfRequestedResource is called.
In shouldWaitForLoadingOfRequestedResource am making a custom request and am recieving the data from server but avplayer is still not playing the file. Here is my code :
func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool {
resourceLoader.delegateQueue?.async {
var request: URLRequest? = loadingRequest.request
request?.url = self.url
// Add header
request?.setValue(HeadersForWS.DeviceOS, forHTTPHeaderField: HeadersForWS.DeviceType)
request?.setValue(Utility.getUserId(), forHTTPHeaderField:HeadersForWS.UserId )
if let aRequest = request {
let session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
session.dataTask(with: aRequest as URLRequest) { (data, response, error) in
guard let data = data else {
try? loadingRequest.finishLoading()
return
}
loadingRequest.dataRequest?.respond(with: data)
loadingRequest.finishLoading()
}.resume()
}
}
return true
}
If I save the data received to a file sample.mp4 and pull the file from app and then try to play then it plays means am receiving the video data properly. What may be the problem here??
Thanks in advance

Related

How to handle errors while saving multiple images to the user's device?

My app as the functionality of choosing multiple images from the app main screen, and save the selected images to the user gallery.
As an example (image from google):
After the user clicking "save" I am doing the following in order to save the chosen images to the user's gallery.
Running through all of the images and saving each image that on clicked.
func saveSelectedImagesToDevice() {
for imageList in imagesListCells {
for image in imageList.images {
if image.selectionState == .onClicked {
downloadImage(from: image.url)
}
}
}
}
Downloading each image
func downloadImage(from url: String) {
guard let url = URL(string: url) else {return}
getData(from: url) { data, response, error in
guard let data = data, error == nil else { return }
guard let image = UIImage(data: data) else {return}
UIImageWriteToSavedPhotosAlbum(image, self, #selector(self.image(_:didFinishSavingWithError:contextInfo:)), nil)
}
}
private func getData(from url: URL, completion: #escaping (Data?, URLResponse?, Error?) -> ()) {
URLSession.shared.dataTask(with: url, completionHandler: completion).resume()
}
#objc func image(_ image: UIImage, didFinishSavingWithError error: Error?, contextInfo: UnsafeRawPointer) {
if let _ = error {
self.delegate?.savedImage(proccessMsg: "Error adding images to the gallery, pleast make sure you enabled photos permissions in phone setting")
}
}
The thing is, because the saving process is asynchronies, in case error occurs in one of the process of downloading an image, I would like to stop all of the other asynchronies processes that running on the background.
At the moment, in case of error, the error been called for each one of the images.
Any ideas how can I manage it different in order to keep the process asynchronies but to be able to stop all processes in case of error?
You would have to change completely the architecture of the download to make it cancellable. A data task is cancellable, but yours is not because you have not retained any way of referencing it.
Apple suggests to not using the shared instance if you want to create multiple sessions. You could try to achieve this by creating a single session instance and invalidate it as soon as you receive an error.
Keep in mind that if you want to re-start the session you need to re instantiate a new one.
e.g.
let session = URLSession(configuration: .default)
func downloadImage(from url: String) {
guard let url = URL(string: url) else {return}
session.dataTask(with: url) { [weak self] data, response, error in
guard let self = self else { return }
if let error = error {
print("You have an error: ",error.localizedDescription)
self.session.invalidateAndCancel()
}else if let data = data,
let image = UIImage(data: data) {
UIImageWriteToSavedPhotosAlbum(image, self, #selector(self.image(_:didFinishSavingWithError:contextInfo:)), nil)
}
}.resume()
}

It takes nearly 10 seconds before audio from remote URL starts playing

I am able to play the sound from one URL. But takes nearly 10 seconds to play the audio.
#IBAction func playBtnTap(_ sender: Any) {
let url = URL(string: muscUrl!)!
let playerItem = AVPlayerItem(url: url)
self.player = AVPlayer(playerItem: playerItem)
self.player!.play()
}
I am new to iOS. I am not sure, how can I download the URL audio and then play that audio? I don't want that 10 sec delay to play audio.
Update, I tried :
override func viewDidLoad() {
super.viewDidLoad()
let urlstring = "http://radio.spainmedia.es/wp-content/uploads/2015/12/tailtoddle_lo4.mp3"
let url = NSURL(string: urlstring)
print("the url = \(url!)")
downloadFileFromURL(url: url!)
}
func downloadFileFromURL(url:NSURL){
var downloadTask:URLSessionDownloadTask
var request = URLRequest(url:url as URL)
downloadTask = URLSession.shared.downloadTask(with: request, completionHandler: { (URL, response, error) -> Void in
self.play(url: URL as! NSURL)
})
downloadTask.resume()
}
func play(url:NSURL) {
print("Playing \(url)")
do {
self.player = try AVAudioPlayer(contentsOf: url as URL)
player!.prepareToPlay()
player!.volume = 1.0
player!.play()
} catch let error as NSError {
//self.player = nil
print(error.localizedDescription)
} catch {
print("AVAudioPlayer init failed")
}
}
getting error in console :
The operation couldn’t be completed. (OSStatus error 1954115647.)
in line self.player = try AVAudioPlayer(contentsOf: url as URL)
Your first code is correct. I don't experience any significant delay when I run it on my device. Naturally it can take some time on a slow Internet connection to fill the buffer before playback can start; but your second code, downloading and then playing, would therefore be even slower.
If you want to know what's happening with the AVPlayer's buffer, use KVO to track changes in its timeControlStatus.

Video playback in Swift from URL

I am currently trying to play a video on iOS from a URL in Swift. URLs with endings like ".mp4" or ".m4v" work, but when I try to play a shared video link from Dropbox it doesn't.
I already changed the ending of the Dropbox link to "dl=1" but anyway nothing happens.
Playing a Youtube-video in a webview is not a solution for me, because I want the native iOS player.
Does anyone know what to do to play URLs from Dropbox or maybe recommend cloud services where this does work?
Thank you!
I didn't know dropbox streaming spec.
But if dropbox url doesn't have hint for playing like mp4,
you can use AVAssetLoaderDelegate like proxy server.
https://developer.apple.com/documentation/avfoundation/avassetresourceloaderdelegate
AVAssetLoaderDelegate excute when url has custom scheme.
If you use "foo" scheme, 'shouldWaitForLoadingOfRequestedResource' will be called. And you should make another request for dropbox with same http header.
Try like this
func play() {
let url = URL(string: "foo://bar.mp4")!
let asset = AVURLAsset(url: url)
let playerItem = AVPlayerItem(asset: asset)
asset.resourceLoader.setDelegate(self, queue: nil)
player.replaceCurrentItem(with: playerItem)
player.play()
}
func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool {
var newRequest = URLRequest(url: URL(string: "dropbox url")!)
newRequest.allHTTPHeaderFields = loadingRequest.request.allHTTPHeaderFields
let sessionTask = URLSession.shared.dataTask(with: newRequest) { data, response, error in
if let responseData = data {
loadingRequest.dataRequest?.respond(with: responseData)
}
loadingRequest.finishLoading() // Let player know that finish loding
}
sessionTask.resume()
return true // Wait for sessionTask response.
}

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)

Downloading files in iOS [duplicate]

This question already has answers here:
How to download file in swift?
(16 answers)
Closed 6 years ago.
I'm trying to download a file using Swift. This is the downloader class in my code:
class Downloader {
class func load(URL: URL) {
let sessionConfig = URLSessionConfiguration.default
let session = URLSession(configuration: sessionConfig, delegate: nil, delegateQueue: nil)
let request = NSMutableURLRequest(url: URL)
request.httpMethod = "GET"
let task = session.dataTask(with: URL)
task.resume()
}
}
I call the function like this:
if let URL = URL(string: "https://web4host.net/5MB.zip") {
Downloader.load(URL: URL)
}
but this error message pops up:
2017-02-16 04:27:37.154780 WiFi Testing[78708:7989639] [] __nw_connection_get_connected_socket_block_invoke 2 Connection has no connected handler
2017-02-16 04:27:37.167092 WiFi Testing[78708:7989639] [] __nw_connection_get_connected_socket_block_invoke 3 Connection has no connected handler
2017-02-16 04:27:37.169050 WiFi Testing[78708:7989627] PAC stream failed with
2017-02-16 04:27:37.170688 WiFi Testing[78708:7989639] [] nw_proxy_resolver_create_parsed_array PAC evaluation error: kCFErrorDomainCFNetwork: 2
Could someone tell me what I'm doing wrong and how I could fix it? Thanks!
The code to receive the data is missing.
Either use delegate methods of URLSession or implement the dataTask method with the completion handler.
Further for a GET request you don't need an URLRequest – never use NSMutableURLRequest in Swift 3 anyway – , just pass the URL and don't use URL as a variable name, it's a struct in Swift 3
class Downloader {
class func load(url: URL) { // better func load(from url: URL)
let sessionConfig = URLSessionConfiguration.default
let session = URLSession(configuration: sessionConfig, delegate: nil, delegateQueue: nil)
let task = session.dataTask(with: url) { (data, response, error) in
// handle the error
// process the data
}
task.resume()
}
}

Resources