import SwiftUI import UIKit /// Owns the pipeline and mirrors its status to the UI. The pipeline posts its /// callbacks to the main thread, so the @Published mutations happen there. final class ReceiverController: ObservableObject { @Published var status = "Starting…" @Published var showStatus = true private let pipeline: ReceiverPipeline private let pixelSize: CGSize init() { let localIP = LocalAddress.primaryIPv4() let size = ReceiverController.screenPixelSize() pixelSize = size pipeline = ReceiverPipeline( localIP: localIP, displaySize: { [weak self] in self?.pixelSize ?? .zero }, onStatus: { [weak self] text in self?.apply(status: text) }, onFirstFrame: { [weak self] in self?.apply(showStatus: false) }, onVideoSize: { _ in }) } func start() { pipeline.start() } func stop() { pipeline.stop() } func bindSink(_ sink: RenderSink) { pipeline.attachSink(sink) } private func apply(status: String? = nil, showStatus: Bool? = nil) { if let s = status { self.status = s } if let v = showStatus { self.showStatus = v } } static func screenPixelSize() -> CGSize { let scale = UIScreen.main.scale let b = UIScreen.main.bounds return CGSize(width: b.width * scale, height: b.height * scale) } } struct ContentView: View { @StateObject private var controller = ReceiverController() @Environment(\.scenePhase) private var scenePhase var body: some View { ZStack { Color.black.ignoresSafeArea() VideoSurfaceView { sink in controller.bindSink(sink) } .ignoresSafeArea() if controller.showStatus { VStack { Spacer() Text(controller.status) .font(.footnote) .foregroundStyle(.white) .multilineTextAlignment(.center) .padding(.horizontal, 24) .padding(.vertical, 12) .background(.black.opacity(0.55), in: RoundedRectangle(cornerRadius: 10)) Spacer() Spacer() } .transition(.opacity) } } .onAppear { controller.start() } .onChange(of: scenePhase) { _, phase in switch phase { case .active: controller.start() case .inactive, .background: controller.stop() @unknown default: break } } } } @main struct ReceiverApp: App { var body: some Scene { WindowGroup { ContentView() .preferredColorScheme(.dark) .statusBarHidden(true) .persistentSystemOverlays(.hidden) } } }