Screen sharing¶
A screen share is a video track like the camera: LocalParticipant.screenShareTrack. You switch its
capture on and off with LocalParticipant.setScreenShare(enabled:) and publish it to each friend
yourself. This page covers the two capture modes, how to set up the one that can share other apps, and
what happens when sharing starts and stops.
Supported platforms¶
Screen sharing is supported on iOS only. On macOS, setScreenShare(enabled:) and
set(source: .screenShareVideo, enabled:) throw ScreenShareError.notImplemented(feature:plannedVersion:), and
LocalParticipant.isScreenSharing is always false.
Capture modes¶
There are two modes. You do not choose one in code: the SDK picks it from how your App is built.
| In-app | Broadcast | |
|---|---|---|
| What is shared | Your own App only | The whole screen, including other apps and the home screen |
| When it is used | Every other case | Your App contains a Broadcast Upload Extension and has the App Group set up (see Set up Broadcast) |
| What the user sees | iOS asks for permission to record the screen | The system broadcast picker, with your Extension preselected; sharing starts when the user taps Start |
| In the background | Capture stops while your App is in the background, and every recipient gets the placeholder; it resumes when your App comes back | Capture continues |
| Who can start it | Your App, with setScreenShare(enabled: true) |
Your App, or the user from Control Center |
| How it stops | Your App calls setScreenShare(enabled: false) |
Your App calls setScreenShare(enabled: false), or the user taps the red status bar indicator |
Both conditions are needed for Broadcast: the App Group must exist, and the Extension with the expected bundle ID must be inside your App. With only one of them, the SDK uses In-app.
Set up Broadcast¶
Broadcast needs a Broadcast Upload Extension: a separate process that iOS runs to record the screen, which hands the frames to your App.
1. Add the Extension¶
- In Xcode, choose File > New > Target and pick Broadcast Upload Extension.
- Name it, for example BroadcastExtension, and press Finish. You do not need the setup UI extension Xcode offers.
- In the new target's Signing & Capabilities, set its bundle ID to your main App's bundle ID followed by
.broadcast. For an App with bundle ID com.example.app, that is com.example.app.broadcast. - Link RevueSDK to the Extension target as well.
- Replace the contents of the Extension's SampleHandler class: import RevueSDK and inherit from
RevueSampleHandler. It does all the work; you add nothing else.
import RevueSDK
import ReplayKit
/// SampleHandler.swift in your Broadcast Upload Extension target. RevueSampleHandler sends the
/// frames to your App; you only change the superclass that Xcode's template generated.
class SampleHandler: RevueSampleHandler, @unchecked Sendable {
// Optional: log to Console.app, at the levels your App sets with Revue.setLogLevel.
override var enableLogging: Bool { true }
}
2. Put both targets in one App Group¶
The Extension and your App talk through a shared App Group. Do this for both targets, your main App and the Extension:
- Select the target and open Signing & Capabilities.
- Press + Capability and add App Groups.
- Add the group group.<main App bundle ID>, for example group.com.example.app. Both targets must list the same group.
Other identifiers¶
When your Extension's bundle ID or your App Group does not follow these names, tell the SDK in Info.plist. Put the App Group in both targets' Info.plist, and the Extension's bundle ID in the main App's:
<key>RevueAppGroupIdentifier</key>
<string>group.com.example.shared</string>
<key>RevueScreenSharingExtension</key>
<string>com.example.app.ScreenShare</string>
Start sharing¶
Always share through LocalParticipant.screenShareTrack. Your App does not create a screen share track of its
own: setScreenShare(enabled:), the status bar indicator and broadcasts started from Control Center all act on
this one track. setScreenShare(enabled: true) returns the same track.
setScreenShare(enabled: true) starts capture only. The SDK never publishes a screen share: nobody sees
it until you publish it. Each friend has their own publication, so publish the track to every friend with
LocalParticipant.publish(videoTrack:to:options:). A friend gets one video publication, so unpublish your
camera to them first.
import RevueSDK
/// Start a screen share and send it to everyone in the room in place of the camera.
/// setScreenShare only starts capturing; publishing the track to each participant is up to you.
/// Each participant gets one video publication, so the camera publication to them is removed first.
@MainActor
func startScreenShare() async throws {
let local = Room.shared.localParticipant!
guard let screen = try await local.setScreenShare(enabled: true) else { return }
for participant in Room.shared.remoteParticipants.values {
if let camera = local.videoPublication(source: .camera, to: participant.identity) {
local.unpublish(publication: camera)
}
if local.videoPublication(source: .screenShareVideo, to: participant.identity) == nil {
_ = try await local.publish(videoTrack: screen, to: participant)
}
}
}
When setScreenShare(enabled: true) returns depends on the mode:
- In-app: once ReplayKit has started capturing. It throws when the user does not allow screen recording.
- Broadcast: right away, after showing the system broadcast picker. Capture starts only when the user taps Start, which may be later or never.
When capture really starts¶
Watch LocalParticipant.isScreenSharingPublisher (or read LocalParticipant.isScreenSharing) to show the
state of your share button. It is the same signal in both modes: true once capture has started, false once it
has stopped. In In-app mode it becomes true with the first captured frame; in Broadcast mode, when the
broadcast starts.
import RevueSDK
import Combine
import Foundation
/// Update the screen share button from isScreenSharingPublisher. It sends true once capturing has
/// really started (in Broadcast mode, after the user taps Start), and false once it has stopped.
@MainActor
final class ScreenShareButtonModel: ObservableObject {
@Published private(set) var isSharing = false
private var subscription: AnyCancellable?
init() {
subscription = Room.shared.localParticipant.isScreenSharingPublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] sharing in self?.isSharing = sharing }
}
}
In Broadcast mode, a broadcast of your Extension starts capturing by itself, whichever way it was started:
- Your App called
setScreenShare(enabled: true)and the user tapped Start in the picker. You do not callsetScreenShare(enabled: true)again. - The user started a broadcast of your Extension from Control Center, without your App asking.
Either way the SDK starts capture and isScreenSharing becomes true, but it still does not publish anything:
no friend receives the screen share until you publish it. If the user cancels the picker, nothing happens:
no event is sent, isScreenSharing stays false, and you can call setScreenShare(enabled: true) again.
Friends who join later¶
Publishing goes to the friends who are there when you publish. A friend who joins while you are sharing does not get the screen share by themselves: publish it to them when they connect.
import RevueSDK
/// The SDK never publishes a screen share by itself. A friend who joins while you are sharing
/// gets nothing until you publish the screen share to them too.
final class ScreenShareForNewParticipants: RoomDelegate {
func room(_ room: Room, participantDidConnect participant: RemoteParticipant) {
Task { @MainActor in
let local = Room.shared.localParticipant!
guard local.isScreenSharing else { return }
if let camera = local.videoPublication(source: .camera, to: participant.identity) {
local.unpublish(publication: camera)
}
_ = try? await local.publish(videoTrack: local.screenShareTrack, to: participant)
}
}
}
Stop sharing¶
There are two ways to stop, and they end the same way:
- Your App calls
setScreenShare(enabled: false). - In Broadcast mode, the user taps the red status bar indicator and stops the broadcast.
Either way, capture stops, every recipient gets the placeholder instead of the screen, and the publications
stay: your friends keep your screen share tile, and isScreenSharing becomes false. To take the tile away,
unpublish the publications yourself with LocalParticipant.unpublish(publication:). Turning the screen share
on again sends it to the same publications.
import RevueSDK
/// Stop the screen share. Capturing stops and every recipient gets the placeholder, but the
/// publications stay: their screen share tiles remain until you unpublish them.
/// Calling setScreenShare(enabled: true) again resumes sending to the same publications.
@MainActor
func stopScreenShare() async throws {
try await Room.shared.localParticipant.setScreenShare(enabled: false)
}
Neither way changes the track's mute: switching capture on and off does not mute or unmute
screenShareTrack. If you muted it with mute(), turning sharing on again still sends the placeholder until
you call unmute(). See Mute a local track.
Capture options¶
ScreenShareCaptureOptions holds the options of a screen share: dimensions, fps, showCursor,
appAudio, useBroadcastExtension, useScreenCaptureKit, includeCurrentApplication and
excludeWindowIDs. Pass it when you turn the screen share on with
LocalParticipant.set(source:enabled:captureOptions:), with .screenShareVideo as the source.
setScreenShare(enabled:) takes no options.
Not supported yet
No field of ScreenShareCaptureOptions has an effect yet. Passing it gives the same result as setScreenShare(enabled: true), and the mode is still chosen from how your App is built.
import RevueSDK
/// Turn the screen share on through set(source:enabled:captureOptions:), passing
/// ScreenShareCaptureOptions. Its fields have no effect yet: this does the same as
/// setScreenShare(enabled: true).
@MainActor
func startScreenShareWithOptions() async throws {
let options = ScreenShareCaptureOptions(fps: 15, appAudio: false)
try await Room.shared.localParticipant.set(source: .screenShareVideo, enabled: true, captureOptions: options)
}
Broadcast state¶
In Broadcast mode, BroadcastManager.shared reports the broadcast itself: BroadcastManager.isBroadcasting,
BroadcastManager.isBroadcastingPublisher, and BroadcastManager.delegate, a BroadcastManagerDelegate.
The delegate's broadcastManager(didChangeState:) is called on the main thread when a broadcast of your
Extension starts or ends, so you can update your views from it directly. The delegate is weak: keep a
reference to it.
import RevueSDK
import Foundation
/// Broadcast mode only: get told when a broadcast of your Extension starts or ends, including one the
/// user started from Control Center. Called on the main thread. The delegate is weak: keep a reference.
final class BroadcastStateObserver: NSObject, BroadcastManagerDelegate {
let onChange: @MainActor @Sendable (Bool) -> Void
init(onChange: @escaping @MainActor @Sendable (Bool) -> Void) {
self.onChange = onChange
super.init()
BroadcastManager.shared.delegate = self
}
func broadcastManager(didChangeState isBroadcasting: Bool) {
MainActor.assumeIsolated { onChange(isBroadcasting) }
}
}
Most Apps do not need it: isScreenSharingPublisher covers both modes. Do not use
BroadcastManager.requestActivation() and BroadcastManager.requestStop() to start and stop sharing; call
setScreenShare(enabled:), which shows the picker and stops the broadcast for you.
Errors¶
| Situation | What is thrown |
|---|---|
| In-app: the user does not allow screen recording | The ReplayKit error itself, from setScreenShare(enabled: true). It is not wrapped in a RevueSDK error type. |
| macOS | ScreenShareError.notImplemented(feature:plannedVersion:) |
Broadcast mode throws nothing when the picker is cancelled or the broadcast fails to start: watch
isScreenSharingPublisher instead.
Troubleshooting¶
| Problem | What to check |
|---|---|
setScreenShare(enabled: true) shares only your App, with no picker |
The SDK is using In-app. Check that the Extension is embedded in your App, its bundle ID is your App's bundle ID followed by .broadcast (or matches RevueScreenSharingExtension), and both targets have the same App Group. |
| Sharing started, but your friend sees nothing | The screen share is not published to them. Publish screenShareTrack to each friend, including friends who joined after you started. |
| Your friend sees the placeholder | Sharing was stopped, the track is muted, or, in In-app mode, your App is in the background. |
| The picker opens but nothing comes through | Turn on enableLogging in your RevueSampleHandler subclass, then open the Console app on your Mac, select the device and filter on RevueSampleHandler. An error saying the bundle settings are wrong means the Extension cannot reach the App Group. |