부가 기능
부가 기능에서는 필수 개발 작업이 아닌 부가적인 개발 또는 제어 기능을 설명합니다.
미디어 제어
SDK에서는 다양한 미디어 제어 기능을 지원합니다.
로컬 미디어 시작하기
LocalMedia 클래스의 start() 메서드를 통해 로컬 미디어(Capturer) 동작을 시작합니다. 로컬 비디오에 UIRenderView가 연결되어 있으면 로컬 비디오 화면이 UIRenderView를 통해 재생됩니다. 이 메서드는 명시적으로 호출해 주지 않아도 미디어 게시가 시작되면 자동으로 호출됩니다.
코드예제로컬 미디어 시작 Syntax
Task {
try? await localMedia.start()
}
로컬 미디어 중지하기
LocalMedia 클래스의 stop() 메서드를 통해 Room에 로컬 미디어의 송출을 중단합니다.
코드예제로컬 미디어 중지 Syntax
localMedia.stop()
로컬 미디어 카메라 전환하기
LocalMedia 클래스의 switchCamera() 메서드를 통해 로컬 미디어의 카메라의 카메라 또는 카메라 영상 좌우 반전 여부 등을 설정할 수 있습니다.
코드예제로컬 미디어 카메라 전환 Syntax
// 카메라 전환
Task {
try? await localMedia.switchCamera(position: .back, isMirror: true)
}
로컬 미디어의 오디오 레벨 얻기
로컬 미디어의 입력 오디오 레벨은 LocalMedia 클래스의 audioLevel 프로퍼티로 얻어 올 수 있습니다. 오디오 레벨은 0~100에 해당하는 값입니다.
코드예제로컬 미디어 오디오 레벨 획득 Syntax
// 카메라 전환
var level = localMedia.audioLevel
리모트 오디오의 오디오 레벨 정보 얻기
로컬 오디오 레벨 정보는 Room 클래스의 getAudioLevels() 메서드를 통해 얻어야 합니다. 메서드 호출 시, 응답값으로 현재 활성화된 오디오의 참여자 Id와 해당 참여자의 오디오 레벨이 배열로 전달됩니다.
코드예제리모트 오디오 레벨 정보 획득 Syntax
/// 현재 활성화된 리모트 오디오 레벨 얻기
/// 모든 참여자의 오디오 레벨이 전달되지 않고, 현재 활성화된 참여자의 오디오 레벨만 전달
room.getAudioLevels() { levels in
// levels: [String: Int]
}
오디오, 비디오 속성 변경하기
로컬 미디어의 오디오, 비디오와 각 참여자의 오디오, 비디오는 전송되는 스트림의 정보와 간단한 제어 기능을 제공합니다.
로컬 비디오 속성
로컬 비디오의 속성 값을 가져오거나 변경합니다. 각 속성에 대한 자세한 설명은 LocalVideo 문서를 참고하시기 바랍니다.
코드예제로컬 비디오 속성 변경 Syntax
// 속성 값 얻기
let id = video.id
let owner = video.owner
let extraValue = video.extraValue
// 활성화 여부 변경
video.active = true
로컬 오디오 속성
로컬 오디오의 속성 값을 가져오거나 변경합니다. 각 속성에 대한 자세한 설명은 LocalAudio 문서를 참고하시기 바랍니다.
코드예제로컬 오디오 속성 변경 Syntax
// 속성 값 얻기
let id = audio.id
let owner = audio.owner
let extraValue = audio.extraValue
// 활성화 여부 변경
audio.active = true
// 오디오 항상 활성화 변경
audio.alwaysOn = true
리모트 비디오 속성
리모트 비디오의 속성 값을 가져오거나 변경합니다. 각 속성에 대한 자세한 설명은 RemoteVideo 문서를 참고하시기 바랍니다.
코드예제리모트 비디오 속성 변경 Syntax
// 속성 값 얻기
let id = video.id
let owner = video.owner
let active = video.active
let extraValue = video.extraValue
let profile = video.profile
// 비디오 일시 중지
try video.setPaused(true)
// 비디오 프로파일 변경
try video.setProfile(.high)
리모트 오디오 속성
리모트 오디오의 속성 값을 가져오거나 변경합니다. 각 속성의 세부 내용은 RemoteAudio 문서를 참고하시기 바랍니다.
코드예제리모트 오디오 속성 변경 Syntax
// 속성 값 얻기
let id = audio.id
let owner = audio.owner
let active = audio.active
let extraValue = audio.extraValue
let alwaysOn = audio.alwaysOn
화면 공유 구현
iOS 화면 공유는 별도 타깃으로 구성되어야 하며, 앱 타깃과 화면공유 타깃 각각 작업해야 합니다.
Boradcast Upload Extension에서 화면 캡처와 송출을 담당하고, SDK 측에서는 해당 익스텐션과의 연동 처리를 담당합니다.
사전 작업 수행하기
iOS 확장 기능(Extension)을 동작시키기 위해서는 Apple Developer 사이트에서 다음의 사전 작업을 필수로 수행해야 합니다. 각 사전 작업에 대한 자세한 설명은 링크된 Apple 공식 문서를 참고하시기 바랍니다.
표사전 작업항목 | 설명 |
---|---|
앱 ID 등록 | Provisioning profile(권한 설정 프로파일)에서 앱을 식별하기 위해 앱 ID 등록 필요 |
앱 그룹 등록 | 앱 그룹을 활성화하려면 하나 이상의 앱 그룹 등록 필요 |
Provisioning Profile 설정 | Xcode에서 수동으로 앱에 서명하기 위해 필요 |
프로젝트 설정하기
-
Xcode에서 프로젝트 파일을 열고, 메뉴바에서 Editor > Add Target…으로 이동합니다.
-
팝업창의 iOS 탭에서 Broadcast Upload Extension을 선택 후, [Next] 버튼을 클릭합니다.
그림생성할 타겟 템플릿 선택
-
Target > Signing & Capabilities > App Groups으로 이동 후, 메인 앱과 Extension 타깃의 앱 그룹을 설정합니다
-
Target > Capabilities 탭으로 이동 후 Background Modes를 추가하고, Voice over IP를 선택하여 메인 앱의 백그라운드 모드를 활성화합니다.
- Voice over IP를 선택하지 않은 경우, 앱이 백그라운드 진입 시 연결이 종료됩니다.
그림백그라운드 모드 활성화
-
Extension Target > General 탭으로 이동 후, Indentity를 열고 Extension의 Display Name을 설정합니다. 일반적으로 메인 앱 이름과 동일하게 변경하는 것을 권장합니다.
그림Display Name 설정
-
(선택 작업) Extension의 RPBroadcastSampleHandler는 기본적으로 SampleHandler라는 클래스로 생성됩니다. 특정 클래스로 이름을 변경하려면, Target > Info 탭으로 이동 후 NSExtension의 NSExtensionPrincipalClass 항목에서 이름을 변경합니다.
-
화면 공유를 위한 Extension은 Room에서 실행을 요청할 수 있습니다. Room 클래스의 requestScreenShare() 메서드는 앱 그룹과 번들 ID가 일치하는 익스텐션의 실행을 요청합니다. 일치하는 익스텐션이 있으면 Picker에 해당 목록이 표시되고, 사용자가 선택하면 해당 익스텐션이 실행됩니다.
서비스 앱, 익스텐션 개발하기
사전 작업을 완료 후, 본격적으로 서비스 앱과 익스텐션 개발을 시작합니다.
-
화면 공유를 시작하기 위해, 화면 공유를 요청하면 iOS에서 내부의 Broadcast Picker가 Broadcast Upload Extension 목록을 가져옵니다. 여기에 표시되는 이름은 Broadcast Upload Extension의 display name에 설정된 값이 표시됩니다.
room.requestScreenShare() 메서드를 호출하면 ConnetLive 인증 처리와 Broadcast Upload Extension을 Broadcast Picker에 표시하게 됩니다.코드예제화면 공유 시작 Syntax
@Published var screenShareStatus: ScreenShareData.ExtensionStatus = .notAvailable func startScreenSharing() { guard let room = self.room else { return } let appGroup = "" let extensionBundleId = "" // 화면 공유는 시뮬레이터를 지원하지 않습니다. #if !targetEnvironment(simulator) try? room.requestScreenShare(appGroup: appGroup, extensionName: extensionBundleId) { [weak self] status in print("[startScreenSharing] 익스텐션 상태: \(status)") self?.screenShareStatus = status } #endif }
-
Room 객체에 화면 공유 중지를 요청합니다. 화면 공유 상태가 즉시 중지 상태로 변경되며 완료 이벤트는 추가로 전달되지 않습니다. 화면 공유는 별도 프로세스로 동작하는 Broadcast Upload Extension을 사용합니다. Broadcast Upload Extension은 중지 요청 후 각 세션 정리 및 Broadcast Upload Extension 정리에 약 5초 정도의 시간을 필요로 합니다. iOS에서 자체적으로 표시하는 Broadcast 중지 얼럿이 표시되어야 종료가 완료된 것이므로, 다음 화면 공유는 최소 5초 이상 시간이 지난 후 시도해야 합니다.
코드예제화면 공유 중지 Syntax
@Published var screenShareStatus: ScreenShareData.ExtensionStatus = .notAvailable func stopScreensharing(isLeftRoom: Bool = false) { guard let room = self.room else { return } room.stopScreenShare(isLeftRoom: isLeftRoom) self.screenShareStatus = .notAvailable }
-
Room에 화면 공유를 요청하면 ScreenShareData.ExtensionStatus 값이 익스텐션의 상태에 따라 Callback으로 전달됩니다.
코드예제ScreenShareData.ExtensionStatus 값 전달 Syntax
public enum ExtensionStatus: Int { case notAvailable = 0 case broadcastReady = 1 case broadcastStarted = 2 case broadcastClosed = 3 }
-
앱에서 화면 공유를 시작하면, Broadcast Upload Extension의 핸들러가 실행됩니다. 익스텐션에서 처리할 내용들을 구성합니다.
코드예제Broadcast Upload Extension 핸들러 실행 Syntax
class ConnectLiveSampleHandler: RPBroadcastSampleHandler { let appGroupName = "group.xxxx" var screenShare: ScreenShare? var disconnectSemaphore: DispatchSemaphore? override func broadcastStarted(withSetupInfo setupInfo: [String : NSObject]?) { // User has requested to start the broadcast. Setup info from the UI extension can be supplied but optional. do { screenShare = try ConnectLive.createScreenShare(appGroup: appGroupName) } catch { finishBroadcastWithError(error) } disconnectSemaphore = DispatchSemaphore(value: 0) screenShare?.start() { [weak self] error, msg in guard let self = self else { return } // 오류에 따라 메시지 처리 var key = "default" switch error { case .invalidData: key = "invalidData" case .stopBroadcast: key = "stopBroadcast" case .meetingFinished: key = "meetingFinished" case .broadcastFailed: key = "broadcastFailed" case .broadcastClosed: key = "broadcastClosed" @unknown default: break } let message = NSLocalizedString(key, tableName: "Localizable", bundle: Bundle.main, value: "", comment: "").appending(msg) let userInfo = [NSLocalizedFailureReasonErrorKey: message] let error = NSError(domain: "app.domain.***", code: -1, userInfo: userInfo) self.finishBroadcastWithError(error) } } override func broadcastPaused() { // User has requested to pause the broadcast. Samples will stop being delivered. screenShare?.pause() } override func broadcastResumed() { // User has requested to resume the broadcast. Samples delivery will resume. screenShare?.resume() } override func broadcastFinished() { // User has requested to finish the broadcast. screenShare?.stop { [weak self] in self?.disconnectSemaphore?.signal() } disconnectSemaphore?.wait() screenShare = nil } override func processSampleBuffer(_ sampleBuffer: CMSampleBuffer, with sampleBufferType: RPSampleBufferType) { switch sampleBufferType { case RPSampleBufferType.video: screenShare?.sendVideoFrame(sampleBuffer: sampleBuffer) break case RPSampleBufferType.audioApp: // Handle audio sample buffer for app audio break case RPSampleBufferType.audioMic: // Handle audio sample buffer for mic audio break @unknown default: // Handle other sample buffer types fatalError("Unknown type of sample buffer") } } }