Download a live audio stream with undefined duration in iOS using Swift

mohan pandey
AppCoda Tutorials
Published in
4 min readDec 9, 2018

--

How many times have you thought of downloading a FM stream that you are listening to? Or have you tried to find a solution of how to download a live audio stream that has no definitive duration and plays over the internet? Well, we together will find the solution in this article.

Now coming to the real deal, there are a few steps we will follow along with and we will solve the problem that occurs in accomplishing this task.

Radio At Work !!!

Problems:

  1. When we initialize AVPlayer with an infinite audio URL, it will start playing it. But if you try to get AVAssetTrack out of the AVPlayer object, you will get an empty array.
  2. Now if you do not have Audio Track, how on earth you can capture it and download the track? You cannot use MTAudioProcessingTapRef to tap on the Audio Track and download it.
  3. One can think of using InputStream to simply initiate the stream with the URL and read the bytes of it. But… again it is not possible because as soon as you will try to read the buffer, the InputStream would encounter end.

But wait why I have been telling you how we will fail… This article then should be named different ways you cannot download the live audio stream, but it isn’t! So, now we will explore our solution… So get ready!

Get ready for listening to live audio over internet

Solution:

  1. Get your URL ready from where you want to play and download the audio stream:
let url = URL(string: "Your Url")!

2. Now create an AVPlayerItem from that url:

playerItem = CachingPlayerItem(url: url, recordingName: recordingName ?? "default.mp3")///CachingPlayerItem is a custom class that is a subclass of AVPlayerItem.

Initializing CachingPlayerItem object with url will prepare an AVURLAsset from the given url. Whole init function for initializing the CachingPlayerItem is given below:

init(url: URL, customFileExtension: String?, recordingName: String) {guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false),let scheme = components.scheme,var urlWithCustomScheme = url.withScheme(cachingPlayerItemScheme) else {fatalError("Urls without a scheme are not supported")}self.recordingName = recordingNameself.url = urlself.initialScheme = schemeif let ext = customFileExtension {urlWithCustomScheme.deletePathExtension()urlWithCustomScheme.appendPathExtension(ext)self.customFileExtension = ext}let asset = AVURLAsset(url: urlWithCustomScheme)asset.resourceLoader.setDelegate(resourceLoaderDelegate, queue: DispatchQueue.main)super.init(asset: asset, automaticallyLoadedAssetKeys: nil)resourceLoaderDelegate.owner = selfaddObserver(self, forKeyPath: "status", options: NSKeyValueObservingOptions.new, context: nil)NotificationCenter.default.addObserver(self, selector: #selector(playbackStalledHandler), name:NSNotification.Name.AVPlayerItemPlaybackStalled, object: self)}

3. After AVPlayerItem is initialized, make an AVPlayer object from it so that it can kick start the process of loading audio stream from the given url:

player = AVPlayer(playerItem: playerItem)player.automaticallyWaitsToMinimizeStalling = false

If you look at init method, we set the delegate of asset.resourceLoader so the following function of AVAssetResourceLoaderDelegate will be called:

func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool {if playingFromData {// Nothing to load.} else if session == nil {// If we're playing from a url, we need to download the file.// We start loading the file on first request only.guard let initialUrl = owner?.url else {fatalError("internal inconsistency")}startDataRequest(with: initialUrl)}pendingRequests.insert(loadingRequest)processPendingRequests()return true}

Once recourse loading is started, we called the func startDataRequest(with url: URL) method.

func startDataRequest(with url: URL) {var recordingName = "default.mp3"if let recording = owner?.recordingName{recordingName = recording}fileURL = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent(recordingName)let configuration = URLSessionConfiguration.defaultconfiguration.requestCachePolicy = .reloadIgnoringLocalAndRemoteCacheDatasession = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)session?.dataTask(with: url).resume()outputStream = OutputStream(url: fileURL, append: true)outputStream?.schedule(in: RunLoop.current, forMode: RunLoopMode.defaultRunLoopMode)outputStream?.open()}

Now as we initiated a dataTask with the given URL, we will start receiving data bytes into the delegate function:

func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data)

4. Now we have started receiving live audio stream…😰. Last piece of the puzzle is to store the audio stream. It is the simplest part. We just create an OutputStream object, open it then append the bytes we are receiving in the above delegate function and thats it, we saved the desired part of the live audio stream.

func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {let bytesWritten = data.withUnsafeBytes{outputStream?.write($0, maxLength: data.count)}print("bytes written :\(bytesWritten!) to \(fileURL)")}

Conclusion

We have successfully downloaded the desired part of a live audio stream which has no definitive length over internet.

The final product would look something like this:

Source Code

You can find the source code on my GitHub. Thanks for reading!

Follow us on social media platforms:
Facebook: facebook.com/AppCodamobile/
Twitter: twitter.com/AppCodaMobile
Instagram: instagram.com/AppCodadotcom

If you enjoyed this article, please click the 👏 button and share to help others find it! Feel free to leave a comment below.

--

--