Learn More

Apple SharePlay

Assem Sherief
Jan 24

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:

  1. 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.
  2. 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.

Job Opportunities

Check current job openings

Find Us

  • 1595 Bedford Hwy, Suite 168
  • Bedford, NS B4A 3Y4
  • Canada

Located in Sunnyside Mall, near Pete's.

View on Google Maps

Business Inquiries

Media & Entertainment

Learning

Defence

Cybersecure Badge