2017-08-20 1 views
3

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

es

Over, 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) 
    } 

} 

Antwort

1

Sie können nicht mit dieser Situation umgehen wie für iOS diese Datei beschädigt ist, weil Header nicht korrekt ist. System denkt, dass Sie eine normale Audiodatei abspielen werden, aber es gibt nicht alle Informationen darüber. Sie wissen nicht, welche Audiodauer, nur wenn Sie ein Live-Streaming haben. Live-Streaming unter iOS erfolgt über das HTTP-Live-Streaming-Protokoll. Ihr iOS-Code ist korrekt. Sie müssen Ihr Backend modifizieren und eine m3u8-Playlist für Live-Streaming-Audios bereitstellen, dann akzeptiert iOS es als Live-Stream und der Audio-Player startet die Tracks.

Einige verwandte Informationen können here gefunden werden. Als iOS-Entwickler mit guten Erfahrungen beim Streaming von Audio/Video kann ich Ihnen sagen, dass der Code zum Abspielen von Live/VOD der gleiche ist.

+0

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