본문 바로가기

프로그래밍/iOS,macOS

아이폰 로컬 화면 공유 : Broadcast Extension

화면 전체를 녹화하기 위해서는 ReplayKit 과 Broadcast Extention 이 필요.
별도 프로세스로 동작하므로, 앱과 extention 간의 커뮤니케이션 필요.( app group, xpc or socket 등)

broadcast extension 은 메모리 사용량에 50M 제약이 있으므로, 캡처되는 샘플 프레임의 사이즈와 프레임 레이트등을 고려해서 구성해야 한다. 비디오 사이즈에 따라 인코딩이나 전송단 버퍼링 등에서 50메가를 넘는 경우가 많아 실시간이나 부드러운 프레임 레이트의 제공에는 한계가 있다.
제한 메모리 사용량을 넘으면 익스텐션이 바로 종료되어 버리므로, 오디오까지 인코딩하는 경우 메모리 관리가 빡빡한 편. 

익스텐션

앱에서 브로드캐스트 익스텐션 타겟 추가

xcode 13.2

 

 

익스텐션 타겟이 생성되고, 기본 클래스가 생성됨.

 

클래스명을 다른 이름으로 변경하려는 경우
NSExtension > NSExtensionPrincipalClass 항목을 $(PRODUCT_MODULE_NAME).변경할이름

 

 

 

샘플버퍼

익스텐션에서 샘플 프레임 데이터가 전달되는데, 해당 데이터를 서비스에 맞는 프로토콜을 통해 전달해주면 됨 

override func processSampleBuffer(_ sampleBuffer: CMSampleBuffer, with sampleBufferType: RPSampleBufferType) {
                                  
   switch sampleBufferType {
   case RPSampleBufferType.video:
      var orientation: CGImagePropertyOrientation = .up
      if let orientationAttachment = CMGetAttachment(sampleBuffer, 
                                                     key: RPVideoSampleOrientationKey as CFString,
                                                     attachmentModeOut: nil) as? NSNumber {
         orientation = CGImagePropertyOrientation(rawValue: orientationAttachment.uint32Value) ?? CGImagePropertyOrientation.up
      }
      
      //
      mySomeDelevery.frame(sampleBuffer, orientation)
      
   case RPSampleBufferType.audioApp:
      break
      
      
   case RPSampleBufferType.audioMic:
      break
      
      
   @unknown default:
      fatalError("")
   }
}

 

앱측 익스텐션 구동

앱에서는 RPSystemBroadcastPickerView를 통해 익스텐션을 구동시킬 수 있는데... UI 가 정해져 있다보니 hidden으로 만들어 두고, 임의의 버튼 선택시에 해당 뷰에 포함된 버튼이 동작하도록하는 편법으로 구성

import ReplayKit

class MyViewController: UIViewController {
   .
   .
   let broadcastPicker: RPSystemBroadcastPickerView = .init()
   
   override func viewDidLoad() {
      .
      .
      self.broadcastPicker.isHidden = true
      self.broadcastPicker.showMicrophoneButton = false
      self.broadcastPicker.preferredExtension = "broadcast extention bundle id"
      self.view.addSubview(self.broadcastPicker)
      .
      .
   }
   .
   .
   
   @IBAction func startAction(_ sender: UIButton) {
      if let button = self.broadcastPicker.subviews.first(wherer: { $0 is UIButton}) as? UIButton {
         button.sendAction(for: .touchUpInside)
      }
   }
   
}

 

 

앱과 익스텐션간 메시징

앱과 익스텐션은 별도 프로세스 이므로, 상호 데이터를 주고 받기 위한 구성이 필요하다.
가장 간단한 방법은 앱그룹을 사용해 키:값 기반으로 데이터를 주고 받을 수 있다.
앱과 익스텐션에 App Groups 를 추가하고, 동일한 그룹으로 설정한다.

 

주고 받을 키와 데이터 정의

enum ShareCommandKey: String {
   case broadcastStartValue
}

struct ShareCommandData: Codable {
   var data: String
}

enum ShareEvent: Int {
   case start
   case stop
}

 

익스텐션에서 앱 그룹을 위한 공유 데이터 설정

var commandData: ShareData?
var sharedUserDefaults: UserDefaults = UserDefaults(suitName: "app.group")!


override func broadcastStarted(withSetupInfo setupInfo: [String: NSObject]?) {
   // 초기화시에 앱이 저장한 데이터를 가져오는 경우
   if let data = sharedUserDefaults.value(forKey: ShareCommand.broadcastRequest.rawValue) as? Data {
      commandData = try? PropertyListDecoder().decode(ShareCommandData.self, from: data)
   } else {
      broadcastError(error: .invalid)
   }
   
   // 앱에서 전달하는 이벤트를 확인하기 위한 옵저버
   sharedUserDefaults.addObserver(self, forKeyPath: ShareCommnadKey.broadcastStatus.rawValue, options: [.initial, .new], context: nil)
}

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
   guard let keyPath = keyPath, let change = change else { return }
   
   if let event = ShareEvent(rawValue: change[.newKey] as? Int ?? -1) {
      print("path:\(keyPath), event:\(event)")
   }
}


// 익스텐션에서 앱으로 보내기 위한 경우
func setStatus() {
   DispatchQueue.global().async {
      self.shareUserDefaults.set( "data", forKey: "key" )
      self.shareUserDefaults.synchronize()
   }
}

 

앱쪽에도 비슷한 형식으로 구성.......