oficina / meet Public

Clone
git clone https://amadan.net/oficina/meet

Plain git — no account needed to clone.

Download

Download this file

1import CallsBroadcastKit
2import CallsKit
3import ReplayKit
4import SwiftUI
5
6/// Listens for the broadcast extension for as long as a call is on screen,
7/// and turns each broadcast into the call's screen share.
8///
9/// The order is the reverse of the Mac's. There, the button asks and the
10/// capture answers; here the person starts a broadcast at the system's
11/// sheet, the extension connects, and only then does the call learn there
12/// is a screen to send. A broadcast that arrives when the call cannot take
13/// it — not yet admitted, or already sharing — is closed at once, and the
14/// extension ends it with a sentence of its own.
15@MainActor final class PhoneScreenShare: ObservableObject {
16 private var inbox: BroadcastInbox?
17
18 func listen(for call: Call, leave: @escaping () -> Void) {
19 guard inbox == nil, let group = BroadcastPath.appGroup,
20 let path = BroadcastPath.socket(appGroup: group)
21 else { return }
22 let inbox = BroadcastInbox(path: path)
23 inbox.onConnection = { [weak call] connection in
24 MainActor.assumeIsolated {
25 guard let call, call.state?.phase == "admitted", !call.sharingScreen else {
26 connection.close()
27 return
28 }
29 let capture = BroadcastScreenCapture(connection)
30 // A broadcast stopped from Control Center while the app is in the
31 // background ends the only reason the call was still up there.
32 capture.afterEnded = {
33 if UIApplication.shared.applicationState == .background { leave() }
34 }
35 Task { await call.shareScreen(capture) }
36 }
37 }
38 do {
39 try inbox.start()
40 self.inbox = inbox
41 } catch {
42 // No App Group on this build (the simulator's unsigned one, say):
43 // the button still shows, and the extension says it could not
44 // reach a call. Nothing else in the call depends on this.
45 }
46 }
47
48 func stop() {
49 inbox?.stop()
50 inbox = nil
51 }
52}
53
54/// The system's broadcast button, dressed as the call's other controls.
55///
56/// RPSystemBroadcastPickerView is the only way an app may start a
57/// broadcast, and it only responds to a tap on its own button. So the
58/// picker is the button: its image is replaced with the call's symbol and
59/// tint, and it is sized like a CallActionStyle control beside the others.
60/// `preferredExtension` makes the sheet offer Calls' own extension rather
61/// than every broadcast service on the phone.
62struct BroadcastButton: UIViewRepresentable {
63 static let extensionIdentifier = Bundle.main.bundleIdentifier.map { $0 + ".broadcast" }
64
65 func makeUIView(context: Context) -> RPSystemBroadcastPickerView {
66 let picker = RPSystemBroadcastPickerView(frame: CGRect(x: 0, y: 0, width: 44, height: 44))
67 picker.preferredExtension = Self.extensionIdentifier
68 picker.showsMicrophoneButton = false
69 picker.accessibilityLabel = "Share your screen"
70 style(picker)
71 return picker
72 }
73 func updateUIView(_ picker: RPSystemBroadcastPickerView, context: Context) { style(picker) }
74
75 private func style(_ picker: RPSystemBroadcastPickerView) {
76 guard let button = picker.subviews.compactMap({ $0 as? UIButton }).first else { return }
77 let symbol = UIImage(
78 systemName: "rectangle.on.rectangle",
79 withConfiguration: UIImage.SymbolConfiguration(textStyle: .title3))
80 button.setImage(symbol, for: .normal)
81 button.tintColor = .systemOrange
82 button.backgroundColor = UIColor.systemOrange.withAlphaComponent(0.12)
83 button.layer.cornerRadius = 14
84 button.accessibilityLabel = "Share your screen"
85 button.frame = picker.bounds
86 button.autoresizingMask = [.flexibleWidth, .flexibleHeight]
87 }
88}
89