본문으로 건너뛰기
Version: 1.0

1대1 통화 화면 공유

1대1 통화에서 화면 공유를 구현하는 예제 코드입니다.

Note

0.8 버전에서 화면 공유 캡처 및 송신은 iOS에서만 지원되며, 화면 공유 수신은 Android와 iOS에서 지원됩니다. 여기서는 iOS 앱에서 화면 공유를 보내는 흐름을 기준으로 작성된 예제 코드를 제공합니다.

필수 조건

화면 공유를 구현하기 전에 다음 작업을 수행해야 합니다.

  • 애플리케이션에서 화면 공유를 캡처하려면 Broadcast Upload Extension 또는 이에 상응하는 기능을 구현해야 합니다.
    • Broadcast Upload Extension을 구현하려면 Xcode에서 프로젝트에 새 Target을 추가하고, "Broadcast Upload Extension" 템플릿을 선택한 다음 이 확장을 활성화하세요.
  • 화면 공유 스트림 전송에 사용할 포트 번호, 수신 토큰 및 전송 토큰을 정의하세요.

1대1 통화의 화면 공유 작동 절차

1대1 통화에서 화면 공유가 작동하는 절차는 다음과 같습니다.

  1. 송신 측 앱 클라이언트에서 미리 정의한 포트 번호, 수신 토큰 및 전송 토큰으로 구성된 ScreenShareKey를 생성하고 PlanetKitMakeCallParamBuilder 또는 PlanetKitVerifyCallParamBuildersetScreenShareKey(key)에 설정합니다.
  2. 사용자가 화면 공유를 요청하면 송신 측 앱 클라이언트에서 NWConnection 또는 이에 상응하는 기능을 사용하여 앱과 SDK 간의 연결을 설정하고, 토큰과 함께 정의된 포트로 화면 공유 스트림을 보냅니다.
  3. ScreenShareKey의 정보가 NWConnection에서 수신한 정보와 일치하면 Flutter용 PlanetKit이 자동으로 화면 공유를 시작합니다.
  4. 수신 측 앱 클라이언트에서 onPeerScreenShareStarted 이벤트를 받아 화면 공유가 시작되었음을 알게 되면 뷰 인스턴스를 생성하고 addPeerScreenShareView()를 호출해 PlanetKit이 화면 공유 비디오를 렌더링하게 합니다.

PlanetKit에 화면 공유 키 설정(송신 측)

PlanetKitMakeCallParamBuildersetScreenShareKey()로 화면 공유 키를 설정하세요. 미리 정의된 포트 번호, 전송 토큰, 수신 토큰을 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"));

Swift로 화면 캡처링 및 송신 모듈 구현(송신 측)

NWConnection을 통해 앱과 SDK 간의 연결을 설정하는 화면 캡처링 모듈을 구현하세요.

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

연결이 성공적으로 설정되면 생성된 NWConnection에 화면 공유 스트림을 보내야 합니다. 캡처한 화면 공유 스트림을 보내기 위한 SampleHandler 클래스를 구현하세요.

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

화면 공유 보기(수신 측)

onPeerDidStartScreenShare 이벤트 변경을 감지하고, 이벤트가 발생했을 때 피어의 화면 공유 뷰를 추가하기 위한 코드를 구현하세요.

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
}

피어의 화면 공유를 렌더링하려면 PlanetKitVideoViewBuilder를 사용하여 PlanetKitVideoView를 만들고 PlanetKitCall에 추가해야 합니다.

피어의 화면 공유에 대한 PlanetKitVideoView를 생성한 뒤 addPeerScreenShareView(viewId)를 호출하여 피어의 화면 공유 뷰를 PlanetKitCall에 추가하세요.

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

관련 예제 코드

관련 문서