Screen share in 1-to-1 calls
This page provides a code example for implementing screen share in a 1-to-1 call.
As of version 0.8, 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 1-to-1 calls
In 1-to-1 calls, screen share works as follows:
- The app client of the sender creates a
ScreenShareKey
consisting of the predefined port number, reception token, and transmission token, and sets it insetScreenShareKey(key)
ofPlanetKitMakeCallParamBuilder
orPlanetKitVerifyCallParamBuilder
. - 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. - If the information in
ScreenShareKey
matches the information received fromNWConnection
, PlanetKit for Flutter automatically starts screen share. - The app client of the receiver detects that screen share has started after receiving the
onPeerScreenShareStarted
event, and creates a view instance and calladdPeerScreenShareView()
to have PlanetKit render the screen share video.
Set the screen share key in PlanetKit (sender)
Set the screen share key with setScreenShareKey()
of PlanetKitMakeCallParamBuilder
. You must pass the pre-defined port number, transmission token, and reception token to setScreenShareKey()
.
var builder = PlanetKitMakeCallParamBuilder()
.setMyUserId(myUserId)
.setMyServiceId(serviceId)
.setPeerUserId(peerId)
.setPeerServiceId(serviceId)
.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 onPeerDidStartScreenShare
event, and implement code to add the peer's screen share view when the event occurs.
final _eventHandler = PlanetKitCallEventHandler(
onConnected: (_, __, ___) => {},
onWaitConnected: (_) => {},
onDisconnected: (_, __, ___, ____) => {},
onVerified: (_, __) => {},
onPeerScreenShareStarted: (call) => _addPeerScreenShareView,
onPeerScreenShareStopped: (call) => _removePeerScreenShareView);
void _addPeerScreenShareView(PlanetKitCall call) {
// show screen share view
}
void _removePeerScreenShareView(PlanetKitCall call) {
// remove screen share view
}
To render the peer's screen share, you must use PlanetKitVideoViewBuilder
to create PlanetKitVideoView
and add it to PlanetKitCall
.
After creating the PlanetKitVideoView
for the peer's screen share, add the peer's screen share view to PlanetKitCall
by calling addPeerScreenShareView(viewId)
.
class ScreenShareView extends StatelessWidget {
const ScreenShareView({super.key, this.call});
final PlanetKitCall? call;
Widget build(BuildContext context) {
final screenShareView = PlanetKitVideoViewBuilder.instance.create();
screenShareView.onCreate.listen((id) {
call?.addPeerScreenShareView(id);
});
screenShareView.onDispose.listen((id) {
call?.removePeerScreenShareView(id);
});
return screenShareView;
}
}