그룹 통화 화면 공유
그룹 통화(컨퍼런스)에서 화면 공유를 구현하는 예제 코드입니다.
필수 조건(iOS에만 해당)
화면 공유를 구현하기 전에 다음 작업을 수행해야 합니다.
- 애플리케이션에서 화면 공유를 캡처하려면 Broadcast Upload Extension 또는 이에 상응하는 기능을 구현해야 합니다.
- Broadcast Upload Extension을 구현하려면 Xcode에서 프로젝트에 새 Target을 추가하고, "Broadcast Upload Extension" 템플릿을 선택한 다음 이 확장을 활성화하세요.
- 화면 공유 스트림 전송에 사용할 포트 번호, 수신 토큰 및 전송 토큰을 정의하세요.
송신 측 - 화면 공유 전송 시작
화면 공유를 시작하기 위한 코드 구현 방식은 iOS와 macOS가 서로 다릅니다.
iOS
PlanetKit에 화면 공유 키 설정
송신 측 앱 클라이언트에서 미리 정의한 포트 번호, 수신 토큰 및 전송 토큰을 PlanetKitJoinConferenceSettingBuilder
의 withEnableScreenShareKey(broadcastPort:broadcastPeerToken:broadcastMyToken:)
에 설정하세요.
var settingsBuilder = try! PlanetKitJoinConferenceSettingBuilder()
.withEnableScreenShareKey(broadcastPort: PORT_NUMBER, broadcastPeerToken: "USER_DEFINED_TOKEN_EXT", broadcastMyToken: "USER_DEFINED_TOKEN_APP")
화면 캡처링 및 송신 모듈 구현
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)
}
}
}
PlanetKit에서 화면 공유 시작
PlanetKitConferenceDelegate
의 didStartMyBroadcast()
를 구현하세요. 화면 공유 스트림 전송을 시작하려면 didStartMyBroadcast()
에서 startMyScreenShare()
를 호출하세요.
subgroupName
파라미터는 화면 공유가 전송될 목적지를 결정합니다.subgroupName
파라미터가nil
이면 화면 공유가 기본 방(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
화면 캡처 장치를 모으고 그중 하나를 선택하세요.
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
}
화면 공유 전송을 시작하려면 startMyScreenShare()
를 호출하세요.
subgroupName
파라미터는 화면 공유가 전송될 목적지를 결정합니다.subgroupName
파라미터가nil
이면 화면 공유가 기본 방(main room)으로 전송됩니다.
func startMyScreenShare(_ device: PlanetKitScreenCaptureDevice) {
...
conference.startMyScreenShare(device: device, subgroupName: nil) { success in
guard success else { return }
// UI code here if success
}
}
송신 측 - 화면 공유 목적지 변경하기
화면 공유의 목적지는 목적지 유형에 따라 다음과 같이 변경할 수 있습니다.
- 기본 방이 아닌 다른 서브그룹으로 목적지를 변경하려면 서브그룹 이름을 인자로
changeMyScreenShareDestination()
을 호출하세요. - 기본 방으로 목적지를 변경하려면
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
}
송신 측 - 화면 공유 전송 중지
화면 공유를 중지하기 위한 코드 구현 방식은 iOS와 macOS가 서로 다릅니다.
iOS
PlanetKitConferenceDelegate
의 didFinishMyBroadcast()
와 didErrorMyBroadcast()
를 구현하세요. 화면 공유를 중지하려면 didFinishMyBroadcast()
와 didErrorMyBroadcast()
에서 stopMyScreenShare()
를 호출하세요.
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
화면 공유를 중지하려면 stopMyScreenShare()
를 호출하세요.
conference.stopMyScreenShare() { success in
self.delegate?.screenShare(self, willDismiss: dismiss)
}
수신 측 - 화면 공유 업데이트 이벤트 수신
PlanetKitConferenceDelegate
의 screenShareDidUpdate
이벤트를 통해 피어의 화면 공유가 시작 혹은 중지됐는지 확인합니다.
이미 피어 컨트롤을 생성한 경우 다음 단계를 건너뛰고 수신 측 - 피어의 화면 공유 비디오 시작 또는 중지 단계에서 피어 컨트롤을 사용하세요. 그렇지 않으면 다음 단계와 같이 피어 컨트롤을 생성하세요.
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)
}
}
}
수신 측 - 피어 컨트롤 생성 및 등록
피어가 통화에 연결되면 피어에 대한 피어 컨트롤을 생성하고 등록하세요.
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
}
}
}
수신 측 - 피어의 화면 공유 비디오 시작 또는 중지
PlanetKitPeerControlDelegate
의 didUpdateScreenShare
이벤트를 통해 피어의 화면 공유가 시작 혹은 중지됐는지 확인합니다.
- 피어의 화면 공유가 시작되면 피어의 화면 공유 뷰를 추가하고
PlanetKitPeerControl
의startScreenShare()
를 호출하세요. - 피어의 화면 공유가 중지되면 피어의 화면 공유 뷰를 제거하고
PlanetKitPeerControl
의stopScreenShare()
를 호출하세요.
#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))
}
}
}