This PR improves integration of playlists at the user interface level. This requires:
Improving our Player API to consistently work with PlayerItems.
Adding a way to attach contextual information to a PlayerItem.
Delivering a small UI component based on List that makes it straightforward to display items loaded in a player and to edit them (rearrange or delete).
Design considerations
Implementing better playlist support at the UI level was more difficult than initially expected. Several design options were investigated. The choices made in this PR are motivated below.
Metadata required by a playlist UI
A playlist UI component must be able to display all items stored in a player at any time. But a PlayerItem currently has no associated metadata at creation time: Metadata is only available once the item has been loaded by the player, a process that occurs when the item is about to be played.
This means that at least some metadata must be made immediately available at the time an item is inserted into a player. Otherwise we cannot meaningfully represent the item in a playlist UI, which would lead to a poor user experience.
This is why I introduced the concept of source, added as parameter to all PlayerItem creation methods. This way we can add information about the context of creation of a PlayerItem, readily available for item display in a playlist UI.
The type of this source will be discussed in the next section, but note that we want it to be independent from the metadata that will possibly be loaded with the item. This would namely lead to inconsistent display, with some values displayed initially, replaced by other values when the item has been loaded. Again a poor user experience.
[!NOTE]
This approach also avoids pitfalls of our initial demo playlist implementation. Our sync code was namely unidirectional, which means media list mutations would lead to player item list mutations, but not the other way around. In particular, we had no way of inserting a meaningful media if a player item was added directly to the player with its public API.
Source type
In general we want to preserve the type of objects so that we can access their API directly and safely, without any kind of downcasting. This is what we achieved with our tracker API, for example.
If we wanted the same for sources associated with PlayerItems we would need to make PlayerItem generic and, transitively, Player and all surrounding components as well. This would also force items to be accepted by a player only if they all have matching source types and probably force us to introduce protocols for type erasure, a process especially annoying when @Published properties are involved.
For this reason I see no other option than storing source as Any. This of course means a downcasting is required but an app controls the context of creation of its items and therefore the kinds of downcastings that are required.
Playlist UI component
A natural way to display Player items is to use a SwiftUI List:
List and SwiftUI components in general work with either identifiable or hashable types. We could make PlayerItem identifiable (each item already has a unique UUID after all) but this means we would need to return a UUID from current item APIs, which is not optimal. Instead, I chose to make PlayerItem hashable and to use PlayerItem itself as a way to identify the current item. This means index-based APIs, which already were discussed as inconsistent in the past, had to be replaced with item-based APIs to get or set the currentItem.
Despite my attempts I could not make a List extension that allows binding of a Player, like we did for Slider. I was constantly fighting the type system and non-trivial generic constraints. Fortunately List customization APIs are provided on View, which means we can write a simple Playlist view wrapper, with a more expressive name, the same semantics and the same customization options.
Changes made
Make PlayerItem hashable. This increases its compatibility with SwiftUI APIs which often rely on this constraint.
Add source support to PlayerItem creation, making it possible to provide free context information about what created the item in the first place.
Add Playlist UI component for easy display of items contained in a player. Rows can rely on item source information to display meaningful data.
Make all playlist-related player APIs work with PlayerItem, not sometimes with indices, sometimes with PlayerItem. This makes it possible to easily bind UI components that manipulate the current item in the associated list of items. The currentIndex and setCurrentIndex(_:) methods have been replaced with a single currentItem getter / setter that does not throw anymore (the corresponding error has been removed as well).
Provide monoscopic support as PlayerMetadata information. This way VideoView can automatically apply correct behavior without additional work. The viewport modifier has therefore been replaced with an orientation modifier that has an effect when playing monoscopic content only.
Remove reload feature from the playlist demo. This was leading to unnecessary complexity (storing the original items) but added no real value. This was the code will be easier to read as an implementation example.
Remove templates from the demo. The associated complexity was really not required, medias are sufficient.
Store layout state in view models to restore views in their previous state after returning from PiP.
Wrap CoreBusiness types related to the media composition consistently.
Make scheme configuration consistent.
Checklist
[x] APIs have been properly documented (if relevant).
[x] The documentation has been updated (if relevant).
[x] New unit tests have been written (if relevant).
Description
This PR improves integration of playlists at the user interface level. This requires:
Player
API to consistently work withPlayerItem
s.PlayerItem
.List
that makes it straightforward to display items loaded in a player and to edit them (rearrange or delete).Design considerations
Implementing better playlist support at the UI level was more difficult than initially expected. Several design options were investigated. The choices made in this PR are motivated below.
Metadata required by a playlist UI
A playlist UI component must be able to display all items stored in a player at any time. But a
PlayerItem
currently has no associated metadata at creation time: Metadata is only available once the item has been loaded by the player, a process that occurs when the item is about to be played.This means that at least some metadata must be made immediately available at the time an item is inserted into a player. Otherwise we cannot meaningfully represent the item in a playlist UI, which would lead to a poor user experience.
This is why I introduced the concept of
source
, added as parameter to allPlayerItem
creation methods. This way we can add information about the context of creation of aPlayerItem
, readily available for item display in a playlist UI.The type of this source will be discussed in the next section, but note that we want it to be independent from the metadata that will possibly be loaded with the item. This would namely lead to inconsistent display, with some values displayed initially, replaced by other values when the item has been loaded. Again a poor user experience.
Source type
In general we want to preserve the type of objects so that we can access their API directly and safely, without any kind of downcasting. This is what we achieved with our tracker API, for example.
If we wanted the same for
source
s associated withPlayerItem
s we would need to makePlayerItem
generic and, transitively,Player
and all surrounding components as well. This would also force items to be accepted by a player only if they all have matching source types and probably force us to introduce protocols for type erasure, a process especially annoying when@Published
properties are involved.For this reason I see no other option than storing
source
asAny
. This of course means a downcasting is required but an app controls the context of creation of its items and therefore the kinds of downcastings that are required.Playlist UI component
A natural way to display
Player
items is to use a SwiftUIList
:List
and SwiftUI components in general work with either identifiable or hashable types. We could makePlayerItem
identifiable (each item already has a unique UUID after all) but this means we would need to return a UUID from current item APIs, which is not optimal. Instead, I chose to makePlayerItem
hashable and to usePlayerItem
itself as a way to identify the current item. This means index-based APIs, which already were discussed as inconsistent in the past, had to be replaced with item-based APIs to get or set thecurrentItem
.List
extension that allows binding of aPlayer
, like we did forSlider
. I was constantly fighting the type system and non-trivial generic constraints. FortunatelyList
customization APIs are provided onView
, which means we can write a simplePlaylist
view wrapper, with a more expressive name, the same semantics and the same customization options.Changes made
PlayerItem
hashable. This increases its compatibility with SwiftUI APIs which often rely on this constraint.source
support toPlayerItem
creation, making it possible to provide free context information about what created the item in the first place.Playlist
UI component for easy display of items contained in a player. Rows can rely on itemsource
information to display meaningful data.PlayerItem
, not sometimes with indices, sometimes withPlayerItem
. This makes it possible to easily bind UI components that manipulate the current item in the associated list of items. ThecurrentIndex
andsetCurrentIndex(_:)
methods have been replaced with a singlecurrentItem
getter / setter that does not throw anymore (the corresponding error has been removed as well).PlayerMetadata
information. This wayVideoView
can automatically apply correct behavior without additional work. Theviewport
modifier has therefore been replaced with anorientation
modifier that has an effect when playing monoscopic content only.Checklist