반응형
해당 글에서는 화면 공유 기술 중 “System Broadcast Picker 기능”과 관련되어 실제 개발 구축을 하는 과정에 대해 작성하였습니다.
💡 전반적인 화면 공유 기술 및 앱 내에서 화면 공유 기술에 대해서 궁금하시면 이전에 작성한 글을 참고하시면 도움이 됩니다.
💡 개발 이전에 구축과정은 필수 전제로 수행이 되어야 합니다. 이전에 작성한 글을 참고하시면 됩니다.
1) 화면공유 프로세스
💡 해당 공유 프로세스를 구축하기 전에 전제로 Braodcast Extension과 App Group, Pakcakge에 대한 설정이 필요하였습니다. 이를 구축하였다는 전제에 실제 개발을 진행합니다.
💡 [해당 개발을 위한 흐름]
1. 사용자가 “화면공유” 버튼을 누르면 함수가 수행이 됩니다.
2. 해당 함수는 SwiftUI 환경에서 UIViewController를 불러오는 형태로 구성하였습니다.
3. UIViewController가 호출이 될 때 Broadcast를 수행시킵니다.
4. App Group으로 서로 연결된 Extenstion이 수행이 됩니다.
5. 해당 BroadCast Extenstion은 Pacakage를 불러와서 쉽게 처리를 위해 사용합니다.
6. 실시간은 공유되는 정보에 대해서 값을 전달받습니다.
2) 기술 구현 : 설명 및 소스코드
1. 권한을 추가해 줍니다 : NSPhotoLibraryAddUsageDescription
💡 해당 기술에서는 화면공유한 결과물을 사진폴더에 저장을 하도록 구현이 되어 있습니다. 그렇기에 앱이 사용자의 사진 라이브러리에 대한 추가 전용 액세스를 요청합니다.
# Key
Privacy - Photo Library Additions Usage Description
2. SampleHandler.swift : RPBroadcastSampleHandler
💡 해당 파일에서는 ‘Target의 ‘Broadcast upload Extension’을 이용하여서 구성하였습니다.
💡 해당 화면에서는 화면공유가 시작되고 멈추거나 재 시작하고 종료되는 과정에 대해서 각각 구현한 Handler입니다.
import BroadcastWriter
import ReplayKit
import Photos
/**
* MARK: 화면 공유를 관리하는 Handler
*/
class SampleHandler: RPBroadcastSampleHandler {
private var writer: BroadcastWriter?
private let fileManager: FileManager = .default
private let nodeURL: URL
private let videoOutputFullFileName = "\\(Date().timeIntervalSince1970)"
// MARK: 화면공유 서비스 시작 전에 수행됨
override init() {
print("🧩 Broadcast Sample Handler Created")
nodeURL = fileManager.temporaryDirectory
.appendingPathComponent(UUID().uuidString)
.appendingPathExtension(for: .mpeg4Movie)
fileManager.removeFileIfExists(url: nodeURL)
super.init()
}
// MARK: 화면공유가 시작될때의 함수
override func broadcastStarted(withSetupInfo setupInfo: [String : NSObject]?) {
let screen: UIScreen = .main
do {
writer = try .init(
outputURL: nodeURL,
screenSize: screen.bounds.size,
screenScale: screen.scale
)
} catch {
assertionFailure(error.localizedDescription)
finishBroadcastWithError(error)
return
}
do {
try writer?.start()
} catch {
finishBroadcastWithError(error)
}
}
// MARK: 화면공유로부터 전달받는 데이터
override func processSampleBuffer(_ sampleBuffer: CMSampleBuffer, with sampleBufferType: RPSampleBufferType) {
guard let writer = writer else {
debugPrint("processSampleBuffer: Writer is nil")
return
}
do {
let captured = try writer.processSampleBuffer(sampleBuffer, with: sampleBufferType)
debugPrint("processSampleBuffer captured", captured)
} catch {
debugPrint("processSampleBuffer error:", error.localizedDescription)
}
}
// MARK: 화면공유를 멈추는 경우
override func broadcastPaused() {
debugPrint("=== paused")
writer?.pause()
}
// MARK: 화면공유를 재 시작하는 경우
override func broadcastResumed() {
debugPrint("=== resumed")
writer?.resume()
}
// MARK: 화면공유를 종료하는 경우
override func broadcastFinished() {
guard let writer = writer else {
return
}
let dispatchGroup = DispatchGroup()
dispatchGroup.enter()
let outputURL: URL
do {
outputURL = try writer.finish()
} catch {
debugPrint("writer failure", error)
dispatchGroup.leave()
return
}
PHPhotoLibrary.shared().performChanges({
PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: outputURL)
}) { completed, error in
if completed {
print("🎥 Video \\(self.videoOutputFullFileName) has been moved to camera roll")
}
if error != nil {
print ("🧨 Cannot move the video \\(self.videoOutputFullFileName) to camera roll, error: \\(error!.localizedDescription)")
}
dispatchGroup.leave()
}
dispatchGroup.wait() // <= blocks the thread here
}
}
// MARK: 화면공유가 저장되는곳을 관리하는 매니져
extension FileManager {
func removeFileIfExists(url: URL) {
guard fileExists(atPath: url.path) else { return }
do {
try removeItem(at: url)
} catch {
print("error removing item \\(url)", error)
}
}
}
[참고] 해당 사이트를 참고하였습니다.
3. ContentView.swift : View
💡 해당 화면에서는 UIKit으로 구성된 화면을 SwiftUI에서 호출하는 과정입니다.
💡 해당 버튼을 누르면 실제 화면 공유를 위한 페이지가 출력이 됩니다.
💡 해당 화면에서는 UIKit으로 구성된 화면을 SwiftUI에서 호출하는 과정입니다.
💡 시스템 전체 화면공유 버튼을 누르면 ScreenShareViewController()의 UIViewController를 호출합니다.
//
// ContentView.swift
// TestApp
//
// Created by Lee on 2023/03/08.
//
import SwiftUI
import Foundation
struct ContentView: View {
var body: some View {
NavigationView{
VStack{
NavigationLink(destination: ScreenShareView()) {
Text("시스템 전체 화면공유")
.frame(width: 350, height: 20)
.foregroundColor(Color.white)
.padding()
.background(Color.blue)
.cornerRadius(10)
}
}.padding()
}
}
}
/**
* UIKit으로 구성된 ScreenShareViewController를 호출합니다
*/
struct ScreenShareView : UIViewControllerRepresentable {
typealias UIViewControllerType = ScreenShareViewController
// MARK: UIViewController를 생성합니다.
func makeUIViewController(context: Context) -> UIViewControllerType {
return ScreenShareViewController() // MARK: ScreenShareViewController의 UIViewController를 호출합니다.
}
// MARK: UIViewController를 변경하였을때 수행합니다.
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
//
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
[참고] SwiftUI에서 UIKit 파일을 불러는 방법에 대해서 참고하였습니다.
4. ScreenShareViewController: UIViewController
💡 UIKit으로 구성된 UIViewController이며 화면공유에 사용되는 팝업을 띄어주는 형태로 구성되어 있습니다.
💡 해당 부분에서는 App Group으로 ‘Broadcast upload Extension’ 타깃과의 연결을 하며 시작을 할 경우 데이터가 전달이 됩니다.
//
// ScreenShareViewController.swift
// TestApp
//
// Created by Lee on 2023/03/08.
//
import ReplayKit
import UIKit
// MARK: Extenstion을 구동시키는 방법
class ScreenShareViewController: UIViewController {
let broadcastPicker = RPSystemBroadcastPickerView(frame: CGRect(x: 10, y: 50, width: 100, height: 100))
override func viewDidLoad() {
super.viewDidLoad()
// Broadcast Upload Extension의 Bundle ID 값
broadcastPicker.preferredExtension = "org.test.app.TestApp.SampleHandler"
// 화면 공유시 마이크 사용여부
broadcastPicker.showsMicrophoneButton = false
// MARK: 버튼을 누르지 않고 페이지에 접근되었을때 즉시 수행되는 방법
for subview in broadcastPicker.subviews {
if let button = subview as? UIButton {
button.sendActions(for: UIControl.Event.allTouchEvents)
}
}
// MARK: 버튼을 눌렀을때 수행하는 방법
// for subview in broadcastPicker.subviews {
// let b = subview as! UIButton
// b.setImage(nil, for: .normal)
// b.setTitle("녹화시작", for: .normal)
// b.setTitleColor(.black, for: .normal)
// }
view.addSubview(broadcastPicker)
}
}
3) 실행 화면
1. 앱 실행
💡 일반 앱을 수행하는 것이 아니라 SampleHandler를 통해서 앱을 실행해야 수행이 됩니다.
2. 동작과정
💡 구성한 View 화면에서 "시스템 전체 화면공유" 버튼을 누릅니다.
💡 아래와 같은 화면이 출력되고 "방송 시작" 버튼을 누릅니다.
💡 방송이 시작되며 녹화 기능이 수행되면서 전체 화면 공유를 할 수 있는 데이터 전송됩니다.
💡 녹화한 데이터의 경우 즉시 처리 할 수 있지만 해당 예시에서는 동영상 파일로 사진폴더에 저장되도록 구성되어 있습니다.
3. 미리 콘솔을 남겨두었던 부분이 수행이 잘됨을 확인하였습니다
💡 추후 데이터 처리에 대해서는 기회가 되면 글로 남기겠습니다.
오늘도 감사합니다. 😀
반응형
'Swift > 이해하기' 카테고리의 다른 글
[Swift] 저장소 이해하기 : NotificationCenter, UserDefaults, AppGroup(FileManager) (0) | 2023.05.04 |
---|---|
[Swift] iOS 앱 상태 이해 및 백그라운드로 이동방법 : 앱 라이프 사이클, 앱 벗어나기 (1) | 2023.04.25 |
[Swift] 화면 공유 기술 - 2 : 전체 시스템 화면 공유 구축 (0) | 2023.03.10 |
[Swift] 화면 공유 기술 - 1 : In-App 화면 공유 (0) | 2023.03.06 |
[Swift] SwiftUI에서 Storyboard(UIKit) 화면을 불러오는 방법 : UIViewControllerRepresentable (0) | 2023.03.06 |