Skip to main content
Version: 1.0

Screen share in group calls

This page provides a code example for implementing screen share in a group call.

Note

As of version 0.9, screen share capturing and transmission are only supported on iOS, while reception of screen share is supported on both Android and iOS. This page provides a code example based on the screen share capturing and transmission flow for iOS apps.

Prerequisites

Before implementing screen share, 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.

How screen share works in group calls

In group calls, screen share works as follows:

  1. The app client of the sender creates a ScreenShareKey consisting of the predefined port number, reception token, and transmission token, and sets it in setScreenShareKey(key) of PlanetKitJoinConferenceParamBuilder.
  2. When the user requests screen share, the app client of the sender uses NWConnection or equivalents to establish a connection between the app and the SDK, and sends the screen share streams to the port defined along with the tokens.
  3. If the information in ScreenShareKey matches the information received from NWConnection, PlanetKit for Flutter automatically starts screen share.
  4. The app client of the receiver detects that screen share has started after receiving the onScreenShareUpdate event, and creates a view instance and call startScreenShare() to have PlanetKit render the screen share video.

Set the screen share key in PlanetKit (sender)

Set the screen share key with setScreenShareKey() of PlanetKitJoinConferenceParamBuilder. You must pass the pre-defined port number, transmission token, and reception token to setScreenShareKey().

var builder = PlanetKitJoinConferenceParamBuilder()
.setMyUserId(myUserId)
.setMyServiceId(serviceId)
.setRoomServiceId(_serviceId)
.setRoomId(roomId)
.setAccessToken(accessToken)
.setScreenShareKey(ScreenShareKey(broadcastPort: PORT_NUMBER, broadcastPeerToken: "USER_DEFINED_TOKEN_EXT", broadcastMyToken: "USER_DEFINED_TOKEN_APP"));

Implement a screen capturing and transmission module in Swift (sender)

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)
}
}
}

View the screen share (receiver)

Listen for the PlanetKitPeerControlHandler.onScreenShareUpdate event, and implement code to add the peer's screen share view when the event occurs.

class Peer {
final PlanetKitPeerControl control;
bool screenShareAvailable = false;

Peer({required this.control});

void register() async {
final handler = PlanetKitPeerControlHandler(
onScreenShareUpdate: (control, screenShareState) {
screenShareAvailable =
screenShareState == PlanetKitScreenShareState.enabled ? true : false;
});
await control.register(handler);
}

void unregister() async {
await control.unregister();
}

void startScreenShare(String viewId) async {
await control.startScreenShare(viewId);
}

void stopScreenShare(String viewId) async {
await control.stopScreenShare(viewId);
}
}

To render the peer's screen share, you must use PlanetKitVideoViewBuilder to create PlanetKitVideoView and add it to PlanetKitPeerControl.

After creating the PlanetKitVideoView for the peer's screen share, add the peer's screen share view to PlanetKitPeerControl by calling startScreenShare(viewId).

class PeerView extends StatelessWidget {
final Peer peer;
PeerView({required this.peer});


Widget build(BuildContext context) {
if (peer.screenShareAvailable) {
return ScreenShareView(peer: peer);
} else {
return Text("screen share not available");
}
}
}

class ScreenShareView extends StatelessWidget {
const ScreenShareView({super.key, required this.peer});
final Peer peer;


Widget build(BuildContext context) {
final screenShareView = PlanetKitVideoViewBuilder.instance
.create(PlanetKitViewScaleType.fitCenter);

screenShareView.onCreate.listen((id) {
peer.startScreenShare(id);
});

screenShareView.onDispose.listen((id) {
peer.stopScreenShare(id);
});

return screenShareView;
}
}