ios - AVURLAsset when Response doesn't have 'Content-Lenght' header -


my ios app uses avplayer play streaming audio server , storing on device. implemented avassetresourceloaderdelegate, intercept stream. change scheme (from http fake scheme, avassetresourceloaderdelegate method gets called:

func resourceloader(_ resourceloader: avassetresourceloader, shouldwaitforloadingofrequestedresource loadingrequest: avassetresourceloadingrequest) -> bool

i followed tutorial:

http://blog.jaredsinclair.com/post/149892449150/implementing-avassetresourceloaderdelegate-a

over there, put original scheme back, , create session pulling audio server. works when server provides content-length (size of audio file in bytes) header streamed audio file.

but stream audio files cannot provide length ahead of time (let's live podcast stream). in case, avurlasset sets length -1 , fails with:

"error domain=avfoundationerrordomain code=-11849 \"operation stopped\" userinfo={nsunderlyingerror=0x61800004abc0 {error domain=nsosstatuserrordomain code=-12873 \"(null)\"}, nslocalizedfailurereason=this media may damaged., nslocalizeddescription=operation stopped}"

and cannot bypass error. tried go hacky way, provide fake content-length: 999999999, in case, once entire audio stream downloaded, session fails with:

loaded far: 10349852 out of 99999999 request timed out. //audio file got downloaded, size 10349852 //avplayer tries next chunk , fails request times out

have ever faced problem before?

p.s. if keep original http scheme in avurlasset, avplayer knows how handle scheme, plays audio file fine (even w/o content-length), not know how w/o failing. also, in case, avassetresourceloaderdelegate never used, cannot intercept , copy content of audio file local storage.

here implementation:

import avfoundation  @objc protocol cachingplayeritemdelegate {      // called when file downloaded     @objc optional func playeritem(playeritem: cachingplayeritem, didfinishdownloadingdata data: nsdata)      // called every time new portion of data received     @objc optional func playeritemdownloaded(playeritem: cachingplayeritem, diddownloadbytessofar bytesdownloaded: int, outof bytesexpected: int)      // called after prebuffering finished, player item ready play. called once, after initial pre-buffering     @objc optional func playeritemreadytoplay(playeritem: cachingplayeritem)      // called when media did not arrive in time 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? // used if play 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 cache                 // nothing here             } else if session == nil { // if we're playing url, need download 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>()             loadingrequest in pendingrequests {                 fillincontentinforation(contentinformationrequest: loadingrequest.contentinformationrequest)                 let didrespondcompletely = respondwithdataforrequest(datarequest: loadingrequest.datarequest!)                 if didrespondcompletely {                     requestscompleted.insert(loadingrequest)                     loadingrequest.finishloading()                 }             }             in requestscompleted {                 pendingrequests.remove(i)             }         }          func fillincontentinforation(contentinformationrequest: avassetresourceloadingcontentinformationrequest?) {             // if play cache make no url requests, therefore have no responses, need fill in contentinformationrequest manually             if playingfromcache {                 contentinformationrequest?.contenttype = self.mimetype                 contentinformationrequest?.contentlength = int64(songdata!.length)                 contentinformationrequest?.isbyterangeaccesssupported = true                 return             }              // have no response 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 data @ request             if songdata == nil || songdata!.length < startoffset {                 return false             }              // total data have startoffset whatever has been downloaded far             let bytesunread = songdata!.length - int(startoffset)              // respond or whaterver available if can't satisfy request 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 initializer 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 initializer 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 situation ios file damaged because header incorrect. system think going play regular audio file doesn't have info it. don't know audio duration be, if have live streaming. live streaming on ios done using http live streaming protocol. ios code correct. have modify backend , provide m3u8 playlist live streaming audios, ios accept live stream , audio player start tracks.

some related info can found here. ios developer experience in streaming audio / video can tell code play live / vod same.


Comments

Popular posts from this blog

ubuntu - PHP script to find files of certain extensions in a directory, returns populated array when run in browser, but empty array when run from terminal -

php - How can i create a user dashboard -

javascript - How to detect toggling of the fullscreen-toolbar in jQuery Mobile? -