Apple SharePlay
In this article, we’re going to shed some light on the new SharePlay feature that Apple introduced as part of iOS, iPadOS, tvOS 15.1. SharePlay allows users to share and enjoy the content together—while also helping attract new users along the way.

Background
SharePlay is a feature that allows users to share activities together while on a FaceTime call, such as sharing their screen, drawing together or sharing media such as music or video content, which is what we’re going to be focusing on today.
So let’s jump right in and see how we can implement SharePlay for a video streaming app.
Logic
When two or more users are on a FaceTime call, they can share what is called a GroupActivity
which represents the activity that the users are sharing together, such as watching a movie.
This GroupActivity
creates a GroupSession
to represent the current session in which the users are sharing the activity and these get passed from the initiating device to the other devices through FaceTime Framework.
However, code-wise it’s kind of similar on all devices as the system takes care of creating those sessions and delivering them to the app, we never create a GroupSession
manually. we’ll see how this works in a little bit.
Setup
In order to use SharePlay in our app, we need to add the group activities capability to our app target, which should automatically add it to our provisioning profile as well.

Initiating the Shared activity
So, as the logic suggests, we need to create a Group Activity that represents our group watching activity in our app. This activity needs to provide two things:
- Some sort of item that represents the content being shared, so let’s create an object to do that for us and call it
SharePlayItem
for example. - GroupActivityMetadata: which provides the metadata that will be used by the system to show things like the title, subtitle and the type of this activity, in this case we’ll use the type
.watchTogether
public struct SharePlayItem: Codable {
public var id: String
public var title: String?
public var subtitle: String?
public var url: URL?
public var image: CGImage?
}
Keep in mind that this object needs to conform to Codable
since it’s going to be shared across network to the other devices.
public struct MediaWatchingActivity: GroupActivity {
let item: SharePlayItem
public var metadata: GroupActivityMetadata {
var metadata = GroupActivityMetadata()
metadata.type = .watchTogether
metadata.title = item.title
metadata.subtitle = item.subtitle
metadata.previewImage = item.image
metadata.fallbackURL = item.url
return metadata
}
}
Important: if we have a tvOS version of our app and we don’t want to enable SharePlay on it or if it’s not going to be ready at the same time as the iOS version, we need to make sure to set the metadata property supportsContinuationOnTV to false.
Now that we have the activity and the item that’s going to be shared through it, it’s time to initiate the activity sharing, and we do that by activating the group activity where we’d normally just start the local playback functionality, and we leave the decision about playing locally or sharing with others to the OS, which in turn is going to ask the user through a prompt what they want to do.
So lets create a prepareToPlay
function that’s going to create our activity using the media item being played, then ask the activity to prepareForActivation
, doing that will trigger the user prompt through the OS and the user gets to pick if they want to play locally, share with the other users on the call, or cancel playback altogether.
If the user decides to play locally then we’d run or already existing local playback code, but if they decide to share, then at this point we’ll ask the activity to activate
which will allow FaceTime to create a GroupSession
for this activity and start sharing both with other devices in the call.
In the below code, we use Task
because async functions like prepareForActivation()
can only be called by async callers, so that they can suspend when the called async function suspends, but since we want to call it from a sync function (prepareToPlay
in that case) we use Task
because it packages the code in the closure and sends it to the system to be immediately executed on the next available thread, much like the async
function on a global dispatch queue.
public func prepareToPlay(sharePlayItem: SharePlayItem) {
Task {
let activity = MediaWatchingActivity(item: sharePlayItem)
switch await activity.prepareForActivation() {
// Play locally
case .activationDisabled:
// Run local playback code
// Share via SharePlay
case .activationPreferred:
do {
_ = try await activity.activate()
} catch { }
// Do nothing
case .cancelled:
break
default:
break
}
}
}
Joining the Session
As mentioned earlier, we never create the GroupSession
manually, that is a job for the FaceTime framework to do, which means that in all devices, wether initiating the sharing or receiving it, we get a session object which we need to join to start the shared experience with others.
It might be a good idea to have a dedicated class to handle the coordination between FaceTime and the playback, so let’s create a singleton class, we’ll call it CoordinationManager
, that would listen on the incoming sessions, Apple recommends using async await
for that purpose. Also, we should make the prepareToPlay
function part of this class and call it as:
coordinationManager.shared.prepareToPlay(sharePlayItem: sharedItem)
Once a session is received, we join it to allow FaceTime to channel events through, also we keep track of its state so that once it becomes invalid we stop listening on it, also we keep track of the activity, so for example if the item changes (i.e user switches content) the other users will receive the new item and start watching it as well.
in the below code, we use a little bit of swift reactive framework Combine
to handle the listening on group session. Publishing a property with the @Published
attribute creates a publisher of this type. You access the publisher with the $
operator. When the property changes, publishing occurs in the property’s willSet
block, meaning subscribers receive the new value before it’s actually set on the property.
public class CoordinationManager {
public static let shared = CoordinationManager()
private var subscriptions = Set<AnyCancellable>()
@Published public var groupSession: GroupSession<MediaWatchingActivity>?
private init() {
Task {
/// On init, start listening to the sessions and set the shared
/// item when the group watching activity is activated
for await groupSession in MediaWatchingActivity.sessions() {
self.groupSession = groupSession
subscriptions.removeAll()
groupSession.$state.sink { [weak self] state in
if case .invalidated = state {
self?.groupSession = nil
self?.subscriptions.removeAll()
}
}.store(in: &subscriptions)
groupSession.join()
groupSession.$activity.sink { [weak self] activity in
// Run Local playback code based on received activity.item
}.store(in: &subscriptions)
}
}
}
AVPlayer syncing
If we run the app at this point, we’ll be able to share content successfully with others on a FaceTime call, but only one thing is going to be missing from the experience, Syncing. So far what we’ve done will start playback on all devices, but it won’t sync the user actions (i.e play/pause, seeking). AVPlayer takes care of all this in the background, but it needs us to do a few simple steps in order for it to work.
First, our AVPlayer needs to have a reference to the session so that it can receive events from other AVPlayers on that session, so we’ll need to pass that session to the player’s [playbackCoordinator](<https://developer.apple.com/documentation/avfoundation/avplaybackcoordinator>)
, which is a new property added in iOS, tvOS 15.0 to support SharePlay.
player.playbackCoordinator.coordinateWithSession(groupSession)
Next, AVPlayer needs some sort of identifier to know that the items playing on all the other players are the same as its own, AVPlayerPlaybackCoordinatorDelegate offers a function to help us provide that, now AVPlayer by default will try to use the media item URL as an identifier if we don’t provide that, but if that’s not guaranteed to be the same across all the devices, then we need to use this delegate function.
public func playbackCoordinator(_ coordinator: AVPlayerPlaybackCoordinator, identifierFor playerItem: AVPlayerItem) -> String {
item.id
}
Last but not least, we must not forget to set the class that conforms to this protocol as our playbackCoordinator
delegate.
player.playbackCoordinator.delegate = self
Terminating the session
Just as we join the session to start the SharePlay magic, we might also want to end it depending on our app design, so for example we might want to do that on closing the video player screen.
There are two ways with which the session can be terminated, either for one user only or for the whole group and they can be achieved by calling .leave()
or .end()
on the session itself.
// One user leave the session
groupSession.leave()
// End session for all users
groupSession.end()
And that’s it 🙂 that’s the basic implementation for SharePlay, it might differ a little bit based on your app design, but the logic should be more or less the same.
References
That concludes our tutorial on SharePlay, below are some of the links and reference you might find useful.
General Info
Job Opportunities
- Nicole Seamone
- HR Director
- E: nicole.seamone@redspace.com
- T: 902 444 3490 x9019
Find Us
- 1595 Bedford Hwy, Suite 168
- Bedford, NS B4A 3Y4
- Canada
Located in Sunnyside Mall, near Pete's.
Business Inquiries
Media & Entertainment
- Mike Rudolph
- Director of Client Engineering
- E: mike.rudolph@redspace.com
- T: 902 414 1809
Learning
- Andrew Hamilton
- Director of Client Engineering
- E: andrew.hamilton@redspace.com
- T: 902 444.3490 x8933
Defence
- Ken Howard
- Defence Practice Lead
- E: ken.howard@redspace.com
- T: 902 222 2740