VoIPStats.swift 49 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326
  1. // _____ _
  2. // |_ _| |_ _ _ ___ ___ _ __ __ _
  3. // | | | ' \| '_/ -_) -_) ' \/ _` |_
  4. // |_| |_||_|_| \___\___|_|_|_\__,_(_)
  5. //
  6. // Threema iOS Client
  7. // Copyright (c) 2018-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. internal protocol VoIPStatsRepresentation {
  22. func getShortRepresentation() -> String
  23. func getRepresentation() -> String
  24. }
  25. @objc public enum CandidatePairVariant: Int {
  26. case NONE = 0x00
  27. case OVERVIEW = 0x01
  28. case DETAILED = 0x02
  29. case OVERVIEW_AND_DETAILED = 0x03 // OVERVIEW | DETAILED
  30. }
  31. @objc public class VoIPStatsOptions: NSObject {
  32. @objc public var transport = false
  33. @objc public var inboundRtp = false
  34. @objc public var outboundRtp = false
  35. @objc public var codecs = false
  36. @objc public var selectedCandidatePair = false
  37. @objc public var candidatePairsFlag = CandidatePairVariant.NONE
  38. @objc public var tracks = false
  39. @objc public var framesReceived = false
  40. @objc public var crypto = false
  41. }
  42. internal enum Direction {
  43. case inbound
  44. case outbound
  45. }
  46. internal enum CodecMimeTypePrimary {
  47. case unknown
  48. case audio
  49. case video
  50. internal static func fromRepresentation(string: String?) -> CodecMimeTypePrimary {
  51. if string == nil {
  52. return .unknown
  53. }
  54. switch string {
  55. case "audio":
  56. return .audio
  57. case "video":
  58. return .video
  59. default:
  60. return .unknown
  61. }
  62. }
  63. internal func toShortRepresentation() -> String {
  64. switch self {
  65. case .audio:
  66. return "a"
  67. case .video:
  68. return "v"
  69. case .unknown:
  70. return "?"
  71. }
  72. }
  73. internal func toRepresentation() -> String {
  74. switch self {
  75. case .audio:
  76. return "audio"
  77. case .video:
  78. return "video"
  79. case .unknown:
  80. return "?"
  81. }
  82. }
  83. }
  84. @objc public class VoIPStats: NSObject, VoIPStatsRepresentation {
  85. private let report: RTCStatisticsReport
  86. private let options: VoIPStatsOptions
  87. private let previousState: VoIPStatsState?
  88. private let timestamp: CFTimeInterval
  89. private var transport: Transport?
  90. private var crypto: Crypto?
  91. private var selectedCandidatePair: CandidatePair?
  92. private var inboundRtpAudio: InboundRtp?
  93. private var inboundRtpVideo: InboundRtp?
  94. private var outboundRtpAudio: OutboundRtp?
  95. private var outboundRtpVideo: OutboundRtp?
  96. private var inboundTrackVideo: Track?
  97. private var outboundTrackVideo: Track?
  98. private var inboundCodecs: [String: Codec]?
  99. private var outboundCodecs: [String: Codec]?
  100. private let transceivers: [RTCRtpTransceiver]
  101. private var rtpTransceivers: [RtpTransceiver]?
  102. private var candidatePairs: [CandidatePair]?
  103. private var totalFramesReceived: UInt64?
  104. internal class BytesTransferred: VoIPStatsRepresentation {
  105. public let sent: UInt64?
  106. public let received: UInt64?
  107. internal init(_ entry: RTCStatistics) {
  108. if let sentNumber = entry.values["bytesSent"] as? NSNumber {
  109. self.sent = UInt64(truncating: sentNumber)
  110. } else {
  111. self.sent = nil
  112. }
  113. if let receivedNumber = entry.values["bytesReceived"] as? NSNumber {
  114. self.received = UInt64(truncating: receivedNumber)
  115. } else {
  116. self.received = nil
  117. }
  118. }
  119. public func getShortRepresentation() -> String {
  120. var result = "tx="
  121. if let sent = self.sent {
  122. result += "\(toHumanReadableByteCount(sent))"
  123. } else {
  124. result += "n/a"
  125. }
  126. result += ", rx="
  127. if let received = self.received {
  128. result += "\(toHumanReadableByteCount(received))"
  129. } else {
  130. result += "n/a"
  131. }
  132. return result
  133. }
  134. public func getRepresentation() -> String {
  135. return getShortRepresentation()
  136. }
  137. }
  138. internal class Candidate: VoIPStatsRepresentation {
  139. public let address: String?
  140. public let type: String?
  141. public let protocol_: String?
  142. public let network: String?
  143. internal init(_ entry: RTCStatistics) {
  144. self.address = entry.values["ip"] as? String
  145. self.type = entry.values["candidateType"] as? String
  146. self.protocol_ = entry.values["protocol"] as? String
  147. self.network = entry.values["networkType"] as? String
  148. }
  149. public func getShortRepresentation() -> String {
  150. var result = "\(self.address ?? "?.?.?.?") \(self.type ?? "n/a") \(self.protocol_ ?? "n/a")"
  151. if let network = self.network {
  152. result += " " + network
  153. }
  154. return result
  155. }
  156. public func getRepresentation() -> String {
  157. var result = "address=\(self.address ?? "?.?.?.?"), type=\(self.type ?? "n/a"), protocol=\(self.protocol_ ?? "n/a")"
  158. if let network = self.network {
  159. result += ", network=\(network)"
  160. }
  161. return result
  162. }
  163. }
  164. internal class CandidatePair: VoIPStatsRepresentation {
  165. internal enum State: String {
  166. case unknown
  167. case frozen
  168. case waiting
  169. case in_progress = "in-progress"
  170. case succeeded
  171. case failed
  172. }
  173. public let id: String
  174. public let priority: UInt64
  175. public let local: Candidate?
  176. public let remote: Candidate?
  177. public let nominated: Bool?
  178. public let state: State
  179. public let bytesTransferred: BytesTransferred
  180. public let roundTripTime: RoundTripTime
  181. public let availableOutgoingBitrate: Double?
  182. public let usesRelay: Bool
  183. static public func fromId(_ id: String, report: RTCStatisticsReport) -> CandidatePair? {
  184. // Lookup pair
  185. let id = id.dropFirst(20)
  186. for entry in report.statistics.values {
  187. let entryId = candidatePairId(statsId: entry.id)
  188. if entryId == id {
  189. return CandidatePair(entry, report: report)
  190. }
  191. }
  192. return nil
  193. }
  194. public init(_ entry: RTCStatistics, report: RTCStatisticsReport) {
  195. // Id
  196. self.id = candidatePairId(statsId: entry.id)
  197. // Priority
  198. if let p = entry.values["priority"] as? NSNumber {
  199. self.priority = UInt64(truncating: p)
  200. } else {
  201. self.priority = 0
  202. }
  203. // Candidates
  204. let localCandidateId = entry.values["localCandidateId"] as? String
  205. let remoteCandidateId = entry.values["remoteCandidateId"] as? String
  206. (self.local, self.remote) = CandidatePair.lookupCandidates(
  207. localCandidateId: localCandidateId, remoteCandidateId: remoteCandidateId, report: report)
  208. // Nominated
  209. if let n = entry.values["nominated"] as? NSNumber {
  210. self.nominated = Bool(truncating: n)
  211. } else {
  212. self.nominated = nil
  213. }
  214. // State
  215. if let s = entry.values["state"] as? String {
  216. if let newState = State.init(rawValue: s) {
  217. self.state = newState
  218. } else {
  219. self.state = .unknown
  220. }
  221. } else {
  222. self.state = .unknown
  223. }
  224. // Bytes transferred
  225. self.bytesTransferred = BytesTransferred(entry)
  226. // RTT
  227. self.roundTripTime = RoundTripTime(entry)
  228. // Available bitrate
  229. if let aOB = entry.values["availableOutgoingBitrate"] as? NSNumber {
  230. self.availableOutgoingBitrate = Double(truncating: aOB)
  231. } else {
  232. self.availableOutgoingBitrate = nil
  233. }
  234. // Check use relay
  235. if self.local != nil, self.local?.type == "relay" {
  236. self.usesRelay = true
  237. }
  238. else if self.remote != nil, self.remote?.type == "relay" {
  239. self.usesRelay = true
  240. }
  241. else {
  242. self.usesRelay = false
  243. }
  244. }
  245. static internal func lookupCandidates(localCandidateId: String?, remoteCandidateId: String?, report: RTCStatisticsReport)
  246. -> (Candidate?, Candidate?) {
  247. var localCandidate: Candidate?
  248. var remoteCandidate: Candidate?
  249. if localCandidateId != nil || remoteCandidateId != nil {
  250. for entry in report.statistics.values {
  251. if localCandidateId != nil && entry.id == localCandidateId {
  252. localCandidate = Candidate(entry)
  253. }
  254. if remoteCandidateId != nil && entry.id == remoteCandidateId {
  255. remoteCandidate = Candidate(entry)
  256. }
  257. }
  258. }
  259. return (localCandidate, remoteCandidate)
  260. }
  261. public func getShortRepresentation() -> String {
  262. var result = "pair=\(self.state) "
  263. if let nominated = self.nominated, nominated == true {
  264. result += " nominated"
  265. }
  266. result += "\n"
  267. result += "local="
  268. if local != nil {
  269. result += local!.getShortRepresentation()
  270. } else {
  271. result += "n/a"
  272. }
  273. result += "\n"
  274. result += "remote="
  275. if remote != nil {
  276. result += remote!.getShortRepresentation()
  277. } else {
  278. result += "n/a"
  279. }
  280. result += "\n"
  281. result += "relayed=\(usesRelay)\n"
  282. result += "\(bytesTransferred.getShortRepresentation())"
  283. if availableOutgoingBitrate != nil {
  284. result += " bitrate=\(String(format: "%.0fkbps", availableOutgoingBitrate! / 1000))"
  285. }
  286. result += "\n"
  287. result += "\(roundTripTime.getShortRepresentation())"
  288. return result
  289. }
  290. public func getRepresentation() -> String {
  291. var result = "id=\(self.id), state=\(self.state), priority="
  292. if self.priority > 0 {
  293. result += String(self.priority)
  294. } else {
  295. result += "n/a"
  296. }
  297. result += ", active="
  298. if let active = self.nominated {
  299. result += active ? "yes" : "no"
  300. } else {
  301. result += "n/a"
  302. }
  303. result += ", \(self.roundTripTime.getRepresentation())" +
  304. ", \(self.bytesTransferred.getRepresentation())" +
  305. "\n Local: \(self.local?.getRepresentation() ?? "n/a")" +
  306. "\n Remote: \(self.remote?.getRepresentation() ?? "n/a")"
  307. return result
  308. }
  309. public func getStatusChar() -> String {
  310. /*
  311. * '-' -> frozen: The pair has been held back due to another pair with the same
  312. * foundation that is currently in the waiting state.
  313. * '.' -> waiting: Pair checking has not started, yet.
  314. * '+' -> in-progress: Pair checking is in progress. In the webrtc.org implementation,
  315. * this is also being used for pairs that (temporarily) have no connection.
  316. * 'o' -> succeeded: A connection could be established via this pair.
  317. * 'x' -> failed: No connection could be established via this pair and no further
  318. * attempts will be made.
  319. */
  320. switch self.state {
  321. case .unknown: return "?"
  322. case .frozen: return "-"
  323. case .waiting: return "."
  324. case .in_progress: return "+"
  325. case .succeeded: return "o"
  326. case .failed: return "x"
  327. }
  328. }
  329. }
  330. internal class Codec: VoIPStatsRepresentation {
  331. internal struct CodecMimeType {
  332. public let primary: CodecMimeTypePrimary
  333. public let secondary: String
  334. }
  335. public let codecId: String
  336. public let direction: Direction
  337. private let mimeType: CodecMimeType
  338. private let clockRate: UInt64?
  339. public init(_ entry: RTCStatistics) {
  340. self.codecId = entry.id
  341. self.direction = codecId.contains("Inbound") ? Direction.inbound : Direction.outbound
  342. self.mimeType = Codec.getMimeType(mimeTypeString: entry.values["mimeType"] as? String)
  343. if let clockRateNumber = entry.values["clockRate"] as? NSNumber {
  344. self.clockRate = UInt64(truncating: clockRateNumber)
  345. } else {
  346. self.clockRate = nil
  347. }
  348. }
  349. public func getShortRepresentation() -> String {
  350. var result = "\(self.mimeType.primary.toShortRepresentation())/\(self.mimeType.secondary)"
  351. if clockRate == nil {
  352. return result
  353. }
  354. result += "@"
  355. let clockRateK = clockRate! / 1000
  356. if clockRateK >= 1 {
  357. result += "\(clockRateK)k"
  358. } else {
  359. result += " \(self.clockRate!)"
  360. }
  361. return result
  362. }
  363. public func getRepresentation() -> String {
  364. var result = "mime-type=\(self.mimeType.primary.toRepresentation())/\(self.mimeType.secondary)"
  365. result += ", clock-rate="
  366. if clockRate != nil {
  367. result += "\(self.clockRate!)"
  368. } else {
  369. result += "n/a"
  370. }
  371. return result
  372. }
  373. public static func getMimeType(mimeTypeString: String?) -> CodecMimeType {
  374. if mimeTypeString == nil {
  375. return CodecMimeType(primary: CodecMimeTypePrimary.unknown, secondary: "?")
  376. }
  377. if let mimeType = mimeTypeString?.split(separator: "/") {
  378. if mimeType.count != 2 {
  379. return CodecMimeType(primary: CodecMimeTypePrimary.unknown, secondary: "?")
  380. }
  381. return CodecMimeType(primary: CodecMimeTypePrimary.fromRepresentation(string: String(mimeType[0])), secondary: String(mimeType[1]))
  382. }
  383. return CodecMimeType(primary: CodecMimeTypePrimary.unknown, secondary: "?")
  384. }
  385. }
  386. internal class Rtp: VoIPStatsRepresentation {
  387. private let codecs: [String: Codec]
  388. public let codecId: String?
  389. public let kind: String
  390. public var jitter: Double?
  391. public var packetsTotal: UInt64?
  392. public var bytesTotal: UInt64?
  393. public var packetsLost: UInt64?
  394. public var packetLossPercent: Float64?
  395. public var qualityLimitationReason: String?
  396. public var qualityLimitationResolutionChanges: UInt64?
  397. public var implementation: String?
  398. public var averageFps: Float?
  399. public var bitrate: Double?
  400. public init(_ entry: RTCStatistics, codecs: [String: Codec]) {
  401. self.codecs = codecs
  402. self.codecId = entry.values["codecId"] as? String
  403. self.kind = entry.values["kind"] as? String ?? "?"
  404. }
  405. public func getShortRepresentation() -> String {
  406. var shortRepresentation = "\(kind)"
  407. if packetsTotal != nil && packetsLost != nil {
  408. shortRepresentation.append(" packets-lost=\(packetsLost!)/\(packetsTotal!)(\(String(format: "%.1f", packetLossPercent ?? 0))%)")
  409. }
  410. else if packetsTotal != nil {
  411. shortRepresentation.append(" packets=\(packetsTotal!)")
  412. }
  413. if jitter != nil {
  414. shortRepresentation.append(" jitter=\(jitter!)")
  415. }
  416. if bitrate != nil {
  417. shortRepresentation.append(" bitrate=\(String(format: "%.0f", bitrate! / 1000))kbps")
  418. }
  419. if averageFps != nil {
  420. shortRepresentation.append(" avfps=\(String(format: "%.1f", averageFps!))")
  421. }
  422. shortRepresentation.append(" codec=")
  423. if codecId != nil, let codec = codecs[codecId!] {
  424. shortRepresentation.append(codec.getShortRepresentation())
  425. } else {
  426. shortRepresentation.append("?")
  427. }
  428. if implementation != nil {
  429. switch implementation {
  430. case "HWEncoder":
  431. shortRepresentation.append(" (hw)")
  432. break
  433. case "SWEncoder":
  434. shortRepresentation.append(" (sw)")
  435. break
  436. case "unknown":
  437. break
  438. default:
  439. shortRepresentation.append(" (\(implementation!))")
  440. }
  441. }
  442. if qualityLimitationReason != nil {
  443. shortRepresentation.append(" limit=\(qualityLimitationReason!.replacingOccurrences(of: "bandwidth", with: "bw"))")
  444. if qualityLimitationResolutionChanges != nil {
  445. shortRepresentation.append("/\(qualityLimitationResolutionChanges!)")
  446. }
  447. }
  448. return shortRepresentation
  449. }
  450. public func getRepresentation() -> String {
  451. return getShortRepresentation()
  452. }
  453. }
  454. internal class InboundRtp: Rtp {
  455. public init(_ entry: RTCStatistics, codecs: [String: Codec], previousState: VoIPStatsState?, timestamp: CFTimeInterval) {
  456. super.init(entry, codecs: codecs)
  457. if let j = entry.values["jitter"] as? NSNumber {
  458. self.jitter = Double(truncating: j)
  459. } else {
  460. self.jitter = nil
  461. }
  462. if let pt = entry.values["packetsReceived"] as? NSNumber {
  463. self.packetsTotal = UInt64(truncating: pt)
  464. } else {
  465. self.packetsTotal = nil
  466. }
  467. if let bt = entry.values["bytesReceived"] as? NSNumber {
  468. self.bytesTotal = UInt64(truncating: bt)
  469. } else {
  470. self.bytesTotal = nil
  471. }
  472. if let pl = entry.values["packetsLost"] as? NSNumber {
  473. self.packetsLost = UInt64(truncating: pl)
  474. } else {
  475. self.packetsLost = nil
  476. }
  477. self.packetLossPercent = calculatePacketLoss()
  478. var totalInterFrameDelay: Double? = nil
  479. if let tifd = entry.values["totalInterFrameDelay"] as? NSNumber {
  480. totalInterFrameDelay = Double(truncating: tifd)
  481. }
  482. var framesDecoded: UInt64? = nil
  483. if let fd = entry.values["framesDecoded"] as? NSNumber {
  484. framesDecoded = UInt64(truncating: fd)
  485. }
  486. if totalInterFrameDelay != nil && framesDecoded != nil {
  487. averageFps = calculateAverageFps(totalInterFrameDelay: totalInterFrameDelay!, framesDecoded: framesDecoded!)
  488. }
  489. if previousState != nil, previousState!.videoBytesReceived != nil && bytesTotal != nil {
  490. bitrate = calculateVideoBitrate(previousTimestamp: previousState!.timestampUs, previousBytes: previousState!.videoBytesReceived!, currentTimestamp: timestamp, currentBytes: bytesTotal!)
  491. }
  492. }
  493. private func calculatePacketLoss() -> Float64? {
  494. if packetsTotal == nil || packetsLost == nil {
  495. return nil
  496. }
  497. if packetsLost! > UInt64(0) {
  498. return Float64(packetsLost!) / Float64(packetsTotal!) * Float64(100)
  499. } else {
  500. return 0
  501. }
  502. }
  503. private func calculateAverageFps(totalInterFrameDelay: Double, framesDecoded: UInt64) -> Float {
  504. if framesDecoded == 0 {
  505. return 0.0
  506. }
  507. return Float(1.0 / (totalInterFrameDelay / Double(framesDecoded)))
  508. }
  509. }
  510. internal class OutboundRtp: Rtp {
  511. public init(_ entry: RTCStatistics, codecs: [String: Codec], previousState: VoIPStatsState?, timestamp: CFTimeInterval) {
  512. super.init(entry, codecs: codecs)
  513. if let pt = entry.values["packetsSent"] as? NSNumber {
  514. self.packetsTotal = UInt64(truncating: pt)
  515. } else {
  516. self.packetsTotal = nil
  517. }
  518. if let bt = entry.values["bytesSent"] as? NSNumber {
  519. self.bytesTotal = UInt64(truncating: bt)
  520. } else {
  521. self.bytesTotal = nil
  522. }
  523. self.qualityLimitationReason = entry.values["qualityLimitationReason"] as? String
  524. if let qlrc = entry.values["qualityLimitationResolutionChanges"] as? NSNumber {
  525. self.qualityLimitationResolutionChanges = UInt64(truncating: qlrc)
  526. } else {
  527. self.qualityLimitationResolutionChanges = nil
  528. }
  529. self.implementation = entry.values["encoderImplementation"] as? String
  530. if previousState != nil, previousState!.videoBytesSent != nil && bytesTotal != nil {
  531. bitrate = calculateVideoBitrate(previousTimestamp: previousState!.timestampUs, previousBytes: previousState!.videoBytesSent!, currentTimestamp: timestamp, currentBytes: bytesTotal!)
  532. }
  533. }
  534. }
  535. internal class Track: VoIPStatsRepresentation {
  536. public let kind: String
  537. public var frameWidth: UInt64?
  538. public var frameHeight: UInt64?
  539. public var freezeCount: UInt64?
  540. public var pauseCount: UInt64?
  541. public var ended: Bool?
  542. public var remoteSource: Bool?
  543. public var detached: Bool?
  544. public var totalFramesReceived: UInt64?
  545. public init(_ entry: RTCStatistics) {
  546. self.kind = entry.values["kind"] as? String ?? "?"
  547. if let fw = entry.values["frameWidth"] as? NSNumber {
  548. self.frameWidth = UInt64(truncating: fw)
  549. } else {
  550. self.frameWidth = nil
  551. }
  552. if let fh = entry.values["frameHeight"] as? NSNumber {
  553. self.frameHeight = UInt64(truncating: fh)
  554. } else {
  555. self.frameHeight = nil
  556. }
  557. if let fc = entry.values["freezeCount"] as? NSNumber {
  558. self.freezeCount = UInt64(truncating: fc)
  559. } else {
  560. self.freezeCount = nil
  561. }
  562. if let pc = entry.values["pauseCount"] as? NSNumber {
  563. self.pauseCount = UInt64(truncating: pc)
  564. } else {
  565. self.pauseCount = nil
  566. }
  567. if let e = entry.values["ended"] as? NSNumber {
  568. self.ended = Bool(truncating: e)
  569. } else {
  570. self.ended = nil
  571. }
  572. if let rs = entry.values["remoteSource"] as? NSNumber {
  573. self.remoteSource = Bool(truncating: rs)
  574. } else {
  575. self.remoteSource = nil
  576. }
  577. if let tfr = entry.values["framesReceived"] as? NSNumber {
  578. self.totalFramesReceived = UInt64(truncating: tfr)
  579. } else {
  580. self.totalFramesReceived = nil
  581. }
  582. if let d = entry.values["detached"] as? NSNumber {
  583. self.detached = Bool(truncating: d)
  584. } else {
  585. self.detached = nil
  586. }
  587. }
  588. public func getShortRepresentation() -> String {
  589. var result = "\(self.kind)"
  590. if frameWidth != nil && frameHeight != nil {
  591. result += " res=\(frameWidth!)x\(frameHeight!)"
  592. }
  593. if freezeCount != nil {
  594. result += " freeze=\(freezeCount!)"
  595. }
  596. if pauseCount != nil {
  597. result += " pause=\(pauseCount!)"
  598. }
  599. return result
  600. }
  601. public func getRepresentation() -> String {
  602. return getShortRepresentation()
  603. }
  604. public func areFramesReceived() -> Bool {
  605. if kind == "video" {
  606. if totalFramesReceived != nil, ended != nil, remoteSource != nil, totalFramesReceived! > UInt64(0) && ended == false && remoteSource == true {
  607. return true
  608. }
  609. }
  610. return false
  611. }
  612. }
  613. internal class RtpTransceiver: VoIPStatsRepresentation {
  614. public let transceiver: RTCRtpTransceiver
  615. public init(_ rtpTransceiver: RTCRtpTransceiver) {
  616. self.transceiver = rtpTransceiver
  617. }
  618. public func getShortRepresentation() -> String {
  619. var result = "kind=\(mediaType())"
  620. result += ", mid=\(transceiver.mid)"
  621. if transceiver.currentDirection(&transceiver.direction) {
  622. result += ", cur-dir=\(direction())"
  623. }
  624. result += "\n sender: \(addParametersShortRepresentation(transceiver.sender.parameters))"
  625. result += "\n receiver: \(addParametersShortRepresentation(transceiver.receiver.parameters))"
  626. return result
  627. }
  628. public func getRepresentation() -> String {
  629. var result = "kind=\(mediaType())"
  630. result += ", mid=\(transceiver.mid)"
  631. result += ", direction=\(direction())"
  632. if transceiver.currentDirection(&transceiver.direction) {
  633. result += ", cur-dir=\(direction())"
  634. }
  635. result += "\n Sender: \(addParametersRepresentation(transceiver.sender.parameters))"
  636. result += "\n Receiver: \(addParametersRepresentation(transceiver.receiver.parameters))"
  637. return result
  638. }
  639. private func addParametersShortRepresentation(_ parameters: RTCRtpParameters) -> String {
  640. var result = ""
  641. var plain = 0
  642. var encrypted = 0
  643. // Add amount of encrypted vs. non-encrypted header extensions
  644. for headerExtension in parameters.headerExtensions {
  645. if headerExtension.isEncrypted {
  646. encrypted += 1
  647. } else {
  648. plain += 1
  649. }
  650. }
  651. result += "#exts=\(encrypted)e/\(plain)p, "
  652. // Add codecs
  653. result += "cs="
  654. if parameters.codecs.count > 0 {
  655. for codec in parameters.codecs {
  656. // Add codec
  657. result += "\(codec.name)/\(shortClockRate(codec: codec))"
  658. if let numChannels = codec.numChannels {
  659. result += "/\(numChannels)"
  660. }
  661. // Add codec attributes
  662. // Note: We only care about Opus CBR attributes
  663. if codec.name == "opus" {
  664. let cbr = codec.parameters["cbr"] as? String
  665. result += "["
  666. result += "cbr=\(cbr ?? "?")]"
  667. }
  668. result += " "
  669. }
  670. } else {
  671. result += "?"
  672. }
  673. return result
  674. }
  675. private func addParametersRepresentation(_ parameters: RTCRtpParameters) -> String {
  676. var result = ""
  677. // Add codecs
  678. result += "\n Codecs (\(parameters.codecs.count))"
  679. if parameters.codecs.count > 0 {
  680. for (_, codec) in parameters.codecs.enumerated() {
  681. // add codec
  682. result += "\n - name=\(codec.name), clock-rate=\(shortClockRate(codec: codec))"
  683. if let numChannels = codec.numChannels {
  684. result += ", #channels=\(numChannels)"
  685. }
  686. // Add codec attributes
  687. result += ", attributes="
  688. for (key,value) in codec.parameters {
  689. result += "\(key)=\(value) "
  690. }
  691. }
  692. }
  693. // Add header extensions
  694. var extensionResult = ""
  695. var plain = 0
  696. var encrypted = 0
  697. for (_, headerExtension) in parameters.headerExtensions.enumerated() {
  698. extensionResult += "\n - id=\(headerExtension.id)"
  699. extensionResult += ", encrypted=\(headerExtension.isEncrypted ? "true" : "false")"
  700. extensionResult += ", uri=\(headerExtension.uri)"
  701. if headerExtension.isEncrypted {
  702. encrypted += 1
  703. } else {
  704. plain += 1
  705. }
  706. }
  707. result += "\n Header Extensions (\(encrypted)e/\(plain)p)\(extensionResult)"
  708. return result
  709. }
  710. private func mediaType() -> String {
  711. switch transceiver.mediaType {
  712. case .audio:
  713. return "audio"
  714. case .video:
  715. return "video"
  716. default:
  717. return "?"
  718. }
  719. }
  720. private func direction() -> String {
  721. switch transceiver.direction {
  722. case .sendRecv:
  723. return "send/recv"
  724. case .sendOnly:
  725. return "send"
  726. case .recvOnly:
  727. return "recv"
  728. case .inactive:
  729. return "inactive"
  730. default:
  731. return "?"
  732. }
  733. }
  734. private func shortClockRate(codec: RTCRtpCodecParameters) -> String {
  735. let clockRateK = codec.clockRate!.intValue / 1000
  736. if clockRateK >= 1 {
  737. return "\(clockRateK)k"
  738. }
  739. return codec.clockRate!.stringValue
  740. }
  741. }
  742. internal class RoundTripTime: VoIPStatsRepresentation {
  743. private let latest: Double?
  744. private var average: Double?
  745. public init(_ entry: RTCStatistics) {
  746. if let latestNumber = entry.values["currentRoundTripTime"] as? NSNumber {
  747. self.latest = Double(truncating: latestNumber)
  748. } else {
  749. self.latest = nil
  750. }
  751. average = nil
  752. let totalRoundTripTime = entry.values["totalRoundTripTime"] as? NSNumber
  753. let responsesReceived = entry.values["responsesReceived"] as? NSNumber
  754. if totalRoundTripTime != nil, responsesReceived != nil {
  755. if UInt64(truncating: responsesReceived!).signum() == 1 {
  756. let t = Decimal(Double(truncating: totalRoundTripTime!))
  757. let r = Decimal(UInt64(truncating: responsesReceived!))
  758. let av = NSDecimalNumber(decimal: t / r)
  759. average = Double(truncating: av)
  760. }
  761. }
  762. }
  763. public func getShortRepresentation() -> String {
  764. var result = "rtt-latest="
  765. if latest != nil {
  766. result += "\(String(format: "%.3f", latest!))"
  767. } else {
  768. result += "n/a"
  769. }
  770. result += " rtt-avg="
  771. if average != nil {
  772. result += "\(String(format: "%.3f", average!))"
  773. } else {
  774. result += "n/a"
  775. }
  776. return result
  777. }
  778. public func getRepresentation() -> String {
  779. return getShortRepresentation()
  780. }
  781. }
  782. internal class Transport: VoIPStatsRepresentation {
  783. public let selectedCandidatePairId: String
  784. private let bytesTransferred: BytesTransferred
  785. private let dtlsState: String
  786. public init(_ entry: RTCStatistics) {
  787. if let candidatePairIdString = entry.values["selectedCandidatePairId"] as? String {
  788. self.selectedCandidatePairId = candidatePairId(statsId: candidatePairIdString)
  789. } else {
  790. self.selectedCandidatePairId = "???"
  791. }
  792. self.bytesTransferred = BytesTransferred(entry)
  793. self.dtlsState = entry.values["dtlsState"] as? String ?? "n/a"
  794. }
  795. public func getShortRepresentation() -> String {
  796. return "dtls=\(dtlsState) \(bytesTransferred.getShortRepresentation())"
  797. }
  798. public func getRepresentation() -> String {
  799. return "dtls-state=\(dtlsState), selected-candidate-pair-id=\(self.selectedCandidatePairId), \(bytesTransferred.getRepresentation())"
  800. }
  801. }
  802. internal class Crypto: VoIPStatsRepresentation {
  803. private let dtlsVersion: String
  804. private let dtlsCipher: String
  805. private let srtpCipher: String
  806. public init(_ entry: RTCStatistics) {
  807. self.dtlsVersion = VoIPStats.Crypto.dtlsVersionString(dtlsVersionBytes: entry.values["tlsVersion"] as? String)
  808. self.dtlsCipher = entry.values["dtlsCipher"] as? String ?? "?"
  809. self.srtpCipher = entry.values["srtpCipher"] as? String ?? "?"
  810. }
  811. private static func dtlsVersionString(dtlsVersionBytes: String?) -> String {
  812. if (dtlsVersionBytes == nil) {
  813. return "?";
  814. }
  815. switch dtlsVersionBytes {
  816. case "FEFF":
  817. return "1.0";
  818. case "FEFD":
  819. return "1.2";
  820. default:
  821. return "?";
  822. }
  823. }
  824. public func getShortRepresentation() -> String {
  825. return "dtls=v\(dtlsVersion):\(dtlsCipher) srtp=\(srtpCipher)"
  826. }
  827. public func getRepresentation() -> String {
  828. return "dtls-version=\(dtlsVersion), dtls-cipher=\(dtlsCipher), srtp-cipher=\(srtpCipher)"
  829. }
  830. }
  831. public init(report: RTCStatisticsReport, options: VoIPStatsOptions, transceivers:[RTCRtpTransceiver], previousState: VoIPStatsState?) {
  832. self.report = report
  833. self.options = options
  834. self.transceivers = transceivers
  835. self.previousState = previousState
  836. self.timestamp = report.timestamp_us
  837. super.init()
  838. self.extract()
  839. }
  840. // O(n^2) but could be optimised for O(n) if needed
  841. internal func extract() {
  842. self.inboundCodecs = [String: Codec]()
  843. self.outboundCodecs = [String: Codec]()
  844. if self.options.candidatePairsFlag != CandidatePairVariant.NONE {
  845. self.candidatePairs = [CandidatePair]()
  846. }
  847. // Extract values
  848. for entry in report.statistics.values {
  849. switch entry.type {
  850. case "codec":
  851. let codec = Codec(entry)
  852. if codec.direction == .inbound {
  853. self.inboundCodecs![codec.codecId] = codec
  854. } else {
  855. self.outboundCodecs![codec.codecId] = codec
  856. }
  857. break
  858. case "candidate-pair":
  859. if options.candidatePairsFlag != .NONE {
  860. self.candidatePairs!.append(CandidatePair(entry, report:report))
  861. }
  862. break
  863. case "inbound-rtp":
  864. if options.inboundRtp {
  865. let kind = entry.values["kind"] as? String
  866. if kind == "audio" {
  867. self.inboundRtpAudio = InboundRtp(entry, codecs: inboundCodecs!, previousState: previousState, timestamp: timestamp)
  868. }
  869. else if kind == "video" {
  870. self.inboundRtpVideo = InboundRtp(entry, codecs: inboundCodecs!, previousState: previousState, timestamp: timestamp)
  871. }
  872. }
  873. break
  874. case "outbound-rtp":
  875. if options.outboundRtp {
  876. let kind = entry.values["kind"] as? String
  877. if kind == "audio" {
  878. self.outboundRtpAudio = OutboundRtp(entry, codecs: outboundCodecs!, previousState: previousState, timestamp: timestamp)
  879. }
  880. else if kind == "video" {
  881. self.outboundRtpVideo = OutboundRtp(entry, codecs: outboundCodecs!, previousState: previousState, timestamp: timestamp)
  882. }
  883. }
  884. break
  885. case "track":
  886. if options.tracks {
  887. let kind = entry.values["kind"] as? String
  888. let inbound = entry.values["remoteSource"] as? NSNumber
  889. if kind == "video" && inbound != nil {
  890. if inbound!.boolValue {
  891. self.inboundTrackVideo = Track(entry)
  892. } else {
  893. self.outboundTrackVideo = Track(entry)
  894. }
  895. }
  896. }
  897. if options.framesReceived {
  898. let kind = entry.values["kind"] as? String
  899. let inbound = entry.values["remoteSource"] as? NSNumber
  900. if kind == "video" && inbound != nil {
  901. if inbound!.boolValue {
  902. let track = Track(entry)
  903. if track.totalFramesReceived != nil && track.areFramesReceived() {
  904. if totalFramesReceived == nil {
  905. totalFramesReceived = track.totalFramesReceived
  906. } else {
  907. totalFramesReceived! += track.totalFramesReceived!
  908. }
  909. }
  910. }
  911. }
  912. }
  913. break
  914. case "transport":
  915. if options.transport {
  916. self.transport = Transport(entry)
  917. }
  918. if options.crypto {
  919. self.crypto = Crypto(entry)
  920. }
  921. if !options.selectedCandidatePair {
  922. break
  923. }
  924. if let candidatePairIdString = entry.values["selectedCandidatePairId"] as? String {
  925. if let candidatePair = CandidatePair.fromId(candidatePairIdString, report: report) {
  926. self.selectedCandidatePair = candidatePair
  927. }
  928. }
  929. break
  930. default:
  931. break // Ignore
  932. }
  933. }
  934. // Sort candidate pairs by priority
  935. if let candidatePairs = self.candidatePairs {
  936. self.candidatePairs = candidatePairs.sorted(by: { $0.priority > $1.priority })
  937. }
  938. // Add transceivers (if any)
  939. rtpTransceivers = [RtpTransceiver]()
  940. for transceiver in transceivers {
  941. rtpTransceivers?.append(RtpTransceiver(transceiver))
  942. }
  943. }
  944. @objc public func getShortRepresentation() -> String {
  945. var result = ""
  946. if let transport = self.transport, options.transport {
  947. result += "\(transport.getShortRepresentation())\n"
  948. }
  949. if let crypto = self.crypto, options.crypto {
  950. result += "\(crypto.getShortRepresentation())\n"
  951. }
  952. if let candidatePairs = self.candidatePairs, self.options.candidatePairsFlag.rawValue & CandidatePairVariant.OVERVIEW.rawValue != 0 {
  953. result += "pairs(\(candidatePairs.count))=\(candidatePairs.map({ $0.getStatusChar() }).joined())\n"
  954. }
  955. if let selectedCandidatePair = self.selectedCandidatePair {
  956. result += "\(selectedCandidatePair.getShortRepresentation())\n"
  957. }
  958. result += "\n"
  959. if let inboundRtpAudio = self.inboundRtpAudio {
  960. result += "in: \(inboundRtpAudio.getShortRepresentation())\n"
  961. }
  962. if let inboundRtpVideo = self.inboundRtpVideo {
  963. result += "in: \(inboundRtpVideo.getShortRepresentation())\n"
  964. }
  965. if let outboundRtpAudio = self.outboundRtpAudio {
  966. result += "out: \(outboundRtpAudio.getShortRepresentation())\n"
  967. }
  968. if let outboundRtpVideo = self.outboundRtpVideo {
  969. result += "out: \(outboundRtpVideo.getShortRepresentation())\n"
  970. }
  971. result += "\n"
  972. if let inboundTrackVideo = self.inboundTrackVideo {
  973. result += "in/track- \(inboundTrackVideo.getShortRepresentation())\n"
  974. }
  975. if let outboundTrackVideo = self.outboundTrackVideo {
  976. result += "out/track- \(outboundTrackVideo.getShortRepresentation())\n"
  977. }
  978. if options.codecs {
  979. if let inboundCodecs = self.inboundCodecs {
  980. result += "in/codecs "
  981. for codec in inboundCodecs.values {
  982. result += codec.getShortRepresentation()
  983. result += " "
  984. }
  985. result += "\n"
  986. }
  987. if let outboundCodecs = self.outboundCodecs {
  988. result += "out/codecs "
  989. for codec in outboundCodecs.values {
  990. result += codec.getShortRepresentation()
  991. result += " "
  992. }
  993. result += "\n"
  994. }
  995. }
  996. if let rtpTransceivers = self.rtpTransceivers {
  997. result += "\n"
  998. for transceiver in rtpTransceivers {
  999. result += "transceiver \(transceiver.getShortRepresentation())\n"
  1000. }
  1001. result += "\n"
  1002. }
  1003. if let candidatePairs = self.candidatePairs, self.options.candidatePairsFlag.rawValue & CandidatePairVariant.DETAILED.rawValue != 0 {
  1004. result += "\(candidatePairs.map({ $0.getShortRepresentation() }).joined(separator: "\n"))\n"
  1005. }
  1006. // Strip newline (if any)
  1007. result.removeLast(1)
  1008. return result
  1009. }
  1010. @objc public func getRepresentation() -> String {
  1011. var result = ""
  1012. if let transport = self.transport {
  1013. result += "Transport: \(transport.getRepresentation())\n"
  1014. }
  1015. if let crypto = self.crypto {
  1016. result += "Crypto: \(crypto.getRepresentation())\n"
  1017. }
  1018. if let candidatePairs = self.candidatePairs, self.options.candidatePairsFlag.rawValue & CandidatePairVariant.OVERVIEW.rawValue != 0 {
  1019. result += "Candidate Pairs Overview (\(candidatePairs.count)): \(candidatePairs.map({ $0.getStatusChar() }).joined())\n"
  1020. }
  1021. if let selectedCandidatePair = self.selectedCandidatePair {
  1022. result += "Selected Candidate Pair: \(selectedCandidatePair.getRepresentation())\n"
  1023. }
  1024. if let inboundRtpAudio = self.inboundRtpAudio {
  1025. result += "Inbound RTP Audio: \(inboundRtpAudio.getRepresentation())\n"
  1026. }
  1027. if let inboundRtpVideo = self.inboundRtpVideo {
  1028. result += "Inbound RTP Video: \(inboundRtpVideo.getRepresentation())\n"
  1029. }
  1030. if let outboundRtpAudio = self.outboundRtpAudio {
  1031. result += "Outbound RTP Audio: \(outboundRtpAudio.getRepresentation())\n"
  1032. }
  1033. if let outboundRtpVideo = self.outboundRtpVideo {
  1034. result += "Outbound RTP Video: \(outboundRtpVideo.getRepresentation())\n"
  1035. }
  1036. if let inboundTrackVideo = self.inboundTrackVideo {
  1037. result += "Inbound Track Video: \(inboundTrackVideo.getRepresentation())\n"
  1038. }
  1039. if let outboundTrackVideo = self.outboundTrackVideo {
  1040. result += "Outbound Track Video: \(outboundTrackVideo.getRepresentation())\n"
  1041. }
  1042. if let inboundCodecs = self.inboundCodecs {
  1043. result += "Inbound Codecs (\(inboundCodecs.count))\n"
  1044. for codec in inboundCodecs.values {
  1045. result += "- \(codec.getShortRepresentation())\n"
  1046. }
  1047. }
  1048. if let outboundCodecs = self.outboundCodecs {
  1049. result += "Outbound Codecs (\(outboundCodecs.count))\n"
  1050. for codec in outboundCodecs.values {
  1051. result += "- \(codec.getShortRepresentation())\n"
  1052. }
  1053. }
  1054. if let rtpTransceivers = self.rtpTransceivers {
  1055. result += "Transceivers (\(rtpTransceivers.count))\n"
  1056. for transceiver in rtpTransceivers {
  1057. result += "- \(transceiver.getRepresentation())"
  1058. }
  1059. result += "\n"
  1060. }
  1061. if let candidatePairs = self.candidatePairs, self.options.candidatePairsFlag.rawValue & CandidatePairVariant.DETAILED.rawValue != 0 {
  1062. result += "Candidate Pairs (\(candidatePairs.count))\n"
  1063. result += candidatePairs.map({ "- \($0.getRepresentation())\n" }).joined()
  1064. }
  1065. // Strip newline (if any)
  1066. result.removeLast(1)
  1067. return result
  1068. }
  1069. @objc public func isReceivingVideo() -> Bool {
  1070. // check previous and actual frames and compare
  1071. if totalFramesReceived != nil, previousState != nil, previousState?.videoFramesReceived != nil {
  1072. let diff = totalFramesReceived!.distance(to: previousState!.videoFramesReceived!)
  1073. if diff != 0 {
  1074. return true
  1075. }
  1076. }
  1077. return false
  1078. }
  1079. public func usesRelay() -> Bool {
  1080. if selectedCandidatePair != nil , selectedCandidatePair!.usesRelay {
  1081. return true
  1082. }
  1083. return false
  1084. }
  1085. public func buildVoIPStatsState() -> VoIPStatsState {
  1086. return VoIPStatsState(timestampUs: timestamp, videoBytesSent: outboundRtpVideo?.bytesTotal ?? nil, videoBytesReceived: inboundRtpVideo?.bytesTotal ?? nil, videoFramesReceived: totalFramesReceived ?? nil)
  1087. }
  1088. }
  1089. public struct VoIPStatsState {
  1090. public let timestampUs: Double
  1091. public let videoBytesSent: UInt64?
  1092. public let videoBytesReceived: UInt64?
  1093. public let videoFramesReceived: UInt64?
  1094. }
  1095. // Convert byte count into human readable number
  1096. // Based on: https://stackoverflow.com/a/3758880
  1097. internal func toHumanReadableByteCount(_ value: UInt64) -> String {
  1098. let unit:UInt64 = 1024
  1099. if value < unit {
  1100. return "\(value)B"
  1101. } else {
  1102. let value = Double(value)
  1103. let unit = Double(unit)
  1104. let exp = (log(value) / log(unit)).binade
  1105. return "\(String(format: "%.1f", value / pow(unit, exp)))\("KMGTPE"[Int(exp - 1)])iB"
  1106. }
  1107. }
  1108. // Convert ms to seconds
  1109. internal func msToSeconds(_ value: String?) -> String {
  1110. guard let value = Double(value) else {
  1111. return "n/a"
  1112. }
  1113. return String(format: "%.3f", value / 1000)
  1114. }
  1115. internal func msToSeconds(_ value: Double?) -> String {
  1116. guard value != nil else {
  1117. return "n/a"
  1118. }
  1119. return String(format: "%.3f", value! / 1000)
  1120. }
  1121. // Calculate a fraction
  1122. internal func toFraction(_ dividend: String?, divisor: String?) -> String {
  1123. guard let dividend = Double(dividend), let divisor = Double(divisor), divisor > 0 else {
  1124. return "n/a"
  1125. }
  1126. return String(format: "%.1f", dividend / divisor)
  1127. }
  1128. internal func candidatePairId(statsId: String?) -> String {
  1129. if statsId == nil {
  1130. return "???"
  1131. }
  1132. let substring = String(statsId!.dropFirst(20))
  1133. if substring.count > 0 {
  1134. return substring
  1135. }
  1136. return "???"
  1137. }
  1138. internal func calculateVideoBitrate(previousTimestamp: Double, previousBytes: UInt64, currentTimestamp: Double, currentBytes: UInt64) -> Double? {
  1139. let bytesSent = currentBytes.distance(to: previousBytes)
  1140. let microSecondsElapsed = currentTimestamp - previousTimestamp
  1141. if microSecondsElapsed < 0 {
  1142. debugPrint("Previous state must not have a higher timestamp than current state")
  1143. return nil
  1144. }
  1145. if microSecondsElapsed < 100000 {
  1146. debugPrint("State timestamps should be at least 100ms apart")
  1147. return nil
  1148. }
  1149. return Double((8 * Double(bytesSent)) / (microSecondsElapsed / 1000 / 1000))
  1150. }