CallsignalingProtocol.swift 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. // _____ _
  2. // |_ _| |_ _ _ ___ ___ _ __ __ _
  3. // | | | ' \| '_/ -_) -_) ' \/ _` |_
  4. // |_| |_||_|_| \___\___|_|_|_\__,_(_)
  5. //
  6. // Threema iOS Client
  7. // Copyright (c) 2020 Threema GmbH
  8. //
  9. // This program is free software: you can redistribute it and/or modify
  10. // it under the terms of the GNU Affero General Public License, version 3,
  11. // as published by the Free Software Foundation.
  12. //
  13. // This program is distributed in the hope that it will be useful,
  14. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. // GNU Affero General Public License for more details.
  17. //
  18. // You should have received a copy of the GNU Affero General Public License
  19. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  20. import Foundation
  21. public class CallsignalingProtocol {
  22. static let minBitrate: UInt32 = 200
  23. static let minResolutionWidth: CGFloat = 320
  24. static let minResolutionHeight: CGFloat = 240
  25. static let minFps: UInt32 = 15
  26. // MARK: Objects
  27. public struct ThreemaVideoCallSignalingMessage {
  28. public var videoQualityProfile: ThreemaVideoCallQualityProfile?
  29. public var captureStateChange: ThreemaVideoCallCaputreState?
  30. }
  31. public struct ThreemaVideoCallQualityProfile {
  32. public var bitrate: UInt32
  33. public var maxResolution: CGSize
  34. public var maxFps: UInt32
  35. public var profile: ThreemaVideoCallQualityProfiles?
  36. public func debug() -> String {
  37. let width = String(format: "%.0f", maxResolution.width)
  38. let height = String(format: "%.0f", maxResolution.height)
  39. return "profile=\(profile?.debug() ?? "n/a"), \(bitrate)kps, \(maxFps)fps, \(width)x\(height)"
  40. }
  41. }
  42. public enum ThreemaVideoCallQualityProfiles: Int {
  43. case max
  44. case high
  45. case low
  46. public func qualityProfile() -> ThreemaVideoCallQualityProfile {
  47. switch self {
  48. case .max:
  49. return ThreemaVideoCallQualityProfile.init(bitrate: 4000, maxResolution: CGSize(width: 1920, height: 1080), maxFps: 25, profile: .max)
  50. case .high:
  51. return ThreemaVideoCallQualityProfile.init(bitrate: 2000, maxResolution: CGSize(width: 1280, height: 720), maxFps: 25, profile: .high)
  52. case .low:
  53. return ThreemaVideoCallQualityProfile.init(bitrate: 400, maxResolution: CGSize(width: 960, height: 540), maxFps: 20, profile: .low)
  54. }
  55. }
  56. public func debug() -> String {
  57. switch self {
  58. case .max:
  59. return "MAX"
  60. case .high:
  61. return "HIGH"
  62. case .low:
  63. return "LOW"
  64. default:
  65. return "n/a"
  66. }
  67. }
  68. }
  69. public struct ThreemaVideoCallCaputreState {
  70. public var device: ThreemaVideoCallCaputreDevice
  71. public var state: ThreemaVideoCallMode
  72. }
  73. public enum ThreemaVideoCallCaputreDevice: Int {
  74. case camera
  75. case screenSharing
  76. case microphone
  77. }
  78. public enum ThreemaVideoCallMode: Int {
  79. case off
  80. case on
  81. }
  82. }
  83. extension CallsignalingProtocol {
  84. // MARK: Encode / Decode
  85. public static func encodeMute(_ isMicrophoneMuted: Bool) -> Data? {
  86. var envelop = Callsignaling_Envelope()
  87. var captureState = Callsignaling_CaptureState()
  88. captureState.device = .microphone
  89. captureState.state = isMicrophoneMuted ? .off : .on
  90. envelop.captureStateChange = captureState
  91. return try? envelop.serializedData()
  92. }
  93. public static func encodeVideoCapture(_ isCapture: Bool) -> Data? {
  94. var envelop = Callsignaling_Envelope()
  95. var captureState = Callsignaling_CaptureState()
  96. captureState.device = .camera
  97. captureState.state = isCapture ? .on : .off
  98. envelop.captureStateChange = captureState
  99. return try? envelop.serializedData()
  100. }
  101. public static func encodeVideoQuality(_ profile: ThreemaVideoCallQualityProfiles) -> Data? {
  102. let threemaVideoCallQualityProfile = profile.qualityProfile()
  103. var envelop = Callsignaling_Envelope()
  104. var videoQualityProfile = Callsignaling_VideoQualityProfile()
  105. videoQualityProfile.profile = Callsignaling_VideoQualityProfile.QualityProfile(rawValue: threemaVideoCallQualityProfile.profile!.rawValue)!
  106. videoQualityProfile.maxBitrateKbps = threemaVideoCallQualityProfile.bitrate
  107. videoQualityProfile.maxFps = threemaVideoCallQualityProfile.maxFps
  108. var resolution = Callsignaling_Resolution()
  109. resolution.width = UInt32(threemaVideoCallQualityProfile.maxResolution.width)
  110. resolution.height = UInt32(threemaVideoCallQualityProfile.maxResolution.height)
  111. videoQualityProfile.maxResolution = resolution
  112. envelop.videoQualityProfile = videoQualityProfile
  113. return try? envelop.serializedData()
  114. }
  115. public static func decodeThreemaVideoCallSignalingMessage(_ data: Data) -> ThreemaVideoCallSignalingMessage {
  116. var threemaVideoCallQualityProfile: ThreemaVideoCallQualityProfile?
  117. var threemaVideoCallCaputreState: ThreemaVideoCallCaputreState?
  118. let envelop = try? Callsignaling_Envelope(serializedData: data)
  119. switch envelop?.content {
  120. case .captureStateChange(envelop?.captureStateChange):
  121. threemaVideoCallCaputreState = ThreemaVideoCallCaputreState.init(device: ThreemaVideoCallCaputreDevice(rawValue: envelop!.captureStateChange.device.rawValue)!, state: ThreemaVideoCallMode(rawValue: envelop!.captureStateChange.state.rawValue)!)
  122. case .videoQualityProfile(envelop?.videoQualityProfile):
  123. threemaVideoCallQualityProfile = ThreemaVideoCallQualityProfile.init(bitrate: envelop!.videoQualityProfile.maxBitrateKbps, maxResolution: CGSize(width: Int(envelop!.videoQualityProfile.maxResolution.width), height: Int(envelop!.videoQualityProfile.maxResolution.height)), maxFps: envelop!.videoQualityProfile.maxFps, profile: ThreemaVideoCallQualityProfiles(rawValue: envelop!.videoQualityProfile.profile.rawValue)!)
  124. default: break
  125. }
  126. return ThreemaVideoCallSignalingMessage(videoQualityProfile: threemaVideoCallQualityProfile, captureStateChange: threemaVideoCallCaputreState)
  127. }
  128. }
  129. extension CallsignalingProtocol {
  130. // MARK: Public static functions
  131. public static func currentThreemaVideoCallQualitySettingTitle() -> String {
  132. return threemaVideoCallQualitySettingTitle(for: UserSettings.shared().threemaVideoCallQualitySetting)
  133. }
  134. public static func threemaVideoCallQualitySettingTitle(for setting: ThreemaVideoCallQualitySetting) -> String {
  135. switch setting {
  136. case ThreemaVideoCallQualitySettingAuto:
  137. return NSLocalizedString("settings_threema_calls_video_quality_profile_auto", comment: "")
  138. case ThreemaVideoCallQualitySettingMaximumQuality:
  139. return NSLocalizedString("settings_threema_calls_video_quality_profile_max", comment: "")
  140. case ThreemaVideoCallQualitySettingLowDataConsumption:
  141. return NSLocalizedString("settings_threema_calls_video_quality_profile_low", comment: "")
  142. default:
  143. return "Unknown"
  144. }
  145. }
  146. public static func threemaVideoCallQualitySettingSubtitle(for setting: ThreemaVideoCallQualitySetting) -> String {
  147. switch setting {
  148. case ThreemaVideoCallQualitySettingAuto:
  149. return NSLocalizedString("settings_threema_calls_video_quality_profile_auto_description", comment: "")
  150. case ThreemaVideoCallQualitySettingMaximumQuality:
  151. return NSLocalizedString("settings_threema_calls_video_quality_profile_max_description", comment: "")
  152. case ThreemaVideoCallQualitySettingLowDataConsumption:
  153. return NSLocalizedString("settings_threema_calls_video_quality_profile_low_description", comment: "")
  154. default:
  155. return "Unknown"
  156. }
  157. }
  158. public static func threemaVideoCallQualitySettingCount() -> Int {
  159. return 3
  160. }
  161. public static func isThreemaVideoCallQualitySettingAuto() -> Bool {
  162. return UserSettings.shared()?.threemaVideoCallQualitySetting == ThreemaVideoCallQualitySettingAuto
  163. }
  164. public static func threemaVideoCallQualitySettingSelected(for setting: ThreemaVideoCallQualitySetting) -> Bool {
  165. return UserSettings.shared()?.threemaVideoCallQualitySetting == setting
  166. }
  167. public static func localCaptureQualityProfile() -> ThreemaVideoCallQualityProfile {
  168. if UserSettings.shared()?.threemaVideoCallQualitySetting == ThreemaVideoCallQualitySettingMaximumQuality {
  169. return ThreemaVideoCallQualityProfiles.max.qualityProfile()
  170. }
  171. return ThreemaVideoCallQualityProfiles.high.qualityProfile()
  172. }
  173. public static func localPeerQualityProfile() -> ThreemaVideoCallQualityProfile {
  174. let reachability = Reachability.forInternetConnection()
  175. switch UserSettings.shared()?.threemaVideoCallQualitySetting {
  176. case ThreemaVideoCallQualitySettingAuto:
  177. if reachability!.currentReachabilityStatus() == ReachableViaWiFi {
  178. return ThreemaVideoCallQualityProfiles.high.qualityProfile()
  179. } else {
  180. return ThreemaVideoCallQualityProfiles.low.qualityProfile()
  181. }
  182. case ThreemaVideoCallQualitySettingMaximumQuality:
  183. return ThreemaVideoCallQualityProfiles.max.qualityProfile()
  184. case ThreemaVideoCallQualitySettingLowDataConsumption:
  185. return ThreemaVideoCallQualityProfiles.low.qualityProfile()
  186. default: break
  187. }
  188. return ThreemaVideoCallQualityProfiles.low.qualityProfile()
  189. }
  190. public static func findCommonProfile(remoteProfile: ThreemaVideoCallQualityProfile?, networkIsRelayed: Bool, _ localProfile: ThreemaVideoCallQualityProfile? = nil) -> ThreemaVideoCallQualityProfile {
  191. let localQualityProfile = localProfile != nil ? localProfile! : localPeerQualityProfile()
  192. guard (remoteProfile != nil) else {
  193. return localQualityProfile
  194. }
  195. if let foundProfile = remoteProfile?.profile {
  196. if foundProfile == .low || localQualityProfile.profile == .low {
  197. return ThreemaVideoCallQualityProfiles.low.qualityProfile()
  198. }
  199. else if foundProfile == .high || localQualityProfile.profile == .high {
  200. return ThreemaVideoCallQualityProfiles.high.qualityProfile()
  201. }
  202. else if foundProfile == .max || localQualityProfile.profile == .max {
  203. return networkIsRelayed ? ThreemaVideoCallQualityProfiles.high.qualityProfile() : ThreemaVideoCallQualityProfiles.max.qualityProfile()
  204. }
  205. }
  206. // Unknown profile
  207. let maxBitrateKbps = max(min(localQualityProfile.bitrate, remoteProfile!.bitrate), minBitrate)
  208. let maxResolutionWidth = max(min(localQualityProfile.maxResolution.width, remoteProfile!.maxResolution.width), minResolutionWidth)
  209. let maxResolutionHeight = max(min(localQualityProfile.maxResolution.height, remoteProfile!.maxResolution.height), minResolutionHeight)
  210. let maxFps = max(min(localQualityProfile.maxFps, remoteProfile!.maxFps), minFps)
  211. return ThreemaVideoCallQualityProfile.init(bitrate: maxBitrateKbps, maxResolution: CGSize(width: maxResolutionWidth, height: maxResolutionHeight), maxFps: maxFps, profile: remoteProfile!.profile)
  212. }
  213. public static func printDebugQualityProfiles(remoteProfile: ThreemaVideoCallQualityProfile?, networkIsRelayed: Bool) -> String {
  214. return "\(printLocalQualityProfile())\n\(printPeerQualityProfile(remoteProfile: remoteProfile))\n\(printCommonQualityProfile(remoteProfile: remoteProfile, networkIsRelayed: networkIsRelayed))"
  215. }
  216. private static func printLocalQualityProfile() -> String {
  217. let localQualityProfile = localPeerQualityProfile()
  218. return "L=VoipVideoParams{\(localQualityProfile.debug())}"
  219. }
  220. private static func printPeerQualityProfile(remoteProfile: ThreemaVideoCallQualityProfile?) -> String {
  221. guard remoteProfile != nil else {
  222. return "R=VoipVideoParams{n/a}"
  223. }
  224. return "R=VoipVideoParams{\(remoteProfile!.debug())}"
  225. }
  226. private static func printCommonQualityProfile(remoteProfile: ThreemaVideoCallQualityProfile?, networkIsRelayed: Bool) -> String {
  227. let common = findCommonProfile(remoteProfile: remoteProfile, networkIsRelayed: networkIsRelayed)
  228. return "C=VoipVideoParams{\(common.debug())}"
  229. }
  230. }