├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE-MIT ├── README.md ├── examples ├── fixtures │ ├── sign_irene_qcif.y4m │ └── silent_qcif.y4m ├── getstats.demo.js ├── scream.demo.js └── wdio │ ├── .eslintrc │ ├── test.spec.js │ └── wdio.conf.js ├── gruntfile.js ├── lib ├── browser │ ├── .eslintrc │ ├── getStats.js │ ├── startAnalyzing.js │ └── url.js ├── getConnectionInformation.js ├── getStats.js ├── helpers │ └── calcResult.js ├── startAnalyzing.js ├── stopAnalyzing.js └── webdriverrtc.js ├── package.json └── test ├── .eslintrc ├── bootstrap.js ├── fixtures ├── example.json └── test.json └── spec └── unit.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0"], 3 | "plugins": [ 4 | "syntax-async-functions", 5 | "transform-regenerator", 6 | "transform-runtime", 7 | "add-module-exports" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | indent_style = space 10 | indent_size = 4 11 | 12 | end_of_line = lf 13 | charset = utf-8 14 | trim_trailing_whitespace = true 15 | insert_final_newline = true 16 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/ 2 | bin/ 3 | ./*.js 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "standard", 3 | "parser": "babel-eslint", 4 | "rules": { 5 | "indent": [2, 4], 6 | "generator-star-spacing": 0 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | build 27 | 28 | # Users Environment Variables 29 | .lock-wscript 30 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | examples 2 | test 3 | lib 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | notifications: 2 | email: false 3 | 4 | language: node_js 5 | 6 | node_js: 7 | - '0.10' 8 | 9 | script: "npm test" 10 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 WebdriverIO 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![WebdriverRTC](http://www.christian-bromann.com/wdrtc.png) 2 | 3 | WebdriverRTC [![Build Status](https://travis-ci.org/webdriverio/webdriverrtc.svg?branch=master)](https://travis-ci.org/webdriverio/webdriverrtc) [![Gitter](https://badges.gitter.im/webdriverio/webdriverio.svg)](https://gitter.im/webdriverio/webdriverio?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![devDependencies Status](https://david-dm.org/webdriverio/webdriverrtc/dev-status.svg)](https://david-dm.org/webdriverio/webdriverrtc?type=dev) 4 | ============ 5 | 6 | This project is an extension to [WebdriverIO](http://webdriver.io) and enables your client instance to grep statistical data from a running WebRTC peer connection. According to the [w3 WebRTC draft](http://www.w3.org/TR/webrtc/#dom-peerconnection-getstats) all `RTCPeerConnection` objects provide a method called [`getStats`](http://www.w3.org/TR/webrtc/#widl-RTCPeerConnection-getStats-void-MediaStreamTrack-selector-RTCStatsCallback-successCallback-RTCPeerConnectionErrorCallback-failureCallback) which returns a [`RTCStats`](http://www.w3.org/TR/webrtc/#idl-def-RTCStats) object with useful information about things like packet losts or the audio input level which can be helpful in order to test your network connection or environment (e.g. did my "Mute" button really work?). 7 | 8 | This means that you can access all statistical data from `chrome://webrtc-internals` using Selenium as part of your integration tests. 9 | 10 | ![chrome-internals](http://www.christian-bromann.com/webrtc-internals.png) 11 | 12 | ## Prerequisites 13 | 14 | To use WebdriverRTC you need at least WebdriverIO `>=v4` 15 | 16 | ## How does it work 17 | 18 | WebdriverRTC masquerades the url command and injects a script after the page has been loaded to overwrite the standard `RTCPeerConnection` interface and get access to all created `RTCPeerConnection` objects. After you start analyzing it repeats calling the `getStats` method with a specific interval and saves all results to an internal object lying in the window scope. Then you can use WebdriverRTC commands to access these information. Currently only the Chrome browser is supported. But there's more to come. 19 | 20 | ## Example 21 | 22 | First install WebdriverRTC via NPM: 23 | 24 | ```sh 25 | $ npm install webdriverrtc 26 | ``` 27 | 28 | Then enhance your client instance using the `init` method: 29 | 30 | ```js 31 | // init WebdriverIO 32 | var matrix = require('webdriverio').multiremote({ 33 | 'browserA': { 34 | desiredCapabilities: { 35 | browserName: 'chrome' 36 | } 37 | }, 38 | 'browserB': { 39 | desiredCapabilities: { 40 | browserName: 'chrome' 41 | } 42 | } 43 | }); 44 | 45 | var WebdriverRTC = require('webdriverrtc'); 46 | WebdriverRTC.init(matrix, { 47 | browser: 'browserA' // define browser that collects data 48 | }); 49 | ``` 50 | 51 | Now start your selenium session and do everything required to establish a WebRTC connection. __Note__ that you need to run WebdriverIO in multiremote mode if you don't have something fancy that autoconnects your browser. Multiremote can be really helpful in these situations where you need to control more then one browser. After the connection was established run `startAnalyzing`, make a pause for a specific amount of time and then grab the stats for that time period. 52 | 53 | ```js 54 | matrix 55 | .init() 56 | .url('https://apprtc.appspot.com/r/' + channel) 57 | .click('#confirm-join-button') 58 | .pause(5000) 59 | .startAnalyzing() 60 | .getConnectionInformation(function(err, connectionType) { 61 | console.log(connectionType); 62 | }) 63 | .pause(10000) 64 | .getStats(10000, function(err, mean, median, max, min, rawdata) { 65 | console.log('mean:', mean); 66 | console.log('median:', median); 67 | console.log('max:', max); 68 | console.log('min:', min); 69 | console.log('rawdata', rawdata); // contains the complete RTCStatsReport with even more information (mostly browser specific) 70 | }) 71 | .end(); 72 | ``` 73 | 74 | ## Commands 75 | 76 | WebdriverRTC enhances your client with a small amount of new commands in order to use this plugin properly: 77 | 78 | ### startAnalyzing() 79 | 80 | Start with WebRTC analyzing. If you want to take stats of a specific RTCPeerConnection object you can use this function to return that object. Also necessary if your app creates an object immediatelly after the page got loaded. 81 | 82 | Example: 83 | 84 | ```js 85 | browserA.startAnalyzing(function() { 86 | return appController.call_.pcClient_.pc_; 87 | }); 88 | ``` 89 | 90 | ### getConnectionInformation(callback) 91 | 92 | Get basic information about the connection. Example: 93 | 94 | ```js 95 | matrix.getConnectionInformation(function(connection) { 96 | console.log(connection); 97 | /** 98 | * returns: 99 | * { 100 | * "transport": "udp", 101 | * "remote": { 102 | * "candidateType": "local", 103 | * "ipAddress": "192.168.1.7", 104 | * "port": "52193" 105 | * }, 106 | * "local": { 107 | * "candidateType": "local", 108 | * "ipAddress": "192.168.1.7", 109 | * "port": 55375 110 | * } 111 | * } 112 | */ 113 | }); 114 | ``` 115 | 116 | ### getStats(duration) 117 | 118 | Returns all stats within given duration in different formats. 119 | 120 | #### duration 121 | You can specify a specific time frame in which you want to receive the stats. If you pass in a number you will receive stats within the last x (your number) ms. You can also be more specific and pass in an object with `from` and `to` attribues and desired timestamps as value respectively. If you pass in null, you will receive the last taken stat trace. 122 | 123 | Type: *Number|Object*
124 | 125 | ```js 126 | matrix 127 | .pause(10000) 128 | .getStats(10000).then(function(mean) { 129 | /** 130 | * this test would fail if you are too loud during the test ;-) 131 | */ 132 | assert.ok(max.audio.outbound.inputLevel < 1000, 'You are too loud!'); 133 | expect(video.rtt).to.be.within(0, 15); 134 | }); 135 | ``` 136 | 137 | This is how an example result object does look like: 138 | 139 | ```json 140 | { 141 | "audio": { 142 | "inbound": { 143 | "bytesReceived": 31431, 144 | "jitter": 0.5, 145 | "packetsReceived": 295.83, 146 | "packetsLost": 0, 147 | "outputLevel": 8112.5 148 | }, 149 | "outbound": { 150 | "jitter": 0.83, 151 | "rtt": 1.5, 152 | "packetsLost": 0, 153 | "packetsSent": 297, 154 | "bytesSent": 30884.33, 155 | "inputLevel": 465.33 156 | } 157 | }, 158 | "video": { 159 | "captureJitterMs": 25, 160 | "encodeUsagePercent": 75, 161 | "frameWidthInput": 640, 162 | "captureQueueDelayMsPerS": 0.83, 163 | "bandwidth": { 164 | "actualEncBitrate": 160375, 165 | "availableReceiveBandwidth": 325032.67, 166 | "targetEncBitrate": 154050.5, 167 | "transmitBitrate": 160351.5, 168 | "retransmitBitrate": 0, 169 | "bucketDelay": 6.67, 170 | "availableSendBandwidth": 154050.5 171 | }, 172 | "frameRateSent": 16, 173 | "avgEncodeMs": 8.5, 174 | "bytesSent": 71676.5, 175 | "frameWidthSent": 640, 176 | "frameHeightInput": 480, 177 | "rtt": 3.17, 178 | "frameHeightSent": 480, 179 | "packetsLost": 0, 180 | "packetsSent": 100, 181 | "frameRateInput": 14.5 182 | } 183 | } 184 | ``` 185 | 186 | ## Examples 187 | 188 | There are two examples prepared which show how easy it is to trace WebRTC statistics. Before running them make sure you have the project cloned and WebdriverIO installed: 189 | 190 | ```sh 191 | $ g clone git@github.com:webdriverio/webdriverrtc.git 192 | $ cd webdriverrtc 193 | $ npm install webdriverio 194 | ``` 195 | 196 | Then start the `getstats.demo.js` by running: 197 | 198 | ```sh 199 | $ node ./examples/getstats.demo.js 200 | ``` 201 | 202 | It should start two Selenium sessions and should trace the WebRTC connection, created on [https://apprtc.appspot.com](https://apprtc.appspot.com). You will get the result formatted as `mean`, `median`, `max`, `min`. 203 | 204 | The other examples will prove that you can let your tests fail according on the results of the recorded stat. Run the script and start to scream or clap in your hands and it will return with an error message. 205 | 206 | ```sh 207 | $ node ./examples/scream.demo.js 208 | ``` 209 | 210 | ## Contributing 211 | Please fork, add specs, and send pull requests! In lieu of a formal styleguide, take care to maintain the existing coding style. 212 | 213 | ## Release History 214 | * 2015-02-04   v0.1.0   first working version released 215 | * 2016-08-17   v1.0.0   make webdriverrtc compatible with WebdriverIO v4 216 | -------------------------------------------------------------------------------- /examples/fixtures/sign_irene_qcif.y4m: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdriverio-community/webdriverrtc/14e8400f96e097437ce4c0f004b2acf8779ffb76/examples/fixtures/sign_irene_qcif.y4m -------------------------------------------------------------------------------- /examples/fixtures/silent_qcif.y4m: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webdriverio-community/webdriverrtc/14e8400f96e097437ce4c0f004b2acf8779ffb76/examples/fixtures/silent_qcif.y4m -------------------------------------------------------------------------------- /examples/getstats.demo.js: -------------------------------------------------------------------------------- 1 | /* global appController */ 2 | 3 | /** 4 | * Example script that opens "https://apprtc.appspot.com" creates an WebRTC connection 5 | * and traces this connection for 10 seconds to print the available results 6 | * 7 | * @author Christian Bromann 8 | * @license MIT 9 | */ 10 | 11 | var WebdriverIO = require('webdriverio') 12 | var WebdriverRTC = require('../') 13 | var path = require('path') 14 | var args = [ 15 | 'use-fake-device-for-media-stream', 16 | 'use-fake-ui-for-media-stream' 17 | ] 18 | 19 | var argsBrowserA = args.slice(0, args.length) 20 | argsBrowserA.push('use-file-for-fake-video-capture=' + path.join(__dirname, 'fixtures', 'sign_irene_qcif.y4m')) 21 | 22 | var argsBrowserB = args.slice(0, args.length) 23 | argsBrowserB.push('use-file-for-fake-video-capture=' + path.join(__dirname, 'fixtures', 'silent_qcif.y4m')) 24 | 25 | var matrix = WebdriverIO.multiremote({ 26 | browserA: { 27 | desiredCapabilities: { 28 | browserName: 'chrome', 29 | chromeOptions: { args: argsBrowserA } 30 | } 31 | }, 32 | browserB: { 33 | desiredCapabilities: { 34 | browserName: 'chrome', 35 | chromeOptions: { args: argsBrowserB } 36 | } 37 | } 38 | }) 39 | 40 | WebdriverRTC.init(matrix, { 41 | browser: 'browserA' 42 | }) 43 | 44 | var channel = Math.round(Math.random() * 100000000000) 45 | var browserA = matrix.select('browserA') 46 | 47 | matrix 48 | .init() 49 | .url('https://apprtc.appspot.com/r/' + channel) 50 | .click('#confirm-join-button') 51 | .pause(5000) 52 | .call(function () { 53 | return browserA.startAnalyzing(function () { 54 | return appController.call_.pcClient_.pc_ 55 | }).getConnectionInformation().then(function (connectionType) { 56 | console.log(connectionType) 57 | }).pause(2000).getStats(2000).then(function (result) { 58 | console.log(result) 59 | }) 60 | }) 61 | .end() 62 | -------------------------------------------------------------------------------- /examples/scream.demo.js: -------------------------------------------------------------------------------- 1 | /* global appController */ 2 | 3 | /** 4 | * This tests fails if you clap or scream while the test is running 5 | * 6 | * @author Christian Bromann 7 | * @license MIT 8 | */ 9 | 10 | var WebdriverIO = require('webdriverio') 11 | var WebdriverRTC = require('../') 12 | var assert = require('assert') 13 | var inputLevel 14 | 15 | var matrix = WebdriverIO.multiremote({ 16 | browserA: { 17 | desiredCapabilities: { 18 | browserName: 'chrome', 19 | chromeOptions: { args: ['use-fake-ui-for-media-stream'] } 20 | } 21 | }, 22 | browserB: { 23 | desiredCapabilities: { 24 | browserName: 'chrome', 25 | chromeOptions: { args: ['use-fake-ui-for-media-stream'] } 26 | } 27 | } 28 | }) 29 | 30 | WebdriverRTC.init(matrix, { 31 | browser: 'browserA' 32 | }) 33 | 34 | var channel = Math.round(Math.random() * 100000000000) 35 | var browserA = matrix.select('browserA') 36 | 37 | matrix 38 | .init() 39 | .url('https://apprtc.appspot.com/r/' + channel) 40 | .click('#confirm-join-button') 41 | .pause(5000) 42 | .call(function () { 43 | return browserA.startAnalyzing(function () { 44 | return appController.call_.pcClient_.pc_ 45 | }) 46 | .pause(5000) 47 | .getStats(5000).then(function (result) { 48 | inputLevel = result.max.audio.outbound.inputLevel 49 | }) 50 | }) 51 | .end().then(function () { 52 | /** 53 | * assert in next event loop so that node actually throws the error 54 | */ 55 | process.nextTick(function () { 56 | assert.ok(inputLevel < 5000, 'This was too loud! Your audio input level was ' + inputLevel + '.') 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /examples/wdio/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true, 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /examples/wdio/test.spec.js: -------------------------------------------------------------------------------- 1 | /*global browser, appController */ 2 | var assert = require('assert') 3 | 4 | describe('does webrtc test', function () { 5 | var channel = Math.round(Math.random() * 100000000000) 6 | var stats 7 | 8 | it('records connection data and prints them out', function () { 9 | /** 10 | * open webrtc connection by going to a random channel on apprtc.appspot.com 11 | */ 12 | browser.url('/r/' + channel) 13 | browser.click('#confirm-join-button').pause(5000) 14 | 15 | /** 16 | * start analyzing 17 | */ 18 | var browserA = browser.select('browserA') 19 | browserA.startAnalyzing(function () { 20 | return appController.call_.pcClient_.pc_ 21 | }) 22 | 23 | var connectionType = browserA.getConnectionInformation() 24 | console.log(connectionType) 25 | 26 | /** 27 | * record data for 5s 28 | */ 29 | browser.pause(5000) 30 | 31 | /** 32 | * print data 33 | */ 34 | stats = browserA.getStats(5000) 35 | console.log('mean:', stats.mean) 36 | console.log('median:', stats.median) 37 | console.log('max:', stats.max) 38 | console.log('min:', stats.min) 39 | }) 40 | 41 | it('check audio input level being lower than 5db', function () { 42 | var inputLevel = stats.max.audio.outbound.inputLevel 43 | assert.ok(inputLevel < 5000, 'This was too loud! Your audio input level was ' + inputLevel + '.') 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /examples/wdio/wdio.conf.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | 3 | exports.config = { 4 | // 5 | // ================== 6 | // Specify Test Files 7 | // ================== 8 | // Define which test specs should run. The pattern is relative to the directory 9 | // from which `wdio` was called. Notice that, if you are calling `wdio` from an 10 | // NPM script (see https://docs.npmjs.com/cli/run-script) then the current working 11 | // directory is where your package.json resides, so `wdio` will be called from there. 12 | // 13 | specs: [ 14 | path.join(__dirname, '/test.spec.js') 15 | ], 16 | // Patterns to exclude. 17 | exclude: [ 18 | // 'path/to/excluded/files' 19 | ], 20 | // 21 | // ============ 22 | // Capabilities 23 | // ============ 24 | // Define your capabilities here. WebdriverIO can run multiple capabilities at the same 25 | // time. Depending on the number of capabilities, WebdriverIO launches several test 26 | // sessions. Within your capabilities you can overwrite the spec and exclude options in 27 | // order to group specific specs to a specific capability. 28 | // 29 | // First, you can define how many instances should be started at the same time. Let's 30 | // say you have 3 different capabilities (Chrome, Firefox, and Safari) and you have 31 | // set maxInstances to 1; wdio will spawn 3 processes. Therefore, if you have 10 spec 32 | // files and you set maxInstances to 10, all spec files will get tested at the same time 33 | // and 30 processes will get spawned. The property handles how many capabilities 34 | // from the same test should run tests. 35 | // 36 | maxInstances: 10, 37 | // 38 | // If you have trouble getting all important capabilities together, check out the 39 | // Sauce Labs platform configurator - a great tool to configure your capabilities: 40 | // https://docs.saucelabs.com/reference/platforms-configurator 41 | // 42 | capabilities: { 43 | browserA: { 44 | desiredCapabilities: { 45 | browserName: 'chrome', 46 | chromeOptions: { args: [ 47 | 'use-fake-device-for-media-stream', 48 | 'use-fake-ui-for-media-stream', 49 | 'use-file-for-fake-video-capture=' + path.join(__dirname, '..', 'fixtures', 'sign_irene_qcif.y4m') 50 | ]} 51 | } 52 | }, 53 | browserB: { 54 | desiredCapabilities: { 55 | browserName: 'chrome', 56 | chromeOptions: { args: [ 57 | 'use-fake-device-for-media-stream', 58 | 'use-fake-ui-for-media-stream', 59 | 'use-file-for-fake-video-capture=' + path.join(__dirname, '..', 'fixtures', 'silent_qcif.y4m') 60 | ]} 61 | } 62 | } 63 | }, 64 | // 65 | // =================== 66 | // Test Configurations 67 | // =================== 68 | // Define all options that are relevant for the WebdriverIO instance here 69 | // 70 | // By default WebdriverIO commands are executed in a synchronous way using 71 | // the wdio-sync package. If you still want to run your tests in an async way 72 | // e.g. using promises you can set the sync option to false. 73 | sync: true, 74 | // 75 | // Level of logging verbosity: silent | verbose | command | data | result | error 76 | logLevel: 'silent', 77 | // 78 | // Enables colors for log output. 79 | coloredLogs: true, 80 | // 81 | // Saves a screenshot to a given path if a command fails. 82 | screenshotPath: './errorShots/', 83 | // 84 | // Set a base URL in order to shorten url command calls. If your url parameter starts 85 | // with "/", then the base url gets prepended. 86 | baseUrl: 'https://apprtc.appspot.com', 87 | // 88 | // Default timeout for all waitFor* commands. 89 | waitforTimeout: 10000, 90 | // 91 | // Default timeout in milliseconds for request 92 | // if Selenium Grid doesn't send response 93 | connectionRetryTimeout: 90000, 94 | // 95 | // Default request retries count 96 | connectionRetryCount: 3, 97 | // 98 | // Initialize the browser instance with a WebdriverIO plugin. The object should have the 99 | // plugin name as key and the desired plugin options as properties. Make sure you have 100 | // the plugin installed before running any tests. The following plugins are currently 101 | // available: 102 | // WebdriverCSS: https://github.com/webdriverio/webdrivercss 103 | // WebdriverRTC: https://github.com/webdriverio/webdriverrtc 104 | // Browserevent: https://github.com/webdriverio/browserevent 105 | plugins: { 106 | webdriverrtc: { 107 | browser: 'browserA' 108 | } 109 | }, 110 | // 111 | // Test runner services 112 | // Services take over a specific job you don't want to take care of. They enhance 113 | // your test setup with almost no effort. Unlike plugins, they don't add new 114 | // commands. Instead, they hook themselves up into the test process. 115 | // services: [],// 116 | // Framework you want to run your specs with. 117 | // The following are supported: Mocha, Jasmine, and Cucumber 118 | // see also: http://webdriver.io/guide/testrunner/frameworks.html 119 | // 120 | // Make sure you have the wdio adapter package for the specific framework installed 121 | // before running any tests. 122 | framework: 'mocha', 123 | // 124 | // Test reporter for stdout. 125 | // The only one supported by default is 'dot' 126 | // see also: http://webdriver.io/guide/testrunner/reporters.html 127 | reporters: ['dot'], 128 | // 129 | // Options to be passed to Mocha. 130 | // See the full list at http://mochajs.org/ 131 | mochaOpts: { 132 | ui: 'bdd', 133 | timeout: 60000 134 | } 135 | // 136 | // ===== 137 | // Hooks 138 | // ===== 139 | // WebdriverIO provides several hooks you can use to interfere with the test process in order to enhance 140 | // it and to build services around it. You can either apply a single function or an array of 141 | // methods to it. If one of them returns with a promise, WebdriverIO will wait until that promise got 142 | // resolved to continue. 143 | // 144 | // Gets executed once before all workers get launched. 145 | // onPrepare: function (config, capabilities) { 146 | // }, 147 | // 148 | // Gets executed before test execution begins. At this point you can access all global 149 | // variables, such as `browser`. It is the perfect place to define custom commands. 150 | // before: function (capabilities, specs) { 151 | // }, 152 | // 153 | // Hook that gets executed before the suite starts 154 | // beforeSuite: function (suite) { 155 | // }, 156 | // 157 | // Hook that gets executed _before_ a hook within the suite starts (e.g. runs before calling 158 | // beforeEach in Mocha) 159 | // beforeHook: function () { 160 | // }, 161 | // 162 | // Hook that gets executed _after_ a hook within the suite starts (e.g. runs after calling 163 | // afterEach in Mocha) 164 | // afterHook: function () { 165 | // }, 166 | // 167 | // Function to be executed before a test (in Mocha/Jasmine) or a step (in Cucumber) starts. 168 | // beforeTest: function (test) { 169 | // }, 170 | // 171 | // Runs before a WebdriverIO command gets executed. 172 | // beforeCommand: function (commandName, args) { 173 | // }, 174 | // 175 | // Runs after a WebdriverIO command gets executed 176 | // afterCommand: function (commandName, args, result, error) { 177 | // }, 178 | // 179 | // Function to be executed after a test (in Mocha/Jasmine) or a step (in Cucumber) starts. 180 | // afterTest: function (test) { 181 | // }, 182 | // 183 | // Hook that gets executed after the suite has ended 184 | // afterSuite: function (suite) { 185 | // }, 186 | // 187 | // Gets executed after all tests are done. You still have access to all global variables from 188 | // the test. 189 | // after: function (result, capabilities, specs) { 190 | // }, 191 | // 192 | // Gets executed after all workers got shut down and the process is about to exit. It is not 193 | // possible to defer the end of the process using a promise. 194 | // onComplete: function(exitCode) { 195 | // } 196 | } 197 | -------------------------------------------------------------------------------- /gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | grunt.initConfig({ 3 | pkgFile: 'package.json', 4 | clean: ['build'], 5 | babel: { 6 | dist: { 7 | files: [{ 8 | expand: true, 9 | cwd: './lib', 10 | src: ['**/*.js', '!browser/*.js'], 11 | dest: 'build', 12 | ext: '.js' 13 | }] 14 | } 15 | }, 16 | mochaTest: { 17 | unit: { 18 | src: ['./test/bootstrap.js', './test/spec/**/*'], 19 | options: { 20 | reporter: 'spec', 21 | require: ['babel-register'], 22 | timeout: 120000 23 | } 24 | } 25 | }, 26 | eslint: { 27 | options: { 28 | parser: 'babel-eslint' 29 | }, 30 | target: ['index.js', 'lib/**/*.js', 'examples/**/*.js', 'test/**/*.js'] 31 | }, 32 | contributors: { 33 | options: { 34 | commitMessage: 'update contributors' 35 | } 36 | }, 37 | bump: { 38 | options: { 39 | commitMessage: 'v%VERSION%', 40 | pushTo: 'upstream' 41 | } 42 | }, 43 | watch: { 44 | commands: { 45 | files: ['lib/*.js', 'lib/helpers/*.js'], 46 | tasks: ['babel'], 47 | options: { spawn: false } 48 | }, 49 | browserscripts: { 50 | files: ['lib/browser/*.js'], 51 | tasks: ['copy:browserscripts'], 52 | options: { spawn: false } 53 | } 54 | }, 55 | copy: { 56 | browserscripts: { 57 | files: [{ 58 | expand: true, 59 | cwd: 'lib', 60 | src: 'browser/*.js', 61 | dest: 'build' 62 | }] 63 | } 64 | } 65 | }) 66 | 67 | require('load-grunt-tasks')(grunt) 68 | grunt.registerTask('default', ['build']) 69 | grunt.registerTask('build', 'Build webdriverrtc', function () { 70 | grunt.task.run([ 71 | 'eslint', 72 | 'clean', 73 | 'babel', 74 | 'copy' 75 | ]) 76 | }) 77 | grunt.registerTask('release', 'Bump and tag version', function (type) { 78 | grunt.task.run([ 79 | 'build', 80 | 'contributors', 81 | 'bump:' + (type || 'patch') 82 | ]) 83 | }) 84 | } 85 | -------------------------------------------------------------------------------- /lib/browser/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true 4 | }, 5 | rules: { 6 | semi: ["error", "always", { "omitLastInOneLineBlock": true}] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /lib/browser/getStats.js: -------------------------------------------------------------------------------- 1 | module.exports = function getStats (from, to, interval) { 2 | var recordedStats = Object.keys(window._webdriverrtc || {}); 3 | var snapshots = []; 4 | 5 | /** 6 | * if no appropiate parameter was given return the last stat 7 | */ 8 | if (!from) { 9 | return window._webdriverrtc[recordedStats[recordedStats.length - 1]]; 10 | } 11 | 12 | recordedStats.forEach(function (timestamp) { 13 | timestamp = parseInt(timestamp, 10); 14 | if (timestamp > (from - interval / 2) && (timestamp - interval / 2) < to) { 15 | snapshots.push(window._webdriverrtc[timestamp]); 16 | } 17 | }); 18 | 19 | return snapshots; 20 | }; 21 | -------------------------------------------------------------------------------- /lib/browser/startAnalyzing.js: -------------------------------------------------------------------------------- 1 | /** 2 | * this script gets executed in the browser 3 | */ 4 | module.exports = function startAnalyzing (_, pcSelectorMethod, interval) { 5 | var cb = arguments[arguments.length - 1]; 6 | 7 | /** 8 | * merge objects (copied from deepmerge) 9 | */ 10 | function merge (target, src) { 11 | var dst = {}; 12 | 13 | if (target && typeof target === 'object') { 14 | Object.keys(target).forEach(function (key) { 15 | dst[key] = target[key]; 16 | }); 17 | } 18 | Object.keys(src).forEach(function (key) { 19 | if (typeof src[key] !== 'object' || !src[key]) { 20 | dst[key] = src[key]; 21 | } else { 22 | if (!target[key]) { 23 | dst[key] = src[key]; 24 | } else { 25 | dst[key] = merge(target[key], src[key]); 26 | } 27 | } 28 | }); 29 | 30 | return dst; 31 | } 32 | 33 | /** 34 | * sanitize stat values 35 | */ 36 | function sanitize (val) { 37 | if (typeof val !== 'string' && typeof val !== 'number') { 38 | return undefined; 39 | } 40 | 41 | return parseInt(val, 10); 42 | } 43 | 44 | /** 45 | * record stats 46 | */ 47 | function traceStats (results) { 48 | var result = { 49 | audio: {}, 50 | video: {}, 51 | results: results 52 | }; 53 | 54 | for (var i = 0; i < results.length; ++i) { 55 | var res = results[i]; 56 | 57 | /** 58 | * RTCOutboundRTPStreamStats 59 | */ 60 | if (res.googCodecName === 'opus' && res.bytesSent && res.packetsSent) { 61 | result.audio.outbound = { 62 | bytesSent: sanitize(res.bytesSent), 63 | packetsSent: sanitize(res.packetsSent), 64 | rtt: sanitize(res.googRtt), 65 | inputLevel: sanitize(res.audioInputLevel), 66 | packetsLost: sanitize(res.packetsLost), 67 | jitter: sanitize(res.googJitterReceived) 68 | }; 69 | 70 | /** 71 | * RTCInboundRTPStreamStats 72 | */ 73 | } else if (res.googCodecName === 'opus' && res.bytesReceived && res.packetsReceived) { 74 | result.audio.inbound = { 75 | bytesReceived: sanitize(res.bytesReceived), 76 | packetsReceived: sanitize(res.packetsReceived), 77 | outputLevel: sanitize(res.audioOutputLevel), 78 | packetsLost: sanitize(res.packetsLost), 79 | jitter: sanitize(res.googJitterReceived) 80 | }; 81 | } 82 | 83 | if (res.googCodecName === 'VP8') { 84 | result.video = merge(result.video, { 85 | bytesSent: sanitize(res.bytesSent), 86 | packetsSent: sanitize(res.packetsSent), 87 | packetsLost: sanitize(res.packetsLost), 88 | frameWidthInput: sanitize(res.googFrameWidthInput), 89 | frameHeightInput: sanitize(res.googFrameHeightInput), 90 | frameWidthSent: sanitize(res.googFrameWidthSent), 91 | frameHeightSent: sanitize(res.googFrameHeightSent), 92 | frameRateInput: sanitize(res.googFrameRateInput), 93 | frameRateSent: sanitize(res.googFrameRateSent), 94 | rtt: sanitize(res.googRtt), 95 | avgEncodeMs: sanitize(res.googAvgEncodeMs), 96 | captureJitterMs: sanitize(res.googCaptureJitterMs), 97 | captureQueueDelayMsPerS: sanitize(res.googCaptureQueueDelayMsPerS), 98 | encodeUsagePercent: sanitize(res.googEncodeUsagePercent) 99 | }); 100 | } 101 | 102 | if (res.type === 'VideoBwe') { 103 | result.video.bandwidth = { 104 | actualEncBitrate: sanitize(res.googActualEncBitrate), 105 | availableSendBandwidth: sanitize(res.googAvailableSendBandwidth), 106 | availableReceiveBandwidth: sanitize(res.googAvailableReceiveBandwidth), 107 | retransmitBitrate: sanitize(res.googRetransmitBitrate), 108 | targetEncBitrate: sanitize(res.googTargetEncBitrate), 109 | bucketDelay: sanitize(res.googBucketDelay), 110 | transmitBitrate: sanitize(res.googTransmitBitrate) 111 | }; 112 | } 113 | } 114 | 115 | var timestamp = new Date().getTime(); 116 | window._webdriverrtc[timestamp] = result; 117 | } 118 | 119 | /** 120 | * get RTCStatsReports 121 | */ 122 | function getStats () { 123 | pc.getStats(function (res) { 124 | var items = []; 125 | var connectionType = {}; 126 | 127 | res.result().forEach(function (result) { 128 | var item = {}; 129 | result.names().forEach(function (name) { 130 | item[name] = result.stat(name); 131 | }); 132 | item.id = result.id; 133 | item.type = result.type; 134 | item.timestamp = result.timestamp; 135 | 136 | if (item.type === 'googCandidatePair' && item.googActiveConnection === 'true') { 137 | connectionType = { 138 | local: { 139 | candidateType: item.googLocalCandidateType, 140 | ipAddress: item.googLocalAddress 141 | }, 142 | remote: { 143 | candidateType: item.googRemoteCandidateType, 144 | ipAddress: item.googRemoteAddress 145 | }, 146 | transport: item.googTransportType 147 | }; 148 | 149 | return; 150 | } 151 | 152 | items.push(item); 153 | }); 154 | 155 | traceStats(items); 156 | window._webdriverrtcTimeout = setTimeout(getStats.bind(window, items), interval); 157 | 158 | if (typeof cb === 'function') { 159 | cb(connectionType); 160 | cb = undefined; 161 | } 162 | }); 163 | } 164 | 165 | var pc = pcSelectorMethod() || window.webdriverRTCPeerConnectionBucket; 166 | 167 | if (!pc || pc.constructor.name !== 'RTCPeerConnection') { 168 | throw new Error('RTCPeerConnection not found'); 169 | } 170 | 171 | window._webdriverrtc = {}; 172 | window._webdriverrtcTimeout = null; 173 | getStats(); 174 | }; 175 | -------------------------------------------------------------------------------- /lib/browser/url.js: -------------------------------------------------------------------------------- 1 | module.exports = function url () { 2 | var OriginalRTCPeerConnection; 3 | 4 | /** 5 | * method to masquerade RTCPeerConnection 6 | */ 7 | var masqueradeFunction = function (param1, param2, param3) { 8 | var pc = new OriginalRTCPeerConnection(param1, param2, param3); 9 | window.webdriverRTCPeerConnectionBucket = pc; 10 | return pc; 11 | }; 12 | 13 | if (window.RTCPeerConnection) { 14 | OriginalRTCPeerConnection = window.RTCPeerConnection; 15 | window.RTCPeerConnection = masqueradeFunction; 16 | } else if (window.webkitRTCPeerConnection) { 17 | OriginalRTCPeerConnection = window.webkitRTCPeerConnection; 18 | window.webkitRTCPeerConnection = masqueradeFunction; 19 | } else if (window.mozRTCPeerConnection) { 20 | OriginalRTCPeerConnection = window.mozRTCPeerConnection; 21 | window.mozRTCPeerConnection = masqueradeFunction; 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /lib/getConnectionInformation.js: -------------------------------------------------------------------------------- 1 | import { ErrorHandler } from 'webdriverio' 2 | 3 | /** 4 | * get connection information 5 | * simple command that returns the connection information we saved when running startAnalyzing 6 | */ 7 | export default function getConnectionInformation () { 8 | if (!this.connection) { 9 | throw new ErrorHandler('CommandError', 10 | 'No information got recoreded yet. Please run the startAnalyzing command first' 11 | ) 12 | } 13 | 14 | return this.connection 15 | }; 16 | -------------------------------------------------------------------------------- /lib/getStats.js: -------------------------------------------------------------------------------- 1 | import { ErrorHandler } from 'webdriverio' 2 | import calcResult from './helpers/calcResult' 3 | import getStatsScript from './browser/getStats' 4 | 5 | /** 6 | * get latest stat 7 | */ 8 | export default async function getStats (duration) { 9 | const now = (new Date()).getTime() 10 | const rawData = [] 11 | let from = now 12 | let to = now 13 | 14 | if (typeof duration === 'number') { 15 | from = now - duration 16 | to = now 17 | } else if (typeof duration === 'object' && typeof duration.from === 'number' && duration.to === 'number') { 18 | from = duration.from 19 | to = duration.to 20 | } 21 | 22 | const stats = (await this.browser.execute(getStatsScript, from, to, this.interval)).value 23 | 24 | if (!stats) { 25 | throw new ErrorHandler('CommandError', 'There was a problem receiving the results') 26 | } 27 | 28 | stats.forEach((result, i) => { 29 | rawData.push(result.results) 30 | delete stats[i].results 31 | }) 32 | 33 | if (stats.length === 1) { 34 | return stats[0] 35 | } 36 | 37 | let mean = calcResult['+'](stats) 38 | mean = calcResult['/'](mean, stats.length) 39 | 40 | let median = calcResult['[]'](stats) 41 | median = calcResult['-|-'](median) 42 | 43 | let max = calcResult['max'](stats) 44 | let min = calcResult['min'](stats) 45 | 46 | return { mean, median, max, min, rawData } 47 | } 48 | -------------------------------------------------------------------------------- /lib/helpers/calcResult.js: -------------------------------------------------------------------------------- 1 | function calculateArray (operation, results) { 2 | var ret = {} 3 | results.forEach((result) => { 4 | calcResult[operation](result, ret) 5 | }) 6 | return ret 7 | } 8 | 9 | /** 10 | * calculate result 11 | */ 12 | const calcResult = { 13 | /** 14 | * adds all consecutive result values 15 | */ 16 | '+': (results, result) => { 17 | if (Array.isArray(results)) { 18 | var ret = {} 19 | results.forEach((result) => calcResult['+'](result, ret)) 20 | return ret 21 | } 22 | 23 | Object.keys(results).forEach((attr) => { 24 | if (typeof results[attr] === 'object') { 25 | if (!result[attr]) { 26 | result[attr] = {} 27 | } 28 | 29 | return calcResult['+'](results[attr] || {}, result[attr]) 30 | } else if (typeof results[attr] !== 'number') { 31 | return false 32 | } 33 | 34 | result[attr] = (result[attr] || 0) + results[attr] 35 | }) 36 | }, 37 | /** 38 | * devides each object value by divisor 39 | */ 40 | '/': (divident, divisor, toFixed = 2) => { 41 | var result = JSON.parse(JSON.stringify(divident)) 42 | Object.keys(result).forEach((attr) => { 43 | if (typeof result[attr] === 'object') { 44 | result[attr] = calcResult['/'](result[attr] || {}, divisor, toFixed) 45 | return result[attr] 46 | } 47 | 48 | result[attr] /= divisor 49 | result[attr] = result[attr].toFixed(toFixed) / 1 50 | }) 51 | 52 | return result 53 | }, 54 | /** 55 | * keeps the max value 56 | */ 57 | 'max': (results = {}, result) => { 58 | if (Array.isArray(results)) { 59 | let ret = {} 60 | results.forEach((result) => calcResult['max'](result, ret)) 61 | return ret 62 | } 63 | 64 | Object.keys(results).forEach((attr) => { 65 | if (typeof results[attr] === 'object') { 66 | if (!result[attr]) { 67 | result[attr] = {} 68 | } 69 | 70 | return calcResult['max'](results[attr] || {}, result[attr] || {}) 71 | } else if (typeof results[attr] !== 'number') { 72 | return false 73 | } 74 | 75 | if (!result[attr]) { 76 | result[attr] = results[attr] 77 | } else if (results[attr] > result[attr]) { 78 | result[attr] = results[attr] 79 | } 80 | }) 81 | }, 82 | /** 83 | * keeps the min value 84 | */ 85 | 'min': (results, result) => { 86 | if (Array.isArray(results)) { 87 | return calculateArray('min', results) 88 | } 89 | 90 | Object.keys(results).forEach((attr) => { 91 | if (typeof results[attr] === 'object') { 92 | if (!result[attr]) { 93 | result[attr] = {} 94 | } 95 | 96 | return calcResult['min'](results[attr] || {}, result[attr] || {}) 97 | } 98 | 99 | if (typeof results[attr] !== 'number') { 100 | return false 101 | } 102 | 103 | if (!result[attr]) { 104 | result[attr] = results[attr] 105 | } else if (results[attr] < result[attr]) { 106 | result[attr] = results[attr] 107 | } 108 | }) 109 | }, 110 | /** 111 | * puts all consecutive results in one array 112 | */ 113 | '[]': (results, result) => { 114 | if (Array.isArray(results)) { 115 | return calculateArray('[]', results) 116 | } 117 | 118 | /** 119 | * first sum up each result 120 | */ 121 | Object.keys(results).forEach((attr) => { 122 | if (typeof results[attr] === 'object') { 123 | if (!result[attr]) { 124 | result[attr] = {} 125 | } 126 | 127 | return calcResult['[]'](results[attr] || {}, result[attr]) 128 | } 129 | 130 | /** 131 | * `!result[attr].push` to ensure that result[attr] is an array. Sometimes it results into an 132 | * empty object (`{}`) 133 | */ 134 | if (!result[attr] || !result[attr].push) { 135 | result[attr] = [] 136 | } 137 | 138 | result[attr].push(results[attr]) 139 | }) 140 | }, 141 | /** 142 | * if results are listed in an array this method takes 143 | * the middle value of that array 144 | */ 145 | '-|-': (result, toFixed) => { 146 | var ret = {} 147 | toFixed = toFixed || 2 148 | 149 | Object.keys(result).forEach((attr) => { 150 | if (typeof result[attr] === 'object' && !(result[attr] instanceof Array)) { 151 | ret[attr] = calcResult['-|-'](result[attr] || {}, toFixed) 152 | return ret[attr] 153 | } 154 | 155 | /** 156 | * first sort list 157 | */ 158 | result[attr].sort((a, b) => a - b) 159 | 160 | /** 161 | * if array length is even take the middle value 162 | */ 163 | var resultLength = result[attr].length 164 | if (resultLength % 2 === 0) { 165 | ret[attr] = (result[attr][resultLength / 2 - 1] + (result[attr][resultLength / 2])) / 2 166 | } else { 167 | ret[attr] = result[attr][Math.floor(resultLength / 2)] 168 | } 169 | }) 170 | 171 | return ret 172 | } 173 | } 174 | 175 | export default calcResult 176 | -------------------------------------------------------------------------------- /lib/startAnalyzing.js: -------------------------------------------------------------------------------- 1 | import { ErrorHandler } from 'webdriverio' 2 | import startAnalyzingScript from './browser/startAnalyzing' 3 | 4 | /** 5 | * initiate WebRTC analyzing 6 | */ 7 | export default async function startAnalyzing (selectorMethod = () => false) { 8 | if (this.analyzingScriptIsInjected) { 9 | throw new ErrorHandler('CommandError', 'analyzing already started') 10 | } 11 | 12 | await this.browser.timeouts('script', 1000) 13 | let res = await this.browser.selectorExecuteAsync( 14 | 'body', 15 | startAnalyzingScript, 16 | selectorMethod, 17 | this.interval 18 | ) 19 | 20 | if (!res || Object.keys(res).length === 0) { 21 | throw new ErrorHandler('CommandError', 'WebRTC connection didn\'t get established') 22 | } 23 | 24 | const ipAddressLocal = res.local.ipAddress.split(/:/) 25 | const ipAddressRemote = res.remote.ipAddress.split(/:/) 26 | 27 | res.local.ipAddress = ipAddressLocal[0] 28 | res.local.port = ipAddressLocal[1] 29 | res.remote.ipAddress = ipAddressRemote[0] 30 | res.remote.port = ipAddressRemote[1] 31 | 32 | this.connection = res 33 | this.analyzingScriptIsInjected = true 34 | return this.connection 35 | } 36 | -------------------------------------------------------------------------------- /lib/stopAnalyzing.js: -------------------------------------------------------------------------------- 1 | /** 2 | * stop tracing webrtc stats 3 | */ 4 | export default async function (clearStats = false) { 5 | this.connection = undefined 6 | this.analyzingScriptIsInjected = false 7 | 8 | return await this.browser.execute((clearStats) => { 9 | if (clearStats) { 10 | window._webdriverrtc = undefined 11 | } 12 | 13 | return window.clearTimeout(window._webdriverrtcTimeout) 14 | }, clearStats) 15 | } 16 | -------------------------------------------------------------------------------- /lib/webdriverrtc.js: -------------------------------------------------------------------------------- 1 | import startAnalyzing from './startAnalyzing' 2 | import stopAnalyzing from './stopAnalyzing' 3 | import getStats from './getStats' 4 | import url from 'webdriverio/build/lib/protocol/url' 5 | import getConnectionInformation from './getConnectionInformation' 6 | import urlScript from './browser/url' 7 | 8 | /** 9 | * WebdriverRTC 10 | */ 11 | class WebdriverRTC { 12 | constructor (webdriverInstance, options = {}) { 13 | /** 14 | * browser that measures connection required 15 | */ 16 | if (typeof options.browser !== 'string') { 17 | throw new Error('Please specify the browser to measure connection data with!') 18 | } 19 | 20 | /** 21 | * check if browser name exists in matrix 22 | */ 23 | if (webdriverInstance.getInstances().indexOf(options.browser) === -1) { 24 | throw new Error('Specified browser was not found in browser matrix!') 25 | } 26 | 27 | this.interval = 1000 28 | this.browser = webdriverInstance.select(options.browser) 29 | this.analyzingScriptIsInjected = false 30 | 31 | /** 32 | * add WebdriverRTC commands to choosen matrix browser 33 | */ 34 | this.browser.addCommand('startAnalyzing', startAnalyzing.bind(this)) 35 | this.browser.addCommand('stopAnalyzing', stopAnalyzing.bind(this)) 36 | this.browser.addCommand('getStats', getStats.bind(this)) 37 | this.browser.addCommand('getConnectionInformation', getConnectionInformation.bind(this)) 38 | 39 | /** 40 | * overwrite url command in order to masquarade RTCPeerConnection objects 41 | */ 42 | this.browser.addCommand('_url', url) 43 | this.browser.addCommand('url', async function (...args) { 44 | const res = await this._url.apply(this, args) 45 | await this.execute(urlScript) 46 | return res 47 | }, true) 48 | } 49 | } 50 | 51 | /** 52 | * expose WebdriverRTC 53 | */ 54 | export function init (webdriverInstance, options) { 55 | return new WebdriverRTC(webdriverInstance, options) 56 | } 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webdriverrtc", 3 | "version": "1.0.0", 4 | "description": "WebRTC testing tool for WebdriverIO", 5 | "author": "Christian Bromann ", 6 | "homepage": "https://github.com/webdriverio/webdriverrtc", 7 | "license": "MIT", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/webdriverio/webdriverrtc" 11 | }, 12 | "keywords": [ 13 | "webdriverio", 14 | "webdriver", 15 | "webrtc", 16 | "rtcpeerconnection", 17 | "selenium", 18 | "internals", 19 | "chrome" 20 | ], 21 | "bugs": { 22 | "url": "https://github.com/webdriverio/webdriverrtc/issues" 23 | }, 24 | "main": "./build/webdriverrtc.js", 25 | "scripts": { 26 | "test": "./node_modules/.bin/mocha" 27 | }, 28 | "devDependencies": { 29 | "babel-cli": "^6.11.4", 30 | "babel-core": "^6.13.2", 31 | "babel-eslint": "^6.1.2", 32 | "babel-plugin-add-module-exports": "^0.2.1", 33 | "babel-plugin-syntax-async-functions": "^6.13.0", 34 | "babel-plugin-transform-regenerator": "^6.11.4", 35 | "babel-plugin-transform-runtime": "^6.12.0", 36 | "babel-preset-es2015": "^6.13.2", 37 | "babel-preset-stage-0": "^6.5.0", 38 | "babel-register": "^6.11.6", 39 | "chai": "^3.5.0", 40 | "eslint-config-standard": "^5.3.5", 41 | "eslint-plugin-promise": "^2.0.1", 42 | "eslint-plugin-standard": "^2.0.0", 43 | "grunt": "^1.0.1", 44 | "grunt-babel": "^6.0.0", 45 | "grunt-bump": "^0.8.0", 46 | "grunt-cli": "^1.2.0", 47 | "grunt-contrib-clean": "^1.0.0", 48 | "grunt-contrib-connect": "^1.0.2", 49 | "grunt-contrib-copy": "^1.0.0", 50 | "grunt-contrib-watch": "^1.0.0", 51 | "grunt-eslint": "^19.0.0", 52 | "grunt-mocha-test": "^0.12.7", 53 | "grunt-npm": "0.0.2", 54 | "load-grunt-tasks": "^3.5.2", 55 | "mocha": "^3.0.2", 56 | "wdio-dot-reporter": "0.0.6", 57 | "wdio-mocha-framework": "^0.4.0", 58 | "webdriverio": "^4.2.5" 59 | }, 60 | "dependencies": { 61 | "deepmerge": "^0.2.7" 62 | }, 63 | "engines": { 64 | "node": ">= 0.12.0" 65 | }, 66 | "contributors": [ 67 | "Christian Bromann " 68 | ] 69 | } 70 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true, 4 | "jasmine": true, 5 | "es6": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/bootstrap.js: -------------------------------------------------------------------------------- 1 | /** 2 | * require dependencies 3 | */ 4 | require('chai').should() 5 | global.expect = require('chai').expect 6 | -------------------------------------------------------------------------------- /test/fixtures/example.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "attr1": 13, 3 | "attr2": 2, 4 | "attr3": { 5 | "attr1": 43, 6 | "attr2": 46, 7 | "attr3": { 8 | "attr1": { 9 | "attr1": 15 10 | } 11 | } 12 | } 13 | },{ 14 | "attr1": 22, 15 | "attr2": 10, 16 | "attr3": { 17 | "attr1": 3, 18 | "attr2": 7, 19 | "attr3": { 20 | "attr1": { 21 | "attr1": 1 22 | } 23 | } 24 | } 25 | },{ 26 | "attr1": 29, 27 | "attr2": 34, 28 | "attr3": { 29 | "attr1": 17, 30 | "attr2": 25, 31 | "attr3": { 32 | "attr1": { 33 | "attr1": 4 34 | } 35 | } 36 | } 37 | },{ 38 | "attr1": 14, 39 | "attr2": 42, 40 | "attr3": { 41 | "attr1": 9, 42 | "attr2": 2, 43 | "attr3": { 44 | "attr1": { 45 | "attr1": 49 46 | } 47 | } 48 | } 49 | }] 50 | -------------------------------------------------------------------------------- /test/fixtures/test.json: -------------------------------------------------------------------------------- 1 | [{"audio":{"inbound":{"packetsLost":0,"outputLevel":86,"bytesReceived":22872,"jitter":4,"packetsReceived":226},"outbound":{"jitter":null,"rtt":null,"packetsLost":null,"packetsSent":225,"bytesSent":22789,"inputLevel":94}},"video":{"bandwidth":{"actualEncBitrate":369827,"availableReceiveBandwidth":0,"targetEncBitrate":412653,"transmitBitrate":304582,"retransmitBitrate":0,"bucketDelay":0,"availableSendBandwidth":412653}},"results":[{"id":"googTrack_687f5b0e-915c-469f-bdf6-c8216713bd14","googTrackId":"687f5b0e-915c-469f-bdf6-c8216713bd14","type":"googTrack","timestamp":{}},{"id":"googTrack_2c4f63c0-f828-400a-857a-ca25a55bf6ae","googTrackId":"2c4f63c0-f828-400a-857a-ca25a55bf6ae","type":"googTrack","timestamp":{}},{"googInitiator":"false","id":"googLibjingleSession_3693304454062870897","type":"googLibjingleSession","timestamp":{}},{"id":"googTrack_605ae15e-5d25-4f9a-a80b-c514971477ec","googTrackId":"605ae15e-5d25-4f9a-a80b-c514971477ec","type":"googTrack","timestamp":{}},{"id":"googTrack_23084d23-7251-4528-9a9c-63975d7cdab0","googTrackId":"23084d23-7251-4528-9a9c-63975d7cdab0","type":"googTrack","timestamp":{}},{"googFingerprint":"41:E4:D5:74:77:4C:3A:9A:61:D5:B1:29:4C:C7:18:37:98:08:DD:B0:AD:E7:B2:99:07:78:09:04:DA:15:1F:CD","googFingerprintAlgorithm":"sha-256","googDerBase64":"MIIBFTCBvaADAgECAgkArKLiaqCDL58wCgYIKoZIzj0EAwIwETEPMA0GA1UEAwwGV2ViUlRDMB4XDTE2MDgxNzE5MzcyNVoXDTE2MDkxNzE5MzcyNVowETEPMA0GA1UEAwwGV2ViUlRDMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERF46M3VeSprnGRul2AhvcKMbOjmjgZ4jRqnJkiNuZ2eqr7aGGXzv02eSL5OKJ9qU4tzGpC+1jNJaiG7zuQIxWjAKBggqhkjOPQQDAgNHADBEAiAqYO4eiKsAyzVS0x7AWxMP8DiP2ti8hGIlpQ5bpnvldQIgMGEE9r8PxWuEYM89zqSK/E55WGvPV2ODX6XS7/B06f8=","id":"googCertificate_41:E4:D5:74:77:4C:3A:9A:61:D5:B1:29:4C:C7:18:37:98:08:DD:B0:AD:E7:B2:99:07:78:09:04:DA:15:1F:CD","type":"googCertificate","timestamp":{}},{"googFingerprint":"B2:9D:72:2C:16:52:90:4E:42:56:78:F4:FB:F1:71:E0:F2:56:93:06:BD:BB:34:F7:5B:80:97:0D:C3:C8:A9:20","googFingerprintAlgorithm":"sha-256","googDerBase64":"MIIBFjCBvaADAgECAgkA9iBy3Ra9D5MwCgYIKoZIzj0EAwIwETEPMA0GA1UEAwwGV2ViUlRDMB4XDTE2MDgxNzE5MzcyNVoXDTE2MDkxNzE5MzcyNVowETEPMA0GA1UEAwwGV2ViUlRDMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEM7rETA1ec+WSm04MhYvTzNG9SEcPxGBzNXuw+FGtQGOA8yVjiBtWtxi3Nxtl4E9Tj6T1eHySLkznK/bK+zC78jAKBggqhkjOPQQDAgNIADBFAiEAgDTN5uWVfKH0dLR8kxm2zPYHZN1F4ZGBvdzAZRefN28CIGLZncfi0FwnZFlntSB2m0O7+Vt23nLKWF16wgQd/3sS","id":"googCertificate_B2:9D:72:2C:16:52:90:4E:42:56:78:F4:FB:F1:71:E0:F2:56:93:06:BD:BB:34:F7:5B:80:97:0D:C3:C8:A9:20","type":"googCertificate","timestamp":{}},{"localCertificateId":"googCertificate_41:E4:D5:74:77:4C:3A:9A:61:D5:B1:29:4C:C7:18:37:98:08:DD:B0:AD:E7:B2:99:07:78:09:04:DA:15:1F:CD","googComponent":"1","id":"Channel-audio-1","selectedCandidatePairId":"Conn-audio-1-0","dtlsCipher":"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256","srtpCipher":"AES_CM_128_HMAC_SHA1_80","type":"googComponent","remoteCertificateId":"googCertificate_B2:9D:72:2C:16:52:90:4E:42:56:78:F4:FB:F1:71:E0:F2:56:93:06:BD:BB:34:F7:5B:80:97:0D:C3:C8:A9:20","timestamp":{}},{"candidateType":"host","ipAddress":"192.168.1.6","id":"Cand-7FCQuDVB","transport":"udp","networkType":"unknown","priority":"2113937151","type":"localcandidate","portNumber":"63331","timestamp":{}},{"candidateType":"host","ipAddress":"192.168.1.6","id":"Cand-Js3A6w8y","transport":"udp","priority":"2113937151","type":"remotecandidate","portNumber":"55964","timestamp":{}},{"localCandidateId":"Cand-YcQhLxuG","googRemoteAddress":"74.125.136.87:26946","googRemoteCandidateType":"relay","googRtt":"2267","bytesSent":"0","googTransportType":"udp","type":"googCandidatePair","googWritable":"false","googActiveConnection":"false","remoteCandidateId":"Cand-uOIrojwF","bytesReceived":"0","googLocalAddress":"85.179.129.152:63334","googChannelId":"Channel-audio-1","packetsDiscardedOnSend":"0","packetsSent":"0","googLocalCandidateType":"prflx","id":"Conn-audio-1-1","googReadable":"true","timestamp":{}},{"candidateType":"serverreflexive","ipAddress":"85.179.129.152","id":"Cand-AwbKRfaP","transport":"udp","priority":"1677729535","type":"remotecandidate","portNumber":"55964","timestamp":{}},{"localCandidateId":"Cand-7FCQuDVB","googRemoteAddress":"85.179.129.152:55964","googRemoteCandidateType":"stun","googRtt":"3000","bytesSent":"0","googTransportType":"udp","type":"googCandidatePair","googWritable":"false","googActiveConnection":"false","remoteCandidateId":"Cand-AwbKRfaP","bytesReceived":"0","googLocalAddress":"192.168.1.6:63331","googChannelId":"Channel-audio-1","packetsDiscardedOnSend":"0","packetsSent":"0","googLocalCandidateType":"local","id":"Conn-audio-1-2","googReadable":"false","timestamp":{}},{"candidateType":"serverreflexive","ipAddress":"85.179.129.152","id":"Cand-ew/8za+M","transport":"udp","priority":"1677729535","type":"remotecandidate","portNumber":"55965","timestamp":{}},{"localCandidateId":"Cand-7FCQuDVB","googRemoteAddress":"85.179.129.152:55965","googRemoteCandidateType":"stun","googRtt":"3000","bytesSent":"0","googTransportType":"udp","type":"googCandidatePair","googWritable":"false","googActiveConnection":"false","remoteCandidateId":"Cand-ew/8za+M","bytesReceived":"0","googLocalAddress":"192.168.1.6:63331","googChannelId":"Channel-audio-1","packetsDiscardedOnSend":"0","packetsSent":"0","googLocalCandidateType":"local","id":"Conn-audio-1-3","googReadable":"false","timestamp":{}},{"candidateType":"relayed","ipAddress":"74.125.136.87","id":"Cand-uOIrojwF","transport":"udp","priority":"33562879","type":"remotecandidate","portNumber":"26946","timestamp":{}},{"localCandidateId":"Cand-7FCQuDVB","googRemoteAddress":"74.125.136.85:29605","googRemoteCandidateType":"relay","googRtt":"3000","bytesSent":"0","googTransportType":"udp","type":"googCandidatePair","googWritable":"false","googActiveConnection":"false","remoteCandidateId":"Cand-cUIj6b5K","bytesReceived":"0","googLocalAddress":"192.168.1.6:63331","googChannelId":"Channel-audio-1","packetsDiscardedOnSend":"0","packetsSent":"0","googLocalCandidateType":"local","id":"Conn-audio-1-4","googReadable":"false","timestamp":{}},{"candidateType":"relayed","ipAddress":"74.125.136.85","id":"Cand-cUIj6b5K","transport":"udp","priority":"16785151","type":"remotecandidate","portNumber":"29605","timestamp":{}},{"googCodecName":"opus","transportId":"Channel-audio-1","googDecodingCTSG":"0","type":"ssrc","googAccelerateRate":"0","googSpeechExpandRate":"0.0650635","packetsLost":"0","id":"ssrc_1940985005_recv","googTrackId":"605ae15e-5d25-4f9a-a80b-c514971477ec","googJitterBufferMs":"74","googPreemptiveExpandRate":"0","timestamp":{},"googPreferredJitterBufferMs":"20","ssrc":"1940985005","googSecondaryDecodedRate":"0","googExpandRate":"0.0650635","audioOutputLevel":"86","mediaType":"audio","googDecodingPLCCNG":"44","bytesReceived":"22872","googDecodingPLC":"16","googDecodingCTN":"498","packetsReceived":"226","googDecodingCNG":"0","googDecodingNormal":"438","googCurrentDelayMs":"93","googJitterReceived":"4"},{"googCodecName":"opus","googEchoCancellationReturnLossEnhancement":"-100","transportId":"Channel-audio-1","ssrc":"3634126469","mediaType":"audio","audioInputLevel":"94","bytesSent":"22789","type":"ssrc","googEchoCancellationReturnLoss":"-100","googTypingNoiseState":"false","packetsSent":"225","id":"ssrc_3634126469_send","googTrackId":"687f5b0e-915c-469f-bdf6-c8216713bd14","aecDivergentFilterFraction":"0","timestamp":{}},{"googCodecName":"VP9","transportId":"Channel-audio-1","googFrameRateDecoded":"14","googTargetDelayMs":"109","type":"ssrc","googRenderDelayMs":"10","googFirsSent":"0","googMaxDecodeMs":"5","googPlisSent":"0","googCaptureStartNtpTimeMs":"0","packetsLost":"0","googFrameRateOutput":"14","id":"ssrc_3350494056_recv","googTrackId":"23084d23-7251-4528-9a9c-63975d7cdab0","googJitterBufferMs":"94","timestamp":{},"googMinPlayoutDelayMs":"0","ssrc":"3350494056","mediaType":"video","googNacksSent":"0","bytesReceived":"167101","googFrameWidthReceived":"640","packetsReceived":"175","codecImplementationName":"libvpx","googDecodeMs":"3","googCurrentDelayMs":"109","googFrameHeightReceived":"480","googFrameRateReceived":"13"},{"googCodecName":"VP9","transportId":"Channel-audio-1","googRtt":"1","googFirsReceived":"0","type":"ssrc","googEncodeUsagePercent":"30","googFrameRateInput":"13","googFrameHeightInput":"480","googFrameWidthSent":"640","packetsLost":"0","packetsSent":"192","id":"ssrc_2105429706_send","googTrackId":"2c4f63c0-f828-400a-857a-ca25a55bf6ae","googFrameRateSent":"14","googFrameHeightSent":"480","timestamp":{},"googAdaptationChanges":"0","googNacksReceived":"0","googCpuLimitedResolution":"false","googBandwidthLimitedResolution":"false","googAvgEncodeMs":"15","ssrc":"2105429706","mediaType":"video","bytesSent":"184890","codecImplementationName":"libvpx","googFrameWidthInput":"640","googViewLimitedResolution":"false","googPlisReceived":"0"},{"googTransmitBitrate":"304582","googBucketDelay":"0","googTargetEncBitrate":"412653","googAvailableSendBandwidth":"412653","id":"bweforvideo","type":"VideoBwe","googAvailableReceiveBandwidth":"0","googRetransmitBitrate":"0","googActualEncBitrate":"369827","timestamp":{}},{"candidateType":"peerreflexive","ipAddress":"85.179.129.152","id":"Cand-YcQhLxuG","transport":"udp","networkType":"unknown","priority":"1845501695","type":"localcandidate","portNumber":"63334","timestamp":{}}]},{"audio":{"inbound":{"packetsLost":0,"outputLevel":26725,"bytesReceived":28346,"jitter":4,"packetsReceived":277},"outbound":{"jitter":5,"rtt":35,"packetsLost":0,"packetsSent":276,"bytesSent":28236,"inputLevel":28816}},"video":{"bandwidth":{"actualEncBitrate":336368,"availableReceiveBandwidth":420553,"targetEncBitrate":446665,"transmitBitrate":458402,"retransmitBitrate":0,"bucketDelay":12,"availableSendBandwidth":457780}},"results":[{"id":"googTrack_687f5b0e-915c-469f-bdf6-c8216713bd14","googTrackId":"687f5b0e-915c-469f-bdf6-c8216713bd14","type":"googTrack","timestamp":{}},{"id":"googTrack_2c4f63c0-f828-400a-857a-ca25a55bf6ae","googTrackId":"2c4f63c0-f828-400a-857a-ca25a55bf6ae","type":"googTrack","timestamp":{}},{"googInitiator":"false","id":"googLibjingleSession_3693304454062870897","type":"googLibjingleSession","timestamp":{}},{"id":"googTrack_605ae15e-5d25-4f9a-a80b-c514971477ec","googTrackId":"605ae15e-5d25-4f9a-a80b-c514971477ec","type":"googTrack","timestamp":{}},{"id":"googTrack_23084d23-7251-4528-9a9c-63975d7cdab0","googTrackId":"23084d23-7251-4528-9a9c-63975d7cdab0","type":"googTrack","timestamp":{}},{"googFingerprint":"41:E4:D5:74:77:4C:3A:9A:61:D5:B1:29:4C:C7:18:37:98:08:DD:B0:AD:E7:B2:99:07:78:09:04:DA:15:1F:CD","googFingerprintAlgorithm":"sha-256","googDerBase64":"MIIBFTCBvaADAgECAgkArKLiaqCDL58wCgYIKoZIzj0EAwIwETEPMA0GA1UEAwwGV2ViUlRDMB4XDTE2MDgxNzE5MzcyNVoXDTE2MDkxNzE5MzcyNVowETEPMA0GA1UEAwwGV2ViUlRDMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERF46M3VeSprnGRul2AhvcKMbOjmjgZ4jRqnJkiNuZ2eqr7aGGXzv02eSL5OKJ9qU4tzGpC+1jNJaiG7zuQIxWjAKBggqhkjOPQQDAgNHADBEAiAqYO4eiKsAyzVS0x7AWxMP8DiP2ti8hGIlpQ5bpnvldQIgMGEE9r8PxWuEYM89zqSK/E55WGvPV2ODX6XS7/B06f8=","id":"googCertificate_41:E4:D5:74:77:4C:3A:9A:61:D5:B1:29:4C:C7:18:37:98:08:DD:B0:AD:E7:B2:99:07:78:09:04:DA:15:1F:CD","type":"googCertificate","timestamp":{}},{"googFingerprint":"B2:9D:72:2C:16:52:90:4E:42:56:78:F4:FB:F1:71:E0:F2:56:93:06:BD:BB:34:F7:5B:80:97:0D:C3:C8:A9:20","googFingerprintAlgorithm":"sha-256","googDerBase64":"MIIBFjCBvaADAgECAgkA9iBy3Ra9D5MwCgYIKoZIzj0EAwIwETEPMA0GA1UEAwwGV2ViUlRDMB4XDTE2MDgxNzE5MzcyNVoXDTE2MDkxNzE5MzcyNVowETEPMA0GA1UEAwwGV2ViUlRDMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEM7rETA1ec+WSm04MhYvTzNG9SEcPxGBzNXuw+FGtQGOA8yVjiBtWtxi3Nxtl4E9Tj6T1eHySLkznK/bK+zC78jAKBggqhkjOPQQDAgNIADBFAiEAgDTN5uWVfKH0dLR8kxm2zPYHZN1F4ZGBvdzAZRefN28CIGLZncfi0FwnZFlntSB2m0O7+Vt23nLKWF16wgQd/3sS","id":"googCertificate_B2:9D:72:2C:16:52:90:4E:42:56:78:F4:FB:F1:71:E0:F2:56:93:06:BD:BB:34:F7:5B:80:97:0D:C3:C8:A9:20","type":"googCertificate","timestamp":{}},{"localCertificateId":"googCertificate_41:E4:D5:74:77:4C:3A:9A:61:D5:B1:29:4C:C7:18:37:98:08:DD:B0:AD:E7:B2:99:07:78:09:04:DA:15:1F:CD","googComponent":"1","id":"Channel-audio-1","selectedCandidatePairId":"Conn-audio-1-0","dtlsCipher":"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256","srtpCipher":"AES_CM_128_HMAC_SHA1_80","type":"googComponent","remoteCertificateId":"googCertificate_B2:9D:72:2C:16:52:90:4E:42:56:78:F4:FB:F1:71:E0:F2:56:93:06:BD:BB:34:F7:5B:80:97:0D:C3:C8:A9:20","timestamp":{}},{"candidateType":"host","ipAddress":"192.168.1.6","id":"Cand-7FCQuDVB","transport":"udp","networkType":"unknown","priority":"2113937151","type":"localcandidate","portNumber":"63331","timestamp":{}},{"candidateType":"host","ipAddress":"192.168.1.6","id":"Cand-Js3A6w8y","transport":"udp","priority":"2113937151","type":"remotecandidate","portNumber":"55964","timestamp":{}},{"localCandidateId":"Cand-7FCQuDVB","googRemoteAddress":"85.179.129.152:55964","googRemoteCandidateType":"stun","googRtt":"3000","bytesSent":"0","googTransportType":"udp","type":"googCandidatePair","googWritable":"false","googActiveConnection":"false","remoteCandidateId":"Cand-AwbKRfaP","bytesReceived":"0","googLocalAddress":"192.168.1.6:63331","googChannelId":"Channel-audio-1","packetsDiscardedOnSend":"0","packetsSent":"0","googLocalCandidateType":"local","id":"Conn-audio-1-1","googReadable":"false","timestamp":{}},{"candidateType":"serverreflexive","ipAddress":"85.179.129.152","id":"Cand-AwbKRfaP","transport":"udp","priority":"1677729535","type":"remotecandidate","portNumber":"55964","timestamp":{}},{"localCandidateId":"Cand-7FCQuDVB","googRemoteAddress":"85.179.129.152:55965","googRemoteCandidateType":"stun","googRtt":"3000","bytesSent":"0","googTransportType":"udp","type":"googCandidatePair","googWritable":"false","googActiveConnection":"false","remoteCandidateId":"Cand-ew/8za+M","bytesReceived":"0","googLocalAddress":"192.168.1.6:63331","googChannelId":"Channel-audio-1","packetsDiscardedOnSend":"0","packetsSent":"0","googLocalCandidateType":"local","id":"Conn-audio-1-2","googReadable":"false","timestamp":{}},{"candidateType":"serverreflexive","ipAddress":"85.179.129.152","id":"Cand-ew/8za+M","transport":"udp","priority":"1677729535","type":"remotecandidate","portNumber":"55965","timestamp":{}},{"localCandidateId":"Cand-YcQhLxuG","googRemoteAddress":"74.125.136.87:26946","googRemoteCandidateType":"relay","googRtt":"2267","bytesSent":"0","googTransportType":"udp","type":"googCandidatePair","googWritable":"false","googActiveConnection":"false","remoteCandidateId":"Cand-uOIrojwF","bytesReceived":"0","googLocalAddress":"85.179.129.152:63334","googChannelId":"Channel-audio-1","packetsDiscardedOnSend":"0","packetsSent":"0","googLocalCandidateType":"prflx","id":"Conn-audio-1-3","googReadable":"false","timestamp":{}},{"candidateType":"relayed","ipAddress":"74.125.136.87","id":"Cand-uOIrojwF","transport":"udp","priority":"33562879","type":"remotecandidate","portNumber":"26946","timestamp":{}},{"localCandidateId":"Cand-7FCQuDVB","googRemoteAddress":"74.125.136.85:29605","googRemoteCandidateType":"relay","googRtt":"3000","bytesSent":"0","googTransportType":"udp","type":"googCandidatePair","googWritable":"false","googActiveConnection":"false","remoteCandidateId":"Cand-cUIj6b5K","bytesReceived":"0","googLocalAddress":"192.168.1.6:63331","googChannelId":"Channel-audio-1","packetsDiscardedOnSend":"0","packetsSent":"0","googLocalCandidateType":"local","id":"Conn-audio-1-4","googReadable":"false","timestamp":{}},{"candidateType":"relayed","ipAddress":"74.125.136.85","id":"Cand-cUIj6b5K","transport":"udp","priority":"16785151","type":"remotecandidate","portNumber":"29605","timestamp":{}},{"googCodecName":"opus","transportId":"Channel-audio-1","googDecodingCTSG":"0","type":"ssrc","googAccelerateRate":"0","googSpeechExpandRate":"0","packetsLost":"0","id":"ssrc_1940985005_recv","googTrackId":"605ae15e-5d25-4f9a-a80b-c514971477ec","googJitterBufferMs":"49","googPreemptiveExpandRate":"0","timestamp":{},"googPreferredJitterBufferMs":"20","ssrc":"1940985005","googSecondaryDecodedRate":"0","googExpandRate":"0","audioOutputLevel":"26725","mediaType":"audio","googDecodingPLCCNG":"44","bytesReceived":"28346","googDecodingPLC":"16","googDecodingCTN":"600","packetsReceived":"277","googDecodingCNG":"0","googDecodingNormal":"540","googCurrentDelayMs":"84","googJitterReceived":"4"},{"googCodecName":"opus","googEchoCancellationReturnLossEnhancement":"-100","transportId":"Channel-audio-1","ssrc":"3634126469","googRtt":"35","mediaType":"audio","audioInputLevel":"28816","bytesSent":"28236","type":"ssrc","googEchoCancellationReturnLoss":"-100","googTypingNoiseState":"false","packetsLost":"0","packetsSent":"276","id":"ssrc_3634126469_send","googTrackId":"687f5b0e-915c-469f-bdf6-c8216713bd14","googJitterReceived":"5","aecDivergentFilterFraction":"0","timestamp":{}},{"googCodecName":"VP9","transportId":"Channel-audio-1","googFrameRateDecoded":"13","googTargetDelayMs":"110","type":"ssrc","googRenderDelayMs":"10","googFirsSent":"0","googMaxDecodeMs":"5","googPlisSent":"0","googCaptureStartNtpTimeMs":"0","packetsLost":"0","googFrameRateOutput":"13","id":"ssrc_3350494056_recv","googTrackId":"23084d23-7251-4528-9a9c-63975d7cdab0","googJitterBufferMs":"95","timestamp":{},"googMinPlayoutDelayMs":"0","ssrc":"3350494056","mediaType":"video","googNacksSent":"0","bytesReceived":"222184","googFrameWidthReceived":"640","packetsReceived":"227","codecImplementationName":"libvpx","googDecodeMs":"3","googCurrentDelayMs":"110","googFrameHeightReceived":"480","googFrameRateReceived":"12"},{"googCodecName":"VP9","transportId":"Channel-audio-1","googRtt":"7","googFirsReceived":"0","type":"ssrc","googEncodeUsagePercent":"30","googFrameRateInput":"14","googFrameHeightInput":"480","googFrameWidthSent":"640","packetsLost":"0","packetsSent":"244","id":"ssrc_2105429706_send","googTrackId":"2c4f63c0-f828-400a-857a-ca25a55bf6ae","googFrameRateSent":"12","googFrameHeightSent":"480","timestamp":{},"googAdaptationChanges":"0","googNacksReceived":"0","googCpuLimitedResolution":"false","googBandwidthLimitedResolution":"false","googAvgEncodeMs":"35","ssrc":"2105429706","mediaType":"video","bytesSent":"239355","codecImplementationName":"libvpx","googFrameWidthInput":"640","googViewLimitedResolution":"false","googPlisReceived":"0"},{"googTransmitBitrate":"458402","googBucketDelay":"12","googTargetEncBitrate":"446665","googAvailableSendBandwidth":"457780","id":"bweforvideo","type":"VideoBwe","googAvailableReceiveBandwidth":"420553","googRetransmitBitrate":"0","googActualEncBitrate":"336368","timestamp":{}},{"candidateType":"peerreflexive","ipAddress":"85.179.129.152","id":"Cand-YcQhLxuG","transport":"udp","networkType":"unknown","priority":"1845501695","type":"localcandidate","portNumber":"63334","timestamp":{}}]},{"audio":{"inbound":{"packetsLost":0,"outputLevel":23950,"bytesReceived":33630,"jitter":4,"packetsReceived":327},"outbound":{"jitter":5,"rtt":35,"packetsLost":0,"packetsSent":327,"bytesSent":33670,"inputLevel":12218}},"video":{"bandwidth":{"actualEncBitrate":455420,"availableReceiveBandwidth":456615,"targetEncBitrate":483398,"transmitBitrate":460839,"retransmitBitrate":0,"bucketDelay":0,"availableSendBandwidth":493184}},"results":[{"id":"googTrack_687f5b0e-915c-469f-bdf6-c8216713bd14","googTrackId":"687f5b0e-915c-469f-bdf6-c8216713bd14","type":"googTrack","timestamp":{}},{"id":"googTrack_2c4f63c0-f828-400a-857a-ca25a55bf6ae","googTrackId":"2c4f63c0-f828-400a-857a-ca25a55bf6ae","type":"googTrack","timestamp":{}},{"googInitiator":"false","id":"googLibjingleSession_3693304454062870897","type":"googLibjingleSession","timestamp":{}},{"id":"googTrack_605ae15e-5d25-4f9a-a80b-c514971477ec","googTrackId":"605ae15e-5d25-4f9a-a80b-c514971477ec","type":"googTrack","timestamp":{}},{"id":"googTrack_23084d23-7251-4528-9a9c-63975d7cdab0","googTrackId":"23084d23-7251-4528-9a9c-63975d7cdab0","type":"googTrack","timestamp":{}},{"googFingerprint":"41:E4:D5:74:77:4C:3A:9A:61:D5:B1:29:4C:C7:18:37:98:08:DD:B0:AD:E7:B2:99:07:78:09:04:DA:15:1F:CD","googFingerprintAlgorithm":"sha-256","googDerBase64":"MIIBFTCBvaADAgECAgkArKLiaqCDL58wCgYIKoZIzj0EAwIwETEPMA0GA1UEAwwGV2ViUlRDMB4XDTE2MDgxNzE5MzcyNVoXDTE2MDkxNzE5MzcyNVowETEPMA0GA1UEAwwGV2ViUlRDMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERF46M3VeSprnGRul2AhvcKMbOjmjgZ4jRqnJkiNuZ2eqr7aGGXzv02eSL5OKJ9qU4tzGpC+1jNJaiG7zuQIxWjAKBggqhkjOPQQDAgNHADBEAiAqYO4eiKsAyzVS0x7AWxMP8DiP2ti8hGIlpQ5bpnvldQIgMGEE9r8PxWuEYM89zqSK/E55WGvPV2ODX6XS7/B06f8=","id":"googCertificate_41:E4:D5:74:77:4C:3A:9A:61:D5:B1:29:4C:C7:18:37:98:08:DD:B0:AD:E7:B2:99:07:78:09:04:DA:15:1F:CD","type":"googCertificate","timestamp":{}},{"googFingerprint":"B2:9D:72:2C:16:52:90:4E:42:56:78:F4:FB:F1:71:E0:F2:56:93:06:BD:BB:34:F7:5B:80:97:0D:C3:C8:A9:20","googFingerprintAlgorithm":"sha-256","googDerBase64":"MIIBFjCBvaADAgECAgkA9iBy3Ra9D5MwCgYIKoZIzj0EAwIwETEPMA0GA1UEAwwGV2ViUlRDMB4XDTE2MDgxNzE5MzcyNVoXDTE2MDkxNzE5MzcyNVowETEPMA0GA1UEAwwGV2ViUlRDMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEM7rETA1ec+WSm04MhYvTzNG9SEcPxGBzNXuw+FGtQGOA8yVjiBtWtxi3Nxtl4E9Tj6T1eHySLkznK/bK+zC78jAKBggqhkjOPQQDAgNIADBFAiEAgDTN5uWVfKH0dLR8kxm2zPYHZN1F4ZGBvdzAZRefN28CIGLZncfi0FwnZFlntSB2m0O7+Vt23nLKWF16wgQd/3sS","id":"googCertificate_B2:9D:72:2C:16:52:90:4E:42:56:78:F4:FB:F1:71:E0:F2:56:93:06:BD:BB:34:F7:5B:80:97:0D:C3:C8:A9:20","type":"googCertificate","timestamp":{}},{"localCertificateId":"googCertificate_41:E4:D5:74:77:4C:3A:9A:61:D5:B1:29:4C:C7:18:37:98:08:DD:B0:AD:E7:B2:99:07:78:09:04:DA:15:1F:CD","googComponent":"1","id":"Channel-audio-1","selectedCandidatePairId":"Conn-audio-1-0","dtlsCipher":"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256","srtpCipher":"AES_CM_128_HMAC_SHA1_80","type":"googComponent","remoteCertificateId":"googCertificate_B2:9D:72:2C:16:52:90:4E:42:56:78:F4:FB:F1:71:E0:F2:56:93:06:BD:BB:34:F7:5B:80:97:0D:C3:C8:A9:20","timestamp":{}},{"candidateType":"host","ipAddress":"192.168.1.6","id":"Cand-7FCQuDVB","transport":"udp","networkType":"unknown","priority":"2113937151","type":"localcandidate","portNumber":"63331","timestamp":{}},{"candidateType":"host","ipAddress":"192.168.1.6","id":"Cand-Js3A6w8y","transport":"udp","priority":"2113937151","type":"remotecandidate","portNumber":"55964","timestamp":{}},{"localCandidateId":"Cand-7FCQuDVB","googRemoteAddress":"85.179.129.152:55964","googRemoteCandidateType":"stun","googRtt":"3000","bytesSent":"0","googTransportType":"udp","type":"googCandidatePair","googWritable":"false","googActiveConnection":"false","remoteCandidateId":"Cand-AwbKRfaP","bytesReceived":"0","googLocalAddress":"192.168.1.6:63331","googChannelId":"Channel-audio-1","packetsDiscardedOnSend":"0","packetsSent":"0","googLocalCandidateType":"local","id":"Conn-audio-1-1","googReadable":"false","timestamp":{}},{"candidateType":"serverreflexive","ipAddress":"85.179.129.152","id":"Cand-AwbKRfaP","transport":"udp","priority":"1677729535","type":"remotecandidate","portNumber":"55964","timestamp":{}},{"localCandidateId":"Cand-7FCQuDVB","googRemoteAddress":"85.179.129.152:55965","googRemoteCandidateType":"stun","googRtt":"3000","bytesSent":"0","googTransportType":"udp","type":"googCandidatePair","googWritable":"false","googActiveConnection":"false","remoteCandidateId":"Cand-ew/8za+M","bytesReceived":"0","googLocalAddress":"192.168.1.6:63331","googChannelId":"Channel-audio-1","packetsDiscardedOnSend":"0","packetsSent":"0","googLocalCandidateType":"local","id":"Conn-audio-1-2","googReadable":"false","timestamp":{}},{"candidateType":"serverreflexive","ipAddress":"85.179.129.152","id":"Cand-ew/8za+M","transport":"udp","priority":"1677729535","type":"remotecandidate","portNumber":"55965","timestamp":{}},{"localCandidateId":"Cand-YcQhLxuG","googRemoteAddress":"74.125.136.87:26946","googRemoteCandidateType":"relay","googRtt":"2267","bytesSent":"0","googTransportType":"udp","type":"googCandidatePair","googWritable":"false","googActiveConnection":"false","remoteCandidateId":"Cand-uOIrojwF","bytesReceived":"0","googLocalAddress":"85.179.129.152:63334","googChannelId":"Channel-audio-1","packetsDiscardedOnSend":"0","packetsSent":"0","googLocalCandidateType":"prflx","id":"Conn-audio-1-3","googReadable":"false","timestamp":{}},{"candidateType":"relayed","ipAddress":"74.125.136.87","id":"Cand-uOIrojwF","transport":"udp","priority":"33562879","type":"remotecandidate","portNumber":"26946","timestamp":{}},{"localCandidateId":"Cand-7FCQuDVB","googRemoteAddress":"74.125.136.85:29605","googRemoteCandidateType":"relay","googRtt":"3000","bytesSent":"0","googTransportType":"udp","type":"googCandidatePair","googWritable":"false","googActiveConnection":"false","remoteCandidateId":"Cand-cUIj6b5K","bytesReceived":"0","googLocalAddress":"192.168.1.6:63331","googChannelId":"Channel-audio-1","packetsDiscardedOnSend":"0","packetsSent":"0","googLocalCandidateType":"local","id":"Conn-audio-1-4","googReadable":"false","timestamp":{}},{"candidateType":"relayed","ipAddress":"74.125.136.85","id":"Cand-cUIj6b5K","transport":"udp","priority":"16785151","type":"remotecandidate","portNumber":"29605","timestamp":{}},{"googCodecName":"opus","transportId":"Channel-audio-1","googDecodingCTSG":"0","type":"ssrc","googAccelerateRate":"0.00909424","googSpeechExpandRate":"0","packetsLost":"0","id":"ssrc_1940985005_recv","googTrackId":"605ae15e-5d25-4f9a-a80b-c514971477ec","googJitterBufferMs":"24","googPreemptiveExpandRate":"0","timestamp":{},"googPreferredJitterBufferMs":"20","ssrc":"1940985005","googSecondaryDecodedRate":"0","googExpandRate":"0","audioOutputLevel":"23950","mediaType":"audio","googDecodingPLCCNG":"44","bytesReceived":"33630","googDecodingPLC":"16","googDecodingCTN":"702","packetsReceived":"327","googDecodingCNG":"0","googDecodingNormal":"642","googCurrentDelayMs":"82","googJitterReceived":"4"},{"googCodecName":"opus","googEchoCancellationReturnLossEnhancement":"-100","transportId":"Channel-audio-1","ssrc":"3634126469","googRtt":"35","mediaType":"audio","audioInputLevel":"12218","bytesSent":"33670","googEchoCancellationEchoDelayStdDev":"20","type":"ssrc","googEchoCancellationReturnLoss":"-100","googTypingNoiseState":"false","packetsLost":"0","packetsSent":"327","id":"ssrc_3634126469_send","googTrackId":"687f5b0e-915c-469f-bdf6-c8216713bd14","googJitterReceived":"5","aecDivergentFilterFraction":"0.6","timestamp":{}},{"googCodecName":"VP9","transportId":"Channel-audio-1","googFrameRateDecoded":"18","googTargetDelayMs":"144","type":"ssrc","googRenderDelayMs":"10","googFirsSent":"0","googMaxDecodeMs":"5","googPlisSent":"0","googCaptureStartNtpTimeMs":"0","packetsLost":"0","googFrameRateOutput":"18","id":"ssrc_3350494056_recv","googTrackId":"23084d23-7251-4528-9a9c-63975d7cdab0","googJitterBufferMs":"129","timestamp":{},"googMinPlayoutDelayMs":"0","ssrc":"3350494056","mediaType":"video","googNacksSent":"0","bytesReceived":"297133","googFrameWidthReceived":"640","packetsReceived":"300","codecImplementationName":"libvpx","googDecodeMs":"5","googCurrentDelayMs":"144","googFrameHeightReceived":"480","googFrameRateReceived":"15"},{"googCodecName":"VP9","transportId":"Channel-audio-1","googRtt":"7","googFirsReceived":"0","type":"ssrc","googEncodeUsagePercent":"30","googFrameRateInput":"17","googFrameHeightInput":"480","googFrameWidthSent":"640","packetsLost":"0","packetsSent":"299","id":"ssrc_2105429706_send","googTrackId":"2c4f63c0-f828-400a-857a-ca25a55bf6ae","googFrameRateSent":"13","googFrameHeightSent":"480","timestamp":{},"googAdaptationChanges":"0","googNacksReceived":"0","googCpuLimitedResolution":"false","googBandwidthLimitedResolution":"false","googAvgEncodeMs":"27","ssrc":"2105429706","mediaType":"video","bytesSent":"296900","codecImplementationName":"libvpx","googFrameWidthInput":"640","googViewLimitedResolution":"false","googPlisReceived":"0"},{"googTransmitBitrate":"460839","googBucketDelay":"0","googTargetEncBitrate":"483398","googAvailableSendBandwidth":"493184","id":"bweforvideo","type":"VideoBwe","googAvailableReceiveBandwidth":"456615","googRetransmitBitrate":"0","googActualEncBitrate":"455420","timestamp":{}},{"candidateType":"peerreflexive","ipAddress":"85.179.129.152","id":"Cand-YcQhLxuG","transport":"udp","networkType":"unknown","priority":"1845501695","type":"localcandidate","portNumber":"63334","timestamp":{}}]},{"audio":{"inbound":{"packetsLost":0,"outputLevel":11279,"bytesReceived":38906,"jitter":5,"packetsReceived":378},"outbound":{"jitter":5,"rtt":35,"packetsLost":0,"packetsSent":378,"bytesSent":39035,"inputLevel":5934}},"video":{"bandwidth":{"actualEncBitrate":614107,"availableReceiveBandwidth":489663,"targetEncBitrate":523070,"transmitBitrate":584407,"retransmitBitrate":0,"bucketDelay":0,"availableSendBandwidth":533639}},"results":[{"id":"googTrack_687f5b0e-915c-469f-bdf6-c8216713bd14","googTrackId":"687f5b0e-915c-469f-bdf6-c8216713bd14","type":"googTrack","timestamp":{}},{"id":"googTrack_2c4f63c0-f828-400a-857a-ca25a55bf6ae","googTrackId":"2c4f63c0-f828-400a-857a-ca25a55bf6ae","type":"googTrack","timestamp":{}},{"googInitiator":"false","id":"googLibjingleSession_3693304454062870897","type":"googLibjingleSession","timestamp":{}},{"id":"googTrack_605ae15e-5d25-4f9a-a80b-c514971477ec","googTrackId":"605ae15e-5d25-4f9a-a80b-c514971477ec","type":"googTrack","timestamp":{}},{"id":"googTrack_23084d23-7251-4528-9a9c-63975d7cdab0","googTrackId":"23084d23-7251-4528-9a9c-63975d7cdab0","type":"googTrack","timestamp":{}},{"googFingerprint":"41:E4:D5:74:77:4C:3A:9A:61:D5:B1:29:4C:C7:18:37:98:08:DD:B0:AD:E7:B2:99:07:78:09:04:DA:15:1F:CD","googFingerprintAlgorithm":"sha-256","googDerBase64":"MIIBFTCBvaADAgECAgkArKLiaqCDL58wCgYIKoZIzj0EAwIwETEPMA0GA1UEAwwGV2ViUlRDMB4XDTE2MDgxNzE5MzcyNVoXDTE2MDkxNzE5MzcyNVowETEPMA0GA1UEAwwGV2ViUlRDMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERF46M3VeSprnGRul2AhvcKMbOjmjgZ4jRqnJkiNuZ2eqr7aGGXzv02eSL5OKJ9qU4tzGpC+1jNJaiG7zuQIxWjAKBggqhkjOPQQDAgNHADBEAiAqYO4eiKsAyzVS0x7AWxMP8DiP2ti8hGIlpQ5bpnvldQIgMGEE9r8PxWuEYM89zqSK/E55WGvPV2ODX6XS7/B06f8=","id":"googCertificate_41:E4:D5:74:77:4C:3A:9A:61:D5:B1:29:4C:C7:18:37:98:08:DD:B0:AD:E7:B2:99:07:78:09:04:DA:15:1F:CD","type":"googCertificate","timestamp":{}},{"googFingerprint":"B2:9D:72:2C:16:52:90:4E:42:56:78:F4:FB:F1:71:E0:F2:56:93:06:BD:BB:34:F7:5B:80:97:0D:C3:C8:A9:20","googFingerprintAlgorithm":"sha-256","googDerBase64":"MIIBFjCBvaADAgECAgkA9iBy3Ra9D5MwCgYIKoZIzj0EAwIwETEPMA0GA1UEAwwGV2ViUlRDMB4XDTE2MDgxNzE5MzcyNVoXDTE2MDkxNzE5MzcyNVowETEPMA0GA1UEAwwGV2ViUlRDMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEM7rETA1ec+WSm04MhYvTzNG9SEcPxGBzNXuw+FGtQGOA8yVjiBtWtxi3Nxtl4E9Tj6T1eHySLkznK/bK+zC78jAKBggqhkjOPQQDAgNIADBFAiEAgDTN5uWVfKH0dLR8kxm2zPYHZN1F4ZGBvdzAZRefN28CIGLZncfi0FwnZFlntSB2m0O7+Vt23nLKWF16wgQd/3sS","id":"googCertificate_B2:9D:72:2C:16:52:90:4E:42:56:78:F4:FB:F1:71:E0:F2:56:93:06:BD:BB:34:F7:5B:80:97:0D:C3:C8:A9:20","type":"googCertificate","timestamp":{}},{"localCertificateId":"googCertificate_41:E4:D5:74:77:4C:3A:9A:61:D5:B1:29:4C:C7:18:37:98:08:DD:B0:AD:E7:B2:99:07:78:09:04:DA:15:1F:CD","googComponent":"1","id":"Channel-audio-1","selectedCandidatePairId":"Conn-audio-1-0","dtlsCipher":"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256","srtpCipher":"AES_CM_128_HMAC_SHA1_80","type":"googComponent","remoteCertificateId":"googCertificate_B2:9D:72:2C:16:52:90:4E:42:56:78:F4:FB:F1:71:E0:F2:56:93:06:BD:BB:34:F7:5B:80:97:0D:C3:C8:A9:20","timestamp":{}},{"candidateType":"host","ipAddress":"192.168.1.6","id":"Cand-7FCQuDVB","transport":"udp","networkType":"unknown","priority":"2113937151","type":"localcandidate","portNumber":"63331","timestamp":{}},{"candidateType":"host","ipAddress":"192.168.1.6","id":"Cand-Js3A6w8y","transport":"udp","priority":"2113937151","type":"remotecandidate","portNumber":"55964","timestamp":{}},{"localCandidateId":"Cand-7FCQuDVB","googRemoteAddress":"85.179.129.152:55964","googRemoteCandidateType":"stun","googRtt":"3000","bytesSent":"0","googTransportType":"udp","type":"googCandidatePair","googWritable":"false","googActiveConnection":"false","remoteCandidateId":"Cand-AwbKRfaP","bytesReceived":"0","googLocalAddress":"192.168.1.6:63331","googChannelId":"Channel-audio-1","packetsDiscardedOnSend":"0","packetsSent":"0","googLocalCandidateType":"local","id":"Conn-audio-1-1","googReadable":"false","timestamp":{}},{"candidateType":"serverreflexive","ipAddress":"85.179.129.152","id":"Cand-AwbKRfaP","transport":"udp","priority":"1677729535","type":"remotecandidate","portNumber":"55964","timestamp":{}},{"localCandidateId":"Cand-7FCQuDVB","googRemoteAddress":"85.179.129.152:55965","googRemoteCandidateType":"stun","googRtt":"3000","bytesSent":"0","googTransportType":"udp","type":"googCandidatePair","googWritable":"false","googActiveConnection":"false","remoteCandidateId":"Cand-ew/8za+M","bytesReceived":"0","googLocalAddress":"192.168.1.6:63331","googChannelId":"Channel-audio-1","packetsDiscardedOnSend":"0","packetsSent":"0","googLocalCandidateType":"local","id":"Conn-audio-1-2","googReadable":"false","timestamp":{}},{"candidateType":"serverreflexive","ipAddress":"85.179.129.152","id":"Cand-ew/8za+M","transport":"udp","priority":"1677729535","type":"remotecandidate","portNumber":"55965","timestamp":{}},{"localCandidateId":"Cand-YcQhLxuG","googRemoteAddress":"74.125.136.87:26946","googRemoteCandidateType":"relay","googRtt":"2267","bytesSent":"0","googTransportType":"udp","type":"googCandidatePair","googWritable":"false","googActiveConnection":"false","remoteCandidateId":"Cand-uOIrojwF","bytesReceived":"0","googLocalAddress":"85.179.129.152:63334","googChannelId":"Channel-audio-1","packetsDiscardedOnSend":"0","packetsSent":"0","googLocalCandidateType":"prflx","id":"Conn-audio-1-3","googReadable":"false","timestamp":{}},{"candidateType":"relayed","ipAddress":"74.125.136.87","id":"Cand-uOIrojwF","transport":"udp","priority":"33562879","type":"remotecandidate","portNumber":"26946","timestamp":{}},{"localCandidateId":"Cand-7FCQuDVB","googRemoteAddress":"74.125.136.85:29605","googRemoteCandidateType":"relay","googRtt":"3000","bytesSent":"0","googTransportType":"udp","type":"googCandidatePair","googWritable":"false","googActiveConnection":"false","remoteCandidateId":"Cand-cUIj6b5K","bytesReceived":"0","googLocalAddress":"192.168.1.6:63331","googChannelId":"Channel-audio-1","packetsDiscardedOnSend":"0","packetsSent":"0","googLocalCandidateType":"local","id":"Conn-audio-1-4","googReadable":"false","timestamp":{}},{"candidateType":"relayed","ipAddress":"74.125.136.85","id":"Cand-cUIj6b5K","transport":"udp","priority":"16785151","type":"remotecandidate","portNumber":"29605","timestamp":{}},{"googCodecName":"opus","transportId":"Channel-audio-1","googDecodingCTSG":"0","type":"ssrc","googAccelerateRate":"0","googSpeechExpandRate":"0","packetsLost":"0","id":"ssrc_1940985005_recv","googTrackId":"605ae15e-5d25-4f9a-a80b-c514971477ec","googJitterBufferMs":"34","googPreemptiveExpandRate":"0","timestamp":{},"googPreferredJitterBufferMs":"20","ssrc":"1940985005","googSecondaryDecodedRate":"0","googExpandRate":"0","audioOutputLevel":"11279","mediaType":"audio","googDecodingPLCCNG":"44","bytesReceived":"38906","googDecodingPLC":"16","googDecodingCTN":"803","packetsReceived":"378","googDecodingCNG":"0","googDecodingNormal":"743","googCurrentDelayMs":"78","googJitterReceived":"5"},{"googCodecName":"opus","googEchoCancellationReturnLossEnhancement":"-100","transportId":"Channel-audio-1","ssrc":"3634126469","googRtt":"35","mediaType":"audio","audioInputLevel":"5934","bytesSent":"39035","googEchoCancellationEchoDelayStdDev":"20","type":"ssrc","googEchoCancellationReturnLoss":"-100","googTypingNoiseState":"false","packetsLost":"0","packetsSent":"378","id":"ssrc_3634126469_send","googTrackId":"687f5b0e-915c-469f-bdf6-c8216713bd14","googJitterReceived":"5","aecDivergentFilterFraction":"0.62","timestamp":{}},{"googCodecName":"VP9","transportId":"Channel-audio-1","googFrameRateDecoded":"14","googTargetDelayMs":"133","type":"ssrc","googRenderDelayMs":"10","googFirsSent":"0","googMaxDecodeMs":"5","googPlisSent":"0","googCaptureStartNtpTimeMs":"0","packetsLost":"0","googFrameRateOutput":"14","id":"ssrc_3350494056_recv","googTrackId":"23084d23-7251-4528-9a9c-63975d7cdab0","googJitterBufferMs":"118","timestamp":{},"googMinPlayoutDelayMs":"0","ssrc":"3350494056","mediaType":"video","googNacksSent":"0","bytesReceived":"354920","googFrameWidthReceived":"640","packetsReceived":"355","codecImplementationName":"libvpx","googDecodeMs":"3","googCurrentDelayMs":"133","googFrameHeightReceived":"480","googFrameRateReceived":"15"},{"googCodecName":"VP9","transportId":"Channel-audio-1","googRtt":"7","googFirsReceived":"0","type":"ssrc","googEncodeUsagePercent":"30","googFrameRateInput":"17","googFrameHeightInput":"480","googFrameWidthSent":"640","packetsLost":"0","packetsSent":"372","id":"ssrc_2105429706_send","googTrackId":"2c4f63c0-f828-400a-857a-ca25a55bf6ae","googFrameRateSent":"14","googFrameHeightSent":"480","timestamp":{},"googAdaptationChanges":"0","googNacksReceived":"0","googCpuLimitedResolution":"false","googBandwidthLimitedResolution":"false","googAvgEncodeMs":"33","ssrc":"2105429706","mediaType":"video","bytesSent":"374571","codecImplementationName":"libvpx","googFrameWidthInput":"640","googViewLimitedResolution":"false","googPlisReceived":"0"},{"googTransmitBitrate":"584407","googBucketDelay":"0","googTargetEncBitrate":"523070","googAvailableSendBandwidth":"533639","id":"bweforvideo","type":"VideoBwe","googAvailableReceiveBandwidth":"489663","googRetransmitBitrate":"0","googActualEncBitrate":"614107","timestamp":{}},{"candidateType":"peerreflexive","ipAddress":"85.179.129.152","id":"Cand-YcQhLxuG","transport":"udp","networkType":"unknown","priority":"1845501695","type":"localcandidate","portNumber":"63334","timestamp":{}}]},{"audio":{"inbound":{"packetsLost":0,"outputLevel":16488,"bytesReceived":44240,"jitter":5,"packetsReceived":429},"outbound":{"jitter":5,"rtt":35,"packetsLost":0,"packetsSent":427,"bytesSent":44368,"inputLevel":13366}},"video":{"bandwidth":{"actualEncBitrate":513796,"availableReceiveBandwidth":535138,"targetEncBitrate":565916,"transmitBitrate":551640,"retransmitBitrate":0,"bucketDelay":6,"availableSendBandwidth":565916}},"results":[{"id":"googTrack_687f5b0e-915c-469f-bdf6-c8216713bd14","googTrackId":"687f5b0e-915c-469f-bdf6-c8216713bd14","type":"googTrack","timestamp":{}},{"id":"googTrack_2c4f63c0-f828-400a-857a-ca25a55bf6ae","googTrackId":"2c4f63c0-f828-400a-857a-ca25a55bf6ae","type":"googTrack","timestamp":{}},{"googInitiator":"false","id":"googLibjingleSession_3693304454062870897","type":"googLibjingleSession","timestamp":{}},{"id":"googTrack_605ae15e-5d25-4f9a-a80b-c514971477ec","googTrackId":"605ae15e-5d25-4f9a-a80b-c514971477ec","type":"googTrack","timestamp":{}},{"id":"googTrack_23084d23-7251-4528-9a9c-63975d7cdab0","googTrackId":"23084d23-7251-4528-9a9c-63975d7cdab0","type":"googTrack","timestamp":{}},{"googFingerprint":"41:E4:D5:74:77:4C:3A:9A:61:D5:B1:29:4C:C7:18:37:98:08:DD:B0:AD:E7:B2:99:07:78:09:04:DA:15:1F:CD","googFingerprintAlgorithm":"sha-256","googDerBase64":"MIIBFTCBvaADAgECAgkArKLiaqCDL58wCgYIKoZIzj0EAwIwETEPMA0GA1UEAwwGV2ViUlRDMB4XDTE2MDgxNzE5MzcyNVoXDTE2MDkxNzE5MzcyNVowETEPMA0GA1UEAwwGV2ViUlRDMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERF46M3VeSprnGRul2AhvcKMbOjmjgZ4jRqnJkiNuZ2eqr7aGGXzv02eSL5OKJ9qU4tzGpC+1jNJaiG7zuQIxWjAKBggqhkjOPQQDAgNHADBEAiAqYO4eiKsAyzVS0x7AWxMP8DiP2ti8hGIlpQ5bpnvldQIgMGEE9r8PxWuEYM89zqSK/E55WGvPV2ODX6XS7/B06f8=","id":"googCertificate_41:E4:D5:74:77:4C:3A:9A:61:D5:B1:29:4C:C7:18:37:98:08:DD:B0:AD:E7:B2:99:07:78:09:04:DA:15:1F:CD","type":"googCertificate","timestamp":{}},{"googFingerprint":"B2:9D:72:2C:16:52:90:4E:42:56:78:F4:FB:F1:71:E0:F2:56:93:06:BD:BB:34:F7:5B:80:97:0D:C3:C8:A9:20","googFingerprintAlgorithm":"sha-256","googDerBase64":"MIIBFjCBvaADAgECAgkA9iBy3Ra9D5MwCgYIKoZIzj0EAwIwETEPMA0GA1UEAwwGV2ViUlRDMB4XDTE2MDgxNzE5MzcyNVoXDTE2MDkxNzE5MzcyNVowETEPMA0GA1UEAwwGV2ViUlRDMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEM7rETA1ec+WSm04MhYvTzNG9SEcPxGBzNXuw+FGtQGOA8yVjiBtWtxi3Nxtl4E9Tj6T1eHySLkznK/bK+zC78jAKBggqhkjOPQQDAgNIADBFAiEAgDTN5uWVfKH0dLR8kxm2zPYHZN1F4ZGBvdzAZRefN28CIGLZncfi0FwnZFlntSB2m0O7+Vt23nLKWF16wgQd/3sS","id":"googCertificate_B2:9D:72:2C:16:52:90:4E:42:56:78:F4:FB:F1:71:E0:F2:56:93:06:BD:BB:34:F7:5B:80:97:0D:C3:C8:A9:20","type":"googCertificate","timestamp":{}},{"localCertificateId":"googCertificate_41:E4:D5:74:77:4C:3A:9A:61:D5:B1:29:4C:C7:18:37:98:08:DD:B0:AD:E7:B2:99:07:78:09:04:DA:15:1F:CD","googComponent":"1","id":"Channel-audio-1","selectedCandidatePairId":"Conn-audio-1-0","dtlsCipher":"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256","srtpCipher":"AES_CM_128_HMAC_SHA1_80","type":"googComponent","remoteCertificateId":"googCertificate_B2:9D:72:2C:16:52:90:4E:42:56:78:F4:FB:F1:71:E0:F2:56:93:06:BD:BB:34:F7:5B:80:97:0D:C3:C8:A9:20","timestamp":{}},{"candidateType":"host","ipAddress":"192.168.1.6","id":"Cand-7FCQuDVB","transport":"udp","networkType":"unknown","priority":"2113937151","type":"localcandidate","portNumber":"63331","timestamp":{}},{"candidateType":"host","ipAddress":"192.168.1.6","id":"Cand-Js3A6w8y","transport":"udp","priority":"2113937151","type":"remotecandidate","portNumber":"55964","timestamp":{}},{"localCandidateId":"Cand-7FCQuDVB","googRemoteAddress":"85.179.129.152:55964","googRemoteCandidateType":"stun","googRtt":"3000","bytesSent":"0","googTransportType":"udp","type":"googCandidatePair","googWritable":"false","googActiveConnection":"false","remoteCandidateId":"Cand-AwbKRfaP","bytesReceived":"0","googLocalAddress":"192.168.1.6:63331","googChannelId":"Channel-audio-1","packetsDiscardedOnSend":"0","packetsSent":"0","googLocalCandidateType":"local","id":"Conn-audio-1-1","googReadable":"false","timestamp":{}},{"candidateType":"serverreflexive","ipAddress":"85.179.129.152","id":"Cand-AwbKRfaP","transport":"udp","priority":"1677729535","type":"remotecandidate","portNumber":"55964","timestamp":{}},{"localCandidateId":"Cand-7FCQuDVB","googRemoteAddress":"85.179.129.152:55965","googRemoteCandidateType":"stun","googRtt":"3000","bytesSent":"0","googTransportType":"udp","type":"googCandidatePair","googWritable":"false","googActiveConnection":"false","remoteCandidateId":"Cand-ew/8za+M","bytesReceived":"0","googLocalAddress":"192.168.1.6:63331","googChannelId":"Channel-audio-1","packetsDiscardedOnSend":"0","packetsSent":"0","googLocalCandidateType":"local","id":"Conn-audio-1-2","googReadable":"false","timestamp":{}},{"candidateType":"serverreflexive","ipAddress":"85.179.129.152","id":"Cand-ew/8za+M","transport":"udp","priority":"1677729535","type":"remotecandidate","portNumber":"55965","timestamp":{}},{"localCandidateId":"Cand-YcQhLxuG","googRemoteAddress":"74.125.136.87:26946","googRemoteCandidateType":"relay","googRtt":"2267","bytesSent":"0","googTransportType":"udp","type":"googCandidatePair","googWritable":"false","googActiveConnection":"false","remoteCandidateId":"Cand-uOIrojwF","bytesReceived":"0","googLocalAddress":"85.179.129.152:63334","googChannelId":"Channel-audio-1","packetsDiscardedOnSend":"0","packetsSent":"0","googLocalCandidateType":"prflx","id":"Conn-audio-1-3","googReadable":"false","timestamp":{}},{"candidateType":"relayed","ipAddress":"74.125.136.87","id":"Cand-uOIrojwF","transport":"udp","priority":"33562879","type":"remotecandidate","portNumber":"26946","timestamp":{}},{"localCandidateId":"Cand-7FCQuDVB","googRemoteAddress":"74.125.136.85:29605","googRemoteCandidateType":"relay","googRtt":"3000","bytesSent":"0","googTransportType":"udp","type":"googCandidatePair","googWritable":"false","googActiveConnection":"false","remoteCandidateId":"Cand-cUIj6b5K","bytesReceived":"0","googLocalAddress":"192.168.1.6:63331","googChannelId":"Channel-audio-1","packetsDiscardedOnSend":"0","packetsSent":"0","googLocalCandidateType":"local","id":"Conn-audio-1-4","googReadable":"false","timestamp":{}},{"candidateType":"relayed","ipAddress":"74.125.136.85","id":"Cand-cUIj6b5K","transport":"udp","priority":"16785151","type":"remotecandidate","portNumber":"29605","timestamp":{}},{"googCodecName":"opus","transportId":"Channel-audio-1","googDecodingCTSG":"0","type":"ssrc","googAccelerateRate":"0.00915527","googSpeechExpandRate":"0","packetsLost":"0","id":"ssrc_1940985005_recv","googTrackId":"605ae15e-5d25-4f9a-a80b-c514971477ec","googJitterBufferMs":"39","googPreemptiveExpandRate":"0","timestamp":{},"googPreferredJitterBufferMs":"40","ssrc":"1940985005","googSecondaryDecodedRate":"0","googExpandRate":"0","audioOutputLevel":"16488","mediaType":"audio","googDecodingPLCCNG":"44","bytesReceived":"44240","googDecodingPLC":"16","googDecodingCTN":"904","packetsReceived":"429","googDecodingCNG":"0","googDecodingNormal":"844","googCurrentDelayMs":"74","googJitterReceived":"5"},{"googCodecName":"opus","googEchoCancellationReturnLossEnhancement":"-100","transportId":"Channel-audio-1","ssrc":"3634126469","googRtt":"35","mediaType":"audio","audioInputLevel":"13366","bytesSent":"44368","googEchoCancellationEchoDelayStdDev":"20","type":"ssrc","googEchoCancellationReturnLoss":"-100","googTypingNoiseState":"false","packetsLost":"0","packetsSent":"427","id":"ssrc_3634126469_send","googTrackId":"687f5b0e-915c-469f-bdf6-c8216713bd14","googJitterReceived":"5","aecDivergentFilterFraction":"0.56","timestamp":{}},{"googCodecName":"VP9","transportId":"Channel-audio-1","googFrameRateDecoded":"14","googTargetDelayMs":"130","type":"ssrc","googRenderDelayMs":"10","googFirsSent":"0","googMaxDecodeMs":"5","googPlisSent":"0","googCaptureStartNtpTimeMs":"0","packetsLost":"0","googFrameRateOutput":"14","id":"ssrc_3350494056_recv","googTrackId":"23084d23-7251-4528-9a9c-63975d7cdab0","googJitterBufferMs":"115","timestamp":{},"googMinPlayoutDelayMs":"0","ssrc":"3350494056","mediaType":"video","googNacksSent":"0","bytesReceived":"413409","googFrameWidthReceived":"640","packetsReceived":"412","codecImplementationName":"libvpx","googDecodeMs":"4","googCurrentDelayMs":"130","googFrameHeightReceived":"480","googFrameRateReceived":"13"},{"googCodecName":"VP9","transportId":"Channel-audio-1","googRtt":"5","googFirsReceived":"0","type":"ssrc","googEncodeUsagePercent":"30","googFrameRateInput":"14","googFrameHeightInput":"480","googFrameWidthSent":"640","packetsLost":"0","packetsSent":"433","id":"ssrc_2105429706_send","googTrackId":"2c4f63c0-f828-400a-857a-ca25a55bf6ae","googFrameRateSent":"12","googFrameHeightSent":"480","timestamp":{},"googAdaptationChanges":"0","googNacksReceived":"0","googCpuLimitedResolution":"false","googBandwidthLimitedResolution":"false","googAvgEncodeMs":"24","ssrc":"2105429706","mediaType":"video","bytesSent":"439014","codecImplementationName":"libvpx","googFrameWidthInput":"640","googViewLimitedResolution":"false","googPlisReceived":"0"},{"googTransmitBitrate":"551640","googBucketDelay":"6","googTargetEncBitrate":"565916","googAvailableSendBandwidth":"565916","id":"bweforvideo","type":"VideoBwe","googAvailableReceiveBandwidth":"535138","googRetransmitBitrate":"0","googActualEncBitrate":"513796","timestamp":{}},{"candidateType":"peerreflexive","ipAddress":"85.179.129.152","id":"Cand-YcQhLxuG","transport":"udp","networkType":"unknown","priority":"1845501695","type":"localcandidate","portNumber":"63334","timestamp":{}}]},{"audio":{"inbound":{"packetsLost":0,"outputLevel":1965,"bytesReceived":49393,"jitter":5,"packetsReceived":479},"outbound":{"jitter":5,"rtt":35,"packetsLost":0,"packetsSent":478,"bytesSent":49691,"inputLevel":1844}},"video":{"bandwidth":{"actualEncBitrate":627198,"availableReceiveBandwidth":575550,"targetEncBitrate":612189,"transmitBitrate":626167,"retransmitBitrate":0,"bucketDelay":0,"availableSendBandwidth":612189}},"results":[{"id":"googTrack_687f5b0e-915c-469f-bdf6-c8216713bd14","googTrackId":"687f5b0e-915c-469f-bdf6-c8216713bd14","type":"googTrack","timestamp":{}},{"id":"googTrack_2c4f63c0-f828-400a-857a-ca25a55bf6ae","googTrackId":"2c4f63c0-f828-400a-857a-ca25a55bf6ae","type":"googTrack","timestamp":{}},{"googInitiator":"false","id":"googLibjingleSession_3693304454062870897","type":"googLibjingleSession","timestamp":{}},{"id":"googTrack_605ae15e-5d25-4f9a-a80b-c514971477ec","googTrackId":"605ae15e-5d25-4f9a-a80b-c514971477ec","type":"googTrack","timestamp":{}},{"id":"googTrack_23084d23-7251-4528-9a9c-63975d7cdab0","googTrackId":"23084d23-7251-4528-9a9c-63975d7cdab0","type":"googTrack","timestamp":{}},{"googFingerprint":"41:E4:D5:74:77:4C:3A:9A:61:D5:B1:29:4C:C7:18:37:98:08:DD:B0:AD:E7:B2:99:07:78:09:04:DA:15:1F:CD","googFingerprintAlgorithm":"sha-256","googDerBase64":"MIIBFTCBvaADAgECAgkArKLiaqCDL58wCgYIKoZIzj0EAwIwETEPMA0GA1UEAwwGV2ViUlRDMB4XDTE2MDgxNzE5MzcyNVoXDTE2MDkxNzE5MzcyNVowETEPMA0GA1UEAwwGV2ViUlRDMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERF46M3VeSprnGRul2AhvcKMbOjmjgZ4jRqnJkiNuZ2eqr7aGGXzv02eSL5OKJ9qU4tzGpC+1jNJaiG7zuQIxWjAKBggqhkjOPQQDAgNHADBEAiAqYO4eiKsAyzVS0x7AWxMP8DiP2ti8hGIlpQ5bpnvldQIgMGEE9r8PxWuEYM89zqSK/E55WGvPV2ODX6XS7/B06f8=","id":"googCertificate_41:E4:D5:74:77:4C:3A:9A:61:D5:B1:29:4C:C7:18:37:98:08:DD:B0:AD:E7:B2:99:07:78:09:04:DA:15:1F:CD","type":"googCertificate","timestamp":{}},{"googFingerprint":"B2:9D:72:2C:16:52:90:4E:42:56:78:F4:FB:F1:71:E0:F2:56:93:06:BD:BB:34:F7:5B:80:97:0D:C3:C8:A9:20","googFingerprintAlgorithm":"sha-256","googDerBase64":"MIIBFjCBvaADAgECAgkA9iBy3Ra9D5MwCgYIKoZIzj0EAwIwETEPMA0GA1UEAwwGV2ViUlRDMB4XDTE2MDgxNzE5MzcyNVoXDTE2MDkxNzE5MzcyNVowETEPMA0GA1UEAwwGV2ViUlRDMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEM7rETA1ec+WSm04MhYvTzNG9SEcPxGBzNXuw+FGtQGOA8yVjiBtWtxi3Nxtl4E9Tj6T1eHySLkznK/bK+zC78jAKBggqhkjOPQQDAgNIADBFAiEAgDTN5uWVfKH0dLR8kxm2zPYHZN1F4ZGBvdzAZRefN28CIGLZncfi0FwnZFlntSB2m0O7+Vt23nLKWF16wgQd/3sS","id":"googCertificate_B2:9D:72:2C:16:52:90:4E:42:56:78:F4:FB:F1:71:E0:F2:56:93:06:BD:BB:34:F7:5B:80:97:0D:C3:C8:A9:20","type":"googCertificate","timestamp":{}},{"localCertificateId":"googCertificate_41:E4:D5:74:77:4C:3A:9A:61:D5:B1:29:4C:C7:18:37:98:08:DD:B0:AD:E7:B2:99:07:78:09:04:DA:15:1F:CD","googComponent":"1","id":"Channel-audio-1","selectedCandidatePairId":"Conn-audio-1-0","dtlsCipher":"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256","srtpCipher":"AES_CM_128_HMAC_SHA1_80","type":"googComponent","remoteCertificateId":"googCertificate_B2:9D:72:2C:16:52:90:4E:42:56:78:F4:FB:F1:71:E0:F2:56:93:06:BD:BB:34:F7:5B:80:97:0D:C3:C8:A9:20","timestamp":{}},{"candidateType":"host","ipAddress":"192.168.1.6","id":"Cand-7FCQuDVB","transport":"udp","networkType":"unknown","priority":"2113937151","type":"localcandidate","portNumber":"63331","timestamp":{}},{"candidateType":"host","ipAddress":"192.168.1.6","id":"Cand-Js3A6w8y","transport":"udp","priority":"2113937151","type":"remotecandidate","portNumber":"55964","timestamp":{}},{"localCandidateId":"Cand-7FCQuDVB","googRemoteAddress":"85.179.129.152:55964","googRemoteCandidateType":"stun","googRtt":"3000","bytesSent":"0","googTransportType":"udp","type":"googCandidatePair","googWritable":"false","googActiveConnection":"false","remoteCandidateId":"Cand-AwbKRfaP","bytesReceived":"0","googLocalAddress":"192.168.1.6:63331","googChannelId":"Channel-audio-1","packetsDiscardedOnSend":"0","packetsSent":"0","googLocalCandidateType":"local","id":"Conn-audio-1-1","googReadable":"false","timestamp":{}},{"candidateType":"serverreflexive","ipAddress":"85.179.129.152","id":"Cand-AwbKRfaP","transport":"udp","priority":"1677729535","type":"remotecandidate","portNumber":"55964","timestamp":{}},{"localCandidateId":"Cand-7FCQuDVB","googRemoteAddress":"85.179.129.152:55965","googRemoteCandidateType":"stun","googRtt":"3000","bytesSent":"0","googTransportType":"udp","type":"googCandidatePair","googWritable":"false","googActiveConnection":"false","remoteCandidateId":"Cand-ew/8za+M","bytesReceived":"0","googLocalAddress":"192.168.1.6:63331","googChannelId":"Channel-audio-1","packetsDiscardedOnSend":"0","packetsSent":"0","googLocalCandidateType":"local","id":"Conn-audio-1-2","googReadable":"false","timestamp":{}},{"candidateType":"serverreflexive","ipAddress":"85.179.129.152","id":"Cand-ew/8za+M","transport":"udp","priority":"1677729535","type":"remotecandidate","portNumber":"55965","timestamp":{}},{"localCandidateId":"Cand-YcQhLxuG","googRemoteAddress":"74.125.136.87:26946","googRemoteCandidateType":"relay","googRtt":"2267","bytesSent":"0","googTransportType":"udp","type":"googCandidatePair","googWritable":"false","googActiveConnection":"false","remoteCandidateId":"Cand-uOIrojwF","bytesReceived":"0","googLocalAddress":"85.179.129.152:63334","googChannelId":"Channel-audio-1","packetsDiscardedOnSend":"0","packetsSent":"0","googLocalCandidateType":"prflx","id":"Conn-audio-1-3","googReadable":"false","timestamp":{}},{"candidateType":"relayed","ipAddress":"74.125.136.87","id":"Cand-uOIrojwF","transport":"udp","priority":"33562879","type":"remotecandidate","portNumber":"26946","timestamp":{}},{"localCandidateId":"Cand-7FCQuDVB","googRemoteAddress":"74.125.136.85:29605","googRemoteCandidateType":"relay","googRtt":"3000","bytesSent":"0","googTransportType":"udp","type":"googCandidatePair","googWritable":"false","googActiveConnection":"false","remoteCandidateId":"Cand-cUIj6b5K","bytesReceived":"0","googLocalAddress":"192.168.1.6:63331","googChannelId":"Channel-audio-1","packetsDiscardedOnSend":"0","packetsSent":"0","googLocalCandidateType":"local","id":"Conn-audio-1-4","googReadable":"false","timestamp":{}},{"candidateType":"relayed","ipAddress":"74.125.136.85","id":"Cand-cUIj6b5K","transport":"udp","priority":"16785151","type":"remotecandidate","portNumber":"29605","timestamp":{}},{"googCodecName":"opus","transportId":"Channel-audio-1","googDecodingCTSG":"0","type":"ssrc","googAccelerateRate":"0","googSpeechExpandRate":"0","packetsLost":"0","id":"ssrc_1940985005_recv","googTrackId":"605ae15e-5d25-4f9a-a80b-c514971477ec","googJitterBufferMs":"43","googPreemptiveExpandRate":"0","timestamp":{},"googPreferredJitterBufferMs":"60","ssrc":"1940985005","googSecondaryDecodedRate":"0","googExpandRate":"0","audioOutputLevel":"1965","mediaType":"audio","googDecodingPLCCNG":"44","bytesReceived":"49393","googDecodingPLC":"16","googDecodingCTN":"1004","packetsReceived":"479","googDecodingCNG":"0","googDecodingNormal":"944","googCurrentDelayMs":"76","googJitterReceived":"5"},{"googCodecName":"opus","googEchoCancellationReturnLossEnhancement":"-100","transportId":"Channel-audio-1","ssrc":"3634126469","googRtt":"35","mediaType":"audio","audioInputLevel":"1844","bytesSent":"49691","googEchoCancellationEchoDelayStdDev":"20","type":"ssrc","googEchoCancellationReturnLoss":"-100","googTypingNoiseState":"false","packetsLost":"0","packetsSent":"478","id":"ssrc_3634126469_send","googTrackId":"687f5b0e-915c-469f-bdf6-c8216713bd14","googJitterReceived":"5","aecDivergentFilterFraction":"0.5","timestamp":{}},{"googCodecName":"VP9","transportId":"Channel-audio-1","googFrameRateDecoded":"13","googTargetDelayMs":"159","type":"ssrc","googRenderDelayMs":"10","googFirsSent":"0","googMaxDecodeMs":"5","googPlisSent":"0","googCaptureStartNtpTimeMs":"0","packetsLost":"0","googFrameRateOutput":"13","id":"ssrc_3350494056_recv","googTrackId":"23084d23-7251-4528-9a9c-63975d7cdab0","googJitterBufferMs":"144","timestamp":{},"googMinPlayoutDelayMs":"0","ssrc":"3350494056","mediaType":"video","googNacksSent":"0","bytesReceived":"479311","googFrameWidthReceived":"640","packetsReceived":"476","codecImplementationName":"libvpx","googDecodeMs":"4","googCurrentDelayMs":"159","googFrameHeightReceived":"480","googFrameRateReceived":"13"},{"googCodecName":"VP9","transportId":"Channel-audio-1","googRtt":"4","googFirsReceived":"0","type":"ssrc","googEncodeUsagePercent":"30","googFrameRateInput":"15","googFrameHeightInput":"480","googFrameWidthSent":"640","packetsLost":"0","packetsSent":"506","id":"ssrc_2105429706_send","googTrackId":"2c4f63c0-f828-400a-857a-ca25a55bf6ae","googFrameRateSent":"14","googFrameHeightSent":"480","timestamp":{},"googAdaptationChanges":"0","googNacksReceived":"0","googCpuLimitedResolution":"false","googBandwidthLimitedResolution":"false","googAvgEncodeMs":"31","ssrc":"2105429706","mediaType":"video","bytesSent":"516587","codecImplementationName":"libvpx","googFrameWidthInput":"640","googViewLimitedResolution":"false","googPlisReceived":"0"},{"googTransmitBitrate":"626167","googBucketDelay":"0","googTargetEncBitrate":"612189","googAvailableSendBandwidth":"612189","id":"bweforvideo","type":"VideoBwe","googAvailableReceiveBandwidth":"575550","googRetransmitBitrate":"0","googActualEncBitrate":"627198","timestamp":{}},{"candidateType":"peerreflexive","ipAddress":"85.179.129.152","id":"Cand-YcQhLxuG","transport":"udp","networkType":"unknown","priority":"1845501695","type":"localcandidate","portNumber":"63334","timestamp":{}}]}] -------------------------------------------------------------------------------- /test/spec/unit.js: -------------------------------------------------------------------------------- 1 | import calcResult from '../../lib/helpers/calcResult' 2 | import exampleResults from '../fixtures/example.json' 3 | import testResult from '../fixtures/test.json' 4 | 5 | describe('calcResult', () => { 6 | it('should find the min value', () => { 7 | let minResult = calcResult['min'](exampleResults) 8 | 9 | minResult.attr1.should.be.equal(13) 10 | minResult.attr2.should.be.equal(2) 11 | minResult.attr3.attr1.should.be.equal(3) 12 | minResult.attr3.attr2.should.be.equal(2) 13 | minResult.attr3.attr3.attr1.attr1.should.be.equal(1) 14 | }) 15 | 16 | it('should find the max value', () => { 17 | let maxResult = calcResult['max'](exampleResults) 18 | 19 | maxResult.attr1.should.be.equal(29) 20 | maxResult.attr2.should.be.equal(42) 21 | maxResult.attr3.attr1.should.be.equal(43) 22 | maxResult.attr3.attr2.should.be.equal(46) 23 | maxResult.attr3.attr3.attr1.attr1.should.be.equal(49) 24 | }) 25 | 26 | it('can calculate results from real world example', function () { 27 | let mean = calcResult['+'](testResult) 28 | mean = calcResult['/'](mean, testResult.length) 29 | 30 | let median = calcResult['[]'](testResult) 31 | median = calcResult['-|-'](median) 32 | 33 | let max = calcResult['max'](testResult) 34 | let min = calcResult['min'](testResult) 35 | 36 | min.video.bandwidth.actualEncBitrate.should.be.equal(336368) 37 | max.video.bandwidth.actualEncBitrate.should.be.equal(627198) 38 | mean.video.bandwidth.actualEncBitrate.should.be.equal(486119.33) 39 | median.video.bandwidth.actualEncBitrate.should.be.equal(484608) 40 | }) 41 | 42 | describe('should be able to calculate the mean', () => { 43 | let addedResults 44 | 45 | it('should add to result', () => { 46 | addedResults = calcResult['+'](exampleResults) 47 | 48 | addedResults.attr1.should.be.equal(78) 49 | addedResults.attr2.should.be.equal(88) 50 | addedResults.attr3.attr1.should.be.equal(72) 51 | addedResults.attr3.attr2.should.be.equal(80) 52 | addedResults.attr3.attr3.attr1.attr1.should.be.equal(69) 53 | }) 54 | 55 | it('should divide the result', () => { 56 | let dividedResults = calcResult['/'](addedResults, exampleResults.length) 57 | 58 | dividedResults.attr1.should.be.equal(19.5) 59 | dividedResults.attr2.should.be.equal(22) 60 | dividedResults.attr3.attr1.should.be.equal(18) 61 | dividedResults.attr3.attr2.should.be.equal(20) 62 | dividedResults.attr3.attr3.attr1.attr1.should.be.equal(17.25) 63 | }) 64 | }) 65 | 66 | describe('should be able to calculate the median', () => { 67 | let resultArray 68 | 69 | it('list results into an array', () => { 70 | resultArray = calcResult['[]'](exampleResults) 71 | 72 | resultArray.attr1.should.be.deep.equal([13, 22, 29, 14]) 73 | resultArray.attr2.should.be.deep.equal([2, 10, 34, 42]) 74 | resultArray.attr3.attr1.should.be.deep.equal([43, 3, 17, 9]) 75 | resultArray.attr3.attr2.should.be.deep.equal([46, 7, 25, 2]) 76 | resultArray.attr3.attr3.attr1.attr1.should.be.deep.equal([15, 1, 4, 49]) 77 | }) 78 | 79 | it('get the middle value of that array if length is even', () => { 80 | let mean = calcResult['-|-'](resultArray) 81 | 82 | mean.attr1.should.be.equal(18.00) 83 | mean.attr2.should.be.equal(22.00) 84 | mean.attr3.attr1.should.be.equal(13.00) 85 | mean.attr3.attr2.should.be.equal(16.00) 86 | mean.attr3.attr3.attr1.attr1.should.be.equal(9.50) 87 | }) 88 | 89 | it('get the middle value of that array if length is uneven', () => { 90 | /** 91 | * add another value to have uneven length 92 | */ 93 | resultArray.attr1.push(18) 94 | resultArray.attr2.push(22) 95 | resultArray.attr3.attr1.push(13) 96 | resultArray.attr3.attr2.push(16) 97 | resultArray.attr3.attr3.attr1.attr1.push(9) 98 | 99 | let median = calcResult['-|-'](resultArray) 100 | 101 | median.attr1.should.be.equal(18.00) 102 | median.attr2.should.be.equal(22.00) 103 | median.attr3.attr1.should.be.equal(13.00) 104 | median.attr3.attr2.should.be.equal(16.00) 105 | median.attr3.attr3.attr1.attr1.should.be.equal(9.00) 106 | }) 107 | }) 108 | }) 109 | --------------------------------------------------------------------------------