Meine iOS-App verwendet den AVPlayer, um Audio-Streams von meinem Server abzuspielen und auf einem Gerät zu speichern. Ich habe AVAssetResourceLoaderDelegate implementiert, damit ich den Stream abfangen konnte. Ich mein Schema ändern (von http
auf ein gefälschtes Schema, so dass AVAssetResourceLoaderDelegate Methode aufgerufen wird:AVURLAsset, wenn Response nicht den Header 'Content-Lenght' hat
func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool
ich dieses Tutorial gefolgt:
http://blog.jaredsinclair.com/post/149892449150/implementing-avassetresourceloaderdelegate-a
esOver, ich die ursprüngliche Regelung setzen Zurück, und erstellen Sie eine Sitzung für das Ziehen von Audio vom Server.Alles funktioniert einwandfrei, wenn mein Server den Header Content-Length
(Größe der Audiodatei in Bytes) für die gestreamte Audiodatei bereitstellt.
Aber manchmal streame ich Audiodateien, wo ich nicht ihre Länge im Voraus bereitstellen kann (sagen wir einen Live-Podcast-Stream). In diesem Fall setzt AVURLAsset Länge -1
und schlägt mit:
"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}"
Und ich diesen Fehler nicht umgehen kann. Ich versuchte, einen hacky Weg zu gehen, bietet gefälschte Content-Length: 999999999
, aber in diesem Fall, sobald der gesamte Audio-Stream heruntergeladen wird, meine Session nicht mit:
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
Haben jemals jemand vor diesem Problem konfrontiert?
P.S. Wenn ich das ursprüngliche http
Schema in AVURLAsset behalte, weiß AVPlayer, wie man mit diesem Schema umgeht, also spielt es Audiodatei in Ordnung (sogar ohne Content-Length
), ich weiß nicht, wie es das ohne Fehler macht. Auch in diesem Fall wird mein AVAssetResourceLoaderDelegate niemals verwendet, sodass ich den Inhalt der Audiodatei nicht auf einen lokalen Speicher abfangen und kopieren kann. Hier
ist die Umsetzung:
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)
}
}
Das ist seltsam tho. Schau dir meine P.S .: '' P.S. Wenn ich das originale HTTP-Schema in AVURLAsset behalte, weiß AVPlayer, wie man mit diesem Schema umgeht, also spielt es die Audiodatei gut ab (auch ohne Content-Length), ich weiß nicht, wie es funktioniert, ohne zu scheitern. Auch in diesem Fall wird mein AVAssetResourceLoaderDelegate niemals verwendet, sodass ich den Inhalt der Audiodatei nicht auf einen lokalen Speicher abfangen und kopieren kann. '' ' Ich sende noch normale mp3-Datei ohne Content-Length-Header, und es spielt gut. – yeralin