Download a live audio stream with undefined duration in iOS using Swift
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.
Problems:
- When we initialize
AVPlayer
with an infinite audio URL, it will start playing it. But if you try to getAVAssetTrack
out of theAVPlayer
object, you will get an empty array. - 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. - 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, theInputStream
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!
Solution:
- 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