Construct an audio player to play your custom audio data, and optionally take advantage of the advanced features of AirPlay 2.
- iOS 12.1+
- Xcode 11.4+
This sample code project builds a robust audio player from the ground up, using the
AVSample classes to manage enqueuing and playback of audio that you provide. The player uses a playlist of playable items, and allows the user to edit the contents of the playlist while playback is in progress.
The example app also uses
AVAudio to indicate that it plays long-form audio content — music, audiobooks, podcasts, or other content that a user will listen to over a substantial period of time. This allows the player to benefit from AirPlay 2. When the app plays to a compatible device such as HomePod, AirPlay 2 dramatically improves playback reliability and performance, and enables advanced features such as multiroom playback.
Specify Long-Form Audio
To use AirPlay 2 for playback to compatible output devices, configure your audio session with the
long route-sharing policy. Typically, you do this once when your app starts up:
.long route-sharing policy is a hint to the system that your audio content is suitable for extended listening sessions. As a side effect, it also allows your audio to benefit from AirPlay 2 for extended buffering and improved responsiveness to commands.
You can choose not to configure your audio as long form if your content is not intended to displace playback of long-form content from apps such as Apple Music, iTunes, or Podcasts.
Provide Audio Content
For your custom audio player, start by deciding how you want to identify content to the player. One way is to manage a playlist, a persistent list of playable items. Alternatively, use a temporary queue of items, a single item, or a continuous stream of audio that has no distinct identity as a separate item.
This sample app demonstrates the use of a playlist, each item representing a single music track. The player has public APIs that app code can use to manipulate the contents and order of the playlist, and start and stop playback. The
Sample class implements these APIs.
Internally, your player should contain logic to enqueue buffers in advance of their scheduled playback time, and to handle transitions between items.
In this example project, a
Sample object provides the enqueuing logic, using
Sample objects to wrap playable items and provide audio buffers on request.
Sample objects use
Sample objects to provide the basic audio data.
These classes are discussed in more detail in the following sections.
Manage Your Playlist
Sample to manage a playlist through a private
current member represents the current state of the player. It indicates which element of the
items array is the currently playing item, and implies a corresponding player state.
nil, there is no current item, and the player is in a stopped state.
nil, it is a valid index into the
itemsarray, and the player is in a playing or paused state.
A number of
Sample methods manage the
Playlist. For example, you use an
insert method to insert an item into the queue:
In general, all of the public methods of
Sample end up invoking one of these helper methods:
restart method forces playback of the currently playing item (if any) to stop before playback restarts with a new list of items:
continue method allows the currently playing item to continue to play, followed by a new list of items that will play after the current item finishes:
Both methods begin by checking that the player state isn’t “stopped.” They then construct a queue of items to play — which may consist of fewer items than the entire playlist — and pass the queue to a corresponding method in the
Sample class. This transfer of control takes place on a serial
Dispatch, so that the
Sample object handles one action at a time.
Sample object receives a queue of items to play, it proceeds with the dual tasks of translating the items into a sequence of sample buffers containing audio data, and enqueuing the buffers for rendering. The
Sample causes an
AVSample object to play audio at the correct time, and an
AVSample object to render enqueued audio sample buffers in time for playback.
The most important
Sample methods are the two methods it uses to accept control from the
continue. Both methods take, as their first parameter, a queue of items to play in order. The methods differ in the way they handle the item that was previously playing, if any.
restart method stops any current playback, which means that the audio renderer can simply flush all enqueued buffers, and restart enqueuing from the first provided item.
continue attempts to let the current playback continue, and allow enqueued buffers to remain enqueued, as far as possible. It does this by examining its new list of items, and finding items that match items that have already been scheduled. It can then do a partial flush of the audio renderer, starting from the playback time of the first nonmatching item, and resume enqueuing sample buffers from that point.
The time-based partial flush uses asynchronous API with a completion handler. Upon completion, enqueuing actually restarts in an additional method,
finish. This division into a pair of methods is an implementation detail.
As it converts its list of items into a sequence of sample buffers, the
Sample must enqueue the buffers to an
AVSample object. Control of enqueuing relies on the
request method of
AVSample, which takes a closure parameter that the renderer invokes whenever it is ready for more sample buffers.
For a single playlist item, the sequence of events is straightforward, but when there are multiple playlist items, the
Sample may need to enqueue sample buffers from a second (or subsequent) item before the first item finishes playing. That means, usually, that enqueuing of sample buffers takes place well in advance of the playback time of those buffers. This happens as follows:
The serializer queues sample buffers as early as possible, when requested by the
Buffer Audio Renderer
After enqueuing the last sample buffer of a playlist item, the serializer places a boundary observer on the
AVSampletimeline, at the expected ending playback time of that item.
Buffer Render Synchronizer
When the boundary observer fires, playback of that item is complete. The serializer discards the item, removes the boundary observer, updates the current item in the
Sample, and generates a notification that is used to update the current item display in the UI. It also places a periodic observer on the timeline, which fires every 0.1 seconds, to generate future timing notifications for updating the playback time display in the UI.
This process of placing boundary observers, and removing them when they fire, repeats as each item finishes enqueuing its buffers.
In the implementation, the important methods are
provide, which enqueues sample buffers and places boundary observers, and
update, which the boundary observer invokes to handle the transition between items.
Retrieve Sample Buffers
Sample object provides the
Sample with a sequence of audio sample buffers for a playback item:
Sample object also manages state associated with the playback item. This includes a
unique property that identifies the item uniquely within the playlist, even when items share the same underlying
It also includes an
end property, which is the time — relative to the start of the item — when playback of buffers enqueued so far will end. This value is important for determining the placement of the boundary observers described in the previous section, on the
Sample object keeps a reference to the source of its audio data while the data is being enqueued. The audio data source is represented by a
Sample, which is an object that you customize for your audio data.
As soon as all data for the item is enqueued, the item can discard its data source object, allowing the system to reclaim its resources (files and memory, for example).
Provide Your Data Source
Ultimately, you will need to provide custom data source code to fetch your custom audio data and package it into
CMSample objects that can be passed to the audio renderer.
In this example project, a
Sample object serves as the data source. It simply reads data from an audio file stored within the app bundle.
The class also contains helper methods that convert an
AVAudio to a
CMSample, which is the required type for data passed to the audio renderer.
The player and serializer implementations contain detailed, formatted logging of their actions. The logging output can be crucial to understanding the behavior of the code during development and testing.
By default, logging suppresses messages about the enqueuing of specific sample buffers, to avoid flooding the console. If desired, you can enable those messages by setting