├── .eslintignore ├── .eslintrc.js ├── LICENSE ├── README.md ├── __mocks__ └── detect-browser.js ├── __tests__ ├── _double │ ├── chrome-77.0.3842.0.json │ ├── firefox-67.0.4.json │ └── safari-12.1.1.json ├── rtcstats-insight.test.js ├── rtcstats-moment.test.js ├── standardize-support.test.js └── standardizers │ ├── base.test.js │ ├── chrome.test.js │ ├── firefox.test.js │ └── safari.test.js ├── dist └── rtcstats-wrapper.js ├── docs ├── BaseRTCStatsReport.html ├── ChromeRTCStatsReport.html ├── FirefoxRTCStatsReport.html ├── RTCStatsInsight.html ├── RTCStatsMoment.html ├── SafariRTCStatsReport.html ├── fonts │ ├── OpenSans-Bold-webfont.eot │ ├── OpenSans-Bold-webfont.svg │ ├── OpenSans-Bold-webfont.woff │ ├── OpenSans-BoldItalic-webfont.eot │ ├── OpenSans-BoldItalic-webfont.svg │ ├── OpenSans-BoldItalic-webfont.woff │ ├── OpenSans-Italic-webfont.eot │ ├── OpenSans-Italic-webfont.svg │ ├── OpenSans-Italic-webfont.woff │ ├── OpenSans-Light-webfont.eot │ ├── OpenSans-Light-webfont.svg │ ├── OpenSans-Light-webfont.woff │ ├── OpenSans-LightItalic-webfont.eot │ ├── OpenSans-LightItalic-webfont.svg │ ├── OpenSans-LightItalic-webfont.woff │ ├── OpenSans-Regular-webfont.eot │ ├── OpenSans-Regular-webfont.svg │ ├── OpenSans-Regular-webfont.woff │ ├── OpenSans-Semibold-webfont.eot │ ├── OpenSans-Semibold-webfont.svg │ ├── OpenSans-Semibold-webfont.ttf │ ├── OpenSans-Semibold-webfont.woff │ ├── OpenSans-SemiboldItalic-webfont.eot │ ├── OpenSans-SemiboldItalic-webfont.svg │ ├── OpenSans-SemiboldItalic-webfont.ttf │ └── OpenSans-SemiboldItalic-webfont.woff ├── global.html ├── index.html ├── scripts │ ├── linenumber.js │ └── prettify │ │ ├── Apache-License-2.0.txt │ │ ├── lang-css.js │ │ └── prettify.js └── styles │ ├── jsdoc-default.css │ ├── prettify-jsdoc.css │ └── prettify-tomorrow.css ├── examples └── insight │ ├── README.md │ ├── index.html │ ├── key.js │ ├── script.js │ └── style.css ├── jest.config.js ├── jsdoc.json ├── package-lock.json ├── package.json ├── rollup.config.js └── src ├── main.js ├── rtcstats-insight.js ├── rtcstats-moment.js ├── shared └── constatnts.js ├── standardize-support.js └── standardizers ├── base.js ├── chrome.js ├── firefox.js └── safari.js /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | sourceType: "module", 4 | "ecmaVersion": 2018 5 | }, 6 | env: { 7 | es6: true, 8 | browser: true 9 | }, 10 | plugins: ["prettier", "jest"], 11 | extends: [ 12 | "eslint:recommended", 13 | "plugin:prettier/recommended", 14 | "plugin:jest/recommended", 15 | ], 16 | rules: { 17 | "no-console": process.env.NODE_ENV !== "production" ? "off" : "error", 18 | "no-debugger": process.env.NODE_ENV !== "production" ? "off" : "error", 19 | "no-useless-escape": "off", 20 | "no-empty": "off", 21 | "no-var": "error", 22 | "no-lonely-if": "error", 23 | "prefer-const": "error", 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 SkyWay Lab 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rtcstats-wrapper 2 | A wrapper of RTCStats for standardization and calculation of momentary values. 3 | 4 | #### features 5 | 6 | - Standardize a result of [getStats()](https://w3c.github.io/webrtc-stats/) which is different between each browsers. 7 | - Calculate momentary status (such as `jitterBufferDelay` at one moment) from a result of getStats(). 8 | 9 | ## Getting Started 10 | ### Installation 11 | ```bash 12 | npm install https://github.com/skyway-lab/rtcstats-wrapper.git 13 | ``` 14 | 15 | ### Usage 16 | #### RTCStats standardizers 17 | Since the implementation of getStats varies between each browsers, you need a shim to standardize them when you use RTCStats in your cross-browser application. 18 | `rtcstats-wrapper` gives it and supports Google Chrome, Firefox and Safari. 19 | 20 | see [reference](https://skyway-lab.github.io/rtcstats-wrapper/global.html#standardizeReport) 21 | 22 | ```javascript 23 | import { standardizeReport, RTCStatsReferences } from 'rtcstats-wrapper'; 24 | 25 | const pc = new RTCPeerConnection(); 26 | // ... 27 | 28 | const report = standardizeReport(await pc.getStats()); 29 | const receiverStats = report.get(RTCStatsReferences.RTCVideoReceivers.key); 30 | const framesDecoded = receiverStats[0].framesDecoded; 31 | // ... 32 | ``` 33 | 34 | #### RTCStatsMoment 35 | Since RTCStats shows statical information, most of its attributes mean total number of each metrics. 36 | Therefore, you need a little calculation to get how network or media pipeline perform at that moment, so we provide the API to get the momentary metrics based on the RTCStats. 37 | After established the connection with `RTCPeerConnection`, then like 38 | 39 | see [reference](https://skyway-lab.github.io/rtcstats-wrapper/RTCStatsMoment.html) 40 | 41 | ```javascript 42 | import { RTCStatsMoment } from 'rtcstats-wrapper'; 43 | 44 | const pc = new RTCPeerConnection(); 45 | const moment = new RTCStatsMoment(); 46 | // establish a conneciton ... 47 | 48 | const report = await pc.getStats(); 49 | moment.update(report); 50 | moment.report(); 51 | //=> { 52 | // "send": { 53 | // "video": { ... }, 54 | // "audio": { ... }, 55 | // }, 56 | // "receive": { 57 | // "video": { ... }, 58 | // "audio": { ... }, 59 | // }, 60 | // "candidatePair": { ... } 61 | //} 62 | ``` 63 | 64 | #### RTCStatsInsight 65 | As one simple use case of RTCStatsMoment I know, is to calculate an momentary value periodically and take some action when a certain threshold is exceeded. 66 | For example, collect occurrences of events to analyze user's network environment information, or give some feedback to the UI to reduce stress caused by call quality felt by users. 67 | RTCStatsInsight is a simple way to do this, and provides an EventEmitter interface as follows: 68 | 69 | see [reference](https://skyway-lab.github.io/rtcstats-wrapper/RTCStatsInsight.html) 70 | 71 | ```javascript 72 | import { 73 | StatusLevels, 74 | RTCStatsInsightEvents, 75 | RTCStatsInsight 76 | } from 'rtcstats-wrapper'; 77 | 78 | const options = { 79 | interval: 3000, 80 | thresholds: { 81 | "audio-rtt": { 82 | unstable: 0.1 83 | }, 84 | "audio-fractionLost": { 85 | unstable: 0.03, 86 | critical: 0.08, 87 | }, 88 | }, 89 | triggerCondition: { 90 | failCount: 2, 91 | within: 3 92 | } 93 | } 94 | 95 | const insight = new RTCStatsInsight(sender, options); 96 | 97 | insight.on(RTCStatsInsightEvents["audio-rtt"].key, event => { 98 | if (event.level === StatusLevels.stable.key) { 99 | console.log("Now back to stable!"); 100 | } 101 | }); 102 | 103 | insight.watch() 104 | ``` 105 | 106 | ## API Reference 107 | see [GitHub Pages](http://skyway-lab.github.io/rtcstats-wrapper/). 108 | -------------------------------------------------------------------------------- /__mocks__/detect-browser.js: -------------------------------------------------------------------------------- 1 | const detectBrowser = jest.genMockFromModule("detect-browser"); 2 | 3 | export default detectBrowser; 4 | -------------------------------------------------------------------------------- /__tests__/_double/firefox-67.0.4.json: -------------------------------------------------------------------------------- 1 | [["outbound_rtcp_audio_0",{"id":"outbound_rtcp_audio_0","timestamp":1562211423194,"type":"remote-inbound-rtp","kind":"audio","localId":"outbound_rtp_audio_0","mediaType":"audio","ssrc":2126859391,"bytesReceived":89801,"jitter":0,"packetsLost":0,"packetsReceived":557,"roundTripTime":0.008}],["outbound_rtcp_video_1",{"id":"outbound_rtcp_video_1","timestamp":1562211423194,"type":"remote-inbound-rtp","kind":"video","localId":"outbound_rtp_video_1","mediaType":"video","ssrc":2942779941,"bytesReceived":291443,"jitter":0.008,"packetsLost":0,"packetsReceived":373,"roundTripTime":0.008}],["inbound_rtp_audio_2",{"id":"inbound_rtp_audio_2","timestamp":1562211423194,"type":"inbound-rtp","kind":"audio","mediaType":"audio","nackCount":0,"remoteId":"inbound_rtcp_audio_2","ssrc":280234831,"bytesReceived":104235,"jitter":0.009,"packetsLost":0,"packetsReceived":575}],["inbound_rtp_video_3",{"id":"inbound_rtp_video_3","timestamp":1562211423194,"type":"inbound-rtp","bitrateMean":461831,"bitrateStdDev":84250.56953516694,"firCount":0,"framerateMean":34.81818181818181,"framerateStdDev":8.28031619949603,"kind":"video","mediaType":"video","nackCount":0,"pliCount":0,"remoteId":"inbound_rtcp_video_3","ssrc":2968877348,"bytesReceived":675946,"discardedPackets":0,"framesDecoded":382,"jitter":0.005,"packetsLost":0,"packetsReceived":809}],["outbound_rtp_audio_0",{"id":"outbound_rtp_audio_0","timestamp":1562211423194,"type":"outbound-rtp","kind":"audio","mediaType":"audio","nackCount":0,"remoteId":"outbound_rtcp_audio_0","ssrc":2126859391,"bytesSent":104566,"packetsSent":577}],["outbound_rtp_video_1",{"id":"outbound_rtp_video_1","timestamp":1562211423194,"type":"outbound-rtp","bitrateMean":212595.27272727274,"bitrateStdDev":98013.80723662447,"firCount":0,"framerateMean":23.727272727272727,"framerateStdDev":7.086478802492944,"kind":"video","mediaType":"video","nackCount":0,"pliCount":0,"qpSum":6487,"remoteId":"outbound_rtcp_video_1","ssrc":2942779941,"bytesSent":321085,"droppedFrames":2,"framesEncoded":258,"packetsSent":392}],["inbound_rtcp_audio_2",{"id":"inbound_rtcp_audio_2","timestamp":1562211423194,"type":"remote-outbound-rtp","kind":"audio","localId":"inbound_rtp_audio_2","mediaType":"audio","ssrc":280234831,"bytesSent":83857,"packetsSent":520}],["inbound_rtcp_video_3",{"id":"inbound_rtcp_video_3","timestamp":1562211423194,"type":"remote-outbound-rtp","kind":"video","localId":"inbound_rtp_video_3","mediaType":"video","ssrc":2968877348,"bytesSent":611629,"packetsSent":759}],["4WfX",{"id":"4WfX","timestamp":1562211423194,"type":"candidate-pair","bytesReceived":810314,"bytesSent":451288,"componentId":1,"lastPacketReceivedTimestamp":1562211423165,"lastPacketSentTimestamp":1562211423196,"localCandidateId":"yLU9","nominated":true,"priority":7962083765675491000,"readable":true,"remoteCandidateId":"YFsB","selected":true,"state":"succeeded","transportId":"transport_0","writable":true}],["yLU9",{"id":"yLU9","timestamp":1562211423194,"type":"local-candidate","address":"172.16.0.202","candidateType":"host","port":56225,"priority":2122252543,"protocol":"udp"}],["UnYu",{"id":"UnYu","timestamp":1562211423194,"type":"local-candidate","address":"192.0.2.2","candidateType":"srflx","port":14559,"priority":1686052863,"protocol":"udp"}],["Fvnd",{"id":"Fvnd","timestamp":1562211423194,"type":"local-candidate","address":"172.16.0.202","candidateType":"host","port":55676,"priority":2105524479,"protocol":"tcp"}],["YFsB",{"id":"YFsB","timestamp":1562211423194,"type":"remote-candidate","address":"172.16.0.202","candidateType":"prflx","port":58386,"priority":1853817087,"protocol":"udp"}]] 2 | -------------------------------------------------------------------------------- /__tests__/_double/safari-12.1.1.json: -------------------------------------------------------------------------------- 1 | [["RTCCertificate_B7:B2:50:EF:ED:80:03:29:4A:AB:90:68:3F:29:E7:33:B1:1E:F5:55:80:00:F5:DB:42:E3:D7:13:C8:DD:62:B2",{"id":"RTCCertificate_B7:B2:50:EF:ED:80:03:29:4A:AB:90:68:3F:29:E7:33:B1:1E:F5:55:80:00:F5:DB:42:E3:D7:13:C8:DD:62:B2","timestamp":1562212769623.0002,"type":"certificate","base64Certificate":"MIIBFzCBvaADAgECAgkAwv9kQnUlVwswCgYIKoZIzj0EAwIwETEPMA0GA1UEAwwGV2ViUlRDMB4XDTE5MDcwMzAzNTkxNFoXDTE5MDgwMzAzNTkxNFowETEPMA0GA1UEAwwGV2ViUlRDMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEGlYR1unkOsTSw6e38qt9VeJnEO7seSt31s5RtRCyBZKmSiQv3bnUDUruLIXAdqDK3nXJTe0/WTBGnEkMBUFk6zAKBggqhkjOPQQDAgNJADBGAiEAwf8mnBTQtGzyAGTmEs8VIf1zgcTKhRTyBBnwTZ8euxICIQDycODZyqIi/TC4GUvXF4bhOfYXAASDEUnE0wgD+8zo5A==","fingerprint":"B7:B2:50:EF:ED:80:03:29:4A:AB:90:68:3F:29:E7:33:B1:1E:F5:55:80:00:F5:DB:42:E3:D7:13:C8:DD:62:B2","fingerprintAlgorithm":"sha-256"}],["RTCCertificate_E5:38:4D:7F:8F:7A:20:96:38:18:DF:60:9A:7B:2C:EA:50:C2:2F:D8:62:1D:8A:AD:6D:1A:80:57:91:02:8D:AD",{"id":"RTCCertificate_E5:38:4D:7F:8F:7A:20:96:38:18:DF:60:9A:7B:2C:EA:50:C2:2F:D8:62:1D:8A:AD:6D:1A:80:57:91:02:8D:AD","timestamp":1562212769623.0002,"type":"certificate","base64Certificate":"MIIBFTCBvaADAgECAgkAtoagK+QXgBwwCgYIKoZIzj0EAwIwETEPMA0GA1UEAwwGV2ViUlRDMB4XDTE5MDcwMzAzNTkxNFoXDTE5MDgwMzAzNTkxNFowETEPMA0GA1UEAwwGV2ViUlRDMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEe/gZh/cZw/U0zFK787uZxKOGUkyRuuYW0kFrYkYQr9mWLgEApbGX/g/6uOAqs7R0UHKPVndERT606gQxvvFs2DAKBggqhkjOPQQDAgNHADBEAiB2G8feVdnJiIsLJP2bklcshJuzAJwJV+SHZFh3+kP20gIgUGAjlwVltfXHWtcWPr/qfYqTWUPDz+H9mdCJmRetyRE=","fingerprint":"E5:38:4D:7F:8F:7A:20:96:38:18:DF:60:9A:7B:2C:EA:50:C2:2F:D8:62:1D:8A:AD:6D:1A:80:57:91:02:8D:AD","fingerprintAlgorithm":"sha-256"}],["RTCCodec_0_Inbound_100",{"id":"RTCCodec_0_Inbound_100","timestamp":1562212769623.0002,"type":"codec","clockRate":90000,"mimeType":"video/VP8","payloadType":100}],["RTCCodec_0_Inbound_101",{"id":"RTCCodec_0_Inbound_101","timestamp":1562212769623.0002,"type":"codec","clockRate":90000,"mimeType":"video/rtx","payloadType":101}],["RTCCodec_0_Inbound_104",{"id":"RTCCodec_0_Inbound_104","timestamp":1562212769623.0002,"type":"codec","clockRate":90000,"mimeType":"video/ulpfec","payloadType":104}],["RTCCodec_0_Inbound_125",{"id":"RTCCodec_0_Inbound_125","timestamp":1562212769623.0002,"type":"codec","clockRate":90000,"mimeType":"video/rtx","payloadType":125}],["RTCCodec_0_Inbound_127",{"id":"RTCCodec_0_Inbound_127","timestamp":1562212769623.0002,"type":"codec","clockRate":90000,"mimeType":"video/red","payloadType":127}],["RTCCodec_0_Inbound_96",{"id":"RTCCodec_0_Inbound_96","timestamp":1562212769623.0002,"type":"codec","clockRate":90000,"mimeType":"video/H264","payloadType":96}],["RTCCodec_0_Inbound_97",{"id":"RTCCodec_0_Inbound_97","timestamp":1562212769623.0002,"type":"codec","clockRate":90000,"mimeType":"video/rtx","payloadType":97}],["RTCCodec_0_Inbound_98",{"id":"RTCCodec_0_Inbound_98","timestamp":1562212769623.0002,"type":"codec","clockRate":90000,"mimeType":"video/H264","payloadType":98}],["RTCCodec_0_Inbound_99",{"id":"RTCCodec_0_Inbound_99","timestamp":1562212769623.0002,"type":"codec","clockRate":90000,"mimeType":"video/rtx","payloadType":99}],["RTCCodec_0_Outbound_100",{"id":"RTCCodec_0_Outbound_100","timestamp":1562212769623.0002,"type":"codec","clockRate":90000,"mimeType":"video/VP8","payloadType":100}],["RTCCodec_0_Outbound_101",{"id":"RTCCodec_0_Outbound_101","timestamp":1562212769623.0002,"type":"codec","clockRate":90000,"mimeType":"video/rtx","payloadType":101}],["RTCCodec_0_Outbound_104",{"id":"RTCCodec_0_Outbound_104","timestamp":1562212769623.0002,"type":"codec","clockRate":90000,"mimeType":"video/ulpfec","payloadType":104}],["RTCCodec_0_Outbound_125",{"id":"RTCCodec_0_Outbound_125","timestamp":1562212769623.0002,"type":"codec","clockRate":90000,"mimeType":"video/rtx","payloadType":125}],["RTCCodec_0_Outbound_127",{"id":"RTCCodec_0_Outbound_127","timestamp":1562212769623.0002,"type":"codec","clockRate":90000,"mimeType":"video/red","payloadType":127}],["RTCCodec_0_Outbound_96",{"id":"RTCCodec_0_Outbound_96","timestamp":1562212769623.0002,"type":"codec","clockRate":90000,"mimeType":"video/H264","payloadType":96}],["RTCCodec_0_Outbound_97",{"id":"RTCCodec_0_Outbound_97","timestamp":1562212769623.0002,"type":"codec","clockRate":90000,"mimeType":"video/rtx","payloadType":97}],["RTCCodec_0_Outbound_98",{"id":"RTCCodec_0_Outbound_98","timestamp":1562212769623.0002,"type":"codec","clockRate":90000,"mimeType":"video/H264","payloadType":98}],["RTCCodec_0_Outbound_99",{"id":"RTCCodec_0_Outbound_99","timestamp":1562212769623.0002,"type":"codec","clockRate":90000,"mimeType":"video/rtx","payloadType":99}],["RTCCodec_1_Inbound_0",{"id":"RTCCodec_1_Inbound_0","timestamp":1562212769623.0002,"type":"codec","clockRate":8000,"mimeType":"audio/PCMU","payloadType":0}],["RTCCodec_1_Inbound_102",{"id":"RTCCodec_1_Inbound_102","timestamp":1562212769623.0002,"type":"codec","clockRate":8000,"mimeType":"audio/ILBC","payloadType":102}],["RTCCodec_1_Inbound_103",{"id":"RTCCodec_1_Inbound_103","timestamp":1562212769623.0002,"type":"codec","clockRate":16000,"mimeType":"audio/ISAC","payloadType":103}],["RTCCodec_1_Inbound_105",{"id":"RTCCodec_1_Inbound_105","timestamp":1562212769623.0002,"type":"codec","clockRate":16000,"mimeType":"audio/CN","payloadType":105}],["RTCCodec_1_Inbound_110",{"id":"RTCCodec_1_Inbound_110","timestamp":1562212769623.0002,"type":"codec","clockRate":48000,"mimeType":"audio/telephone-event","payloadType":110}],["RTCCodec_1_Inbound_111",{"id":"RTCCodec_1_Inbound_111","timestamp":1562212769623.0002,"type":"codec","clockRate":48000,"mimeType":"audio/opus","payloadType":111}],["RTCCodec_1_Inbound_113",{"id":"RTCCodec_1_Inbound_113","timestamp":1562212769623.0002,"type":"codec","clockRate":16000,"mimeType":"audio/telephone-event","payloadType":113}],["RTCCodec_1_Inbound_126",{"id":"RTCCodec_1_Inbound_126","timestamp":1562212769623.0002,"type":"codec","clockRate":8000,"mimeType":"audio/telephone-event","payloadType":126}],["RTCCodec_1_Inbound_13",{"id":"RTCCodec_1_Inbound_13","timestamp":1562212769623.0002,"type":"codec","clockRate":8000,"mimeType":"audio/CN","payloadType":13}],["RTCCodec_1_Inbound_8",{"id":"RTCCodec_1_Inbound_8","timestamp":1562212769623.0002,"type":"codec","clockRate":8000,"mimeType":"audio/PCMA","payloadType":8}],["RTCCodec_1_Inbound_9",{"id":"RTCCodec_1_Inbound_9","timestamp":1562212769623.0002,"type":"codec","clockRate":8000,"mimeType":"audio/G722","payloadType":9}],["RTCCodec_1_Outbound_0",{"id":"RTCCodec_1_Outbound_0","timestamp":1562212769623.0002,"type":"codec","clockRate":8000,"mimeType":"audio/PCMU","payloadType":0}],["RTCCodec_1_Outbound_102",{"id":"RTCCodec_1_Outbound_102","timestamp":1562212769623.0002,"type":"codec","clockRate":8000,"mimeType":"audio/ILBC","payloadType":102}],["RTCCodec_1_Outbound_103",{"id":"RTCCodec_1_Outbound_103","timestamp":1562212769623.0002,"type":"codec","clockRate":16000,"mimeType":"audio/ISAC","payloadType":103}],["RTCCodec_1_Outbound_105",{"id":"RTCCodec_1_Outbound_105","timestamp":1562212769623.0002,"type":"codec","clockRate":16000,"mimeType":"audio/CN","payloadType":105}],["RTCCodec_1_Outbound_110",{"id":"RTCCodec_1_Outbound_110","timestamp":1562212769623.0002,"type":"codec","clockRate":48000,"mimeType":"audio/telephone-event","payloadType":110}],["RTCCodec_1_Outbound_111",{"id":"RTCCodec_1_Outbound_111","timestamp":1562212769623.0002,"type":"codec","clockRate":48000,"mimeType":"audio/opus","payloadType":111}],["RTCCodec_1_Outbound_113",{"id":"RTCCodec_1_Outbound_113","timestamp":1562212769623.0002,"type":"codec","clockRate":16000,"mimeType":"audio/telephone-event","payloadType":113}],["RTCCodec_1_Outbound_126",{"id":"RTCCodec_1_Outbound_126","timestamp":1562212769623.0002,"type":"codec","clockRate":8000,"mimeType":"audio/telephone-event","payloadType":126}],["RTCCodec_1_Outbound_13",{"id":"RTCCodec_1_Outbound_13","timestamp":1562212769623.0002,"type":"codec","clockRate":8000,"mimeType":"audio/CN","payloadType":13}],["RTCCodec_1_Outbound_8",{"id":"RTCCodec_1_Outbound_8","timestamp":1562212769623.0002,"type":"codec","clockRate":8000,"mimeType":"audio/PCMA","payloadType":8}],["RTCCodec_1_Outbound_9",{"id":"RTCCodec_1_Outbound_9","timestamp":1562212769623.0002,"type":"codec","clockRate":8000,"mimeType":"audio/G722","payloadType":9}],["RTCDataChannel_1",{"id":"RTCDataChannel_1","timestamp":1562212769623.0002,"type":"data-channel","bytesReceived":130,"bytesSent":65,"datachannelid":1,"label":"label","messagesReceived":10,"messagesSent":5,"protocol":"","state":"open"}],["RTCIceCandidatePair_Q0xtytaZ_CzTZYmnt",{"id":"RTCIceCandidatePair_Q0xtytaZ_CzTZYmnt","timestamp":1562212769623.0002,"type":"candidate-pair","availableOutgoingBitrate":1211946,"bytesReceived":578993,"bytesSent":579132,"currentRoundTripTime":0.011,"localCandidateId":"RTCIceCandidate_Q0xtytaZ","nominated":true,"priority":9079290933572287000,"remoteCandidateId":"RTCIceCandidate_CzTZYmnt","requestsReceived":8,"requestsSent":1,"responsesReceived":8,"responsesSent":8,"state":"succeeded","totalRoundTripTime":0.026,"transportId":"RTCTransport_0_1","writable":true}],["RTCIceCandidate_CzTZYmnt",{"id":"RTCIceCandidate_CzTZYmnt","timestamp":1562212769623.0002,"type":"remote-candidate","candidateType":"host","deleted":false,"port":61226,"priority":2113937151,"protocol":"udp","transportId":"RTCTransport_0_1"}],["RTCIceCandidate_Q0xtytaZ",{"id":"RTCIceCandidate_Q0xtytaZ","timestamp":1562212769623.0002,"type":"local-candidate","candidateType":"host","deleted":false,"port":55097,"priority":2113937151,"protocol":"udp","transportId":"RTCTransport_0_1"}],["RTCInboundRTPAudioStream_2397415152",{"id":"RTCInboundRTPAudioStream_2397415152","timestamp":1562212769623.0002,"type":"inbound-rtp","codecId":"RTCCodec_1_Inbound_111","isRemote":false,"mediaType":"audio","qpSum":0,"ssrc":2397415152,"trackId":"RTCMediaStreamTrack_receiver_18","transportId":"RTCTransport_0_1","bytesReceived":49663,"fractionLost":0,"jitter":0.004,"packetsLost":0,"packetsReceived":549}],["RTCInboundRTPVideoStream_1507833417",{"id":"RTCInboundRTPVideoStream_1507833417","timestamp":1562212769623.0002,"type":"inbound-rtp","codecId":"RTCCodec_0_Inbound_96","firCount":0,"isRemote":false,"mediaType":"video","nackCount":0,"pliCount":0,"qpSum":0,"ssrc":1507833417,"trackId":"RTCMediaStreamTrack_receiver_17","transportId":"RTCTransport_0_1","bytesReceived":503448,"fractionLost":0,"framesDecoded":294,"packetsLost":0,"packetsReceived":622}],["RTCMediaStreamTrack_receiver_17",{"id":"RTCMediaStreamTrack_receiver_17","timestamp":1562212769623.0002,"type":"track","detached":false,"ended":false,"frameHeight":480,"frameWidth":640,"framesDecoded":294,"framesDropped":1,"framesReceived":295,"remoteSource":true,"trackIdentifier":"3d1c911a-d82c-453b-99e4-bed54e2eddc8"}],["RTCMediaStreamTrack_receiver_18",{"id":"RTCMediaStreamTrack_receiver_18","timestamp":1562212769623.0002,"type":"track","audioLevel":0.008758812219611195,"detached":false,"ended":false,"remoteSource":true,"trackIdentifier":"7683d0c3-513a-45e3-bdcc-66c0913ee90c"}],["RTCMediaStreamTrack_sender_17",{"id":"RTCMediaStreamTrack_sender_17","timestamp":1562212769623.0002,"type":"track","detached":false,"ended":false,"frameHeight":480,"frameWidth":640,"framesSent":295,"remoteSource":false,"trackIdentifier":"d7d9c3fe-493c-4ead-8d72-9a5c49f92978"}],["RTCMediaStreamTrack_sender_18",{"id":"RTCMediaStreamTrack_sender_18","timestamp":1562212769623.0002,"type":"track","audioLevel":0,"detached":false,"ended":false,"remoteSource":false,"trackIdentifier":"ad631492-6ea2-4ebe-b9d2-668241763455"}],["RTCOutboundRTPAudioStream_766226569",{"id":"RTCOutboundRTPAudioStream_766226569","timestamp":1562212769623.0002,"type":"outbound-rtp","codecId":"RTCCodec_1_Outbound_111","isRemote":false,"mediaType":"audio","qpSum":0,"ssrc":766226569,"trackId":"RTCMediaStreamTrack_sender_18","transportId":"RTCTransport_0_1","bytesSent":49549,"packetsSent":549}],["RTCOutboundRTPVideoStream_3315862377",{"id":"RTCOutboundRTPVideoStream_3315862377","timestamp":1562212769623.0002,"type":"outbound-rtp","codecId":"RTCCodec_0_Outbound_96","firCount":0,"isRemote":false,"mediaType":"video","nackCount":0,"pliCount":0,"qpSum":0,"ssrc":3315862377,"trackId":"RTCMediaStreamTrack_sender_17","transportId":"RTCTransport_0_1","bytesSent":504422,"framesEncoded":295,"packetsSent":624}],["RTCPeerConnection",{"id":"RTCPeerConnection","timestamp":1562212769623.0002,"type":"peer-connection","dataChannelsClosed":0,"dataChannelsOpened":2}],["RTCTransport_0_1",{"id":"RTCTransport_0_1","timestamp":1562212769623.0002,"type":"transport","bytesReceived":578993,"bytesSent":579132,"localCertificateId":"RTCCertificate_B7:B2:50:EF:ED:80:03:29:4A:AB:90:68:3F:29:E7:33:B1:1E:F5:55:80:00:F5:DB:42:E3:D7:13:C8:DD:62:B2","remoteCertificateId":"RTCCertificate_E5:38:4D:7F:8F:7A:20:96:38:18:DF:60:9A:7B:2C:EA:50:C2:2F:D8:62:1D:8A:AD:6D:1A:80:57:91:02:8D:AD","selectedCandidatePairId":"RTCIceCandidatePair_Q0xtytaZ_CzTZYmnt"}]] 2 | -------------------------------------------------------------------------------- /__tests__/rtcstats-insight.test.js: -------------------------------------------------------------------------------- 1 | import { detect } from "detect-browser"; 2 | import { RTCStatsInsight } from "../src/rtcstats-insight"; 3 | 4 | jest.mock("detect-browser"); 5 | jest.useFakeTimers(); 6 | 7 | describe("RTCStatsInsight", () => { 8 | let pcMock; 9 | 10 | beforeEach(() => { 11 | pcMock = { getStats: jest.fn().mockResolvedValue(new Map()) }; 12 | detect.mockReturnValue({ 13 | name: "chrome", 14 | version: "77.0.3842.0" 15 | }); 16 | }); 17 | 18 | test("Start polling with watch().", () => { 19 | const pollingInterval = 1000; 20 | const insight = new RTCStatsInsight(pcMock, { interval: pollingInterval }); 21 | 22 | insight.watch(); 23 | 24 | expect(setInterval).toHaveBeenLastCalledWith( 25 | expect.any(Function), 26 | pollingInterval 27 | ); 28 | }); 29 | 30 | test("Stop polling with stop().", () => { 31 | const insight = new RTCStatsInsight(pcMock); 32 | 33 | insight.watch(); 34 | insight.stop(); 35 | 36 | expect(clearInterval).toHaveBeenCalled(); 37 | }); 38 | 39 | test("Collect getStats() according to the interval set in `polling-interval`.", () => { 40 | const insight = new RTCStatsInsight(pcMock); 41 | 42 | insight.watch(); 43 | jest.runOnlyPendingTimers(); 44 | 45 | expect(pcMock.getStats).toHaveBeenCalled(); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /__tests__/rtcstats-moment.test.js: -------------------------------------------------------------------------------- 1 | import { detect } from "detect-browser"; 2 | import { RTCStatsMoment } from "../src/rtcstats-moment"; 3 | 4 | jest.mock("detect-browser"); 5 | 6 | describe("send.video", () => { 7 | beforeEach(() => { 8 | detect.mockReturnValue({ 9 | name: "chrome", 10 | version: "77.0.3842.0" 11 | }); 12 | }); 13 | 14 | test("It gives instant attributes jitter and RTT.", () => { 15 | // setup 16 | const statsDouble = new Map([ 17 | [ 18 | "DummyStats", 19 | { 20 | id: "DummyStats", 21 | type: "remote-inbound-rtp", 22 | kind: "video", 23 | jitter: 0.2, 24 | roundTripTime: 0.3 25 | } 26 | ] 27 | ]); 28 | const moment = new RTCStatsMoment(); 29 | 30 | // execute 31 | moment.update(statsDouble); 32 | const report = moment.report(); 33 | 34 | // asert 35 | expect(report.send.video).toMatchObject({ jitter: 0.2, rtt: 0.3 }); 36 | }); 37 | 38 | test("It calculates averageEncodeTime.", () => { 39 | // setup 40 | const base = { id: "DummyStats", type: "outbound-rtp", kind: "video" }; 41 | const previousStatsDouble = new Map([ 42 | [ 43 | "DummyStats", 44 | { 45 | ...base, 46 | totalEncodeTime: 10, 47 | framesEncoded: 10 48 | } 49 | ] 50 | ]); 51 | const lastStatsDouble = new Map([ 52 | [ 53 | "DummyStats", 54 | { 55 | ...base, 56 | totalEncodeTime: 20, 57 | framesEncoded: 12 58 | } 59 | ] 60 | ]); 61 | const moment = new RTCStatsMoment(); 62 | 63 | // execute 64 | moment.update(previousStatsDouble); 65 | moment.update(lastStatsDouble); 66 | const report = moment.report(); 67 | 68 | // asert 69 | expect(report.send.video).toMatchObject({ averageEncodeTime: 5 }); 70 | }); 71 | 72 | test("It calculates qpValue.", () => { 73 | // setup 74 | const base = { id: "DummyStats", type: "outbound-rtp", kind: "video" }; 75 | const previousStatsDouble = new Map([ 76 | [ 77 | "DummyStats", 78 | { 79 | ...base, 80 | qpSum: 10, 81 | framesEncoded: 10 82 | } 83 | ] 84 | ]); 85 | const lastStatsDouble = new Map([ 86 | [ 87 | "DummyStats", 88 | { 89 | ...base, 90 | qpSum: 20, 91 | framesEncoded: 12 92 | } 93 | ] 94 | ]); 95 | const moment = new RTCStatsMoment(); 96 | 97 | // execute 98 | moment.update(previousStatsDouble); 99 | moment.update(lastStatsDouble); 100 | const report = moment.report(); 101 | 102 | // asert 103 | expect(report.send.video).toMatchObject({ qpValue: 5 }); 104 | }); 105 | 106 | test("It calculates bitrate.", () => { 107 | // setup 108 | const base = { id: "DummyStats", type: "outbound-rtp", kind: "video" }; 109 | const previousStatsDouble = new Map([ 110 | [ 111 | "DummyStats", 112 | { 113 | ...base, 114 | bytesSent: 100, 115 | timestamp: 0 116 | } 117 | ] 118 | ]); 119 | const lastStatsDouble = new Map([ 120 | [ 121 | "DummyStats", 122 | { 123 | ...base, 124 | bytesSent: 1100, 125 | timestamp: 1000 126 | } 127 | ] 128 | ]); 129 | const moment = new RTCStatsMoment(); 130 | 131 | // execute 132 | moment.update(previousStatsDouble); 133 | moment.update(lastStatsDouble); 134 | const report = moment.report(); 135 | 136 | // asert 137 | expect(report.send.video).toMatchObject({ bitrate: 8000 }); 138 | }); 139 | }); 140 | 141 | describe("send.audio", () => { 142 | beforeEach(() => { 143 | detect.mockReturnValue({ 144 | name: "chrome", 145 | version: "77.0.3842.0" 146 | }); 147 | }); 148 | 149 | test("It gives instant attributes jitter, RTT and audioLevel.", () => { 150 | // setup 151 | const statsDouble = new Map([ 152 | [ 153 | "DummyStats_1", 154 | { 155 | id: "DummyStats_1", 156 | type: "remote-inbound-rtp", 157 | kind: "audio", 158 | jitter: 0.2, 159 | roundTripTime: 0.3 160 | } 161 | ], 162 | [ 163 | "DummyStats_2", 164 | { 165 | id: "DummyStats_2", 166 | type: "sender", 167 | kind: "audio", 168 | audioLevel: 0.5 169 | } 170 | ] 171 | ]); 172 | const moment = new RTCStatsMoment(); 173 | 174 | // execute 175 | moment.update(statsDouble); 176 | const report = moment.report(); 177 | 178 | // asert 179 | expect(report.send.audio).toMatchObject({ 180 | jitter: 0.2, 181 | rtt: 0.3, 182 | audioLevel: 0.5 183 | }); 184 | }); 185 | 186 | test("It calculates bitrate.", () => { 187 | // setup 188 | const base = { id: "DummyStats", type: "outbound-rtp", kind: "audio" }; 189 | const previousStatsDouble = new Map([ 190 | [ 191 | "DummyStats", 192 | { 193 | ...base, 194 | bytesSent: 100, 195 | timestamp: 0 196 | } 197 | ] 198 | ]); 199 | const lastStatsDouble = new Map([ 200 | [ 201 | "DummyStats", 202 | { 203 | ...base, 204 | bytesSent: 1100, 205 | timestamp: 1000 206 | } 207 | ] 208 | ]); 209 | const moment = new RTCStatsMoment(); 210 | 211 | // execute 212 | moment.update(previousStatsDouble); 213 | moment.update(lastStatsDouble); 214 | const report = moment.report(); 215 | 216 | // asert 217 | expect(report.send.audio).toMatchObject({ bitrate: 8000 }); 218 | }); 219 | }); 220 | 221 | describe("receive.video", () => { 222 | beforeEach(() => { 223 | detect.mockReturnValue({ 224 | name: "chrome", 225 | version: "77.0.3842.0" 226 | }); 227 | }); 228 | 229 | test("It calculates jitterBufferDelay.", () => { 230 | // setup 231 | const base = { id: "DummyStats", type: "receiver", kind: "video" }; 232 | const previousStatsDouble = new Map([ 233 | [ 234 | "DummyStats", 235 | { 236 | ...base, 237 | jitterBufferDelay: 100, 238 | jitterBufferEmittedCount: 2 239 | } 240 | ] 241 | ]); 242 | const lastStatsDouble = new Map([ 243 | [ 244 | "DummyStats", 245 | { 246 | ...base, 247 | jitterBufferDelay: 1100, 248 | jitterBufferEmittedCount: 12 249 | } 250 | ] 251 | ]); 252 | const moment = new RTCStatsMoment(); 253 | 254 | // execute 255 | moment.update(previousStatsDouble); 256 | moment.update(lastStatsDouble); 257 | const report = moment.report(); 258 | 259 | // asert 260 | expect(report.receive.video).toMatchObject({ jitterBufferDelay: 100 }); 261 | }); 262 | 263 | test("It calculates fractionLost.", () => { 264 | // setup 265 | const base = { id: "DummyStats", type: "inbound-rtp", kind: "video" }; 266 | const previousStatsDouble = new Map([ 267 | [ 268 | "DummyStats", 269 | { 270 | ...base, 271 | packetsLost: 10, 272 | packetsReceived: 20 273 | } 274 | ] 275 | ]); 276 | const lastStatsDouble = new Map([ 277 | [ 278 | "DummyStats", 279 | { 280 | ...base, 281 | packetsLost: 20, 282 | packetsReceived: 100 283 | } 284 | ] 285 | ]); 286 | const moment = new RTCStatsMoment(); 287 | 288 | // execute 289 | moment.update(previousStatsDouble); 290 | moment.update(lastStatsDouble); 291 | const report = moment.report(); 292 | 293 | // asert 294 | expect(report.receive.video).toMatchObject({ fractionLost: 0.2 }); 295 | }); 296 | 297 | test("It calculates qpValue.", () => { 298 | // setup 299 | const base = { id: "DummyStats", type: "inbound-rtp", kind: "video" }; 300 | const previousStatsDouble = new Map([ 301 | [ 302 | "DummyStats", 303 | { 304 | ...base, 305 | qpSum: 10, 306 | framesDecoded: 10 307 | } 308 | ] 309 | ]); 310 | const lastStatsDouble = new Map([ 311 | [ 312 | "DummyStats", 313 | { 314 | ...base, 315 | qpSum: 20, 316 | framesDecoded: 12 317 | } 318 | ] 319 | ]); 320 | const moment = new RTCStatsMoment(); 321 | 322 | // execute 323 | moment.update(previousStatsDouble); 324 | moment.update(lastStatsDouble); 325 | const report = moment.report(); 326 | 327 | // asert 328 | expect(report.receive.video).toMatchObject({ qpValue: 5 }); 329 | }); 330 | 331 | test("It calculates bitrate.", () => { 332 | // setup 333 | const base = { id: "DummyStats", type: "inbound-rtp", kind: "video" }; 334 | const previousStatsDouble = new Map([ 335 | [ 336 | "DummyStats", 337 | { 338 | ...base, 339 | bytesReceived: 100, 340 | timestamp: 0 341 | } 342 | ] 343 | ]); 344 | const lastStatsDouble = new Map([ 345 | [ 346 | "DummyStats", 347 | { 348 | ...base, 349 | bytesReceived: 1100, 350 | timestamp: 1000 351 | } 352 | ] 353 | ]); 354 | const moment = new RTCStatsMoment(); 355 | 356 | // execute 357 | moment.update(previousStatsDouble); 358 | moment.update(lastStatsDouble); 359 | const report = moment.report(); 360 | 361 | // asert 362 | expect(report.receive.video).toMatchObject({ bitrate: 8000 }); 363 | }); 364 | }); 365 | 366 | describe("receive.audio", () => { 367 | beforeEach(() => { 368 | detect.mockReturnValue({ 369 | name: "chrome", 370 | version: "77.0.3842.0" 371 | }); 372 | }); 373 | 374 | test("It gives instant attributes audioLevel.", () => { 375 | // setup 376 | const statsDouble = new Map([ 377 | [ 378 | "DummyStats", 379 | { 380 | id: "DummyStats", 381 | type: "receiver", 382 | kind: "audio", 383 | audioLevel: 0.5 384 | } 385 | ] 386 | ]); 387 | const moment = new RTCStatsMoment(); 388 | 389 | // execute 390 | moment.update(statsDouble); 391 | const report = moment.report(); 392 | 393 | // asert 394 | expect(report.receive.audio).toMatchObject({ audioLevel: 0.5 }); 395 | }); 396 | 397 | test("It calculates jitterBufferDelay.", () => { 398 | // setup 399 | const base = { id: "DummyStats", type: "receiver", kind: "audio" }; 400 | const previousStatsDouble = new Map([ 401 | [ 402 | "DummyStats", 403 | { 404 | ...base, 405 | jitterBufferDelay: 100, 406 | jitterBufferEmittedCount: 2 407 | } 408 | ] 409 | ]); 410 | const lastStatsDouble = new Map([ 411 | [ 412 | "DummyStats", 413 | { 414 | ...base, 415 | jitterBufferDelay: 1100, 416 | jitterBufferEmittedCount: 12 417 | } 418 | ] 419 | ]); 420 | const moment = new RTCStatsMoment(); 421 | 422 | // execute 423 | moment.update(previousStatsDouble); 424 | moment.update(lastStatsDouble); 425 | const report = moment.report(); 426 | 427 | // asert 428 | expect(report.receive.audio).toMatchObject({ jitterBufferDelay: 100 }); 429 | }); 430 | 431 | test("It calculates fractionLost.", () => { 432 | // setup 433 | const base = { id: "DummyStats", type: "inbound-rtp", kind: "audio" }; 434 | const previousStatsDouble = new Map([ 435 | [ 436 | "DummyStats", 437 | { 438 | ...base, 439 | packetsLost: 10, 440 | packetsReceived: 20 441 | } 442 | ] 443 | ]); 444 | const lastStatsDouble = new Map([ 445 | [ 446 | "DummyStats", 447 | { 448 | ...base, 449 | packetsLost: 20, 450 | packetsReceived: 100 451 | } 452 | ] 453 | ]); 454 | const moment = new RTCStatsMoment(); 455 | 456 | // execute 457 | moment.update(previousStatsDouble); 458 | moment.update(lastStatsDouble); 459 | const report = moment.report(); 460 | 461 | // asert 462 | expect(report.receive.audio).toMatchObject({ fractionLost: 0.2 }); 463 | }); 464 | 465 | test("It calculates bitrate.", () => { 466 | // setup 467 | const base = { id: "DummyStats", type: "inbound-rtp", kind: "audio" }; 468 | const previousStatsDouble = new Map([ 469 | [ 470 | "DummyStats", 471 | { 472 | ...base, 473 | bytesReceived: 100, 474 | timestamp: 0 475 | } 476 | ] 477 | ]); 478 | const lastStatsDouble = new Map([ 479 | [ 480 | "DummyStats", 481 | { 482 | ...base, 483 | bytesReceived: 1100, 484 | timestamp: 1000 485 | } 486 | ] 487 | ]); 488 | const moment = new RTCStatsMoment(); 489 | 490 | // execute 491 | moment.update(previousStatsDouble); 492 | moment.update(lastStatsDouble); 493 | const report = moment.report(); 494 | 495 | // asert 496 | expect(report.receive.audio).toMatchObject({ bitrate: 8000 }); 497 | }); 498 | }); 499 | 500 | describe("candidatePair", () => { 501 | beforeEach(() => { 502 | detect.mockReturnValue({ 503 | name: "chrome", 504 | version: "77.0.3842.0" 505 | }); 506 | }); 507 | 508 | test("It gives instant attributes RTT.", () => { 509 | // setup 510 | const statsDouble = new Map([ 511 | [ 512 | "DummyStats", 513 | { 514 | id: "DummyStats", 515 | type: "candidate-pair", 516 | nominated: true, 517 | currentRoundTripTime: 0.3 518 | } 519 | ] 520 | ]); 521 | const moment = new RTCStatsMoment(); 522 | 523 | // execute 524 | moment.update(statsDouble); 525 | const report = moment.report(); 526 | 527 | // asert 528 | expect(report.candidatePair).toMatchObject({ rtt: 0.3 }); 529 | }); 530 | 531 | test("It calculates upstreamBitrate.", () => { 532 | // setup 533 | const base = { id: "DummyStats", type: "candidate-pair", nominated: true }; 534 | const previousStatsDouble = new Map([ 535 | [ 536 | "DummyStats", 537 | { 538 | ...base, 539 | bytesSent: 100, 540 | timestamp: 0 541 | } 542 | ] 543 | ]); 544 | const lastStatsDouble = new Map([ 545 | [ 546 | "DummyStats", 547 | { 548 | ...base, 549 | bytesSent: 1100, 550 | timestamp: 1000 551 | } 552 | ] 553 | ]); 554 | const moment = new RTCStatsMoment(); 555 | 556 | // execute 557 | moment.update(previousStatsDouble); 558 | moment.update(lastStatsDouble); 559 | const report = moment.report(); 560 | 561 | // asert 562 | expect(report.candidatePair).toMatchObject({ upstreamBitrate: 8000 }); 563 | }); 564 | 565 | test("It calculates downstreamBitrate.", () => { 566 | // setup 567 | const base = { id: "DummyStats", type: "candidate-pair", nominated: true }; 568 | const previousStatsDouble = new Map([ 569 | [ 570 | "DummyStats", 571 | { 572 | ...base, 573 | bytesReceived: 100, 574 | timestamp: 0 575 | } 576 | ] 577 | ]); 578 | const lastStatsDouble = new Map([ 579 | [ 580 | "DummyStats", 581 | { 582 | ...base, 583 | bytesReceived: 1100, 584 | timestamp: 1000 585 | } 586 | ] 587 | ]); 588 | const moment = new RTCStatsMoment(); 589 | 590 | // execute 591 | moment.update(previousStatsDouble); 592 | moment.update(lastStatsDouble); 593 | const report = moment.report(); 594 | 595 | // asert 596 | expect(report.candidatePair).toMatchObject({ downstreamBitrate: 8000 }); 597 | }); 598 | }); 599 | -------------------------------------------------------------------------------- /__tests__/standardize-support.test.js: -------------------------------------------------------------------------------- 1 | import { detect } from "detect-browser"; 2 | import { getStandardizer } from "../src/standardize-support.js"; 3 | import { ChromeRTCStatsReport } from "../src/standardizers/chrome.js"; 4 | import { FirefoxRTCStatsReport } from "../src/standardizers/firefox.js"; 5 | import { SafariRTCStatsReport } from "../src/standardizers/safari.js"; 6 | import { BaseRTCStatsReport } from "../src/standardizers/base.js"; 7 | 8 | jest.mock("detect-browser"); 9 | 10 | describe("RTCStatsMoment", () => { 11 | test("It standardizes Google Chrome's RTCStatsReport", () => { 12 | // setup 13 | detect.mockReturnValue({ 14 | name: "chrome", 15 | version: "77.0.3842.0" 16 | }); 17 | const standardizer = getStandardizer(); 18 | 19 | // assert 20 | expect(standardizer).toBe(ChromeRTCStatsReport); 21 | }); 22 | 23 | test("It standardizes Firefox's RTCStatsReport", () => { 24 | // setup 25 | detect.mockReturnValue({ 26 | name: "firefox", 27 | version: "67.0.4" 28 | }); 29 | const standardizer = getStandardizer(); 30 | 31 | // assert 32 | expect(standardizer).toBe(FirefoxRTCStatsReport); 33 | }); 34 | 35 | test("It standardizes Safari's RTCStatsReport", () => { 36 | // setup 37 | detect.mockReturnValue({ 38 | name: "safari", 39 | version: "12.1.1" 40 | }); 41 | const standardizer = getStandardizer(); 42 | 43 | // assert 44 | expect(standardizer).toBe(SafariRTCStatsReport); 45 | }); 46 | 47 | test("It standardizes unknown browser's RTCStatsReport as BaseRTCStatsReport", () => { 48 | // setup 49 | detect.mockReturnValue({ 50 | name: "unknown-browser", 51 | version: "12.1.1" 52 | }); 53 | const standardizer = getStandardizer(); 54 | 55 | // assert 56 | expect(standardizer).toBe(BaseRTCStatsReport); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /__tests__/standardizers/base.test.js: -------------------------------------------------------------------------------- 1 | import { BaseRTCStatsReport } from "../../src/standardizers/base.js"; 2 | 3 | describe("The behaviors of BaseRTCStatsReport", () => { 4 | const statsDouble = new Map([ 5 | ["DummyStats_1", { id: "DummyStats_1", type: "sender", kind: "video" }], 6 | ["DummyStats_2", { id: "DummyStats_2", type: "sender", kind: "video" }], 7 | ["DummyStats_3", { id: "DummyStats_3", type: "sender", kind: "audio" }] 8 | ]); 9 | 10 | test("It returns an array of stats objects when you access the stats with wrapped stats object reference.", () => { 11 | // setup 12 | const report = new BaseRTCStatsReport(statsDouble); 13 | 14 | // excute 15 | const stats = report.get("RTCAudioSenders"); 16 | const statsArray = [{ id: "DummyStats_3", type: "sender", kind: "audio" }]; 17 | 18 | // assert 19 | expect(stats).toMatchObject(statsArray); 20 | }); 21 | 22 | test("It returns undefined when you access the stats with a reference that not appeared in wrapped ones.", () => { 23 | // setup 24 | const statsDouble = new Map(); 25 | const report = new BaseRTCStatsReport(statsDouble); 26 | 27 | // excute 28 | const stats = report.get("StatsRefNotExists"); 29 | 30 | // assert 31 | expect(stats).toBeUndefined(); 32 | }); 33 | 34 | test("A stats attribute that is not implemented should be undefined.", () => { 35 | // setup 36 | const report = new BaseRTCStatsReport(statsDouble); 37 | 38 | // excute 39 | const stats = report.get("RTCAudioSenders"); 40 | 41 | // assert 42 | expect(stats[0].audioLevel).toBeUndefined(); 43 | }); 44 | 45 | test("Can check if it has the key.", () => { 46 | // setup 47 | const report = new BaseRTCStatsReport(statsDouble); 48 | 49 | // assert 50 | expect(report.has("RTCAudioSenders")).toBe(true); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /__tests__/standardizers/chrome.test.js: -------------------------------------------------------------------------------- 1 | import { ChromeRTCStatsReport } from "../../src/standardizers/chrome.js"; 2 | import rawStats from "../_double/chrome-77.0.3842.0.json"; 3 | 4 | describe("ChromeRTCStatsReport standardized Google Chrome's RTCStatsReport", () => { 5 | let report; 6 | 7 | beforeEach(() => { 8 | const restoredReport = new Map(rawStats); 9 | report = new ChromeRTCStatsReport(restoredReport); 10 | }); 11 | 12 | test("RTCCodecs could be retrieved.", () => { 13 | const statsArray = report.get("RTCCodecs"); 14 | expect(statsArray).toBeInstanceOf(Array); 15 | }); 16 | 17 | test("RTCInboundRtpVideoStreams could be retrieved.", () => { 18 | const statsArray = report.get("RTCInboundRtpVideoStreams"); 19 | expect(statsArray).toBeInstanceOf(Array); 20 | }); 21 | test("RTCInboundRtpAudioStreams could be retrieved.", () => { 22 | const statsArray = report.get("RTCInboundRtpAudioStreams"); 23 | expect(statsArray).toBeInstanceOf(Array); 24 | }); 25 | 26 | test("RTCOutboundRtpVideoStreams could be retrieved.", () => { 27 | const statsArray = report.get("RTCOutboundRtpVideoStreams"); 28 | expect(statsArray).toBeInstanceOf(Array); 29 | }); 30 | 31 | test("RTCOutboundRtpAudioStreams could be retrieved.", () => { 32 | const statsArray = report.get("RTCOutboundRtpAudioStreams"); 33 | expect(statsArray).toBeInstanceOf(Array); 34 | }); 35 | 36 | test("RTCRemoteInboundRtpVideoStreams could be retrieved.", () => { 37 | const statsArray = report.get("RTCRemoteInboundRtpVideoStreams"); 38 | expect(statsArray).toBeInstanceOf(Array); 39 | }); 40 | 41 | test("RTCRemoteInboundRtpAudioStreams could be retrieved.", () => { 42 | const statsArray = report.get("RTCRemoteInboundRtpAudioStreams"); 43 | expect(statsArray).toBeInstanceOf(Array); 44 | }); 45 | 46 | test("RTCVideoSources could be retrieved.", () => { 47 | const statsArray = report.get("RTCVideoSources"); 48 | expect(statsArray).toBeInstanceOf(Array); 49 | }); 50 | 51 | test("RTCAudioSources could be retrieved.", () => { 52 | const statsArray = report.get("RTCAudioSources"); 53 | expect(statsArray).toBeInstanceOf(Array); 54 | }); 55 | 56 | test("RTCPeerConnection could be retrieved.", () => { 57 | const statsArray = report.get("RTCPeerConnection"); 58 | expect(statsArray).toBeInstanceOf(Array); 59 | }); 60 | 61 | test("RTCDataChannels could be retrieved.", () => { 62 | const statsArray = report.get("RTCDataChannels"); 63 | expect(statsArray).toBeInstanceOf(Array); 64 | }); 65 | 66 | test("RTCMediaStreams could be retrieved.", () => { 67 | const statsArray = report.get("RTCMediaStreams"); 68 | expect(statsArray).toBeInstanceOf(Array); 69 | }); 70 | 71 | test("RTCVideoSenders could be retrieved.", () => { 72 | const statsArray = report.get("RTCVideoSenders"); 73 | expect(statsArray).toBeInstanceOf(Array); 74 | }); 75 | 76 | test("RTCAudioSenders could be retrieved.", () => { 77 | const statsArray = report.get("RTCAudioSenders"); 78 | expect(statsArray).toBeInstanceOf(Array); 79 | }); 80 | 81 | test("RTCVideoReceivers could be retrieved.", () => { 82 | const statsArray = report.get("RTCVideoReceivers"); 83 | expect(statsArray).toBeInstanceOf(Array); 84 | }); 85 | 86 | test("RTCAudioReceivers could be retrieved.", () => { 87 | const statsArray = report.get("RTCAudioReceivers"); 88 | expect(statsArray).toBeInstanceOf(Array); 89 | }); 90 | 91 | test("RTCTransports could be retrieved.", () => { 92 | const statsArray = report.get("RTCTransports"); 93 | expect(statsArray).toBeInstanceOf(Array); 94 | }); 95 | 96 | test("RTCIceCandidatePairs could be retrieved.", () => { 97 | const statsArray = report.get("RTCIceCandidatePairs"); 98 | expect(statsArray).toBeInstanceOf(Array); 99 | }); 100 | 101 | test("RTCLocalIceCandidates could be retrieved.", () => { 102 | const statsArray = report.get("RTCLocalIceCandidates"); 103 | expect(statsArray).toBeInstanceOf(Array); 104 | }); 105 | 106 | test("RTCRemoteIceCandidates could be retrieved.", () => { 107 | const statsArray = report.get("RTCRemoteIceCandidates"); 108 | expect(statsArray).toBeInstanceOf(Array); 109 | }); 110 | 111 | test("RTCCertificates could be retrieved.", () => { 112 | const statsArray = report.get("RTCCertificates"); 113 | expect(statsArray).toBeInstanceOf(Array); 114 | }); 115 | }); 116 | -------------------------------------------------------------------------------- /__tests__/standardizers/firefox.test.js: -------------------------------------------------------------------------------- 1 | import { FirefoxRTCStatsReport } from "../../src/standardizers/firefox.js"; 2 | import rawStats from "../_double/firefox-67.0.4.json"; 3 | 4 | describe("FirefoxRTCStatsReport standardized Firefox's RTCStatsReport", () => { 5 | let report; 6 | 7 | beforeEach(() => { 8 | const restoredReport = new Map(rawStats); 9 | report = new FirefoxRTCStatsReport(restoredReport); 10 | }); 11 | 12 | test("RTCInboundRtpVideoStreams could be retrieved.", () => { 13 | const statsArray = report.get("RTCInboundRtpVideoStreams"); 14 | expect(statsArray).toBeInstanceOf(Array); 15 | }); 16 | 17 | test("RTCInboundRtpAudioStreams could be retrieved.", () => { 18 | const statsArray = report.get("RTCInboundRtpAudioStreams"); 19 | expect(statsArray).toBeInstanceOf(Array); 20 | }); 21 | 22 | test("RTCOutboundRtpVideoStreams could be retrieved.", () => { 23 | const statsArray = report.get("RTCOutboundRtpVideoStreams"); 24 | expect(statsArray).toBeInstanceOf(Array); 25 | }); 26 | 27 | test("RTCOutboundRtpAudioStreams could be retrieved.", () => { 28 | const statsArray = report.get("RTCOutboundRtpAudioStreams"); 29 | expect(statsArray).toBeInstanceOf(Array); 30 | }); 31 | 32 | test("RTCRemoteInboundRtpVideoStreams could be retrieved.", () => { 33 | const statsArray = report.get("RTCRemoteInboundRtpVideoStreams"); 34 | expect(statsArray).toBeInstanceOf(Array); 35 | }); 36 | 37 | test("RTCRemoteInboundRtpAudioStreams could be retrieved.", () => { 38 | const statsArray = report.get("RTCRemoteInboundRtpAudioStreams"); 39 | expect(statsArray).toBeInstanceOf(Array); 40 | }); 41 | 42 | test("RTCRemoteOutboundRtpVideoStreams could be retrieved.", () => { 43 | const statsArray = report.get("RTCRemoteOutboundRtpVideoStreams"); 44 | expect(statsArray).toBeInstanceOf(Array); 45 | }); 46 | 47 | test("RTCRemoteOutboundRtpAudioStreams could be retrieved.", () => { 48 | const statsArray = report.get("RTCRemoteOutboundRtpAudioStreams"); 49 | expect(statsArray).toBeInstanceOf(Array); 50 | }); 51 | 52 | test("RTCVideoSenders could be retrieved.", () => { 53 | const statsArray = report.get("RTCVideoSenders"); 54 | expect(statsArray).toBeInstanceOf(Array); 55 | }); 56 | 57 | test("RTCAudioSenders could be retrieved.", () => { 58 | const statsArray = report.get("RTCAudioSenders"); 59 | expect(statsArray).toBeInstanceOf(Array); 60 | }); 61 | 62 | test("RTCVideoReceivers could be retrieved.", () => { 63 | const statsArray = report.get("RTCVideoReceivers"); 64 | expect(statsArray).toBeInstanceOf(Array); 65 | }); 66 | 67 | test("RTCAudioReceivers could be retrieved.", () => { 68 | const statsArray = report.get("RTCAudioReceivers"); 69 | expect(statsArray).toBeInstanceOf(Array); 70 | }); 71 | 72 | test("RTCIceCandidatePairs could be retrieved.", () => { 73 | const statsArray = report.get("RTCIceCandidatePairs"); 74 | expect(statsArray).toBeInstanceOf(Array); 75 | }); 76 | 77 | test("RTCLocalIceCandidates could be retrieved.", () => { 78 | const statsArray = report.get("RTCLocalIceCandidates"); 79 | expect(statsArray).toBeInstanceOf(Array); 80 | }); 81 | 82 | test("RTCRemoteIceCandidates could be retrieved.", () => { 83 | const statsArray = report.get("RTCRemoteIceCandidates"); 84 | expect(statsArray).toBeInstanceOf(Array); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /__tests__/standardizers/safari.test.js: -------------------------------------------------------------------------------- 1 | import { SafariRTCStatsReport } from "../../src/standardizers/safari.js"; 2 | import rawStats from "../_double/safari-12.1.1.json"; 3 | 4 | describe("SafariRTCStatsReport standardized Safari's RTCStatsReport", () => { 5 | let report; 6 | 7 | beforeEach(() => { 8 | const restoredReport = new Map(rawStats); 9 | report = new SafariRTCStatsReport(restoredReport); 10 | }); 11 | 12 | test("RTCCodecs could be retrieved.", () => { 13 | const statsArray = report.get("RTCCodecs"); 14 | expect(statsArray).toBeInstanceOf(Array); 15 | }); 16 | 17 | test("RTCInboundRtpVideoStreams could be retrieved.", () => { 18 | const statsArray = report.get("RTCInboundRtpVideoStreams"); 19 | expect(statsArray).toBeInstanceOf(Array); 20 | }); 21 | 22 | test("RTCInboundRtpAudioStreams could be retrieved.", () => { 23 | const statsArray = report.get("RTCInboundRtpAudioStreams"); 24 | expect(statsArray).toBeInstanceOf(Array); 25 | }); 26 | 27 | test("RTCOutboundRtpVideoStreams could be retrieved.", () => { 28 | const statsArray = report.get("RTCOutboundRtpVideoStreams"); 29 | expect(statsArray).toBeInstanceOf(Array); 30 | }); 31 | 32 | test("RTCOutboundRtpAudioStreams could be retrieved.", () => { 33 | const statsArray = report.get("RTCOutboundRtpAudioStreams"); 34 | expect(statsArray).toBeInstanceOf(Array); 35 | }); 36 | 37 | test("RTCPeerConnection could be retrieved.", () => { 38 | const statsArray = report.get("RTCPeerConnection"); 39 | expect(statsArray).toBeInstanceOf(Array); 40 | }); 41 | 42 | test("RTCDataChannels could be retrieved.", () => { 43 | const statsArray = report.get("RTCDataChannels"); 44 | expect(statsArray).toBeInstanceOf(Array); 45 | }); 46 | 47 | test("RTCVideoSenders could be retrieved.", () => { 48 | const statsArray = report.get("RTCVideoSenders"); 49 | expect(statsArray).toBeInstanceOf(Array); 50 | }); 51 | 52 | test("RTCAudioSenders could be retrieved.", () => { 53 | const statsArray = report.get("RTCAudioSenders"); 54 | expect(statsArray).toBeInstanceOf(Array); 55 | }); 56 | 57 | test("RTCVideoReceivers could be retrieved.", () => { 58 | const statsArray = report.get("RTCVideoReceivers"); 59 | expect(statsArray).toBeInstanceOf(Array); 60 | }); 61 | 62 | test("RTCAudioReceivers could be retrieved.", () => { 63 | const statsArray = report.get("RTCAudioReceivers"); 64 | expect(statsArray).toBeInstanceOf(Array); 65 | }); 66 | 67 | test("RTCTransports could be retrieved.", () => { 68 | const statsArray = report.get("RTCTransports"); 69 | expect(statsArray).toBeInstanceOf(Array); 70 | }); 71 | 72 | test("RTCIceCandidatePairs could be retrieved.", () => { 73 | const statsArray = report.get("RTCIceCandidatePairs"); 74 | expect(statsArray).toBeInstanceOf(Array); 75 | }); 76 | 77 | test("RTCLocalIceCandidates could be retrieved.", () => { 78 | const statsArray = report.get("RTCLocalIceCandidates"); 79 | expect(statsArray).toBeInstanceOf(Array); 80 | }); 81 | 82 | test("RTCRemoteIceCandidates could be retrieved.", () => { 83 | const statsArray = report.get("RTCRemoteIceCandidates"); 84 | expect(statsArray).toBeInstanceOf(Array); 85 | }); 86 | 87 | test("RTCCertificates could be retrieved.", () => { 88 | const statsArray = report.get("RTCCertificates"); 89 | expect(statsArray).toBeInstanceOf(Array); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /docs/ChromeRTCStatsReport.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ChromeRTCStatsReport - Documentation 7 | 8 | 9 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 23 | 24 | 25 | 26 | 29 | 30 |
31 | 32 |

ChromeRTCStatsReport

33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 |
41 | 42 |
43 | 44 |

45 | ChromeRTCStatsReport 46 |

47 | 48 |

Wrapped RTCStatsReport class for Google Chrome.

49 | 50 | 51 |
52 | 53 |
54 |
55 | 56 | 57 |
58 | 59 | 60 |

Constructor

61 | 62 | 63 |

new ChromeRTCStatsReport()

64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 |
74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 |
107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 |
131 | 132 |
133 | 134 | 135 |

Extends

136 | 137 | 138 | 139 | 140 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 |

Methods

159 | 160 | 161 | 162 |
163 | 164 | 165 | 166 |

get(key) → {Array.<RTCStats>}

167 | 168 | 169 | 170 | 171 | 172 |
173 |

Get the array of type of stats referred by key.

174 |
175 | 176 | 177 | 178 | 179 | 180 |
181 | 182 | 183 | 184 | 185 | 186 | 187 |
Inherited From:
188 |
191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 |
219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 |
Parameters:
229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 264 | 265 | 266 | 267 | 268 | 269 | 273 | 274 | 275 | 276 | 277 |
NameTypeDescription
key 257 | 258 | 259 | string 260 | 261 | 262 | 263 | 270 |

A stats object reference defined in RTCStatsReferences enum.

271 | 272 |
278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 |
293 |
Returns:
294 | 295 | 296 | 297 |
298 |
299 | Type: 300 |
301 |
302 | 303 | Array.<RTCStats> 304 | 305 | 306 |
307 |
308 | 309 | 310 |
311 |

An array of stats referred by key.

312 |
313 | 314 | 315 |
316 | 317 | 318 | 319 |
320 |
Example
321 | 322 |
const report = new BaseRTCStatsReport(await pc.getStats());
323 | 
324 | if (report.get(RTCStatsReferences.RTCInboundRtpVideoStreams.key)) {
325 | const stats = report.get(
326 |   RTCStatsReferences.RTCInboundRtpVideoStreams.key
327 | )[0];
328 | logger.info(`ts:${stats.timestamp} id:${stats.trackId} recv:${stats.bytesReceived}`);
329 | 330 |
331 | 332 |
333 | 334 | 335 |
336 | 337 | 338 | 339 |

has(key) → {bool}

340 | 341 | 342 | 343 | 344 | 345 |
346 |

Check if the instance has the type of stats referred by key.

347 |
348 | 349 | 350 | 351 | 352 | 353 |
354 | 355 | 356 | 357 | 358 | 359 | 360 |
Inherited From:
361 |
364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 |
392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 |
Parameters:
402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 437 | 438 | 439 | 440 | 441 | 442 | 446 | 447 | 448 | 449 | 450 |
NameTypeDescription
key 430 | 431 | 432 | string 433 | 434 | 435 | 436 | 443 |

A stats object reference defined in RTCStatsReferences enum.

444 | 445 |
451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 |
466 |
Returns:
467 | 468 | 469 | 470 |
471 |
472 | Type: 473 |
474 |
475 | 476 | bool 477 | 478 | 479 |
480 |
481 | 482 | 483 |
484 |

True if the referred stats exists.

485 |
486 | 487 | 488 |
489 | 490 | 491 | 492 |
493 |
Example
494 | 495 |
const report = new BaseRTCStatsReport(await pc.getStats());
496 | 
497 | if (report.has(RTCStatsReferences.RTCInboundRtpVideoStreams.key)) {
498 |   logger.info("receiving video.");
499 | } else {
500 |   logger.info("no video streams receiving.");
501 | }
502 | 503 |
504 | 505 |
506 | 507 | 508 | 509 | 510 | 511 | 512 |
513 | 514 |
515 | 516 | 517 | 518 | 519 |
520 | 521 |
522 | 523 | 526 | 527 | 528 | 529 | 530 | -------------------------------------------------------------------------------- /docs/FirefoxRTCStatsReport.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | FirefoxRTCStatsReport - Documentation 7 | 8 | 9 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 23 | 24 | 25 | 26 | 29 | 30 |
31 | 32 |

FirefoxRTCStatsReport

33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 |
41 | 42 |
43 | 44 |

45 | FirefoxRTCStatsReport 46 |

47 | 48 |

Wrapped RTCStatsReport class for Firefox.

49 | 50 | 51 |
52 | 53 |
54 |
55 | 56 | 57 |
58 | 59 | 60 |

Constructor

61 | 62 | 63 |

new FirefoxRTCStatsReport()

64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 |
74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 |
107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 |
131 | 132 |
133 | 134 | 135 |

Extends

136 | 137 | 138 | 139 | 140 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 |

Classes

151 | 152 |
153 |
FirefoxRTCStatsReport
154 |
155 |
156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 |

Methods

166 | 167 | 168 | 169 |
170 | 171 | 172 | 173 |

get(key) → {Array.<RTCStats>}

174 | 175 | 176 | 177 | 178 | 179 |
180 |

Get the array of type of stats referred by key.

181 |
182 | 183 | 184 | 185 | 186 | 187 |
188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 |
Overrides:
197 |
200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 |
226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 |
Parameters:
236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 271 | 272 | 273 | 274 | 275 | 276 | 280 | 281 | 282 | 283 | 284 |
NameTypeDescription
key 264 | 265 | 266 | string 267 | 268 | 269 | 270 | 277 |

A stats object reference defined in RTCStatsReferences enum.

278 | 279 |
285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 |
300 |
Returns:
301 | 302 | 303 | 304 |
305 |
306 | Type: 307 |
308 |
309 | 310 | Array.<RTCStats> 311 | 312 | 313 |
314 |
315 | 316 | 317 |
318 |

An array of stats referred by key.

319 |
320 | 321 | 322 |
323 | 324 | 325 | 326 |
327 |
Example
328 | 329 |
const report = new BaseRTCStatsReport(await pc.getStats());
330 | 
331 | if (report.get(RTCStatsReferences.RTCInboundRtpVideoStreams.key)) {
332 | const stats = report.get(
333 |   RTCStatsReferences.RTCInboundRtpVideoStreams.key
334 | )[0];
335 | logger.info(`ts:${stats.timestamp} id:${stats.trackId} recv:${stats.bytesReceived}`);
336 | 337 |
338 | 339 |
340 | 341 | 342 |
343 | 344 | 345 | 346 |

has(key) → {bool}

347 | 348 | 349 | 350 | 351 | 352 |
353 |

Check if the instance has the type of stats referred by key.

354 |
355 | 356 | 357 | 358 | 359 | 360 |
361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 |
Overrides:
370 |
373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 |
399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 |
Parameters:
409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 444 | 445 | 446 | 447 | 448 | 449 | 453 | 454 | 455 | 456 | 457 |
NameTypeDescription
key 437 | 438 | 439 | string 440 | 441 | 442 | 443 | 450 |

A stats object reference defined in RTCStatsReferences enum.

451 | 452 |
458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 |
473 |
Returns:
474 | 475 | 476 | 477 |
478 |
479 | Type: 480 |
481 |
482 | 483 | bool 484 | 485 | 486 |
487 |
488 | 489 | 490 |
491 |

True if the referred stats exists.

492 |
493 | 494 | 495 |
496 | 497 | 498 | 499 |
500 |
Example
501 | 502 |
const report = new BaseRTCStatsReport(await pc.getStats());
503 | 
504 | if (report.has(RTCStatsReferences.RTCInboundRtpVideoStreams.key)) {
505 |   logger.info("receiving video.");
506 | } else {
507 |   logger.info("no video streams receiving.");
508 | }
509 | 510 |
511 | 512 |
513 | 514 | 515 | 516 | 517 | 518 | 519 |
520 | 521 |
522 | 523 | 524 | 525 | 526 |
527 | 528 |
529 | 530 | 533 | 534 | 535 | 536 | 537 | -------------------------------------------------------------------------------- /docs/RTCStatsMoment.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | RTCStatsMoment - Documentation 7 | 8 | 9 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 23 | 24 | 25 | 26 | 29 | 30 |
31 | 32 |

RTCStatsMoment

33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 |
41 | 42 |
43 | 44 |

45 | RTCStatsMoment 46 |

47 | 48 |

Class to get the momentary metrics based on the RTCStats.

49 | 50 | 51 |
52 | 53 |
54 |
55 | 56 | 57 |
58 | 59 | 60 |

Constructor

61 | 62 | 63 |

new RTCStatsMoment()

64 | 65 | 66 | 67 | 68 | 69 |
70 |

Create a RTCStatsMoment.

71 |
72 | 73 | 74 | 75 | 76 | 77 |
78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 |
See:
109 |
110 | 113 |
114 | 115 | 116 | 117 |
118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 |
142 |
Example
143 | 144 |
import { RTCStatsMoment } from 'rtcstats-wrapper';
145 | 
146 | const moment = new RTCStatsMoment();
147 | 
148 | const report = await pc.getStats();
149 | moment.update(report);
150 | moment.report();
151 | //=> {
152 | //    "send": {
153 | //        "video": { ... },
154 | //        "audio": { ... },
155 | //    },
156 | //    "receive": {
157 | //        "video": { ... },
158 | //        "audio": { ... },
159 | //    },
160 | //    "candidatePair": { ... }
161 | //}
162 | 163 |
164 | 165 |
166 | 167 |
168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 |

Methods

183 | 184 | 185 | 186 |
187 | 188 | 189 | 190 |

report() → {MomentaryReport}

191 | 192 | 193 | 194 | 195 | 196 |
197 |

Calculate the momentary value based on the updated value. 198 | MomentaryReport does not have attribute that can not be obtained.

199 |
200 | 201 | 202 | 203 | 204 | 205 |
206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 |
239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 |
261 |
Returns:
262 | 263 | 264 | 265 |
266 |
267 | Type: 268 |
269 |
270 | 271 | MomentaryReport 272 | 273 | 274 |
275 |
276 | 277 | 278 | 279 |
280 | 281 | 282 | 283 |
284 |
Example
285 | 286 |
import { RTCStatsMoment } from 'rtcstats-wrapper';
287 | 
288 | const moment = new RTCStatsMoment();
289 | 
290 | const receiver = pc.getReceivers().find(sender => sender.kind === "video");
291 | const report = receiver.getStats();
292 | moment.update(report);
293 | moment.report();
294 | //=> {
295 | //    "send": {
296 | //        "video": { ... },
297 | //    }
298 | //}
299 | 300 |
301 | 302 |
303 | 304 | 305 |
306 | 307 | 308 | 309 |

update(report)

310 | 311 | 312 | 313 | 314 | 315 |
316 |

Update the report.

317 |
318 | 319 | 320 | 321 | 322 | 323 |
324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 |
357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 |
Parameters:
367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 402 | 403 | 404 | 405 | 406 | 407 | 411 | 412 | 413 | 414 | 415 |
NameTypeDescription
report 395 | 396 | 397 | RTCStatsReport 398 | 399 | 400 | 401 | 408 |

original stats report from (pc|sender|receiver).getStats().

409 | 410 |
416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 |
433 |
Example
434 | 435 |
import { RTCStatsMoment } from 'rtcstats-wrapper';
436 | 
437 | const moment = new RTCStatsMoment();
438 | 
439 | const id = setInterval(() => {
440 |   const report = await pc.getStats();
441 |   moment.update(report);
442 | }, INTERVAL);
443 | 444 |
445 | 446 |
447 | 448 | 449 | 450 | 451 | 452 | 453 |
454 | 455 |
456 | 457 | 458 | 459 | 460 |
461 | 462 |
463 | 464 | 467 | 468 | 469 | 470 | 471 | -------------------------------------------------------------------------------- /docs/SafariRTCStatsReport.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | SafariRTCStatsReport - Documentation 7 | 8 | 9 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 23 | 24 | 25 | 26 | 29 | 30 |
31 | 32 |

SafariRTCStatsReport

33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 |
41 | 42 |
43 | 44 |

45 | SafariRTCStatsReport 46 |

47 | 48 |

Wrapped RTCStatsReport class for Safari.

49 | 50 | 51 |
52 | 53 |
54 |
55 | 56 | 57 |
58 | 59 | 60 |

Constructor

61 | 62 | 63 |

new SafariRTCStatsReport()

64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 |
74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 |
107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 |
131 | 132 |
133 | 134 | 135 |

Extends

136 | 137 | 138 | 139 | 140 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 |

Methods

159 | 160 | 161 | 162 |
163 | 164 | 165 | 166 |

get(key) → {Array.<RTCStats>}

167 | 168 | 169 | 170 | 171 | 172 |
173 |

Get the array of type of stats referred by key.

174 |
175 | 176 | 177 | 178 | 179 | 180 |
181 | 182 | 183 | 184 | 185 | 186 | 187 |
Inherited From:
188 |
191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 |
219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 |
Parameters:
229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 264 | 265 | 266 | 267 | 268 | 269 | 273 | 274 | 275 | 276 | 277 |
NameTypeDescription
key 257 | 258 | 259 | string 260 | 261 | 262 | 263 | 270 |

A stats object reference defined in RTCStatsReferences enum.

271 | 272 |
278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 |
293 |
Returns:
294 | 295 | 296 | 297 |
298 |
299 | Type: 300 |
301 |
302 | 303 | Array.<RTCStats> 304 | 305 | 306 |
307 |
308 | 309 | 310 |
311 |

An array of stats referred by key.

312 |
313 | 314 | 315 |
316 | 317 | 318 | 319 |
320 |
Example
321 | 322 |
const report = new BaseRTCStatsReport(await pc.getStats());
323 | 
324 | if (report.get(RTCStatsReferences.RTCInboundRtpVideoStreams.key)) {
325 | const stats = report.get(
326 |   RTCStatsReferences.RTCInboundRtpVideoStreams.key
327 | )[0];
328 | logger.info(`ts:${stats.timestamp} id:${stats.trackId} recv:${stats.bytesReceived}`);
329 | 330 |
331 | 332 |
333 | 334 | 335 |
336 | 337 | 338 | 339 |

has(key) → {bool}

340 | 341 | 342 | 343 | 344 | 345 |
346 |

Check if the instance has the type of stats referred by key.

347 |
348 | 349 | 350 | 351 | 352 | 353 |
354 | 355 | 356 | 357 | 358 | 359 | 360 |
Inherited From:
361 |
364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 |
392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 |
Parameters:
402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 437 | 438 | 439 | 440 | 441 | 442 | 446 | 447 | 448 | 449 | 450 |
NameTypeDescription
key 430 | 431 | 432 | string 433 | 434 | 435 | 436 | 443 |

A stats object reference defined in RTCStatsReferences enum.

444 | 445 |
451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 |
466 |
Returns:
467 | 468 | 469 | 470 |
471 |
472 | Type: 473 |
474 |
475 | 476 | bool 477 | 478 | 479 |
480 |
481 | 482 | 483 |
484 |

True if the referred stats exists.

485 |
486 | 487 | 488 |
489 | 490 | 491 | 492 |
493 |
Example
494 | 495 |
const report = new BaseRTCStatsReport(await pc.getStats());
496 | 
497 | if (report.has(RTCStatsReferences.RTCInboundRtpVideoStreams.key)) {
498 |   logger.info("receiving video.");
499 | } else {
500 |   logger.info("no video streams receiving.");
501 | }
502 | 503 |
504 | 505 |
506 | 507 | 508 | 509 | 510 | 511 | 512 |
513 | 514 |
515 | 516 | 517 | 518 | 519 |
520 | 521 |
522 | 523 | 526 | 527 | 528 | 529 | 530 | -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Bold-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyway-lab/rtcstats-wrapper/fa1fbc0aac9c5dedc561cabc0c01a3abe5abcd36/docs/fonts/OpenSans-Bold-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Bold-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyway-lab/rtcstats-wrapper/fa1fbc0aac9c5dedc561cabc0c01a3abe5abcd36/docs/fonts/OpenSans-Bold-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/OpenSans-BoldItalic-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyway-lab/rtcstats-wrapper/fa1fbc0aac9c5dedc561cabc0c01a3abe5abcd36/docs/fonts/OpenSans-BoldItalic-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-BoldItalic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyway-lab/rtcstats-wrapper/fa1fbc0aac9c5dedc561cabc0c01a3abe5abcd36/docs/fonts/OpenSans-BoldItalic-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Italic-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyway-lab/rtcstats-wrapper/fa1fbc0aac9c5dedc561cabc0c01a3abe5abcd36/docs/fonts/OpenSans-Italic-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Italic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyway-lab/rtcstats-wrapper/fa1fbc0aac9c5dedc561cabc0c01a3abe5abcd36/docs/fonts/OpenSans-Italic-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Light-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyway-lab/rtcstats-wrapper/fa1fbc0aac9c5dedc561cabc0c01a3abe5abcd36/docs/fonts/OpenSans-Light-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Light-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyway-lab/rtcstats-wrapper/fa1fbc0aac9c5dedc561cabc0c01a3abe5abcd36/docs/fonts/OpenSans-Light-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/OpenSans-LightItalic-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyway-lab/rtcstats-wrapper/fa1fbc0aac9c5dedc561cabc0c01a3abe5abcd36/docs/fonts/OpenSans-LightItalic-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-LightItalic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyway-lab/rtcstats-wrapper/fa1fbc0aac9c5dedc561cabc0c01a3abe5abcd36/docs/fonts/OpenSans-LightItalic-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Regular-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyway-lab/rtcstats-wrapper/fa1fbc0aac9c5dedc561cabc0c01a3abe5abcd36/docs/fonts/OpenSans-Regular-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyway-lab/rtcstats-wrapper/fa1fbc0aac9c5dedc561cabc0c01a3abe5abcd36/docs/fonts/OpenSans-Regular-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Semibold-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyway-lab/rtcstats-wrapper/fa1fbc0aac9c5dedc561cabc0c01a3abe5abcd36/docs/fonts/OpenSans-Semibold-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Semibold-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyway-lab/rtcstats-wrapper/fa1fbc0aac9c5dedc561cabc0c01a3abe5abcd36/docs/fonts/OpenSans-Semibold-webfont.ttf -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Semibold-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyway-lab/rtcstats-wrapper/fa1fbc0aac9c5dedc561cabc0c01a3abe5abcd36/docs/fonts/OpenSans-Semibold-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/OpenSans-SemiboldItalic-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyway-lab/rtcstats-wrapper/fa1fbc0aac9c5dedc561cabc0c01a3abe5abcd36/docs/fonts/OpenSans-SemiboldItalic-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/OpenSans-SemiboldItalic-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyway-lab/rtcstats-wrapper/fa1fbc0aac9c5dedc561cabc0c01a3abe5abcd36/docs/fonts/OpenSans-SemiboldItalic-webfont.ttf -------------------------------------------------------------------------------- /docs/fonts/OpenSans-SemiboldItalic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyway-lab/rtcstats-wrapper/fa1fbc0aac9c5dedc561cabc0c01a3abe5abcd36/docs/fonts/OpenSans-SemiboldItalic-webfont.woff -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Home - Documentation 7 | 8 | 9 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 23 | 24 | 25 | 26 | 29 | 30 |
31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 |
51 |

rtcstats-wrapper

52 |

A wrapper of RTCStats for standardization and calculation of momentary values.

53 |

features

54 |
    55 |
  • Standardize a result of getStats() which is different between each browsers.
  • 56 |
  • Calculate momentary status (such as jitterBufferDelay at one moment) from a result of getStats().
  • 57 |
58 |

Getting Started

59 |

Installation

60 |
npm install https://github.com/skyway-lab/rtcstats-wrapper.git
 61 | 
62 |

Usage

63 |

RTCStats standardizers

64 |

Since the implementation of getStats varies between each browsers, you need a shim to standardize them when you use RTCStats in your cross-browser application. 65 | rtcstats-wrapper gives it and supports Google Chrome, Firefox and Safari.

66 |

see reference

67 |
import { standardizeReport, RTCStatsReferences } from 'rtcstats-wrapper';
 68 | 
 69 | const pc = new RTCPeerConnection();
 70 | //  ...
 71 | 
 72 | const report = standardizeReport(await pc.getStats());
 73 | const receiverStats = report.get(RTCStatsReferences.RTCVideoReceivers.key);
 74 | const framesDecoded = receiverStats[0].framesDecoded;
 75 | // ...
 76 | 
77 |

RTCStatsMoment

78 |

Since RTCStats shows statical information, most of its attributes mean total number of each metrics. 79 | Therefore, you need a little calculation to get how network or media pipeline perform at that moment, so we provide the API to get the momentary metrics based on the RTCStats. 80 | After established the connection with RTCPeerConnection, then like

81 |

see reference

82 |
import { RTCStatsMoment } from 'rtcstats-wrapper';
 83 | 
 84 | const pc = new RTCPeerConnection();
 85 | const moment = new RTCStatsMoment();
 86 | // establish a conneciton ...
 87 | 
 88 | const report = await pc.getStats();
 89 | moment.update(report);
 90 | moment.report();
 91 | //=> {
 92 | //    "send": {
 93 | //        "video": { ... },
 94 | //        "audio": { ... },
 95 | //    },
 96 | //    "receive": {
 97 | //        "video": { ... },
 98 | //        "audio": { ... },
 99 | //    },
100 | //    "candidatePair": { ... }
101 | //}
102 | 
103 |

RTCStatsInsight

104 |

As one simple use case of RTCStatsMoment I know, is to calculate an momentary value periodically and take some action when a certain threshold is exceeded. 105 | For example, collect occurrences of events to analyze user's network environment information, or give some feedback to the UI to reduce stress caused by call quality felt by users. 106 | RTCStatsInsight is a simple way to do this, and provides an EventEmitter interface as follows:

107 |

see reference

108 |
import {
109 |   StatusLevels,
110 |   RTCStatsInsightEvents,
111 |   RTCStatsInsight
112 | } from 'rtcstats-wrapper';
113 | 
114 | const options = {
115 |   interval: 3000,
116 |   thresholds: {
117 |     "audio-rtt": {
118 |       unstable: 0.1
119 |     },
120 |     "audio-fractionLost": {
121 |       unstable: 0.03,
122 |       critical: 0.08,
123 |     },
124 |   },
125 |   triggerCondition: {
126 |     failCount: 2,
127 |     within: 3
128 |   }
129 | }
130 | 
131 | const insight = new RTCStatsInsight(sender, options);
132 | 
133 | insight.on(RTCStatsInsightEvents["audio-rtt"].key, event => {
134 |   if (event.level === StatusLevels.stable.key) {
135 |     console.log("Now back to stable!");
136 |   }
137 | });
138 | 
139 | insight.watch()
140 | 
141 |

API Reference

142 |

see GitHub Pages.

143 |
144 | 145 | 146 | 147 | 148 | 149 | 150 |
151 | 152 |
153 | 154 | 157 | 158 | 159 | 160 | 161 | -------------------------------------------------------------------------------- /docs/scripts/linenumber.js: -------------------------------------------------------------------------------- 1 | /*global document */ 2 | (function() { 3 | var source = document.getElementsByClassName('prettyprint source linenums'); 4 | var i = 0; 5 | var lineNumber = 0; 6 | var lineId; 7 | var lines; 8 | var totalLines; 9 | var anchorHash; 10 | 11 | if (source && source[0]) { 12 | anchorHash = document.location.hash.substring(1); 13 | lines = source[0].getElementsByTagName('li'); 14 | totalLines = lines.length; 15 | 16 | for (; i < totalLines; i++) { 17 | lineNumber++; 18 | lineId = 'line' + lineNumber; 19 | lines[i].id = lineId; 20 | if (lineId === anchorHash) { 21 | lines[i].className += ' selected'; 22 | } 23 | } 24 | } 25 | })(); 26 | -------------------------------------------------------------------------------- /docs/scripts/prettify/Apache-License-2.0.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /docs/scripts/prettify/lang-css.js: -------------------------------------------------------------------------------- 1 | PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\f\r ]+/,null," \t\r\n "]],[["str",/^"(?:[^\n\f\r"\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*"/,null],["str",/^'(?:[^\n\f\r'\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*'/,null],["lang-css-str",/^url\(([^"')]*)\)/i],["kwd",/^(?:url|rgb|!important|@import|@page|@media|@charset|inherit)(?=[^\w-]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*)\s*:/i],["com",/^\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//],["com", 2 | /^(?:<\!--|--\>)/],["lit",/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],["lit",/^#[\da-f]{3,6}/i],["pln",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i],["pun",/^[^\s\w"']+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[["kwd",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[["str",/^[^"')]+/]]),["css-str"]); 3 | -------------------------------------------------------------------------------- /docs/scripts/prettify/prettify.js: -------------------------------------------------------------------------------- 1 | var q=null;window.PR_SHOULD_USE_CONTINUATION=!0; 2 | (function(){function L(a){function m(a){var f=a.charCodeAt(0);if(f!==92)return f;var b=a.charAt(1);return(f=r[b])?f:"0"<=b&&b<="7"?parseInt(a.substring(1),8):b==="u"||b==="x"?parseInt(a.substring(2),16):a.charCodeAt(1)}function e(a){if(a<32)return(a<16?"\\x0":"\\x")+a.toString(16);a=String.fromCharCode(a);if(a==="\\"||a==="-"||a==="["||a==="]")a="\\"+a;return a}function h(a){for(var f=a.substring(1,a.length-1).match(/\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\[0-3][0-7]{0,2}|\\[0-7]{1,2}|\\[\S\s]|[^\\]/g),a= 3 | [],b=[],o=f[0]==="^",c=o?1:0,i=f.length;c122||(d<65||j>90||b.push([Math.max(65,j)|32,Math.min(d,90)|32]),d<97||j>122||b.push([Math.max(97,j)&-33,Math.min(d,122)&-33]))}}b.sort(function(a,f){return a[0]-f[0]||f[1]-a[1]});f=[];j=[NaN,NaN];for(c=0;ci[0]&&(i[1]+1>i[0]&&b.push("-"),b.push(e(i[1])));b.push("]");return b.join("")}function y(a){for(var f=a.source.match(/\[(?:[^\\\]]|\\[\S\s])*]|\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\\d+|\\[^\dux]|\(\?[!:=]|[()^]|[^()[\\^]+/g),b=f.length,d=[],c=0,i=0;c=2&&a==="["?f[c]=h(j):a!=="\\"&&(f[c]=j.replace(/[A-Za-z]/g,function(a){a=a.charCodeAt(0);return"["+String.fromCharCode(a&-33,a|32)+"]"}));return f.join("")}for(var t=0,s=!1,l=!1,p=0,d=a.length;p=5&&"lang-"===b.substring(0,5))&&!(o&&typeof o[1]==="string"))c=!1,b="src";c||(r[f]=b)}i=d;d+=f.length;if(c){c=o[1];var j=f.indexOf(c),k=j+c.length;o[2]&&(k=f.length-o[2].length,j=k-c.length);b=b.substring(5);B(l+i,f.substring(0,j),e,p);B(l+i+j,c,C(b,c),p);B(l+i+k,f.substring(k),e,p)}else p.push(l+i,b)}a.e=p}var h={},y;(function(){for(var e=a.concat(m), 9 | l=[],p={},d=0,g=e.length;d=0;)h[n.charAt(k)]=r;r=r[1];n=""+r;p.hasOwnProperty(n)||(l.push(r),p[n]=q)}l.push(/[\S\s]/);y=L(l)})();var t=m.length;return e}function u(a){var m=[],e=[];a.tripleQuotedStrings?m.push(["str",/^(?:'''(?:[^'\\]|\\[\S\s]|''?(?=[^']))*(?:'''|$)|"""(?:[^"\\]|\\[\S\s]|""?(?=[^"]))*(?:"""|$)|'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$))/,q,"'\""]):a.multiLineStrings?m.push(["str",/^(?:'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$)|`(?:[^\\`]|\\[\S\s])*(?:`|$))/, 10 | q,"'\"`"]):m.push(["str",/^(?:'(?:[^\n\r'\\]|\\.)*(?:'|$)|"(?:[^\n\r"\\]|\\.)*(?:"|$))/,q,"\"'"]);a.verbatimStrings&&e.push(["str",/^@"(?:[^"]|"")*(?:"|$)/,q]);var h=a.hashComments;h&&(a.cStyleComments?(h>1?m.push(["com",/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,q,"#"]):m.push(["com",/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\n\r]*)/,q,"#"]),e.push(["str",/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,q])):m.push(["com",/^#[^\n\r]*/, 11 | q,"#"]));a.cStyleComments&&(e.push(["com",/^\/\/[^\n\r]*/,q]),e.push(["com",/^\/\*[\S\s]*?(?:\*\/|$)/,q]));a.regexLiterals&&e.push(["lang-regex",/^(?:^^\.?|[!+-]|!=|!==|#|%|%=|&|&&|&&=|&=|\(|\*|\*=|\+=|,|-=|->|\/|\/=|:|::|;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|[?@[^]|\^=|\^\^|\^\^=|{|\||\|=|\|\||\|\|=|~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\s*(\/(?=[^*/])(?:[^/[\\]|\\[\S\s]|\[(?:[^\\\]]|\\[\S\s])*(?:]|$))+\/)/]);(h=a.types)&&e.push(["typ",h]);a=(""+a.keywords).replace(/^ | $/g, 12 | "");a.length&&e.push(["kwd",RegExp("^(?:"+a.replace(/[\s,]+/g,"|")+")\\b"),q]);m.push(["pln",/^\s+/,q," \r\n\t\xa0"]);e.push(["lit",/^@[$_a-z][\w$@]*/i,q],["typ",/^(?:[@_]?[A-Z]+[a-z][\w$@]*|\w+_t\b)/,q],["pln",/^[$_a-z][\w$@]*/i,q],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i,q,"0123456789"],["pln",/^\\[\S\s]?/,q],["pun",/^.[^\s\w"-$'./@\\`]*/,q]);return x(m,e)}function D(a,m){function e(a){switch(a.nodeType){case 1:if(k.test(a.className))break;if("BR"===a.nodeName)h(a), 13 | a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)e(a);break;case 3:case 4:if(p){var b=a.nodeValue,d=b.match(t);if(d){var c=b.substring(0,d.index);a.nodeValue=c;(b=b.substring(d.index+d[0].length))&&a.parentNode.insertBefore(s.createTextNode(b),a.nextSibling);h(a);c||a.parentNode.removeChild(a)}}}}function h(a){function b(a,d){var e=d?a.cloneNode(!1):a,f=a.parentNode;if(f){var f=b(f,1),g=a.nextSibling;f.appendChild(e);for(var h=g;h;h=g)g=h.nextSibling,f.appendChild(h)}return e} 14 | for(;!a.nextSibling;)if(a=a.parentNode,!a)return;for(var a=b(a.nextSibling,0),e;(e=a.parentNode)&&e.nodeType===1;)a=e;d.push(a)}var k=/(?:^|\s)nocode(?:\s|$)/,t=/\r\n?|\n/,s=a.ownerDocument,l;a.currentStyle?l=a.currentStyle.whiteSpace:window.getComputedStyle&&(l=s.defaultView.getComputedStyle(a,q).getPropertyValue("white-space"));var p=l&&"pre"===l.substring(0,3);for(l=s.createElement("LI");a.firstChild;)l.appendChild(a.firstChild);for(var d=[l],g=0;g=0;){var h=m[e];A.hasOwnProperty(h)?window.console&&console.warn("cannot override language handler %s",h):A[h]=a}}function C(a,m){if(!a||!A.hasOwnProperty(a))a=/^\s*=o&&(h+=2);e>=c&&(a+=2)}}catch(w){"console"in window&&console.log(w&&w.stack?w.stack:w)}}var v=["break,continue,do,else,for,if,return,while"],w=[[v,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"], 18 | "catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],F=[w,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"],G=[w,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"], 19 | H=[G,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"],w=[w,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"],I=[v,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"], 20 | J=[v,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"],v=[v,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"],K=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/,N=/\S/,O=u({keywords:[F,H,w,"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END"+ 21 | I,J,v],hashComments:!0,cStyleComments:!0,multiLineStrings:!0,regexLiterals:!0}),A={};k(O,["default-code"]);k(x([],[["pln",/^[^]*(?:>|$)/],["com",/^<\!--[\S\s]*?(?:--\>|$)/],["lang-",/^<\?([\S\s]+?)(?:\?>|$)/],["lang-",/^<%([\S\s]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\S\s]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\S\s]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\S\s]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]), 22 | ["default-markup","htm","html","mxml","xhtml","xml","xsl"]);k(x([["pln",/^\s+/,q," \t\r\n"],["atv",/^(?:"[^"]*"?|'[^']*'?)/,q,"\"'"]],[["tag",/^^<\/?[a-z](?:[\w-.:]*\w)?|\/?>$/i],["atn",/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^\s"'>]*(?:[^\s"'/>]|\/(?=\s)))/],["pun",/^[/<->]+/],["lang-js",/^on\w+\s*=\s*"([^"]+)"/i],["lang-js",/^on\w+\s*=\s*'([^']+)'/i],["lang-js",/^on\w+\s*=\s*([^\s"'>]+)/i],["lang-css",/^style\s*=\s*"([^"]+)"/i],["lang-css",/^style\s*=\s*'([^']+)'/i],["lang-css", 23 | /^style\s*=\s*([^\s"'>]+)/i]]),["in.tag"]);k(x([],[["atv",/^[\S\s]+/]]),["uq.val"]);k(u({keywords:F,hashComments:!0,cStyleComments:!0,types:K}),["c","cc","cpp","cxx","cyc","m"]);k(u({keywords:"null,true,false"}),["json"]);k(u({keywords:H,hashComments:!0,cStyleComments:!0,verbatimStrings:!0,types:K}),["cs"]);k(u({keywords:G,cStyleComments:!0}),["java"]);k(u({keywords:v,hashComments:!0,multiLineStrings:!0}),["bsh","csh","sh"]);k(u({keywords:I,hashComments:!0,multiLineStrings:!0,tripleQuotedStrings:!0}), 24 | ["cv","py"]);k(u({keywords:"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["perl","pl","pm"]);k(u({keywords:J,hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["rb"]);k(u({keywords:w,cStyleComments:!0,regexLiterals:!0}),["js"]);k(u({keywords:"all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes", 25 | hashComments:3,cStyleComments:!0,multilineStrings:!0,tripleQuotedStrings:!0,regexLiterals:!0}),["coffee"]);k(x([],[["str",/^[\S\s]+/]]),["regex"]);window.prettyPrintOne=function(a,m,e){var h=document.createElement("PRE");h.innerHTML=a;e&&D(h,e);E({g:m,i:e,h:h});return h.innerHTML};window.prettyPrint=function(a){function m(){for(var e=window.PR_SHOULD_USE_CONTINUATION?l.now()+250:Infinity;p=0){var k=k.match(g),f,b;if(b= 26 | !k){b=n;for(var o=void 0,c=b.firstChild;c;c=c.nextSibling)var i=c.nodeType,o=i===1?o?b:c:i===3?N.test(c.nodeValue)?b:o:o;b=(f=o===b?void 0:o)&&"CODE"===f.tagName}b&&(k=f.className.match(g));k&&(k=k[1]);b=!1;for(o=n.parentNode;o;o=o.parentNode)if((o.tagName==="pre"||o.tagName==="code"||o.tagName==="xmp")&&o.className&&o.className.indexOf("prettyprint")>=0){b=!0;break}b||((b=(b=n.className.match(/\blinenums\b(?::(\d+))?/))?b[1]&&b[1].length?+b[1]:!0:!1)&&D(n,b),d={g:k,h:n,i:b},E(d))}}p code { 197 | font-size: 0.85em; 198 | } 199 | 200 | .readme table { 201 | margin-bottom: 1em; 202 | border-collapse: collapse; 203 | border-spacing: 0; 204 | } 205 | 206 | .readme table tr { 207 | background-color: #fff; 208 | border-top: 1px solid #ccc; 209 | } 210 | 211 | .readme table th, 212 | .readme table td { 213 | padding: 6px 13px; 214 | border: 1px solid #ddd; 215 | } 216 | 217 | .readme table tr:nth-child(2n) { 218 | background-color: #f8f8f8; 219 | } 220 | 221 | /** Nav **/ 222 | nav { 223 | float: left; 224 | display: block; 225 | width: 250px; 226 | background: #fff; 227 | overflow: auto; 228 | position: fixed; 229 | height: 100%; 230 | padding: 10px; 231 | border-right: 1px solid #eee; 232 | /* box-shadow: 0 0 3px rgba(0,0,0,0.1); */ 233 | } 234 | 235 | nav li { 236 | list-style: none; 237 | padding: 0; 238 | margin: 0; 239 | } 240 | 241 | .nav-heading { 242 | margin-top: 10px; 243 | font-weight: bold; 244 | } 245 | 246 | .nav-heading a { 247 | color: #888; 248 | font-size: 14px; 249 | display: inline-block; 250 | } 251 | 252 | .nav-item-type { 253 | /* margin-left: 5px; */ 254 | width: 18px; 255 | height: 18px; 256 | display: inline-block; 257 | text-align: center; 258 | border-radius: 0.2em; 259 | margin-right: 5px; 260 | font-weight: bold; 261 | line-height: 20px; 262 | font-size: 13px; 263 | } 264 | 265 | .type-function { 266 | background: #B3E5FC; 267 | color: #0288D1; 268 | } 269 | 270 | .type-class { 271 | background: #D1C4E9; 272 | color: #4527A0; 273 | } 274 | 275 | .type-member { 276 | background: #C8E6C9; 277 | color: #388E3C; 278 | } 279 | 280 | .type-module { 281 | background: #E1BEE7; 282 | color: #7B1FA2; 283 | } 284 | 285 | 286 | /** Footer **/ 287 | footer { 288 | color: hsl(0, 0%, 28%); 289 | margin-left: 250px; 290 | display: block; 291 | padding: 30px; 292 | font-style: italic; 293 | font-size: 90%; 294 | border-top: 1px solid #eee; 295 | } 296 | 297 | .ancestors { 298 | color: #999 299 | } 300 | 301 | .ancestors a { 302 | color: #999 !important; 303 | text-decoration: none; 304 | } 305 | 306 | .clear { 307 | clear: both 308 | } 309 | 310 | .important { 311 | font-weight: bold; 312 | color: #950B02; 313 | } 314 | 315 | .yes-def { 316 | text-indent: -1000px 317 | } 318 | 319 | .type-signature { 320 | color: #aaa 321 | } 322 | 323 | .name, .signature { 324 | font-family: Consolas, Monaco, 'Andale Mono', monospace 325 | } 326 | 327 | .details { 328 | margin-top: 14px; 329 | border-left: 2px solid #DDD; 330 | line-height: 30px; 331 | } 332 | 333 | .details dt { 334 | width: 120px; 335 | float: left; 336 | padding-left: 10px; 337 | } 338 | 339 | .details dd { 340 | margin-left: 70px 341 | } 342 | 343 | .details ul { 344 | margin: 0 345 | } 346 | 347 | .details ul { 348 | list-style-type: none 349 | } 350 | 351 | .details li { 352 | margin-left: 30px 353 | } 354 | 355 | .details pre.prettyprint { 356 | margin: 0 357 | } 358 | 359 | .details .object-value { 360 | padding-top: 0 361 | } 362 | 363 | .description { 364 | margin-bottom: 1em; 365 | margin-top: 1em; 366 | } 367 | 368 | .code-caption { 369 | font-style: italic; 370 | font-size: 107%; 371 | margin: 0; 372 | } 373 | 374 | .prettyprint { 375 | font-size: 13px; 376 | border: 1px solid #ddd; 377 | border-radius: 3px; 378 | box-shadow: 0 1px 3px hsla(0, 0%, 0%, 0.05); 379 | overflow: auto; 380 | } 381 | 382 | .prettyprint.source { 383 | width: inherit 384 | } 385 | 386 | .prettyprint code { 387 | font-size: 12px; 388 | line-height: 18px; 389 | display: block; 390 | background-color: #fff; 391 | color: #4D4E53; 392 | } 393 | 394 | .prettyprint code:empty:before { 395 | content: ''; 396 | } 397 | 398 | .prettyprint > code { 399 | padding: 15px 400 | } 401 | 402 | .prettyprint .linenums code { 403 | padding: 0 15px 404 | } 405 | 406 | .prettyprint .linenums li:first-of-type code { 407 | padding-top: 15px 408 | } 409 | 410 | .prettyprint code span.line { 411 | display: inline-block 412 | } 413 | 414 | .prettyprint.linenums { 415 | padding-left: 70px; 416 | -webkit-user-select: none; 417 | -moz-user-select: none; 418 | -ms-user-select: none; 419 | user-select: none; 420 | } 421 | 422 | .prettyprint.linenums ol { 423 | padding-left: 0 424 | } 425 | 426 | .prettyprint.linenums li { 427 | border-left: 3px #ddd solid 428 | } 429 | 430 | .prettyprint.linenums li.selected, .prettyprint.linenums li.selected * { 431 | background-color: lightyellow 432 | } 433 | 434 | .prettyprint.linenums li * { 435 | -webkit-user-select: text; 436 | -moz-user-select: text; 437 | -ms-user-select: text; 438 | user-select: text; 439 | } 440 | 441 | .params, .props { 442 | border-spacing: 0; 443 | border: 1px solid #ddd; 444 | border-collapse: collapse; 445 | border-radius: 3px; 446 | box-shadow: 0 1px 3px rgba(0,0,0,0.1); 447 | width: 100%; 448 | font-size: 14px; 449 | /* margin-left: 15px; */ 450 | } 451 | 452 | .params .name, .props .name, .name code { 453 | color: #4D4E53; 454 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 455 | font-size: 100%; 456 | } 457 | 458 | .params td, .params th, .props td, .props th { 459 | margin: 0px; 460 | text-align: left; 461 | vertical-align: top; 462 | padding: 10px; 463 | display: table-cell; 464 | } 465 | 466 | .params td { 467 | border-top: 1px solid #eee 468 | } 469 | 470 | .params thead tr, .props thead tr { 471 | background-color: #fff; 472 | font-weight: bold; 473 | } 474 | 475 | .params .params thead tr, .props .props thead tr { 476 | background-color: #fff; 477 | font-weight: bold; 478 | } 479 | 480 | .params td.description > p:first-child, .props td.description > p:first-child { 481 | margin-top: 0; 482 | padding-top: 0; 483 | } 484 | 485 | .params td.description > p:last-child, .props td.description > p:last-child { 486 | margin-bottom: 0; 487 | padding-bottom: 0; 488 | } 489 | 490 | dl.param-type { 491 | /* border-bottom: 1px solid hsl(0, 0%, 87%); */ 492 | margin: 0; 493 | padding: 0; 494 | font-size: 16px; 495 | } 496 | 497 | .param-type dt, .param-type dd { 498 | display: inline-block 499 | } 500 | 501 | .param-type dd { 502 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 503 | display: inline-block; 504 | padding: 0; 505 | margin: 0; 506 | font-size: 14px; 507 | } 508 | 509 | .disabled { 510 | color: #454545 511 | } 512 | 513 | /* navicon button */ 514 | .navicon-button { 515 | display: none; 516 | position: relative; 517 | padding: 2.0625rem 1.5rem; 518 | transition: 0.25s; 519 | cursor: pointer; 520 | user-select: none; 521 | opacity: .8; 522 | } 523 | .navicon-button .navicon:before, .navicon-button .navicon:after { 524 | transition: 0.25s; 525 | } 526 | .navicon-button:hover { 527 | transition: 0.5s; 528 | opacity: 1; 529 | } 530 | .navicon-button:hover .navicon:before, .navicon-button:hover .navicon:after { 531 | transition: 0.25s; 532 | } 533 | .navicon-button:hover .navicon:before { 534 | top: .825rem; 535 | } 536 | .navicon-button:hover .navicon:after { 537 | top: -.825rem; 538 | } 539 | 540 | /* navicon */ 541 | .navicon { 542 | position: relative; 543 | width: 2.5em; 544 | height: .3125rem; 545 | background: #000; 546 | transition: 0.3s; 547 | border-radius: 2.5rem; 548 | } 549 | .navicon:before, .navicon:after { 550 | display: block; 551 | content: ""; 552 | height: .3125rem; 553 | width: 2.5rem; 554 | background: #000; 555 | position: absolute; 556 | z-index: -1; 557 | transition: 0.3s 0.25s; 558 | border-radius: 1rem; 559 | } 560 | .navicon:before { 561 | top: .625rem; 562 | } 563 | .navicon:after { 564 | top: -.625rem; 565 | } 566 | 567 | /* open */ 568 | .nav-trigger:checked + label:not(.steps) .navicon:before, 569 | .nav-trigger:checked + label:not(.steps) .navicon:after { 570 | top: 0 !important; 571 | } 572 | 573 | .nav-trigger:checked + label .navicon:before, 574 | .nav-trigger:checked + label .navicon:after { 575 | transition: 0.5s; 576 | } 577 | 578 | /* Minus */ 579 | .nav-trigger:checked + label { 580 | transform: scale(0.75); 581 | } 582 | 583 | /* × and + */ 584 | .nav-trigger:checked + label.plus .navicon, 585 | .nav-trigger:checked + label.x .navicon { 586 | background: transparent; 587 | } 588 | 589 | .nav-trigger:checked + label.plus .navicon:before, 590 | .nav-trigger:checked + label.x .navicon:before { 591 | transform: rotate(-45deg); 592 | background: #FFF; 593 | } 594 | 595 | .nav-trigger:checked + label.plus .navicon:after, 596 | .nav-trigger:checked + label.x .navicon:after { 597 | transform: rotate(45deg); 598 | background: #FFF; 599 | } 600 | 601 | .nav-trigger:checked + label.plus { 602 | transform: scale(0.75) rotate(45deg); 603 | } 604 | 605 | .nav-trigger:checked ~ nav { 606 | left: 0 !important; 607 | } 608 | 609 | .nav-trigger:checked ~ .overlay { 610 | display: block; 611 | } 612 | 613 | .nav-trigger { 614 | position: fixed; 615 | top: 0; 616 | clip: rect(0, 0, 0, 0); 617 | } 618 | 619 | .overlay { 620 | display: none; 621 | position: fixed; 622 | top: 0; 623 | bottom: 0; 624 | left: 0; 625 | right: 0; 626 | width: 100%; 627 | height: 100%; 628 | background: hsla(0, 0%, 0%, 0.5); 629 | z-index: 1; 630 | } 631 | 632 | .section-method { 633 | margin-bottom: 30px; 634 | padding-bottom: 30px; 635 | border-bottom: 1px solid #eee; 636 | } 637 | 638 | @media only screen and (min-width: 320px) and (max-width: 680px) { 639 | body { 640 | overflow-x: hidden; 641 | } 642 | 643 | nav { 644 | background: #FFF; 645 | width: 250px; 646 | height: 100%; 647 | position: fixed; 648 | top: 0; 649 | right: 0; 650 | bottom: 0; 651 | left: -250px; 652 | z-index: 3; 653 | padding: 0 10px; 654 | transition: left 0.2s; 655 | } 656 | 657 | .navicon-button { 658 | display: inline-block; 659 | position: fixed; 660 | top: 1.5em; 661 | right: 0; 662 | z-index: 2; 663 | } 664 | 665 | #main { 666 | width: 100%; 667 | min-width: 360px; 668 | } 669 | 670 | #main h1.page-title { 671 | margin: 1em 0; 672 | } 673 | 674 | #main section { 675 | padding: 0; 676 | } 677 | 678 | footer { 679 | margin-left: 0; 680 | } 681 | } 682 | 683 | @media only print { 684 | nav { 685 | display: none; 686 | } 687 | 688 | #main { 689 | float: none; 690 | width: 100%; 691 | } 692 | } 693 | -------------------------------------------------------------------------------- /docs/styles/prettify-jsdoc.css: -------------------------------------------------------------------------------- 1 | /* JSDoc prettify.js theme */ 2 | 3 | /* plain text */ 4 | .pln { 5 | color: #000000; 6 | font-weight: normal; 7 | font-style: normal; 8 | } 9 | 10 | /* string content */ 11 | .str { 12 | color: hsl(104, 100%, 24%); 13 | font-weight: normal; 14 | font-style: normal; 15 | } 16 | 17 | /* a keyword */ 18 | .kwd { 19 | color: #000000; 20 | font-weight: bold; 21 | font-style: normal; 22 | } 23 | 24 | /* a comment */ 25 | .com { 26 | font-weight: normal; 27 | font-style: italic; 28 | } 29 | 30 | /* a type name */ 31 | .typ { 32 | color: #000000; 33 | font-weight: normal; 34 | font-style: normal; 35 | } 36 | 37 | /* a literal value */ 38 | .lit { 39 | color: #006400; 40 | font-weight: normal; 41 | font-style: normal; 42 | } 43 | 44 | /* punctuation */ 45 | .pun { 46 | color: #000000; 47 | font-weight: bold; 48 | font-style: normal; 49 | } 50 | 51 | /* lisp open bracket */ 52 | .opn { 53 | color: #000000; 54 | font-weight: bold; 55 | font-style: normal; 56 | } 57 | 58 | /* lisp close bracket */ 59 | .clo { 60 | color: #000000; 61 | font-weight: bold; 62 | font-style: normal; 63 | } 64 | 65 | /* a markup tag name */ 66 | .tag { 67 | color: #006400; 68 | font-weight: normal; 69 | font-style: normal; 70 | } 71 | 72 | /* a markup attribute name */ 73 | .atn { 74 | color: #006400; 75 | font-weight: normal; 76 | font-style: normal; 77 | } 78 | 79 | /* a markup attribute value */ 80 | .atv { 81 | color: #006400; 82 | font-weight: normal; 83 | font-style: normal; 84 | } 85 | 86 | /* a declaration */ 87 | .dec { 88 | color: #000000; 89 | font-weight: bold; 90 | font-style: normal; 91 | } 92 | 93 | /* a variable name */ 94 | .var { 95 | color: #000000; 96 | font-weight: normal; 97 | font-style: normal; 98 | } 99 | 100 | /* a function name */ 101 | .fun { 102 | color: #000000; 103 | font-weight: bold; 104 | font-style: normal; 105 | } 106 | 107 | /* Specify class=linenums on a pre to get line numbering */ 108 | ol.linenums { 109 | margin-top: 0; 110 | margin-bottom: 0; 111 | } 112 | -------------------------------------------------------------------------------- /docs/styles/prettify-tomorrow.css: -------------------------------------------------------------------------------- 1 | /* Tomorrow Theme */ 2 | /* Original theme - https://github.com/chriskempson/tomorrow-theme */ 3 | /* Pretty printing styles. Used with prettify.js. */ 4 | /* SPAN elements with the classes below are added by prettyprint. */ 5 | /* plain text */ 6 | .pln { 7 | color: #4d4d4c; } 8 | 9 | @media screen { 10 | /* string content */ 11 | .str { 12 | color: hsl(104, 100%, 24%); } 13 | 14 | /* a keyword */ 15 | .kwd { 16 | color: hsl(240, 100%, 50%); } 17 | 18 | /* a comment */ 19 | .com { 20 | color: hsl(0, 0%, 60%); } 21 | 22 | /* a type name */ 23 | .typ { 24 | color: hsl(240, 100%, 32%); } 25 | 26 | /* a literal value */ 27 | .lit { 28 | color: hsl(240, 100%, 40%); } 29 | 30 | /* punctuation */ 31 | .pun { 32 | color: #000000; } 33 | 34 | /* lisp open bracket */ 35 | .opn { 36 | color: #000000; } 37 | 38 | /* lisp close bracket */ 39 | .clo { 40 | color: #000000; } 41 | 42 | /* a markup tag name */ 43 | .tag { 44 | color: #c82829; } 45 | 46 | /* a markup attribute name */ 47 | .atn { 48 | color: #f5871f; } 49 | 50 | /* a markup attribute value */ 51 | .atv { 52 | color: #3e999f; } 53 | 54 | /* a declaration */ 55 | .dec { 56 | color: #f5871f; } 57 | 58 | /* a variable name */ 59 | .var { 60 | color: #c82829; } 61 | 62 | /* a function name */ 63 | .fun { 64 | color: #4271ae; } } 65 | /* Use higher contrast and text-weight for printable form. */ 66 | @media print, projection { 67 | .str { 68 | color: #060; } 69 | 70 | .kwd { 71 | color: #006; 72 | font-weight: bold; } 73 | 74 | .com { 75 | color: #600; 76 | font-style: italic; } 77 | 78 | .typ { 79 | color: #404; 80 | font-weight: bold; } 81 | 82 | .lit { 83 | color: #044; } 84 | 85 | .pun, .opn, .clo { 86 | color: #440; } 87 | 88 | .tag { 89 | color: #006; 90 | font-weight: bold; } 91 | 92 | .atn { 93 | color: #404; } 94 | 95 | .atv { 96 | color: #060; } } 97 | /* Style */ 98 | /* 99 | pre.prettyprint { 100 | background: white; 101 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 102 | font-size: 12px; 103 | line-height: 1.5; 104 | border: 1px solid #ccc; 105 | padding: 10px; } 106 | */ 107 | 108 | /* Specify class=linenums on a pre to get line numbering */ 109 | ol.linenums { 110 | margin-top: 0; 111 | margin-bottom: 0; } 112 | 113 | /* IE indents via margin-left */ 114 | li.L0, 115 | li.L1, 116 | li.L2, 117 | li.L3, 118 | li.L4, 119 | li.L5, 120 | li.L6, 121 | li.L7, 122 | li.L8, 123 | li.L9 { 124 | /* */ } 125 | 126 | /* Alternate shading for lines */ 127 | li.L1, 128 | li.L3, 129 | li.L5, 130 | li.L7, 131 | li.L9 { 132 | /* */ } 133 | -------------------------------------------------------------------------------- /examples/insight/README.md: -------------------------------------------------------------------------------- 1 | # Example of RTCStatsInsight 2 | ## serve in localhost 3 | 1. write your SkyWay API key which allows domain `localhost` to `key.js` 4 | 2. build this library 5 | 6 | ```sh 7 | $ # (cd to root directory of this repository.) 8 | $ npm run build 9 | ``` 10 | 11 | 3. serve http locally 12 | 13 | ```sh 14 | $ python3 -m http.server 15 | ``` 16 | 17 | 4. open `/examples/insight/index.html` 18 | 19 | ```sh 20 | $ open http://localhost:8000/ 21 | ``` 22 | 23 | ## try bad network condition 24 | in macOS, you can try bad network condition with `dnctl` and `pfctl`. 25 | 26 | 1. create a new dummy net 27 | 28 | ```sh 29 | $ sudo dnctl pipe 1 config delay 100ms plr 0.01 30 | ``` 31 | 32 | 2. edit and load pf.conf 33 | 34 | ```sh 35 | $ cat /etc/pf.conf 36 | dummynet out quick proto udp all pipe 1 37 | $ sudo pfctl -f /etc/pf.conf 38 | ``` 39 | 40 | 3. enable pfctl 41 | 42 | ```sh 43 | $ sudo pfctl -e 44 | ``` 45 | 46 | 4. After the experimentation, Don't forget disable pfctl :D 47 | 48 | ```sh 49 | $ sudo pfctl -d 50 | ``` 51 | 52 | FYI, in Linux, `NETEM(8)` is famous as a network emulator. 53 | -------------------------------------------------------------------------------- /examples/insight/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | SkyWay - P2P Media example 7 | 8 | 9 | 10 |
11 |

P2P Media example

12 |

13 | Enter remote peer ID to call. 14 |

15 |
16 |
17 | 18 |
19 |
20 | 21 |

Your ID:

22 | 23 | 24 | 25 |
26 |
27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /examples/insight/key.js: -------------------------------------------------------------------------------- 1 | window.__SKYWAY_KEY__ = ""; 2 | -------------------------------------------------------------------------------- /examples/insight/script.js: -------------------------------------------------------------------------------- 1 | const Peer = window.Peer; 2 | const RTCStatsInsight = window.RTCStatsWrapper.RTCStatsInsight; 3 | const RTCStatsInsightEvents = window.RTCStatsWrapper.RTCStatsInsightEvents; 4 | 5 | (async function main() { 6 | const localVideo = document.getElementById("js-local-stream"); 7 | const localId = document.getElementById("js-local-id"); 8 | const callTrigger = document.getElementById("js-call-trigger"); 9 | const closeTrigger = document.getElementById("js-close-trigger"); 10 | const remoteVideo = document.getElementById("js-remote-stream"); 11 | const remoteId = document.getElementById("js-remote-id"); 12 | let insight; 13 | 14 | async function handleMediaConnection(mediaConnection) { 15 | mediaConnection.on("stream", async stream => { 16 | // Render remote stream for caller 17 | remoteVideo.srcObject = stream; 18 | remoteVideo.playsInline = true; 19 | await remoteVideo.play().catch(console.error); 20 | 21 | const pc = mediaConnection.getPeerConnection(); 22 | insight = new RTCStatsInsight(pc); 23 | 24 | for (const eventKey of RTCStatsInsightEvents.enums) { 25 | insight.on(eventKey, event => { 26 | console.log(event); 27 | }); 28 | } 29 | 30 | insight.watch(); 31 | }); 32 | 33 | mediaConnection.once("close", () => { 34 | remoteVideo.srcObject.getTracks().forEach(track => track.stop()); 35 | remoteVideo.srcObject = null; 36 | insight.stop(); 37 | }); 38 | 39 | closeTrigger.addEventListener("click", () => mediaConnection.close(true)); 40 | } 41 | 42 | const localStream = await navigator.mediaDevices 43 | .getUserMedia({ 44 | audio: true, 45 | video: true 46 | }) 47 | .catch(console.error); 48 | 49 | // Render local stream 50 | localVideo.muted = true; 51 | localVideo.srcObject = localStream; 52 | localVideo.playsInline = true; 53 | await localVideo.play().catch(console.error); 54 | 55 | const peer = new Peer({ 56 | key: window.__SKYWAY_KEY__, 57 | debug: 3 58 | }); 59 | 60 | // Register caller handler 61 | callTrigger.addEventListener("click", () => { 62 | // Note that you need to ensure the peer has connected to signaling server 63 | // before using methods of peer instance. 64 | if (!peer.open) { 65 | return; 66 | } 67 | 68 | const mediaConnection = peer.call(remoteId.value, localStream); 69 | handleMediaConnection(mediaConnection); 70 | }); 71 | 72 | peer.once("open", id => (localId.textContent = id)); 73 | 74 | // Register callee handler 75 | peer.on("call", mediaConnection => { 76 | mediaConnection.answer(localStream); 77 | handleMediaConnection(mediaConnection); 78 | }); 79 | 80 | peer.on("error", console.error); 81 | })(); 82 | -------------------------------------------------------------------------------- /examples/insight/style.css: -------------------------------------------------------------------------------- 1 | /* normalize */ 2 | body { margin: 0; } 3 | 4 | /* global styles */ 5 | video { 6 | background-color: #111; 7 | width: 100%; 8 | } 9 | 10 | .heading { 11 | text-align: center; 12 | margin-bottom: 0; 13 | } 14 | 15 | .note { 16 | text-align: center; 17 | } 18 | 19 | .container { 20 | margin-left: auto; 21 | margin-right: auto; 22 | width: 980px; 23 | } 24 | 25 | /* p2p-media styles */ 26 | .p2p-media { 27 | display: flex; 28 | align-items: center; 29 | flex-direction: column; 30 | } 31 | 32 | .p2p-media .remote-stream { 33 | width: 50%; 34 | } 35 | 36 | .p2p-media .local-stream { 37 | width: 30%; 38 | } 39 | 40 | /* p2p-data styles */ 41 | .p2p-data { 42 | display: grid; 43 | grid-template-columns: 30% 1fr; 44 | margin: 0 8px; 45 | } 46 | 47 | .p2p-data .messages { 48 | background-color: #eee; 49 | min-height: 100px; 50 | padding: 8px; 51 | margin-top: 0; 52 | } 53 | 54 | /* room */ 55 | .room { 56 | display: grid; 57 | grid-template-columns: 30% 40% 30%; 58 | gap: 8px; 59 | margin: 0 8px; 60 | } 61 | 62 | .room .remote-streams { 63 | background-color: #f6fbff; 64 | } 65 | 66 | .room .messages { 67 | background-color: #eee; 68 | min-height: 100px; 69 | padding: 8px; 70 | margin-top: 0; 71 | } 72 | 73 | /* broadcast */ 74 | .broadcast { 75 | display: flex; 76 | align-items: center; 77 | flex-direction: column; 78 | } 79 | 80 | .broadcast .broadcast-stream { 81 | width: 50%; 82 | } 83 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /* global module */ 2 | // For a detailed explanation regarding each configuration property, visit: 3 | // https://jestjs.io/docs/en/configuration.html 4 | 5 | module.exports = { 6 | // All imported modules in your tests should be mocked automatically 7 | // automock: false, 8 | 9 | // Stop running tests after `n` failures 10 | // bail: 0, 11 | 12 | // Respect "browser" field in package.json when resolving modules 13 | // browser: false, 14 | 15 | // The directory where Jest should store its cached dependency information 16 | // cacheDirectory: "/private/var/folders/2t/jn3srhkj6sxg2br3w6651q140000gs/T/jest_e1", 17 | 18 | // Automatically clear mock calls and instances between every test 19 | clearMocks: true, 20 | 21 | // Indicates whether the coverage information should be collected while executing the test 22 | // collectCoverage: false, 23 | 24 | // An array of glob patterns indicating a set of files for which coverage information should be collected 25 | // collectCoverageFrom: null, 26 | 27 | // The directory where Jest should output its coverage files 28 | coverageDirectory: "coverage", 29 | 30 | // An array of regexp pattern strings used to skip coverage collection 31 | // coveragePathIgnorePatterns: [ 32 | // "/node_modules/" 33 | // ], 34 | 35 | // A list of reporter names that Jest uses when writing coverage reports 36 | // coverageReporters: [ 37 | // "json", 38 | // "text", 39 | // "lcov", 40 | // "clover" 41 | // ], 42 | 43 | // An object that configures minimum threshold enforcement for coverage results 44 | // coverageThreshold: null, 45 | 46 | // A path to a custom dependency extractor 47 | // dependencyExtractor: null, 48 | 49 | // Make calling deprecated APIs throw helpful error messages 50 | // errorOnDeprecated: false, 51 | 52 | // Force coverage collection from ignored files using an array of glob patterns 53 | // forceCoverageMatch: [], 54 | 55 | // A path to a module which exports an async function that is triggered once before all test suites 56 | // globalSetup: null, 57 | 58 | // A path to a module which exports an async function that is triggered once after all test suites 59 | // globalTeardown: null, 60 | 61 | // A set of global variables that need to be available in all test environments 62 | // globals: {}, 63 | 64 | // An array of directory names to be searched recursively up from the requiring module's location 65 | // moduleDirectories: [ 66 | // "node_modules" 67 | // ], 68 | 69 | // An array of file extensions your modules use 70 | // moduleFileExtensions: [ 71 | // "js", 72 | // "json", 73 | // "jsx", 74 | // "ts", 75 | // "tsx", 76 | // "node" 77 | // ], 78 | 79 | // A map from regular expressions to module names that allow to stub out resources with a single module 80 | // moduleNameMapper: {}, 81 | 82 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 83 | // modulePathIgnorePatterns: [], 84 | 85 | // Activates notifications for test results 86 | // notify: false, 87 | 88 | // An enum that specifies notification mode. Requires { notify: true } 89 | // notifyMode: "failure-change", 90 | 91 | // A preset that is used as a base for Jest's configuration 92 | preset: "rollup-jest", 93 | 94 | // Run tests from one or more projects 95 | // projects: null, 96 | 97 | // Use this configuration option to add custom reporters to Jest 98 | // reporters: undefined, 99 | 100 | // Automatically reset mock state between every test 101 | // resetMocks: false, 102 | 103 | // Reset the module registry before running each individual test 104 | // resetModules: false, 105 | 106 | // A path to a custom resolver 107 | // resolver: null, 108 | 109 | // Automatically restore mock state between every test 110 | // restoreMocks: false, 111 | 112 | // The root directory that Jest should scan for tests and modules within 113 | // rootDir: null, 114 | 115 | // A list of paths to directories that Jest should use to search for files in 116 | // roots: [ 117 | // "" 118 | // ], 119 | 120 | // Allows you to use a custom runner instead of Jest's default test runner 121 | // runner: "jest-runner", 122 | 123 | // The paths to modules that run some code to configure or set up the testing environment before each test 124 | // setupFiles: [], 125 | 126 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 127 | // setupFilesAfterEnv: [], 128 | 129 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 130 | // snapshotSerializers: [], 131 | 132 | // The test environment that will be used for testing 133 | testEnvironment: "node", 134 | 135 | // Options that will be passed to the testEnvironment 136 | // testEnvironmentOptions: {}, 137 | 138 | // Adds a location field to test results 139 | // testLocationInResults: false, 140 | 141 | // The glob patterns Jest uses to detect test files 142 | // testMatch: [ 143 | // "**/__tests__/**/*.[jt]s?(x)", 144 | // "**/?(*.)+(spec|test).[tj]s?(x)" 145 | // ], 146 | 147 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 148 | // testPathIgnorePatterns: [ 149 | // "/node_modules/" 150 | // ], 151 | 152 | // The regexp pattern or array of patterns that Jest uses to detect test files 153 | // testRegex: [], 154 | 155 | // This option allows the use of a custom results processor 156 | // testResultsProcessor: null, 157 | 158 | // This option allows use of a custom test runner 159 | // testRunner: "jasmine2", 160 | 161 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 162 | // testURL: "http://localhost", 163 | 164 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 165 | // timers: "real", 166 | 167 | // A map from regular expressions to paths to transformers 168 | // transform: null, 169 | 170 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 171 | // transformIgnorePatterns: [ 172 | // "/node_modules/" 173 | // ], 174 | 175 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 176 | // unmockedModulePathPatterns: undefined, 177 | 178 | // Indicates whether each individual test should be reported during the run 179 | verbose: true 180 | 181 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 182 | // watchPathIgnorePatterns: [], 183 | 184 | // Whether to use watchman for file crawling 185 | // watchman: true, 186 | }; 187 | -------------------------------------------------------------------------------- /jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["plugins/markdown"], 3 | "source": { 4 | "include": ["src"], 5 | "exclude": [], 6 | "includePattern": ".+\\.js(doc)?$", 7 | "excludePattern": "(^|\\/|\\\\)_" 8 | }, 9 | "sourceType": "module", 10 | "tags": { 11 | "allowUnknownTags": true 12 | }, 13 | "templates": { 14 | "cleverLinks": false, 15 | "monospaceLinks": false, 16 | "default": { 17 | "outputSourceFiles": false 18 | } 19 | }, 20 | "opts": { 21 | "readme": "./README.md", 22 | "destination": "./docs/", 23 | "recurse": true, 24 | "template": "node_modules/minami" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rtcstats-wrapper", 3 | "version": "1.0.0", 4 | "description": "A wrapper of RTCStats for standardization and calculation of momentary values.", 5 | "author": "NTT Communications Corp.", 6 | "license": "MIT", 7 | "homepage": "https://github.com/skyway-lab/rtcstats-wrapper#readme", 8 | "keywords": [ 9 | "WebRTC", 10 | "RTCStats" 11 | ], 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/skyway-lab/rtcstats-wrapper.git" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/skyway-lab/rtcstats-wrapper/issues" 18 | }, 19 | "main": "./dist/rtcstats-wrapper.js", 20 | "scripts": { 21 | "lint": "eslint .", 22 | "test": "jest", 23 | "dev": "jest --watch", 24 | "build": "rollup -c", 25 | "docs": "jsdoc -c ./jsdoc.json" 26 | }, 27 | "devDependencies": { 28 | "bl": ">=0.9.5", 29 | "eslint": "^5.16.0", 30 | "eslint-config-prettier": "^5.0.0", 31 | "eslint-plugin-jest": "^22.7.0", 32 | "eslint-plugin-prettier": "^3.1.0", 33 | "jest": "^24.8.0", 34 | "jsdoc": "^3.6.3", 35 | "minami": "^1.2.3", 36 | "prettier": "^1.18.2", 37 | "rollup": "^1.15.6", 38 | "rollup-jest": "0.0.2", 39 | "rollup-plugin-commonjs": "^10.0.1", 40 | "rollup-plugin-node-builtins": "^2.1.2", 41 | "rollup-plugin-node-resolve": "^5.2.0" 42 | }, 43 | "dependencies": { 44 | "detect-browser": "^4.5.1", 45 | "enum": "^2.5.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from "rollup-plugin-node-resolve"; 2 | import commonjs from "rollup-plugin-commonjs"; 3 | import builtins from "rollup-plugin-node-builtins"; 4 | 5 | export default { 6 | input: "./src/main.js", 7 | output: { 8 | file: "./dist/rtcstats-wrapper.js", 9 | format: "umd", 10 | name: "RTCStatsWrapper" 11 | }, 12 | plugins: [ 13 | resolve({ preferBuiltins: true, mainFields: ["browser"] }), 14 | commonjs(), 15 | builtins() 16 | ] 17 | }; 18 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | export { ChromeRTCStatsReport } from "./standardizers/chrome.js"; 2 | export { FirefoxRTCStatsReport } from "./standardizers/firefox.js"; 3 | export { SafariRTCStatsReport } from "./standardizers/safari.js"; 4 | export { BaseRTCStatsReport } from "./standardizers/base.js"; 5 | export { 6 | RTCStatsReferences, 7 | RTCStatsReferenceMap 8 | } from "./shared/constatnts.js"; 9 | export { RTCStatsMoment } from "./rtcstats-moment.js"; 10 | export { 11 | RTCStatsInsight, 12 | RTCStatsInsightEvents, 13 | StatusLevels 14 | } from "./rtcstats-insight.js"; 15 | export { standardizeReport } from "./standardize-support.js"; 16 | -------------------------------------------------------------------------------- /src/rtcstats-insight.js: -------------------------------------------------------------------------------- 1 | import Enum from "enum"; 2 | import { RTCStatsMoment } from "../src/rtcstats-moment"; 3 | import { EventEmitter } from "events"; 4 | 5 | /** 6 | * Enum for event references of RTCStatsInsight 7 | * 8 | * @readonly 9 | * @property {EnumItem} audio-rtt - RTT of sending audio. 10 | * @property {EnumItem} video-rtt - RTT of sending audio. 11 | * @property {EnumItem} audio-jitter - Jitter about sending audio. 12 | * @property {EnumItem} video-jitter - Jitter about sending video. 13 | * @property {EnumItem} audio-fractionLost - Packet loss rate of receiving audio stream. 14 | * @property {EnumItem} video-fractionLost - Packet loss rate of receiving video stream. 15 | * @property {EnumItem} audio-jitterBufferDelay - Local jitter buffer delay about receiving audio. 16 | * @property {EnumItem} video-jitterBufferDelay - Local jitter buffer delay about receiving video. 17 | * @property {EnumItem} rtt - Current RTT for ICE transport. 18 | * @example 19 | * import { 20 | * RTCStatsInsightEvents, 21 | * RTCStatsInsight 22 | * } from 'rtcstats-wrapper'; 23 | * 24 | * const insight = new RTCStatsInsight(sender); 25 | * 26 | * insight.on(RTCStatsInsightEvents["audio-rtt"].key, event => { 27 | * console.log(event.level); 28 | * }); 29 | * 30 | * insight.watch() 31 | */ 32 | export const RTCStatsInsightEvents = new Enum([ 33 | "audio-rtt", 34 | "video-rtt", 35 | "audio-jitter", 36 | "video-jitter", 37 | "audio-fractionLost", 38 | "video-fractionLost", 39 | "audio-jitterBufferDelay", 40 | "video-jitterBufferDelay", 41 | "rtt" 42 | ]); 43 | 44 | /** 45 | * Enum for levels of RTCStatsInsightEvents. 46 | * 47 | * @readonly 48 | * @property {EnumItem} stable - The call is stable. 49 | * @property {EnumItem} unstable - The call is unstable and may communicated in low quality. 50 | * @property {EnumItem} critical - Highly affected on call quality. 51 | * @property {EnumItem} unknown - This level is for unmonitored metrics. 52 | * @example 53 | * import { 54 | * StatusLevels, 55 | * RTCStatsInsightEvents, 56 | * RTCStatsInsight 57 | * } from 'rtcstats-wrapper'; 58 | * 59 | * const insight = new RTCStatsInsight(sender); 60 | * 61 | * insight.on(RTCStatsInsightEvents["audio-rtt"].key, event => { 62 | * if (event.level === StatusLevels.stable.key) { 63 | * console.log("Now back to stable!"); 64 | * } 65 | * }); 66 | * 67 | * insight.watch() 68 | */ 69 | export const StatusLevels = new Enum([ 70 | "stable", 71 | "unstable", 72 | "critical", 73 | "unknown" 74 | ]); 75 | 76 | /** 77 | * A set of thresholds for emitting each events used in the constructor of RTCStatsInsight. 78 | * Use the event name for the thresholds object's key and use this object for the value. 79 | * Please see example for usage. 80 | * 81 | * @typedef {Object} Thresholds 82 | * @property {Number} unstable - When the value used in thresholds object's key goes greater than this value, the `unstable` level event is fired. 83 | * @property {Number} critical - When the value used in thresholds object's key goes greater than this value, the `critical` level event is fired. 84 | * @example 85 | * const thresholds = { 86 | * "audio-rtt": { 87 | * unstable: 0.1 88 | * }, 89 | * "audio-fractionLost": { 90 | * unstable: 0.03, 91 | * critical: 0.08, 92 | * } 93 | * } 94 | * } 95 | * 96 | * const insight = new RTCStatsInsight(pc, { thresholds }); 97 | * insight.on(RTCStatsInsightEvents["audio-fractionLost"].key, events => { 98 | * // fired when `fractionLost` of receiving audio goes up to 0.03 99 | * // ... 100 | * } 101 | */ 102 | const DEFAULT_THRESHOLDS = { 103 | "audio-rtt": { unstable: 0.4, critical: 0.8 }, 104 | "video-rtt": { unstable: 0.4, critical: 0.8 }, 105 | "audio-jitter": { unstable: 0.05, critical: 0.1 }, 106 | "video-jitter": { unstable: 0.03, critical: 0.1 }, 107 | "audio-fractionLost": { unstable: 0.08, critical: 0.15 }, 108 | "video-fractionLost": { unstable: 0.08, critical: 0.15 }, 109 | "audio-jitterBufferDelay": { unstable: 0.5, critical: 1 }, 110 | "video-jitterBufferDelay": { unstable: 0.05, critical: 0.1 }, 111 | rtt: { unstable: 0.5, critical: 1 } 112 | }; 113 | 114 | class ConnectionStatus { 115 | constructor(options) { 116 | options = options || {}; 117 | this._options = { 118 | failCount: 3, 119 | within: 5, 120 | ...options 121 | }; 122 | this._store = { 123 | unstable: new Array(this._options.within).fill(null), 124 | critical: new Array(this._options.within).fill(null) 125 | }; 126 | this._level = StatusLevels.unknown.key; 127 | } 128 | 129 | get level() { 130 | if (this._store.critical.some(x => x === null)) { 131 | return StatusLevels.unknown.key; 132 | } 133 | 134 | const criticalCount = this._store.critical.filter(Boolean).length; 135 | if (criticalCount > this._options.failCount) { 136 | return StatusLevels.critical.key; 137 | } 138 | 139 | const unstableCount = this._store.critical.filter(Boolean).length; 140 | if (unstableCount > this._options.failCount) { 141 | return StatusLevels.unstable.key; 142 | } 143 | 144 | return StatusLevels.stable.key; 145 | } 146 | 147 | check(value, threshold) { 148 | this._store.critical.unshift(value > threshold.critical); 149 | this._store.critical.pop(); 150 | this._store.unstable.unshift(value > threshold.unstable); 151 | this._store.unstable.pop(); 152 | } 153 | } 154 | 155 | /** 156 | * EventEmitter class that polls getStats() to monitor connection status. 157 | * 158 | * @example 159 | * import { 160 | * StatusLevels, 161 | * RTCStatsInsightEvents, 162 | * RTCStatsInsight 163 | * } from 'rtcstats-wrapper'; 164 | * 165 | * const options = { 166 | * interval: 3000, 167 | * thresholds: { 168 | * "audio-rtt": { 169 | * unstable: 0.1 170 | * }, 171 | * "audio-fractionLost": { 172 | * unstable: 0.03, 173 | * critical: 0.08, 174 | * }, 175 | * }, 176 | * triggerCondition: { 177 | * failCount: 2, 178 | * within: 3 179 | * } 180 | * } 181 | * 182 | * const insight = new RTCStatsInsight(sender, options); 183 | * 184 | * insight.on(RTCStatsInsightEvents["audio-rtt"].key, event => { 185 | * if (event.level === StatusLevels.stable.key) { 186 | * console.log("Now back to stable!"); 187 | * } 188 | * }); 189 | * 190 | * insight.watch() 191 | */ 192 | export class RTCStatsInsight extends EventEmitter { 193 | /** 194 | * Create a RTCStatsInsight. 195 | * 196 | * @constructs 197 | * @param {RTCPeerConnection|RTCRtpReceiver|RTCRtpSender} statsSrc - getStats() method of this object is called in RTCStatsInsight. 198 | * @param {Object} options 199 | * @param {Number} options.interval - The polling interval in milliseconds. default 1000ms. 200 | * @param {Thresholds} options.thresholds - A set of thresholds for emitting each events. 201 | * @param {Object} options.triggerCondition - The trigger condition which defines how much failures makes this to fire an event. `${triggerCondition.failCount}` failures within `${triggerCondition.within}` attemption causes trigger of events. 202 | */ 203 | constructor(statsSrc, options) { 204 | super(); 205 | 206 | options = options || {}; 207 | this._statsSrc = statsSrc; 208 | this._interval = options.interval || 1000; 209 | this._thresholds = { ...DEFAULT_THRESHOLDS, ...options.thresholds }; 210 | this._moment = new RTCStatsMoment(); 211 | this._status = RTCStatsInsightEvents.enums.reduce( 212 | (acc, cur) => 213 | Object.assign(acc, { 214 | [cur]: new ConnectionStatus(options.triggerCondition) 215 | }), 216 | {} 217 | ); 218 | } 219 | 220 | /** 221 | * Start polling getStats(). 222 | * 223 | * @fires RTCStatsInsight#audio-rtt 224 | * @fires RTCStatsInsight#video-rtt 225 | * @fires RTCStatsInsight#audio-jitter 226 | * @fires RTCStatsInsight#video-jitter 227 | * @fires RTCStatsInsight#audio-fractionLost 228 | * @fires RTCStatsInsight#video-fractionLost 229 | * @fires RTCStatsInsight#audio-jitterBufferDelay 230 | * @fires RTCStatsInsight#video-jitterBufferDelay 231 | * @fires RTCStatsInsight#rtt 232 | * @see {RTCStatsInsightEvents} 233 | */ 234 | watch() { 235 | /** 236 | * Fires when an RTT of sending audio stream has been changed. 237 | * By default, `unstable` fires on RTT > 400ms and `critical` fires on RTT > 800ms. 238 | * 239 | * @event RTCStatsInsight#audio-rtt 240 | * @property {string} level - Warning level. This will be "stable" or "unstable" or "critical". 241 | * @property {string} threshold - Threshold for this event to fire. 242 | * @property {string} value - Last measured value when this event fires. 243 | */ 244 | 245 | /** 246 | * Fires when an RTT of sending video stream has been changed. 247 | * By default, `unstable` fires on RTT > 400ms and `critical` fires on RTT > 800ms. 248 | * 249 | * @event RTCStatsInsight#video-rtt 250 | * @property {string} level - Warning level. This will be "stable" or "unstable" or "critical". 251 | * @property {string} threshold - Threshold for this event to fire. 252 | * @property {string} value - Last measured value when this event fires. 253 | */ 254 | 255 | /** 256 | * Fires when a jitter of sending audio stream has been changed. 257 | * By default, `unstable` fires on jitter > 50ms and `critical` fires on jitter > 100ms. 258 | * 259 | * @event RTCStatsInsight#audio-jitter 260 | * @property {string} level - Warning level. This will be "stable" or "unstable" or "critical". 261 | * @property {string} threshold - Threshold for this event to fire. 262 | * @property {string} value - Last measured value when this event fires. 263 | */ 264 | 265 | /** 266 | * Fires when a jitter of sending video stream has been changed. 267 | * By default, `unstable` fires on jitter > 30ms and `critical` fires on jitter > 100ms. 268 | * 269 | * @event RTCStatsInsight#video-jitter 270 | * @property {string} level - Warning level. This will be "stable" or "unstable" or "critical". 271 | * @property {string} threshold - Threshold for this event to fire. 272 | * @property {string} value - Last measured value when this event fires. 273 | */ 274 | 275 | /** 276 | * Fires when the packet loss rate of receiving audio stream has been changed. 277 | * By default, `unstable` fires on packet loss rate > 8% and `critical` fires on packet loss rate > 15%. 278 | * 279 | * @event RTCStatsInsight#audio-fractionLost 280 | * @property {string} level - Warning level. This will be "stable" or "unstable" or "critical". 281 | * @property {string} threshold - Threshold for this event to fire. 282 | * @property {string} value - Last measured value when this event fires. 283 | */ 284 | 285 | /** 286 | * Fires when the packet loss rate of receiving video stream has been changed. 287 | * By default, `unstable` fires on packet loss rate > 8% and `critical` fires on packet loss rate > 15%. 288 | * 289 | * @event RTCStatsInsight#video-fractionLost 290 | * @property {string} level - Warning level. This will be "stable" or "unstable" or "critical". 291 | * @property {string} threshold - Threshold for this event to fire. 292 | * @property {string} value - Last measured value when this event fires. 293 | */ 294 | 295 | /** 296 | * Fires when the jitter buffer delay of receiving audio stream has been changed. 297 | * By default, `unstable` fires on jitter buffer delay > 500ms and `critical` fires on jitter buffer delay > 1000ms. 298 | * 299 | * @event RTCStatsInsight#audio-jitterBufferDelay 300 | * @property {string} level - Warning level. This will be "stable" or "unstable" or "critical". 301 | * @property {string} threshold - Threshold for this event to fire. 302 | * @property {string} value - Last measured value when this event fires. 303 | */ 304 | 305 | /** 306 | * Fires when the jitter buffer delay of receiving video stream has been changed. 307 | * By default, `unstable` fires on jitter buffer delay > 50ms and `critical` fires on jitter buffer delay > 100ms. 308 | * 309 | * @event RTCStatsInsight#video-jitterBufferDelay 310 | * @property {string} level - Warning level. This will be "stable" or "unstable" or "critical". 311 | * @property {string} threshold - Threshold for this event to fire. 312 | * @property {string} value - Last measured value when this event fires. 313 | */ 314 | 315 | /** 316 | * Fires when the rtt of ICE connection has been changed. 317 | * The difference with media RTT is that media RTT uses the value of RTCP packet, and this RTT uses ICE connectivity checks timestamp. 318 | * By default, `unstable` fires on rtt > 500ms and `critical` fires on rtt > 1000ms. 319 | * 320 | * @event RTCStatsInsight#rtt 321 | * @property {string} level - Warning level. This will be "stable" or "unstable" or "critical". 322 | * @property {string} threshold - Threshold for this event to fire. 323 | * @property {string} value - Last measured value when this event fires. 324 | */ 325 | 326 | this._intervalID = setInterval(async () => { 327 | const report = await this._statsSrc.getStats(); 328 | this._moment.update(report); 329 | 330 | const momentum = this._moment.report(); 331 | this._checkStatus(momentum); 332 | }, this._interval); 333 | } 334 | 335 | /** 336 | * Stop polling getStats(). 337 | */ 338 | stop() { 339 | clearInterval(this._intervalID); 340 | } 341 | 342 | get status() { 343 | return this._status; 344 | } 345 | _checkStatus(moment) { 346 | const metrics = [ 347 | { direction: "send", kind: "audio", key: "rtt" }, 348 | { direction: "send", kind: "video", key: "rtt" }, 349 | { direction: "send", kind: "audio", key: "jitter" }, 350 | { direction: "send", kind: "video", key: "jitter" }, 351 | { direction: "receive", kind: "audio", key: "fractionLost" }, 352 | { direction: "receive", kind: "video", key: "fractionLost" }, 353 | { direction: "receive", kind: "audio", key: "jitterBufferDelay" }, 354 | { direction: "receive", kind: "video", key: "jitterBufferDelay" }, 355 | { direction: "candidatePair", key: "rtt" } 356 | ]; 357 | 358 | for (const { direction, kind, key } of metrics) { 359 | const stats = 360 | direction === "candidatePair" 361 | ? moment[direction] 362 | : moment[direction][kind]; 363 | const eventKey = direction === "candidatePair" ? key : `${kind}-${key}`; 364 | 365 | if (stats.hasOwnProperty(key)) { 366 | // Update the value and emit when the the level has been changed. 367 | const currentLevel = this._status[eventKey].level; 368 | this._status[eventKey].check(stats[key], this._thresholds[eventKey]); 369 | 370 | const updatedLevel = this._status[eventKey].level; 371 | if (updatedLevel !== currentLevel) { 372 | if (currentLevel === "unknown" && updatedLevel === "stable") continue; 373 | 374 | this.emit(eventKey, { 375 | level: updatedLevel, 376 | event: eventKey, 377 | threshold: this._thresholds[eventKey][updatedLevel], 378 | value: stats[key] 379 | }); 380 | } 381 | } 382 | } 383 | } 384 | } 385 | -------------------------------------------------------------------------------- /src/standardize-support.js: -------------------------------------------------------------------------------- 1 | import { detect } from "detect-browser"; 2 | import { ChromeRTCStatsReport } from "./standardizers/chrome.js"; 3 | import { FirefoxRTCStatsReport } from "./standardizers/firefox.js"; 4 | import { SafariRTCStatsReport } from "./standardizers/safari.js"; 5 | import { BaseRTCStatsReport } from "./standardizers/base.js"; 6 | 7 | export function getStandardizer() { 8 | const { name, version } = detect(); 9 | const [major, minor, patch] = version.split(".").map(n => parseInt(n)); 10 | const browser = { name, major, minor, patch }; 11 | 12 | switch (browser.name) { 13 | case "chrome": 14 | return ChromeRTCStatsReport; 15 | case "firefox": 16 | return FirefoxRTCStatsReport; 17 | case "safari": 18 | return SafariRTCStatsReport; 19 | default: 20 | return BaseRTCStatsReport; 21 | } 22 | } 23 | 24 | /** 25 | * A function that ditects the browser and returns an instance of this library's 26 | * standardized RTCStatsReport. 27 | * 28 | * @param {RTCStatsReport} report - original stats report from `(pc|sender|receiver).getStats()`. 29 | * @return {RTCStatsReport} A standardized RTCStatsReport. See example to get how to use. 30 | * @example 31 | * import { 32 | * standardizeReport, 33 | * RTCStatsReferences 34 | * } from 'rtcstats-wrapper'; 35 | * 36 | * const report = standardizeReport(await pc.getStats()); 37 | * const receiverStats = report.get(RTCStatsReferences.RTCVideoReceivers.key); 38 | * const framesDecoded = receiverStats[0].framesDecoded; 39 | */ 40 | export function standardizeReport(report) { 41 | const standardizer = getStandardizer(); 42 | return new standardizer(report); 43 | } 44 | -------------------------------------------------------------------------------- /src/standardizers/base.js: -------------------------------------------------------------------------------- 1 | import { 2 | RTCStatsReferences, 3 | RTCStatsReferenceMap 4 | } from "../shared/constatnts.js"; 5 | 6 | /** 7 | * Base class of browser-independent RTCStatsReport. 8 | * This class provides to get an array of specific type of RTCStats with {@link RTCStatsReferences}. 9 | * See the example below. 10 | * 11 | * @throws - When given stats has undefined type or kind. 12 | * @example 13 | * import { 14 | * BaseRTCStatsReport, 15 | * RTCStatsReferences 16 | * } from 'rtcstats-wrapper'; 17 | * 18 | * const report = new BaseRTCStatsReport(await pc.getStats()); 19 | * 20 | * // get stats of incoming RTP stream 21 | * const recvVideoStats = report.get(RTCStatsReferences.RTCInboundRtpVideoStreams.key); 22 | * // get each log of inbound-rtp 23 | * for (const stats of recvVideoStats) { 24 | * logger.info(`ts:${stats.timestamp} id:${stats.trackId} recv:${stats.bytesReceived}`); 25 | * } 26 | */ 27 | export class BaseRTCStatsReport { 28 | /** 29 | * Create a BaseRTCStatsReport. 30 | * 31 | * @constructs 32 | * @param {RTCStatsReport} originalReport - original stats report from `(pc|sender|receiver).getStats()`. 33 | */ 34 | constructor(originalReport) { 35 | const report = new Map(); 36 | 37 | for (const originalStats of originalReport.values()) { 38 | const ref = this._getRTCStatsReference(originalStats); 39 | const stats = {}; 40 | 41 | // get the preferred value from original stats. 42 | for (const attr of RTCStatsReferenceMap.get(ref)) { 43 | if (originalStats[attr] !== undefined) { 44 | stats[attr] = originalStats[attr]; 45 | } 46 | } 47 | 48 | // update the stats object 49 | if (report.has(ref)) { 50 | const statsArray = report.get(ref); 51 | statsArray.push(stats); 52 | report.set(ref, statsArray); 53 | } else { 54 | report.set(ref, [stats]); 55 | } 56 | } 57 | 58 | this._report = report; 59 | } 60 | 61 | /** 62 | * Get the array of type of stats referred by `key`. 63 | * 64 | * @param {string} key - A stats object reference defined in {@link RTCStatsReferences} enum. 65 | * @return {Array} An array of stats referred by `key`. 66 | * @example 67 | * const report = new BaseRTCStatsReport(await pc.getStats()); 68 | * 69 | * if (report.get(RTCStatsReferences.RTCInboundRtpVideoStreams.key)) { 70 | * const stats = report.get( 71 | * RTCStatsReferences.RTCInboundRtpVideoStreams.key 72 | * )[0]; 73 | * logger.info(`ts:${stats.timestamp} id:${stats.trackId} recv:${stats.bytesReceived}`); 74 | */ 75 | get(key) { 76 | return this._report.get(key); 77 | } 78 | 79 | /** 80 | * Check if the instance has the type of stats referred by `key`. 81 | * 82 | * @param {string} key - A stats object reference defined in {@link RTCStatsReferences} enum. 83 | * @return {bool} True if the referred stats exists. 84 | * @example 85 | * const report = new BaseRTCStatsReport(await pc.getStats()); 86 | * 87 | * if (report.has(RTCStatsReferences.RTCInboundRtpVideoStreams.key)) { 88 | * logger.info("receiving video."); 89 | * } else { 90 | * logger.info("no video streams receiving."); 91 | * } 92 | */ 93 | has(key) { 94 | return this._report.has(key); 95 | } 96 | 97 | _getRTCStatsReference(stats) { 98 | switch (stats.type) { 99 | case "codec": 100 | return RTCStatsReferences.RTCCodecs.key; 101 | case "inbound-rtp": 102 | if (stats.kind === "video") { 103 | return RTCStatsReferences.RTCInboundRtpVideoStreams.key; 104 | } else if (stats.kind === "audio") { 105 | return RTCStatsReferences.RTCInboundRtpAudioStreams.key; 106 | } 107 | break; 108 | case "outbound-rtp": 109 | if (stats.kind === "video") { 110 | return RTCStatsReferences.RTCOutboundRtpVideoStreams.key; 111 | } else if (stats.kind === "audio") { 112 | return RTCStatsReferences.RTCOutboundRtpAudioStreams.key; 113 | } 114 | break; 115 | case "remote-inbound-rtp": 116 | if (stats.kind === "video") { 117 | return RTCStatsReferences.RTCRemoteInboundRtpVideoStreams.key; 118 | } else if (stats.kind === "audio") { 119 | return RTCStatsReferences.RTCRemoteInboundRtpAudioStreams.key; 120 | } 121 | break; 122 | case "remote-outbound-rtp": 123 | if (stats.kind === "video") { 124 | return RTCStatsReferences.RTCRemoteOutboundRtpVideoStreams.key; 125 | } else if (stats.kind === "audio") { 126 | return RTCStatsReferences.RTCRemoteOutboundRtpAudioStreams.key; 127 | } 128 | break; 129 | case "media-source": 130 | if (stats.kind === "video") { 131 | return RTCStatsReferences.RTCVideoSources.key; 132 | } else if (stats.kind === "audio") { 133 | return RTCStatsReferences.RTCAudioSources.key; 134 | } 135 | break; 136 | case "csrc": 137 | return RTCStatsReferences.RTCRtpContributingSources.key; 138 | case "peer-connection": 139 | return RTCStatsReferences.RTCPeerConnection.key; 140 | case "data-channel": 141 | return RTCStatsReferences.RTCDataChannels.key; 142 | case "stream": 143 | return RTCStatsReferences.RTCMediaStreams.key; 144 | case "sender": 145 | if (stats.kind === "video") { 146 | return RTCStatsReferences.RTCVideoSenders.key; 147 | } else if (stats.kind === "audio") { 148 | return RTCStatsReferences.RTCAudioSenders.key; 149 | } 150 | break; 151 | case "receiver": 152 | if (stats.kind === "video") { 153 | return RTCStatsReferences.RTCVideoReceivers.key; 154 | } else if (stats.kind === "audio") { 155 | return RTCStatsReferences.RTCAudioReceivers.key; 156 | } 157 | break; 158 | case "transport": 159 | return RTCStatsReferences.RTCTransports.key; 160 | case "candidate-pair": 161 | return RTCStatsReferences.RTCIceCandidatePairs.key; 162 | case "local-candidate": 163 | return RTCStatsReferences.RTCLocalIceCandidates.key; 164 | case "remote-candidate": 165 | return RTCStatsReferences.RTCRemoteIceCandidates.key; 166 | case "certificate": 167 | return RTCStatsReferences.RTCCertificates.key; 168 | case "stunserverconnection": 169 | return RTCStatsReferences.RTCStunServerConnections.key; 170 | default: 171 | throw new Error( 172 | `Received an unknown stats-type string: ${stats.type}.` 173 | ); 174 | } 175 | throw new Error( 176 | `Received an unknown kind of ${stats.type}: ${stats.kind}.` 177 | ); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/standardizers/chrome.js: -------------------------------------------------------------------------------- 1 | import { BaseRTCStatsReport } from "./base.js"; 2 | import { RTCStatsReferences } from "../shared/constatnts.js"; 3 | 4 | /** 5 | * Wrapped RTCStatsReport class for Google Chrome. 6 | * 7 | * @extends BaseRTCStatsReport 8 | */ 9 | export class ChromeRTCStatsReport extends BaseRTCStatsReport { 10 | _getRTCStatsReference(stats) { 11 | switch (stats.type) { 12 | case "track": 13 | if (stats.remoteSource && stats.kind === "video") { 14 | return RTCStatsReferences.RTCVideoReceivers.key; 15 | } else if (stats.remoteSource && stats.kind === "audio") { 16 | return RTCStatsReferences.RTCAudioReceivers.key; 17 | } else if (stats.kind === "video") { 18 | return RTCStatsReferences.RTCVideoSenders.key; 19 | } else if (stats.kind === "audio") { 20 | return RTCStatsReferences.RTCAudioSenders.key; 21 | } 22 | } 23 | return super._getRTCStatsReference(stats); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/standardizers/firefox.js: -------------------------------------------------------------------------------- 1 | import { BaseRTCStatsReport } from "./base.js"; 2 | import { 3 | RTCStatsReferences, 4 | RTCStatsReferenceMap 5 | } from "../shared/constatnts.js"; 6 | 7 | /** 8 | * Get "bound-rtp" stats since Firefox under v69 does not use stats-type 9 | * "track" but "bound-rtp" stats includes the values that can be 10 | * considered as "track". 11 | * 12 | * @private 13 | */ 14 | function getTrackStatsOfFirefox(stats) { 15 | switch (stats.type) { 16 | case "inbound-rtp": 17 | if (stats.kind === "video") { 18 | return RTCStatsReferences.RTCVideoReceivers.key; 19 | } else if (stats.kind === "audio") { 20 | return RTCStatsReferences.RTCAudioReceivers.key; 21 | } 22 | break; 23 | case "outbound-rtp": 24 | if (stats.kind === "video") { 25 | return RTCStatsReferences.RTCVideoSenders.key; 26 | } else if (stats.kind === "audio") { 27 | return RTCStatsReferences.RTCAudioSenders.key; 28 | } 29 | break; 30 | default: 31 | throw new Error(`Received an unknown stats-type string: ${stats.type}.`); 32 | } 33 | } 34 | 35 | /** 36 | * Wrapped RTCStatsReport class for Firefox. 37 | * 38 | * @extends BaseRTCStatsReport 39 | */ 40 | export class FirefoxRTCStatsReport extends BaseRTCStatsReport { 41 | constructor(originalReport) { 42 | super(originalReport); 43 | 44 | // retrieve receiver/sender stats 45 | const statsRefs = [...originalReport.keys()]; 46 | const rtpRefs = statsRefs.filter(ref => /(in|out)bound_rtp_.*/.test(ref)); 47 | 48 | for (const originalRef of rtpRefs) { 49 | const originalStats = originalReport.get(originalRef); 50 | const ref = getTrackStatsOfFirefox(originalStats); 51 | const stats = {}; 52 | 53 | // get the preferred value from original stats. 54 | for (const attr of RTCStatsReferenceMap.get(ref)) { 55 | if (originalStats[attr] !== undefined) { 56 | stats[attr] = originalStats[attr]; 57 | } 58 | } 59 | 60 | // update the stats object 61 | if (this._report.has(ref)) { 62 | const statsArray = this._report.get(ref); 63 | statsArray.push(stats); 64 | this._report.set(ref, statsArray); 65 | } else { 66 | this._report.set(ref, [stats]); 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/standardizers/safari.js: -------------------------------------------------------------------------------- 1 | import { BaseRTCStatsReport } from "./base.js"; 2 | import { RTCStatsReferences } from "../shared/constatnts.js"; 3 | 4 | /** 5 | * Wrapped RTCStatsReport class for Safari. 6 | * 7 | * @extends BaseRTCStatsReport 8 | */ 9 | export class SafariRTCStatsReport extends BaseRTCStatsReport { 10 | _getRTCStatsReference(stats) { 11 | switch (stats.type) { 12 | case "track": 13 | if (stats.remoteSource && stats.hasOwnProperty("frameHeight")) { 14 | return RTCStatsReferences.RTCVideoReceivers.key; 15 | } else if (stats.remoteSource && stats.hasOwnProperty("audioLevel")) { 16 | return RTCStatsReferences.RTCAudioReceivers.key; 17 | } else if (stats.hasOwnProperty("frameHeight")) { 18 | return RTCStatsReferences.RTCVideoSenders.key; 19 | } else if (stats.hasOwnProperty("audioLevel")) { 20 | return RTCStatsReferences.RTCAudioSenders.key; 21 | } 22 | break; 23 | case "inbound-rtp": 24 | if (stats.mediaType === "video") { 25 | return RTCStatsReferences.RTCInboundRtpVideoStreams.key; 26 | } else if (stats.mediaType === "audio") { 27 | return RTCStatsReferences.RTCInboundRtpAudioStreams.key; 28 | } 29 | break; 30 | case "outbound-rtp": 31 | if (stats.mediaType === "video") { 32 | return RTCStatsReferences.RTCOutboundRtpVideoStreams.key; 33 | } else if (stats.mediaType === "audio") { 34 | return RTCStatsReferences.RTCOutboundRtpAudioStreams.key; 35 | } 36 | break; 37 | } 38 | return super._getRTCStatsReference(stats); 39 | } 40 | } 41 | --------------------------------------------------------------------------------