import Foundation // --------------------------------------------------------------- // Screen share, receiving (spec §5.4) // --------------------------------------------------------------- // // The web client is the contract here, and this file is its receiving // half ported rule for rule: a sharer adds its display as one `sendonly` // transceiver of its own, in a stream of its own, announces it with a // `share {on:true}` broadcast, and stops it by stopping the transceiver // and broadcasting `share {on:false}`. Nothing on the wire says which // m-line is the screen. Each receiver works it out, and a native app and // a browser looking at the same sender have to reach the same answer, or // one of them captions a face as a screen. /// What one arriving track is to the person looking at it. enum TrackRole: Equatable { case audio, camera, screen } /// What a connection has recorded about the peer on the other end: the /// mid and stream its camera came in on, the stream everything else from /// it has arrived in, and whether it has said it is sharing. room.js's /// `entry` fields of the same names. struct ScreenClues: Equatable { var cameraMid = "" var cameraStream = "" var peerStream = "" var sharing = false } /// One arriving track, as the classifier needs it. `direction` is the /// transceiver's negotiated direction when there is one and the direction /// it was built with when there is not, spelled the way SDP spells it. struct ArrivingTrack: Equatable { var kind: String var streamId = "" var mid = "" var direction = "" } /// Tells a peer's screen from its camera. room.js's classifyTrack, in /// the same order and for the same reasons, which are set out there at /// length; the short form: /// /// 1. A video track arriving `recvonly` on a connection that sends a /// camera is on an m-line the far side added, and the only one it adds /// is the screen. /// 2. The camera's own mid, once one is known. /// 3. The camera's stream id, for a sender that reports no mid. /// 4. The stream the peer's microphone came in: a sender puts its camera /// there and its screen somewhere else. /// /// And when none of those can speak, the `share` message breaks the tie. /// It is last on purpose: it says the peer is showing a screen somewhere, /// not that this track is it. /// /// `hasVideo` is whether this connection carries a camera of ours. The /// apps always add one, so rule 1 is the rule that decides in practice; /// the rest are here because a browser on the far side may not have one, /// and a port that dropped them would disagree with the browser the day /// that mattered. func classifyTrack(_ known: ScreenClues, _ track: ArrivingTrack, hasVideo: Bool) -> TrackRole { if track.kind != "video" { return .audio } if track.direction == "recvonly" && hasVideo { return .screen } if !known.cameraMid.isEmpty && !track.mid.isEmpty { return track.mid == known.cameraMid ? .camera : .screen } if !known.cameraStream.isEmpty && !track.streamId.isEmpty { return track.streamId == known.cameraStream ? .camera : .screen } if !known.peerStream.isEmpty && !track.streamId.isEmpty { return track.streamId == known.peerStream ? .camera : .screen } if known.cameraMid.isEmpty && known.cameraStream.isEmpty { return known.sharing ? .screen : .camera } // A camera is known but there is nothing on this track to compare with // it: a second video track is the screen. return .screen } /// The words under a screen. The browser's, character for character — /// including the typographic apostrophe — so a person moving between the /// app and a browser tab reads the same caption on the same screen. public func screenCaption(_ name: String) -> String { name + "’s screen" } /// One thing the main stage can show: a person's camera or their screen. public enum StageItem: Hashable, Sendable { case camera(String) case screen(String) public var peer: String { switch self { case .camera(let peer), .screen(let peer): return peer } } } /// What the main stage shows; Speaker view's rules are in SpeakerView.swift. public enum Stage {}