Skip to main content
Version: 5.5

Screen share in a group call

This page provides a code example for screen share in a group call (conference).

Prerequisites (iOS only)

Before implementing screen share in your iOS application, you must do the following:

  • The application must implement Broadcast Upload Extension or equivalents to capture screen share.
    • To implement Broadcast Upload Extension, in Xcode, add a new Target to your project, select "Broadcast Upload Extension" template, and activate the extension.
  • Define a port number, a reception token, and a transmission token that will be used for screen share stream transmission.

Sender - Start sending screen share

The code implementation for starting screen share differs between iOS and macOS.

iOS

Set the screen share key in PlanetKit

The app client of the sender needs to specify predefined port number, reception token, and transmission token, and sets it in withEnableScreenShareKey(broadcastPort:broadcastPeerToken:broadcastMyToken:) of PlanetKitJoinConferenceSettingBuilder.

var settingsBuilder = try! PlanetKitJoinConferenceSettingBuilder()
.withEnableScreenShareKey(broadcastPort: PORT_NUMBER, broadcastPeerToken: "USER_DEFINED_TOKEN_EXT", broadcastMyToken: "USER_DEFINED_TOKEN_APP")

Implement a screen capturing and transmission module

Implement a screen capturing module that establishes connection between your app and the SDK through NWConnection.

class BroadcastSender {
private enum State {
case started
case handshaking
case connected
case failed
}

private let connection: NWConnection
private let queue = DispatchQueue(label: "BroadcastSender.Queue")

static func connection(broadcastPort : UInt16) throws -> NWConnection {
guard let port = NWEndpoint.Port(rawValue: broadcastPort) else {
throw Error.invalidPort
}

let options = NWProtocolTCP.Options()
options.noDelay = true

return NWConnection(host: .ipv4(.loopback), port: port, using: .init(tls: nil, tcp: options))
}

init(broadcastPort: UInt16, rxToken : String, txToken : String) throws {
self.delegate = delegate
self.rxToken = rxToken
self.txToken = txToken

connection = try BroadcastSender.connection(broadcastPort: broadcastPort)
connection.stateUpdateHandler = { [weak self] in
self?.handleConnection(newState: $0)
}
connection.start(queue: DispatchQueue(label: "BroadcastSender.NetworkQueue"))
}

func handShake() {
guard state == .started, let data = txToken.data(using: .utf8) else {
return
}

state = .handshaking

connection.send(content: data, completion: .contentProcessed({ (error) in
self.handShakeProcessed(error: error)
}))
}

func sendVideo(sampleBuffer: CMSampleBuffer) throws {
guard state == .connected,
!sending else {
return
}

try queue.sync {
let data: Data?
// create data with CMSampleBuffer
connection.send(data, completion: .contentProcessed({(error) in NSLog("data processed \(error)")}))
}
}

func handShakeProcessed(error: NWError?) {
queue.sync {
if let error = error {
didFail(error: error)
} else {
handShakeAck()
}
}
}

func handShakeAck() {
guard state == .handshaking, let token = rxToken.data(using: .utf8) else {
return
}

connection.receive(minimumIncompleteLength: token.count, maximumLength: token.count) { (data, context, final, error) in
self.handShakeAckProcessed(data: data, error: error)
}
}

func handShakeAckProcessed(data: Data?, error: NWError?) {
queue.sync {
guard state == .handshaking, let token = rxToken.data(using: .utf8) else {
return
}
if data == token {
state = .connected

} else {

didFail(error: .rejected)
}
}
}

func handleConnection(newState: NWConnection.State) {
queue.sync {
switch newState {
case .ready:
handShake()
case .waiting(let error), .failed(let error):
didFail(error: error)
default:
break
}
}
}
}

If connection is successfully established, you must send screen share streams to the created NWConnection. Implement the SampleHandler class to send the captured screen share streams.

