PeerConnectionStats.js

  1. import EventEmitter from 'events'
  2. import Logger from './Logger'
  3. const logger = Logger.get('PeerConnectionStats')
  4. /**
  5. * @typedef {Object} ConnectionStats
  6. * @property {RTCStatsReport} raw - All RTCPeerConnection stats without parsing. Reference {@link https://developer.mozilla.org/en-US/docs/Web/API/RTCStatsReport}.
  7. * @property {TrackReport} audio - Parsed audio information.
  8. * @property {TrackReport} video - Parsed video information.
  9. * @property {Number} availableOutgoingBitrate - The available outbound capacity of the network connection. The higher the value, the more bandwidth you can assume is available for outgoing data. The value is reported in bits per second.
  10. *
  11. * This value comes from the nominated candidate-pair.
  12. * @property {Number} totalRoundTripTime - Total round trip time is the total time in seconds that has elapsed between sending STUN requests and receiving the responses.
  13. *
  14. * This value comes from the nominated candidate-pair.
  15. * @property {Number} currentRoundTripTime - Current round trip time indicate the number of seconds it takes for data to be sent by this peer to the remote peer and back over the connection described by this pair of ICE candidates.
  16. *
  17. * This value comes from the nominated candidate-pair.
  18. * @property {RTCIceCandidateType} candidateType - Local candidate type from the nominated candidate-pair which indicates the type of ICE candidate the object represents.
  19. */
  20. /**
  21. * @typedef {Object} TrackReport
  22. * @property {Array<InboundStats>} inbounds - Parsed information of each inbound-rtp.
  23. * @property {Array<OutboundStats>} outbounds - Parsed information of each outbound-rtp.
  24. */
  25. /**
  26. * @typedef {Object} InboundStats
  27. * @property {String} id - inbound-rtp Id.
  28. * @property {Number} jitter - Current Jitter measured in seconds.
  29. * @property {String} [mimeType] - Mime type if related report had codec report associated.
  30. * @property {Number} [framesPerSecond] - Current framerate if it's video report.
  31. * @property {Number} [frameHeight] - Current frame height if it's video report.
  32. * @property {Number} [frameWidth] - Current frame width if it's video report.
  33. * @property {Number} timestamp - Timestamp of report.
  34. * @property {Number} totalBytesReceived - Total bytes received is an integer value which indicates the total number of bytes received so far from this synchronization source.
  35. * @property {Number} totalPacketsReceived - Total packets received indicates the total number of packets of any kind that have been received on the connection described by the pair of candidates.
  36. * @property {Number} totalPacketsLost - Total packets lost.
  37. * @property {Number} packetsLostRatioPerSecond - Total packet lost ratio per second.
  38. * @property {Number} packetsLostDeltaPerSecond - Total packet lost delta per second.
  39. * @property {Number} bitrate - Current bitrate in bits per second.
  40. */
  41. /**
  42. * @typedef {Object} OutboundStats
  43. * @property {String} id - outbound-rtp Id.
  44. * @property {String} [mimeType] - Mime type if related report had codec report associated.
  45. * @property {Number} [framesPerSecond] - Current framerate if it's video report.
  46. * @property {Number} [frameHeight] - Current frame height if it's video report.
  47. * @property {Number} [frameWidth] - Current frame width if it's video report.
  48. * @property {String} [qualityLimitationReason] - If it's video report, indicate the reason why the media quality in the stream is currently being reduced by the codec during encoding, or none if no quality reduction is being performed.
  49. * @property {Number} timestamp - Timestamp of report.
  50. * @property {Number} totalBytesSent - Total bytes sent indicates the total number of payload bytes that hve been sent so far on the connection described by the candidate pair.
  51. * @property {Number} bitrate - Current bitrate in bits per second.
  52. */
  53. export const peerConnectionStatsEvents = {
  54. stats: 'stats'
  55. }
  56. export default class PeerConnectionStats extends EventEmitter {
  57. constructor (peer) {
  58. super()
  59. this.peer = peer
  60. this.stats = null
  61. this.emitInterval = null
  62. this.previousStats = null
  63. }
  64. /**
  65. * Initialize the statistics monitoring of the RTCPeerConnection.
  66. */
  67. init () {
  68. logger.info('Initializing peer connection stats')
  69. this.emitInterval = setInterval(async () => {
  70. const stats = await this.peer.getStats()
  71. this.parseStats(stats)
  72. /**
  73. * Peer connection incoming stats.
  74. *
  75. * @event PeerConnection#stats
  76. * @type {ConnectionStats}
  77. */
  78. this.emit(peerConnectionStatsEvents.stats, this.stats)
  79. }, 1000)
  80. }
  81. /**
  82. * Parse incoming RTCPeerConnection stats.
  83. * @param {RTCStatsReport} rawStats - RTCPeerConnection stats.
  84. * @returns {ConnectionStats} RTCPeerConnection stats parsed.
  85. */
  86. parseStats (rawStats) {
  87. this.previousStats = this.stats
  88. const statsObject = {
  89. audio: {
  90. inbounds: [],
  91. outbounds: []
  92. },
  93. video: {
  94. inbounds: [],
  95. outbounds: []
  96. },
  97. raw: rawStats
  98. }
  99. for (const report of rawStats.values()) {
  100. switch (report.type) {
  101. case 'outbound-rtp': {
  102. addOutboundRtpReport(report, this.previousStats, statsObject)
  103. break
  104. }
  105. case 'inbound-rtp': {
  106. addInboundRtpReport(report, this.previousStats, statsObject)
  107. break
  108. }
  109. case 'candidate-pair': {
  110. if (report.nominated) {
  111. addCandidateReport(report, statsObject)
  112. }
  113. break
  114. }
  115. default:
  116. break
  117. }
  118. }
  119. this.stats = statsObject
  120. }
  121. /**
  122. * Stops the monitoring of RTCPeerConnection statistics.
  123. */
  124. stop () {
  125. logger.info('Stopping peer connection stats')
  126. clearInterval(this.emitInterval)
  127. }
  128. }
  129. /**
  130. * Parse and add incoming outbound-rtp report from RTCPeerConnection to final report.
  131. * @param {Object} report - JSON object which represents a report from RTCPeerConnection stats.
  132. * @param {ConnectionStats} previousStats - Previous stats parsed.
  133. * @param {Object} statsObject - Current stats object being parsed.
  134. */
  135. const addOutboundRtpReport = (report, previousStats, statsObject) => {
  136. const mediaType = getMediaType(report)
  137. const codecInfo = getCodecData(report.codecId, statsObject.raw)
  138. const additionalData = getBaseRtpReportData(report, mediaType)
  139. additionalData.totalBytesSent = report.bytesSent
  140. additionalData.id = report.id
  141. additionalData.mid = report.mid
  142. const previousBytesSent = previousStats ? previousStats[mediaType].outbounds.find(x => x.id === additionalData.id)?.totalBytesSent ?? 0 : null
  143. additionalData.bitrate = previousBytesSent ? 8 * (report.bytesSent - previousBytesSent) : 0
  144. if (mediaType === 'video') {
  145. additionalData.qualityLimitationReason = report.qualityLimitationReason
  146. }
  147. statsObject[mediaType].outbounds.push({
  148. ...codecInfo,
  149. ...additionalData
  150. })
  151. }
  152. /**
  153. * Parse and add incoming inbound-rtp report from RTCPeerConnection to final report.
  154. * @param {Object} report - JSON object which represents a report from RTCPeerConnection stats.
  155. * @param {ConnectionStats} previousStats - Previous stats parsed.
  156. * @param {Object} statsObject - Current stats object being parsed.
  157. */
  158. const addInboundRtpReport = (report, previousStats, statsObject) => {
  159. let mediaType = getMediaType(report)
  160. const codecInfo = getCodecData(report.codecId, statsObject.raw)
  161. // Safari is missing mediaType and kind for 'inbound-rtp'
  162. if (!['audio', 'video'].includes(mediaType)) {
  163. if (report.id.includes('Video')) mediaType = 'video'
  164. else mediaType = 'audio'
  165. }
  166. const additionalData = getBaseRtpReportData(report, mediaType)
  167. additionalData.totalBytesReceived = report.bytesReceived
  168. additionalData.totalPacketsReceived = report.packetsReceived
  169. additionalData.totalPacketsLost = report.packetsLost
  170. additionalData.jitter = report.jitter
  171. additionalData.id = report.id
  172. additionalData.mid = report.mid
  173. additionalData.trackIdentifier = report.trackIdentifier
  174. additionalData.bitrate = 0
  175. additionalData.packetsLostRatioPerSecond = 0
  176. additionalData.packetsLostDeltaPerSecond = 0
  177. if (previousStats) {
  178. const previousReport = previousStats[mediaType].inbounds.find(x => x.id === additionalData.id)
  179. if (previousReport) {
  180. const previousBytesReceived = previousReport.totalBytesReceived
  181. additionalData.bitrate = 8 * (report.bytesReceived - previousBytesReceived)
  182. additionalData.packetsLostRatioPerSecond = calculatePacketsLostRatio(additionalData, previousReport)
  183. additionalData.packetsLostDeltaPerSecond = calculatePacketsLostDelta(additionalData, previousReport)
  184. }
  185. }
  186. statsObject[mediaType].inbounds.push({
  187. ...codecInfo,
  188. ...additionalData
  189. })
  190. }
  191. /**
  192. * Parse and add incoming candidate-pair report from RTCPeerConnection to final report.
  193. * Also adds associated local-candidate data to report.
  194. * @param {Object} report - JSON object which represents a report from RTCPeerConnection stats.
  195. * @param {Object} statsObject - Current stats object being parsed.
  196. */
  197. const addCandidateReport = (report, statsObject) => {
  198. statsObject.totalRoundTripTime = report.totalRoundTripTime
  199. statsObject.currentRoundTripTime = report.currentRoundTripTime
  200. statsObject.availableOutgoingBitrate = report.availableOutgoingBitrate
  201. statsObject.candidateType = statsObject.raw.get(report.localCandidateId).candidateType
  202. }
  203. /**
  204. * Get media type.
  205. * @param {Object} report - JSON object which represents a report from RTCPeerConnection stats.
  206. * @returns {String} Media type.
  207. */
  208. const getMediaType = (report) => {
  209. return report.mediaType || report.kind
  210. }
  211. /**
  212. * Get codec information from stats.
  213. * @param {String} codecReportId - Codec report ID.
  214. * @param {RTCStatsReport} rawStats - RTCPeerConnection stats.
  215. * @returns {Object} Object containing codec information.
  216. */
  217. const getCodecData = (codecReportId, rawStats) => {
  218. const { mimeType } = codecReportId ? rawStats.get(codecReportId) ?? {} : {}
  219. return { mimeType }
  220. }
  221. /**
  222. * Get common information for RTP reports.
  223. * @param {Object} report - JSON object which represents a report from RTCPeerConnection stats.
  224. * @param {String} mediaType - Media type.
  225. * @returns {Object} Object containing common information.
  226. */
  227. const getBaseRtpReportData = (report, mediaType) => {
  228. const additionalData = {}
  229. if (mediaType === 'video') {
  230. additionalData.framesPerSecond = report.framesPerSecond
  231. additionalData.frameHeight = report.frameHeight
  232. additionalData.frameWidth = report.frameWidth
  233. }
  234. additionalData.timestamp = report.timestamp
  235. return additionalData
  236. }
  237. /**
  238. * Calculate the ratio packets lost
  239. * @param {Object} actualReport - JSON object which represents a parsed report.
  240. * @param {Object} previousReport - JSON object which represents a parsed report.
  241. * @returns {Number} Packets lost ratio
  242. */
  243. const calculatePacketsLostRatio = (actualReport, previousReport) => {
  244. const currentLostPackages = calculatePacketsLostDelta(actualReport, previousReport)
  245. const currentReceivedPackages = actualReport.totalPacketsReceived - previousReport.totalPacketsReceived
  246. return currentLostPackages / currentReceivedPackages
  247. }
  248. /**
  249. * Calculate the delta packets lost
  250. * @param {Object} actualReport - JSON object which represents a parsed report.
  251. * @param {Object} previousReport - JSON object which represents a parsed report.
  252. * @returns {Number} Packets lost ratio
  253. */
  254. const calculatePacketsLostDelta = (actualReport, previousReport) => {
  255. return actualReport.totalPacketsLost - previousReport.totalPacketsLost
  256. }
  257. JAVASCRIPT
    Copied!