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
Post a Comment