class SampleHandler: RPBroadcastSampleHandler {
private var sender: BroadcastSender?
...
override func broadcastFinished() {
// User has requested to finish the broadcast.
sender?.cancel()
sender = nil
}

let rxToken : String = "USER_DEFINED_TOKEN_APP"
let txToken : String = "USER_DEFINED_TOKEN_EXT"
let broadcastPort : UInt16 = PORT_NUMBER

override func processSampleBuffer(_ sampleBuffer: CMSampleBuffer, with sampleBufferType: RPSampleBufferType) {
switch sampleBufferType {
case RPSampleBufferType.video:
do {
if let sender = sender {
try autoreleasepool {
try sender.sendVideo(sampleBuffer: sampleBuffer)
}
} else {
sender = try BroadcastSender(delegate: self, broadcastPort: broadcastPort, rxToken: rxToken, txToken: txToken)
}
} catch {
finish(error: error)
}
break
...
}
}

private func finish(error: Error) {
sender?.cancel()
sender = nil

if let description = (error as? LocalizedError)?.errorDescription {
self.finishBroadcastWithError(NSError(domain: "BroadcastSender.ErrorDomain", code: 0, userInfo: [NSLocalizedFailureReasonErrorKey: description]))
} else {
self.finishBroadcastWithError(error)
}
}
}

Start screen share on PlanetKit

Implement didStartMyBroadcast() of PlanetKitConferenceDelegate. To start sending the screen share stream, call startMyScreenShare() in didStartMyBroadcast().

  • The subgroupName parameter determines the destination of screen share.
  • If the subgroupName parameter is nil, screen share is sent to the main room.
extension VideoConferenceViewController: PlanetKitConferenceDelegate {
...

func didStartMyBroadcast(_ conference: PlanetKitConference) {
NSLog("Broadcast: start")
DispatchQueue.main.async {
conference.startMyScreenShare(subgroupName: nil) { success in
guard success else {
return
}
}
}
}

...
}

macOS

Collect the devices for screen capture and select one from them.

func collectCaptureDevices() -> [PlanetKitScreenCaptureDevice] {
var captureDevices = [PlanetKitScreenCaptureDevice]()
autoreleasepool {
for display in PlanetKitScreen.shared.displays {
let name: String
if let screen = NSScreen.screens.first(where: { $0.displayID == display.displayID }) {
name = screen.localizedName
}
...

captureDevices.append(PlanetKitScreenCaptureDevice(display: display))
}

let windows = PlanetKitScreen.shared.windows.filter {
$0.sharingType != .none &&
$0.alpha > 0 &&
$0.level == kCGNormalWindowLevel
}
let myAppId = Bundle.main.bundleIdentifier
for window in windows {
guard let image = window.image, window.sharingType != .none, window.bundleId != nil else {
continue
}
if myAppId == window.bundleId {
continue
}
...

captureDevices.append(PlanetKitScreenCaptureDevice(window: window))
}
}
return captureDevices
}

To start sending screen share, call startMyScreenShare().

  • The subgroupName parameter determines the destination of screen share.
  • If the subgroupName parameter is nil, screen share is sent to the main room.
func startMyScreenShare(_ device: PlanetKitScreenCaptureDevice) {
...

conference.startMyScreenShare(device: device, subgroupName: nil) { success in
guard success else { return }
// UI code here if success
}
}

Sender - Change the destination of screen share

Depending on the type of destination, you can change the destination of screen share as follows:

  • To change the destination to a subgroup other than the main room, call changeMyScreenShareDestination() with the subgroup name.
  • To change the destination to the main room, call changeMyScreenShareDestinationToMainRoom().
// Change the destination to a subgroup other than the main room
conference.changeMyScreenShareDestination(subgroupName: subgroupName) { success in
guard success else { return }
// UI code here if success
}

// Change the destination to the main room
conference.changeMyScreenShareDestinationToMainRoom() { success in
guard success else { return }
// UI code here if success
}

Sender - Stop sending screen share

The code implementation for stopping screen share differs between iOS and macOS.

iOS

Implement didFinishMyBroadcast() and didErrorMyBroadcast() of PlanetKitConferenceDelegate. To stop screen share, call stopMyScreenShare() in didFinishMyBroadcast() and didErrorMyBroadcast().

