| 1 | import CallsBroadcastKit |
| 2 | import CoreMedia |
| 3 | import ReplayKit |
| 4 | |
| 5 | /// The broadcast upload extension: ReplayKit's frames in, the app's socket |
| 6 | /// out. It links CallsBroadcastKit and nothing else of Calls, and holds |
| 7 | /// one frame buffer, because it lives under a memory ceiling of about |
| 8 | /// fifty megabytes and is killed without a word past it. |
| 9 | /// |
| 10 | /// It never talks to the meeting. The app is already in the call and owns |
| 11 | /// every peer connection; this only hands it pictures. |
| 12 | final class SampleHandler: RPBroadcastSampleHandler { |
| 13 | private var sender: BroadcastSender? |
| 14 | |
| 15 | override func broadcastStarted(withSetupInfo setupInfo: [String: NSObject]?) { |
| 16 | guard let group = BroadcastPath.appGroup, let path = BroadcastPath.socket(appGroup: group) |
| 17 | else { |
| 18 | finish("Calls could not share your screen on this build.") |
| 19 | return |
| 20 | } |
| 21 | let sender = BroadcastSender(path: path) |
| 22 | // Two seconds: the countdown has just ended, and the app, which is in |
| 23 | // the call, is listening already. Longer would only delay the answer |
| 24 | // to someone who started a broadcast with no call to share it to. |
| 25 | guard sender.connect(timeout: 2) else { |
| 26 | finish("Join a call in Calls, then share your screen from there.") |
| 27 | return |
| 28 | } |
| 29 | sender.onClosed = { [weak self] in |
| 30 | self?.finish("You stopped sharing your screen in Calls.") |
| 31 | } |
| 32 | self.sender = sender |
| 33 | } |
| 34 | |
| 35 | override func processSampleBuffer( |
| 36 | _ sampleBuffer: CMSampleBuffer, with sampleBufferType: RPSampleBufferType |
| 37 | ) { |
| 38 | guard sampleBufferType == .video, let sender, let pixels = sampleBuffer.imageBuffer else { |
| 39 | return |
| 40 | } |
| 41 | let orientation = |
| 42 | (CMGetAttachment(sampleBuffer, key: RPVideoSampleOrientationKey as CFString, attachmentModeOut: nil) |
| 43 | as? NSNumber)?.uint16Value ?? 1 |
| 44 | let time = CMTimeGetSeconds(sampleBuffer.presentationTimeStamp) |
| 45 | sender.send(pixels, orientation: orientation, timestampNs: UInt64(max(0, time) * 1_000_000_000)) |
| 46 | } |
| 47 | |
| 48 | override func broadcastFinished() { |
| 49 | sender?.close() |
| 50 | sender = nil |
| 51 | } |
| 52 | |
| 53 | /// Ends the broadcast with a sentence the system shows the person. The |
| 54 | /// only way an extension can say anything at all. |
| 55 | private func finish(_ sentence: String) { |
| 56 | sender?.close() |
| 57 | sender = nil |
| 58 | finishBroadcastWithError( |
| 59 | NSError(domain: "Calls", code: 1, userInfo: [NSLocalizedDescriptionKey: sentence])) |
| 60 | } |
| 61 | } |
| 62 | |