跳轉到

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

  1. In Xcode, choose File > New > Target and pick Broadcast Upload Extension.
  2. Name it, for example BroadcastExtension, and press Finish. You do not need the setup UI extension Xcode offers.
  3. 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.
  4. Link RevueSDK to the Extension target as well.
  5. 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:

  1. Select the target and open Signing & Capabilities.
  2. Press + Capability and add App Groups.
  3. 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 call setScreenShare(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.

尚未支援

ScreenShareCaptureOptions 的欄位目前都沒有作用。傳入後的結果和 setScreenShare(enabled: true) 相同, 模式仍依 App 的組成自動決定。

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.