extension VideoConferenceViewController: PlanetKitConferenceDelegate {
...

func didFinishMyBroadcast(_ conference: PlanetKitConference) {
NSLog("Broadcast: finish")
DispatchQueue.main.async {
self.conference?.stopMyScreenShare() { success in
}
}
}

func didErrorMyBroadcast(_ conference: PlanetKitConference, error: PlanetKitScreenShare.BroadcastError) {
NSLog("Broadcast: error \(error)")
DispatchQueue.main.async {
self.conference?.stopMyScreenShare() { success in
}
}
}
}

macOS

To stop screen share, call stopMyScreenShare().

conference.stopMyScreenShare() { success in
self.delegate?.screenShare(self, willDismiss: dismiss)
}

Receiver - Receive screen share update events

​Check whether a peer's screen share has started or stopped through the screenShareDidUpdate event of PlanetKitConferenceDelegate.

Note

If you have already created peer control, skip the next step and use it for the Receiver - Start or stop a peer's screen share video step. Otherwise, create peer control as in the next step.

extension VideoConferenceViewController: PlanetKitConferenceDelegate {
...
func screenShareDidUpdate(_ conference: PlanetKitConference, updated: PlanetKitConferenceScreenShareUpdateParam) {
if updated.state == .enabled {
NSLog("start screen share - \(updated.peerId) \(updated.subgroupName ?? "main")")
}
else {
NSLog("stop screen share - \(updated.peerId) \(updated.subgroupName ?? "main")")
}

// UI code here
DispatchQueue.main.async {
self.loadPage(self.currentPage)
}
}
}

Receiver - Create and register peer control

When a peer is connected to the call, create and register peer control for the peer.

extension VideoConferenceViewController {
...

var peers: [PlanetKitConferencePeer]
var peerViews: [ScreenPeerVideoView]

func loadPage(_ page: Int) {
...

peerViews[index].setupPeerControl(conference: conference, peer: peers[index])
}
}

#if os(macOS)
typealias UIView = NSView
#endif

class ScreenPeerVideoView: UIView {
...

var peerControl: PlanetKitPeerControl!

func setupPeerControl(conference: PlanetKitConference, peer: PlanetKitConferencePeer) {
guard let peerControl = conference.createPeerControl(peer: peer) else {
// Error code here
return
}
peerControl.register(self) { success in
// UI code here
self.setScreenShareEnabled(peer.screenShareState == .enabled)
}
self.peerControl = peerControl
}

func finalPeerControl() {
peerControl.unregister() { success in
// UI code here
}
}
}

Receiver - Start or stop a peer's screen share video

Check whether a peer's screen share has started or stopped through the didUpdateScreenShare event of PlanetKitPeerControlDelegate.

  • When the peer's screen share started, add a peer screen share view and call startScreenShare() of PlanetKitPeerControl.
  • When the peer's screen share stopped, remove the peer screen share view and call stopScreenShare() of PlanetKitPeerControl.
#if os(macOS)
typealias UIView = NSView
#endif

class ScreenPeerVideoView: UIView {
...

var peerControl: PlanetKitPeerControl!
var peerScreenShareView: PlanetKitMTKView!

func setScreenShareEnabled(_ enabled: Bool) {
if enabled {
peerScreenShareView = PlanetKitMTKView(frame: view.bounds, device: nil)
view.addSubview(peerScreenShareView)

let subgroupName = peerControl.peer.currentScreenShareSubgroupName
peerControl.startScreenShare(delegate: peerScreenShareView, subgroupName: subgroupName) { success in
// UI code here
}
}
else {
peerScreenShareView.removeFromSuperview()

peerControl.stopScreenShare() { success in
// UI code here
}
}
}
}

extension ScreenPeerVideoView: PlanetKitPeerControlDelegate {
...

func didUpdateScreenShare(_ peerControl: PlanetKitPeerControl, subgroup: PlanetKitSubgroup, status: PlanetKitScreenShareStatus) {
DispatchQueue.main.async {
self.setScreenShareEnabled((status.state == .enabled))
}
}
}