├── dev ├── head.js ├── module.exports.js ├── datachannel.js ├── tail.js ├── parameters.js ├── dataSentReceived.js ├── track.js ├── README.md ├── googCertificate.js ├── ssrc.js ├── bweforvideo.js ├── wrapper.js ├── inbound-rtp.js ├── outbound-rtp.js ├── getStats.js ├── local-candidate.js ├── remote-candidate.js ├── amd.js ├── globals.js ├── googCodecName.audio.js ├── googCodecName.video.js └── candidate-pair.js ├── .gitignore ├── .npmignore ├── .travis.yml ├── npm-test.js ├── bower.json ├── LICENSE.md ├── package.json ├── getStats.d.ts ├── server.js ├── Gruntfile.js ├── README.md ├── getStats.min.js ├── index.html └── getStats.js /dev/head.js: -------------------------------------------------------------------------------- 1 | var getStats = function(mediaStreamTrack, callback, interval) { 2 | -------------------------------------------------------------------------------- /dev/module.exports.js: -------------------------------------------------------------------------------- 1 | if (typeof module !== 'undefined' /* && !!module.exports*/ ) { 2 | module.exports = getStats; 3 | } 4 | 5 | if (typeof window !== 'undefined') { 6 | window.getStats = getStats; 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bower_components 3 | 4 | .*.swp 5 | ._* 6 | .DS_Store 7 | .git 8 | .hg 9 | .npmrc 10 | .lock-wscript 11 | .svn 12 | .idea 13 | .wafpickle-* 14 | config.gypi 15 | CVS 16 | npm-debug.log -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # ignore everything 2 | * 3 | 4 | # but not these files... 5 | !getStats.js 6 | !getStats.d.ts 7 | !getStats.min.js 8 | !index.html 9 | !package.json 10 | !bower.json 11 | !server.js 12 | !README.md 13 | -------------------------------------------------------------------------------- /dev/datachannel.js: -------------------------------------------------------------------------------- 1 | getStatsParser.datachannel = function(result) { 2 | if (result.type !== 'datachannel') return; 3 | 4 | getStatsResult.datachannel = { 5 | state: result.state // open or connecting 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /dev/tail.js: -------------------------------------------------------------------------------- 1 | getStatsLooper(); 2 | 3 | }; 4 | 5 | if (typeof module !== 'undefined' /* && !!module.exports*/ ) { 6 | module.exports = getStats; 7 | } 8 | 9 | if (typeof define === 'function' && define.amd) { 10 | define('getStats', [], function() { 11 | return getStats; 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | - "10" 5 | git: 6 | depth: 5 7 | cache: 8 | directories: 9 | node_modules 10 | install: npm install 11 | before_script: 12 | - npm install grunt-cli@0.1.13 -g 13 | - npm install grunt@0.4.5 14 | - grunt 15 | after_failure: npm install && grunt 16 | matrix: 17 | fast_finish: true 18 | -------------------------------------------------------------------------------- /npm-test.js: -------------------------------------------------------------------------------- 1 | // https://tonicdev.com/npm/getstats 2 | 3 | var getStats; 4 | 5 | try { 6 | getStats = require('getstats'); 7 | } 8 | catch(e) { 9 | getStats = require('./getStats.js'); 10 | } 11 | 12 | const fakeRtcPeerConnection = function() {}; 13 | 14 | getStats(fakeRtcPeerConnection, function(result) { 15 | console.log('result: ', result); 16 | }); 17 | 18 | process.exit() 19 | -------------------------------------------------------------------------------- /dev/parameters.js: -------------------------------------------------------------------------------- 1 | var peer = this; 2 | 3 | if (!(arguments[0] instanceof RTCPeerConnection)) { 4 | throw '1st argument is not instance of RTCPeerConnection.'; 5 | } 6 | 7 | peer = arguments[0]; 8 | 9 | if (arguments[1] instanceof MediaStreamTrack) { 10 | mediaStreamTrack = arguments[1]; // redundant on non-safari 11 | callback = arguments[2]; 12 | interval = arguments[3]; 13 | } 14 | -------------------------------------------------------------------------------- /dev/dataSentReceived.js: -------------------------------------------------------------------------------- 1 | getStatsParser.dataSentReceived = function(result) { 2 | if (!result.googCodecName || (result.mediaType !== 'video' && result.mediaType !== 'audio')) return; 3 | 4 | if (!!result.bytesSent) { 5 | getStatsResult[result.mediaType].bytesSent = parseInt(result.bytesSent); 6 | } 7 | 8 | if (!!result.bytesReceived) { 9 | getStatsResult[result.mediaType].bytesReceived = parseInt(result.bytesReceived); 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /dev/track.js: -------------------------------------------------------------------------------- 1 | getStatsParser.track = function(result) { 2 | if (!isSafari) return; 3 | if (result.type !== 'track') return; 4 | 5 | var sendrecvType = result.remoteSource === true ? 'send' : 'recv'; 6 | 7 | if (result.frameWidth && result.frameHeight) { 8 | getStatsResult.resolutions[sendrecvType].width = result.frameWidth; 9 | getStatsResult.resolutions[sendrecvType].height = result.frameHeight; 10 | } 11 | 12 | // framesSent, framesReceived 13 | }; 14 | -------------------------------------------------------------------------------- /dev/README.md: -------------------------------------------------------------------------------- 1 | # [getStats.js](https://github.com/muaz-khan/getStats) / [Demo](https://www.webrtc-experiment.com/getStats/) 2 | 3 | getStats.js is separated/splitted into unique files. 4 | 5 | This directory contains all those development files. 6 | 7 | `grunt` tools are used to concatenate/merge all these files into `~/getStats.js`. 8 | 9 | ## License 10 | 11 | [getStats.js](https://github.com/muaz-khan/getStats) is released under [MIT licence](https://www.webrtc-experiment.com/licence/) . Copyright (c) [Muaz Khan](http://www.MuazKhan.com/). 12 | -------------------------------------------------------------------------------- /dev/googCertificate.js: -------------------------------------------------------------------------------- 1 | getStatsParser.googCertificate = function(result) { 2 | if (result.type == 'googCertificate') { 3 | getStatsResult.encryption = result.googFingerprintAlgorithm; 4 | } 5 | 6 | // Safari-11 or higher 7 | if (result.type == 'certificate') { 8 | // todo: is it possible to have different encryption methods for senders and receivers? 9 | // if yes, then we need to set: 10 | // getStatsResult.encryption.local = value; 11 | // getStatsResult.encryption.remote = value; 12 | getStatsResult.encryption = result.fingerprintAlgorithm; 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /dev/ssrc.js: -------------------------------------------------------------------------------- 1 | var SSRC = { 2 | audio: { 3 | send: [], 4 | recv: [] 5 | }, 6 | video: { 7 | send: [], 8 | recv: [] 9 | } 10 | }; 11 | 12 | getStatsParser.ssrc = function(result) { 13 | if (!result.googCodecName || (result.mediaType !== 'video' && result.mediaType !== 'audio')) return; 14 | if (result.type !== 'ssrc') return; 15 | var sendrecvType = result.id.split('_').pop(); 16 | 17 | if (SSRC[result.mediaType][sendrecvType].indexOf(result.ssrc) === -1) { 18 | SSRC[result.mediaType][sendrecvType].push(result.ssrc) 19 | } 20 | 21 | getStatsResult[result.mediaType][sendrecvType].streams = SSRC[result.mediaType][sendrecvType].length; 22 | }; 23 | -------------------------------------------------------------------------------- /dev/bweforvideo.js: -------------------------------------------------------------------------------- 1 | getStatsParser.bweforvideo = function(result) { 2 | if (result.type !== 'VideoBwe') return; 3 | 4 | getStatsResult.bandwidth.availableSendBandwidth = result.googAvailableSendBandwidth; 5 | 6 | getStatsResult.bandwidth.googActualEncBitrate = result.googActualEncBitrate; 7 | getStatsResult.bandwidth.googAvailableSendBandwidth = result.googAvailableSendBandwidth; 8 | getStatsResult.bandwidth.googAvailableReceiveBandwidth = result.googAvailableReceiveBandwidth; 9 | getStatsResult.bandwidth.googRetransmitBitrate = result.googRetransmitBitrate; 10 | getStatsResult.bandwidth.googTargetEncBitrate = result.googTargetEncBitrate; 11 | getStatsResult.bandwidth.googBucketDelay = result.googBucketDelay; 12 | getStatsResult.bandwidth.googTransmitBitrate = result.googTransmitBitrate; 13 | }; 14 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "getstats", 3 | "description": "A tiny JavaScript library using WebRTC getStats API to return peer connection stats i.e. bandwidth usage, packets lost, local/remote ip addresses and ports, type of connection etc.", 4 | "version": "1.2.0", 5 | "authors": [ 6 | { 7 | "name": "Muaz Khan", 8 | "email": "muazkh@gmail.com", 9 | "homepage": "https://muazkhan.com/" 10 | } 11 | ], 12 | "main": "./getStats.js", 13 | "keywords": [ 14 | "webrtc", 15 | "getstats", 16 | "webrtc-library", 17 | "library", 18 | "javascript", 19 | "rtcweb", 20 | "webrtc-experiment", 21 | "javascript-library", 22 | "muaz", 23 | "muaz-khan" 24 | ], 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/muaz-khan/getStats.git" 28 | }, 29 | "homepage": "https://www.webrtc-experiment.com/getStats/", 30 | "ignore": [ 31 | "**/.*", 32 | "node_modules", 33 | "test", 34 | "tests" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /dev/wrapper.js: -------------------------------------------------------------------------------- 1 | // a wrapper around getStats which hides the differences (where possible) 2 | // following code-snippet is taken from somewhere on the github 3 | function getStatsWrapper(cb) { 4 | // if !peer or peer.signalingState == 'closed' then return; 5 | 6 | if (typeof window.InstallTrigger !== 'undefined' || isSafari) { // maybe "isEdge?" 7 | peer.getStats(window.mediaStreamTrack || null).then(function(res) { 8 | var items = []; 9 | res.forEach(function(r) { 10 | items.push(r); 11 | }); 12 | cb(items); 13 | }).catch(cb); 14 | } else { 15 | peer.getStats(function(res) { 16 | var items = []; 17 | res.result().forEach(function(res) { 18 | var item = {}; 19 | res.names().forEach(function(name) { 20 | item[name] = res.stat(name); 21 | }); 22 | item.id = res.id; 23 | item.type = res.type; 24 | item.timestamp = res.timestamp; 25 | items.push(item); 26 | }); 27 | cb(items); 28 | }); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | The MIT License 3 | ================ 4 | 5 | Copyright (c) 2014-2018 Muaz Khan \ 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "getstats", 3 | "preferGlobal": true, 4 | "version": "1.2.0", 5 | "author": { 6 | "name": "Muaz Khan", 7 | "email": "muazkh@gmail.com", 8 | "url": "https://muazkhan.com/" 9 | }, 10 | "description": "A tiny JavaScript library using WebRTC getStats API to return peer connection stats i.e. bandwidth usage, packets lost, local/remote ip addresses and ports, type of connection etc.", 11 | "scripts": { 12 | "start": "node server.js" 13 | }, 14 | "main": "./getStats.js", 15 | "types": "./getStats.d.ts", 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/muaz-khan/getStats.git" 19 | }, 20 | "keywords": [ 21 | "webrtc", 22 | "getstats", 23 | "webrtc-library", 24 | "library", 25 | "javascript", 26 | "rtcweb", 27 | "webrtc-experiment", 28 | "javascript-library", 29 | "muaz", 30 | "muaz-khan" 31 | ], 32 | "analyze": false, 33 | "license": "MIT", 34 | "readmeFilename": "README.md", 35 | "bugs": { 36 | "url": "https://github.com/muaz-khan/getStats/issues", 37 | "email": "muazkh@gmail.com" 38 | }, 39 | "homepage": "https://www.webrtc-experiment.com/getStats/", 40 | "_id": "getstats@", 41 | "_from": "getstats@", 42 | "tonicExampleFilename": "npm-test.js", 43 | "devDependencies": { 44 | "grunt": "0.4.5", 45 | "grunt-bump": "0.7.0", 46 | "grunt-cli": "0.1.13", 47 | "grunt-contrib-watch": "^1.1.0", 48 | "grunt-contrib-concat": "0.5.1", 49 | "grunt-contrib-uglify": "0.11.0", 50 | "grunt-jsbeautifier": "0.2.10", 51 | "load-grunt-tasks": "3.4.0" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /dev/inbound-rtp.js: -------------------------------------------------------------------------------- 1 | getStatsParser.inboundrtp = function(result) { 2 | if (!isSafari) return; 3 | if (result.type !== 'inbound-rtp') return; 4 | 5 | var mediaType = result.mediaType || 'audio'; 6 | var sendrecvType = result.isRemote ? 'recv' : 'send'; 7 | 8 | if (!sendrecvType) return; 9 | 10 | if (!!result.bytesSent) { 11 | var kilobytes = 0; 12 | if (!getStatsResult.internal[mediaType][sendrecvType].prevBytesSent) { 13 | getStatsResult.internal[mediaType][sendrecvType].prevBytesSent = result.bytesSent; 14 | } 15 | 16 | var bytes = result.bytesSent - getStatsResult.internal[mediaType][sendrecvType].prevBytesSent; 17 | getStatsResult.internal[mediaType][sendrecvType].prevBytesSent = result.bytesSent; 18 | 19 | kilobytes = bytes / 1024; 20 | 21 | getStatsResult[mediaType][sendrecvType].availableBandwidth = kilobytes.toFixed(1); 22 | getStatsResult[mediaType].bytesSent = kilobytes.toFixed(1); 23 | } 24 | 25 | if (!!result.bytesReceived) { 26 | var kilobytes = 0; 27 | if (!getStatsResult.internal[mediaType][sendrecvType].prevBytesReceived) { 28 | getStatsResult.internal[mediaType][sendrecvType].prevBytesReceived = result.bytesReceived; 29 | } 30 | 31 | var bytes = result.bytesReceived - getStatsResult.internal[mediaType][sendrecvType].prevBytesReceived; 32 | getStatsResult.internal[mediaType][sendrecvType].prevBytesReceived = result.bytesReceived; 33 | 34 | kilobytes = bytes / 1024; 35 | // getStatsResult[mediaType][sendrecvType].availableBandwidth = kilobytes.toFixed(1); 36 | getStatsResult[mediaType].bytesReceived = kilobytes.toFixed(1); 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /dev/outbound-rtp.js: -------------------------------------------------------------------------------- 1 | getStatsParser.outboundrtp = function(result) { 2 | if (!isSafari) return; 3 | if (result.type !== 'outbound-rtp') return; 4 | 5 | var mediaType = result.mediaType || 'audio'; 6 | var sendrecvType = result.isRemote ? 'recv' : 'send'; 7 | 8 | if (!sendrecvType) return; 9 | 10 | if (!!result.bytesSent) { 11 | var kilobytes = 0; 12 | if (!getStatsResult.internal[mediaType][sendrecvType].prevBytesSent) { 13 | getStatsResult.internal[mediaType][sendrecvType].prevBytesSent = result.bytesSent; 14 | } 15 | 16 | var bytes = result.bytesSent - getStatsResult.internal[mediaType][sendrecvType].prevBytesSent; 17 | getStatsResult.internal[mediaType][sendrecvType].prevBytesSent = result.bytesSent; 18 | 19 | kilobytes = bytes / 1024; 20 | 21 | getStatsResult[mediaType][sendrecvType].availableBandwidth = kilobytes.toFixed(1); 22 | getStatsResult[mediaType].bytesSent = kilobytes.toFixed(1); 23 | } 24 | 25 | if (!!result.bytesReceived) { 26 | var kilobytes = 0; 27 | if (!getStatsResult.internal[mediaType][sendrecvType].prevBytesReceived) { 28 | getStatsResult.internal[mediaType][sendrecvType].prevBytesReceived = result.bytesReceived; 29 | } 30 | 31 | var bytes = result.bytesReceived - getStatsResult.internal[mediaType][sendrecvType].prevBytesReceived; 32 | getStatsResult.internal[mediaType][sendrecvType].prevBytesReceived = result.bytesReceived; 33 | 34 | kilobytes = bytes / 1024; 35 | // getStatsResult[mediaType][sendrecvType].availableBandwidth = kilobytes.toFixed(1); 36 | getStatsResult[mediaType].bytesReceived = kilobytes.toFixed(1); 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /dev/getStats.js: -------------------------------------------------------------------------------- 1 | var nomore = false; 2 | 3 | function getStatsLooper() { 4 | getStatsWrapper(function(results) { 5 | if (!results || !results.forEach) return; 6 | 7 | results.forEach(function(result) { 8 | // console.error('result', result); 9 | Object.keys(getStatsParser).forEach(function(key) { 10 | if (typeof getStatsParser[key] === 'function') { 11 | try { 12 | getStatsParser[key](result); 13 | } catch (e) { 14 | console.error(e.message, e.stack, e); 15 | } 16 | } 17 | }); 18 | }); 19 | 20 | try { 21 | if (peer.iceConnectionState.search(/failed|closed|disconnected/gi) !== -1) { 22 | nomore = true; 23 | } 24 | } catch (e) { 25 | nomore = true; 26 | } 27 | 28 | if (nomore === true) { 29 | if (getStatsResult.datachannel) { 30 | getStatsResult.datachannel.state = 'close'; 31 | } 32 | getStatsResult.ended = true; 33 | } 34 | 35 | // allow users to access native results 36 | getStatsResult.results = results; 37 | 38 | if (getStatsResult.audio && getStatsResult.video) { 39 | getStatsResult.bandwidth.speed = (getStatsResult.audio.bytesSent - getStatsResult.bandwidth.helper.audioBytesSent) + (getStatsResult.video.bytesSent - getStatsResult.bandwidth.helper.videoBytesSent); 40 | getStatsResult.bandwidth.helper.audioBytesSent = getStatsResult.audio.bytesSent; 41 | getStatsResult.bandwidth.helper.videoBytesSent = getStatsResult.video.bytesSent; 42 | } 43 | 44 | callback(getStatsResult); 45 | 46 | // second argument checks to see, if target-user is still connected. 47 | if (!nomore) { 48 | typeof interval != undefined && interval && setTimeout(getStatsLooper, interval || 1000); 49 | } 50 | }); 51 | } 52 | -------------------------------------------------------------------------------- /dev/local-candidate.js: -------------------------------------------------------------------------------- 1 | var LOCAL_candidateType = {}; 2 | var LOCAL_transport = {}; 3 | var LOCAL_ipAddress = {}; 4 | var LOCAL_networkType = {}; 5 | 6 | getStatsParser.localcandidate = function(result) { 7 | if (result.type !== 'localcandidate' && result.type !== 'local-candidate') return; 8 | if (!result.id) return; 9 | 10 | if (!LOCAL_candidateType[result.id]) { 11 | LOCAL_candidateType[result.id] = []; 12 | } 13 | 14 | if (!LOCAL_transport[result.id]) { 15 | LOCAL_transport[result.id] = []; 16 | } 17 | 18 | if (!LOCAL_ipAddress[result.id]) { 19 | LOCAL_ipAddress[result.id] = []; 20 | } 21 | 22 | if (!LOCAL_networkType[result.id]) { 23 | LOCAL_networkType[result.id] = []; 24 | } 25 | 26 | if (result.candidateType && LOCAL_candidateType[result.id].indexOf(result.candidateType) === -1) { 27 | LOCAL_candidateType[result.id].push(result.candidateType); 28 | } 29 | 30 | if (result.transport && LOCAL_transport[result.id].indexOf(result.transport) === -1) { 31 | LOCAL_transport[result.id].push(result.transport); 32 | } 33 | 34 | if (result.ipAddress && LOCAL_ipAddress[result.id].indexOf(result.ipAddress + ':' + result.portNumber) === -1) { 35 | LOCAL_ipAddress[result.id].push(result.ipAddress + ':' + result.portNumber); 36 | } 37 | 38 | if (result.networkType && LOCAL_networkType[result.id].indexOf(result.networkType) === -1) { 39 | LOCAL_networkType[result.id].push(result.networkType); 40 | } 41 | 42 | getStatsResult.internal.candidates[result.id] = { 43 | candidateType: LOCAL_candidateType[result.id], 44 | ipAddress: LOCAL_ipAddress[result.id], 45 | portNumber: result.portNumber, 46 | networkType: LOCAL_networkType[result.id], 47 | priority: result.priority, 48 | transport: LOCAL_transport[result.id], 49 | timestamp: result.timestamp, 50 | id: result.id, 51 | type: result.type 52 | }; 53 | 54 | getStatsResult.connectionType.local.candidateType = LOCAL_candidateType[result.id]; 55 | getStatsResult.connectionType.local.ipAddress = LOCAL_ipAddress[result.id]; 56 | getStatsResult.connectionType.local.networkType = LOCAL_networkType[result.id]; 57 | getStatsResult.connectionType.local.transport = LOCAL_transport[result.id]; 58 | }; 59 | -------------------------------------------------------------------------------- /dev/remote-candidate.js: -------------------------------------------------------------------------------- 1 | var REMOTE_candidateType = {}; 2 | var REMOTE_transport = {}; 3 | var REMOTE_ipAddress = {}; 4 | var REMOTE_networkType = {}; 5 | 6 | getStatsParser.remotecandidate = function(result) { 7 | if (result.type !== 'remotecandidate' && result.type !== 'remote-candidate') return; 8 | if (!result.id) return; 9 | 10 | if (!REMOTE_candidateType[result.id]) { 11 | REMOTE_candidateType[result.id] = []; 12 | } 13 | 14 | if (!REMOTE_transport[result.id]) { 15 | REMOTE_transport[result.id] = []; 16 | } 17 | 18 | if (!REMOTE_ipAddress[result.id]) { 19 | REMOTE_ipAddress[result.id] = []; 20 | } 21 | 22 | if (!REMOTE_networkType[result.id]) { 23 | REMOTE_networkType[result.id] = []; 24 | } 25 | 26 | if (result.candidateType && REMOTE_candidateType[result.id].indexOf(result.candidateType) === -1) { 27 | REMOTE_candidateType[result.id].push(result.candidateType); 28 | } 29 | 30 | if (result.transport && REMOTE_transport[result.id].indexOf(result.transport) === -1) { 31 | REMOTE_transport[result.id].push(result.transport); 32 | } 33 | 34 | if (result.ipAddress && REMOTE_ipAddress[result.id].indexOf(result.ipAddress + ':' + result.portNumber) === -1) { 35 | REMOTE_ipAddress[result.id].push(result.ipAddress + ':' + result.portNumber); 36 | } 37 | 38 | if (result.networkType && REMOTE_networkType[result.id].indexOf(result.networkType) === -1) { 39 | REMOTE_networkType[result.id].push(result.networkType); 40 | } 41 | 42 | getStatsResult.internal.candidates[result.id] = { 43 | candidateType: REMOTE_candidateType[result.id], 44 | ipAddress: REMOTE_ipAddress[result.id], 45 | portNumber: result.portNumber, 46 | networkType: REMOTE_networkType[result.id], 47 | priority: result.priority, 48 | transport: REMOTE_transport[result.id], 49 | timestamp: result.timestamp, 50 | id: result.id, 51 | type: result.type 52 | }; 53 | 54 | getStatsResult.connectionType.remote.candidateType = REMOTE_candidateType[result.id]; 55 | getStatsResult.connectionType.remote.ipAddress = REMOTE_ipAddress[result.id]; 56 | getStatsResult.connectionType.remote.networkType = REMOTE_networkType[result.id]; 57 | getStatsResult.connectionType.remote.transport = REMOTE_transport[result.id]; 58 | }; 59 | -------------------------------------------------------------------------------- /getStats.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'getstats' { 2 | interface GetStatsBandwidth { 3 | speed: number; 4 | systemBandwidth: number; 5 | sentPerSecond: number; 6 | encodedPerSecond: number; 7 | helper: { 8 | audioBytesSent: number; 9 | videoBytestSent: number; 10 | videoBytesSent: number; 11 | }; 12 | availableSendBandwidth: number; 13 | googActualEncBitrate: number; 14 | googAvailableSendBandwidth: number; 15 | googAvailableReceiveBandwidth: number; 16 | googRetransmitBitrate: number; 17 | googTargetEncBitrate: number; 18 | googBucketDelay: number; 19 | googTransmitBitrate: number; 20 | } 21 | 22 | interface GetStatsConnectionInfo { 23 | tracks: string[]; 24 | codecs: string[]; 25 | availableBandwidth: string; 26 | streams: number; 27 | framerateMean: number; 28 | bitrateMean: number; 29 | } 30 | 31 | interface GetStatsConnectionStream { 32 | send: GetStatsConnectionInfo; 33 | recv: GetStatsConnectionInfo; 34 | bytesSent: number; 35 | bytesReceived: number; 36 | latency: number; 37 | packetsLost: number; 38 | } 39 | 40 | interface GetStatsNetworkInfo { 41 | candidateType: string[]; 42 | transport: string[]; 43 | ipAddress: string[]; 44 | networkType: string[]; 45 | } 46 | 47 | interface GetStatsConnectionType { 48 | systemNetworkType: string; 49 | systemIpAddress: string[]; 50 | local: GetStatsNetworkInfo; 51 | remote: GetStatsNetworkInfo; 52 | transport: string; 53 | } 54 | 55 | interface GetStatsResolution { 56 | width: string; 57 | height: string; 58 | } 59 | 60 | interface GetStatsResolutions { 61 | send: GetStatsResolution; 62 | recv: GetStatsResolution; 63 | } 64 | 65 | export interface GetStatsResult { 66 | datachannel: { 67 | state: 'open' | 'close'; 68 | }; 69 | isOfferer: boolean; 70 | encryption: string; 71 | bandwidth: GetStatsBandwidth; 72 | audio: GetStatsConnectionStream; 73 | video: GetStatsConnectionStream; 74 | connectionType: GetStatsConnectionType; 75 | resolutions: GetStatsResolutions; 76 | results: any[]; 77 | 78 | nomore: () => void; 79 | } 80 | 81 | export default function getstats( 82 | rtc: RTCPeerConnection, 83 | callback: (result: GetStatsResult) => void, 84 | interval?: number 85 | ): void; 86 | } 87 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | // http://127.0.0.1:9001 2 | // http://localhost:9001 3 | 4 | var port = process.env.PORT || 9001; 5 | 6 | var server = require('http'), 7 | url = require('url'), 8 | path = require('path'), 9 | fs = require('fs'); 10 | 11 | function serverHandler(request, response) { 12 | var uri = url.parse(request.url).pathname, 13 | filename = path.join(process.cwd(), uri); 14 | 15 | fs.exists(filename, function(exists) { 16 | if (!exists) { 17 | response.writeHead(404, { 18 | 'Content-Type': 'text/plain' 19 | }); 20 | response.write('404 Not Found: ' + filename + '\n'); 21 | response.end(); 22 | return; 23 | } 24 | 25 | if (filename.indexOf('favicon.ico') !== -1) { 26 | return; 27 | } 28 | 29 | var isWin = !!process.platform.match(/^win/); 30 | 31 | if (fs.statSync(filename).isDirectory() && !isWin) { 32 | filename += '/index.html'; 33 | } else if (fs.statSync(filename).isDirectory() && !!isWin) { 34 | filename += '\\index.html'; 35 | } 36 | 37 | fs.readFile(filename, 'binary', function(err, file) { 38 | if (err) { 39 | response.writeHead(500, { 40 | 'Content-Type': 'text/plain' 41 | }); 42 | response.write(err + '\n'); 43 | response.end(); 44 | return; 45 | } 46 | 47 | var contentType; 48 | 49 | if (filename.indexOf('.html') !== -1) { 50 | contentType = 'text/html'; 51 | } 52 | 53 | if (filename.indexOf('.js') !== -1) { 54 | contentType = 'application/javascript'; 55 | } 56 | 57 | if (contentType) { 58 | response.writeHead(200, { 59 | 'Content-Type': contentType 60 | }); 61 | } else response.writeHead(200); 62 | 63 | response.write(file, 'binary'); 64 | response.end(); 65 | }); 66 | }); 67 | } 68 | 69 | var app; 70 | 71 | app = server.createServer(serverHandler); 72 | 73 | app = app.listen(port, process.env.IP || "0.0.0.0", function() { 74 | var addr = app.address(); 75 | console.log("Server listening at", addr.address + ":" + addr.port); 76 | }); 77 | -------------------------------------------------------------------------------- /dev/amd.js: -------------------------------------------------------------------------------- 1 | var browserFakeUserAgent = 'Fake/5.0 (FakeOS) AppleWebKit/123 (KHTML, like Gecko) Fake/12.3.4567.89 Fake/123.45'; 2 | 3 | (function(that) { 4 | if (!that) { 5 | return; 6 | } 7 | 8 | if (typeof window !== 'undefined') { 9 | return; 10 | } 11 | 12 | if (typeof global === 'undefined') { 13 | return; 14 | } 15 | 16 | global.navigator = { 17 | userAgent: browserFakeUserAgent, 18 | getUserMedia: function() {} 19 | }; 20 | 21 | if (!global.console) { 22 | global.console = {}; 23 | } 24 | 25 | if (typeof global.console.log === 'undefined' || typeof global.console.error === 'undefined') { 26 | global.console.error = global.console.log = global.console.log || function() { 27 | console.log(arguments); 28 | }; 29 | } 30 | 31 | if (typeof document === 'undefined') { 32 | /*global document:true */ 33 | that.document = { 34 | documentElement: { 35 | appendChild: function() { 36 | return ''; 37 | } 38 | } 39 | }; 40 | 41 | document.createElement = document.captureStream = document.mozCaptureStream = function() { 42 | var obj = { 43 | getContext: function() { 44 | return obj; 45 | }, 46 | play: function() {}, 47 | pause: function() {}, 48 | drawImage: function() {}, 49 | toDataURL: function() { 50 | return ''; 51 | } 52 | }; 53 | return obj; 54 | }; 55 | 56 | that.HTMLVideoElement = function() {}; 57 | } 58 | 59 | if (typeof location === 'undefined') { 60 | /*global location:true */ 61 | that.location = { 62 | protocol: 'file:', 63 | href: '', 64 | hash: '' 65 | }; 66 | } 67 | 68 | if (typeof screen === 'undefined') { 69 | /*global screen:true */ 70 | that.screen = { 71 | width: 0, 72 | height: 0 73 | }; 74 | } 75 | 76 | if (typeof URL === 'undefined') { 77 | /*global screen:true */ 78 | that.URL = { 79 | createObjectURL: function() { 80 | return ''; 81 | }, 82 | revokeObjectURL: function() { 83 | return ''; 84 | } 85 | }; 86 | } 87 | 88 | if (typeof MediaStreamTrack === 'undefined') { 89 | /*global screen:true */ 90 | that.MediaStreamTrack = function() {}; 91 | } 92 | 93 | if (typeof RTCPeerConnection === 'undefined') { 94 | /*global screen:true */ 95 | that.RTCPeerConnection = function() {}; 96 | } 97 | 98 | /*global window:true */ 99 | that.window = global; 100 | })(typeof global !== 'undefined' ? global : null); 101 | -------------------------------------------------------------------------------- /dev/globals.js: -------------------------------------------------------------------------------- 1 | var RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection; 2 | 3 | if (typeof MediaStreamTrack === 'undefined') { 4 | MediaStreamTrack = {}; // todo? 5 | } 6 | 7 | var systemNetworkType = ((navigator.connection || {}).type || 'unknown').toString().toLowerCase(); 8 | 9 | var getStatsResult = { 10 | encryption: 'sha-256', 11 | audio: { 12 | send: { 13 | tracks: [], 14 | codecs: [], 15 | availableBandwidth: 0, 16 | streams: 0, 17 | framerateMean: 0, 18 | bitrateMean: 0 19 | }, 20 | recv: { 21 | tracks: [], 22 | codecs: [], 23 | availableBandwidth: 0, 24 | streams: 0, 25 | framerateMean: 0, 26 | bitrateMean: 0 27 | }, 28 | bytesSent: 0, 29 | bytesReceived: 0, 30 | latency: 0, 31 | packetsLost: 0 32 | }, 33 | video: { 34 | send: { 35 | tracks: [], 36 | codecs: [], 37 | availableBandwidth: 0, 38 | streams: 0, 39 | framerateMean: 0, 40 | bitrateMean: 0 41 | }, 42 | recv: { 43 | tracks: [], 44 | codecs: [], 45 | availableBandwidth: 0, 46 | streams: 0, 47 | framerateMean: 0, 48 | bitrateMean: 0 49 | }, 50 | bytesSent: 0, 51 | bytesReceived: 0, 52 | latency: 0, 53 | packetsLost: 0 54 | }, 55 | bandwidth: { 56 | systemBandwidth: 0, 57 | sentPerSecond: 0, 58 | encodedPerSecond: 0, 59 | helper: { 60 | audioBytesSent: 0, 61 | videoBytestSent: 0 62 | }, 63 | speed: 0 64 | }, 65 | results: {}, 66 | connectionType: { 67 | systemNetworkType: systemNetworkType, 68 | systemIpAddress: '192.168.1.2', 69 | local: { 70 | candidateType: [], 71 | transport: [], 72 | ipAddress: [], 73 | networkType: [] 74 | }, 75 | remote: { 76 | candidateType: [], 77 | transport: [], 78 | ipAddress: [], 79 | networkType: [] 80 | } 81 | }, 82 | resolutions: { 83 | send: { 84 | width: 0, 85 | height: 0 86 | }, 87 | recv: { 88 | width: 0, 89 | height: 0 90 | } 91 | }, 92 | internal: { 93 | audio: { 94 | send: {}, 95 | recv: {} 96 | }, 97 | video: { 98 | send: {}, 99 | recv: {} 100 | }, 101 | candidates: {} 102 | }, 103 | nomore: function() { 104 | nomore = true; 105 | } 106 | }; 107 | 108 | var getStatsParser = { 109 | checkIfOfferer: function(result) { 110 | if (result.type === 'googLibjingleSession') { 111 | getStatsResult.isOfferer = result.googInitiator; 112 | } 113 | } 114 | }; 115 | 116 | var isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); 117 | -------------------------------------------------------------------------------- /dev/googCodecName.audio.js: -------------------------------------------------------------------------------- 1 | getStatsParser.checkAudioTracks = function(result) { 2 | if (result.mediaType !== 'audio') return; 3 | 4 | var sendrecvType = result.id.split('_').pop(); 5 | if (result.isRemote === true) { 6 | sendrecvType = 'recv'; 7 | } 8 | if (result.isRemote === false) { 9 | sendrecvType = 'send'; 10 | } 11 | 12 | if (!sendrecvType) return; 13 | 14 | if (getStatsResult.audio[sendrecvType].codecs.indexOf(result.googCodecName || 'opus') === -1) { 15 | getStatsResult.audio[sendrecvType].codecs.push(result.googCodecName || 'opus'); 16 | } 17 | 18 | if (!!result.bytesSent) { 19 | var kilobytes = 0; 20 | if (!getStatsResult.internal.audio[sendrecvType].prevBytesSent) { 21 | getStatsResult.internal.audio[sendrecvType].prevBytesSent = result.bytesSent; 22 | } 23 | 24 | var bytes = result.bytesSent - getStatsResult.internal.audio[sendrecvType].prevBytesSent; 25 | getStatsResult.internal.audio[sendrecvType].prevBytesSent = result.bytesSent; 26 | 27 | kilobytes = bytes / 1024; 28 | getStatsResult.audio[sendrecvType].availableBandwidth = kilobytes.toFixed(1); 29 | getStatsResult.audio.bytesSent = kilobytes.toFixed(1); 30 | } 31 | 32 | if (!!result.bytesReceived) { 33 | var kilobytes = 0; 34 | if (!getStatsResult.internal.audio[sendrecvType].prevBytesReceived) { 35 | getStatsResult.internal.audio[sendrecvType].prevBytesReceived = result.bytesReceived; 36 | } 37 | 38 | var bytes = result.bytesReceived - getStatsResult.internal.audio[sendrecvType].prevBytesReceived; 39 | getStatsResult.internal.audio[sendrecvType].prevBytesReceived = result.bytesReceived; 40 | 41 | kilobytes = bytes / 1024; 42 | 43 | // getStatsResult.audio[sendrecvType].availableBandwidth = kilobytes.toFixed(1); 44 | getStatsResult.audio.bytesReceived = kilobytes.toFixed(1); 45 | } 46 | 47 | if (result.googTrackId && getStatsResult.audio[sendrecvType].tracks.indexOf(result.googTrackId) === -1) { 48 | getStatsResult.audio[sendrecvType].tracks.push(result.googTrackId); 49 | } 50 | 51 | // calculate latency 52 | if (!!result.googCurrentDelayMs) { 53 | var kilobytes = 0; 54 | if (!getStatsResult.internal.audio.prevGoogCurrentDelayMs) { 55 | getStatsResult.internal.audio.prevGoogCurrentDelayMs = result.googCurrentDelayMs; 56 | } 57 | 58 | var bytes = result.googCurrentDelayMs - getStatsResult.internal.audio.prevGoogCurrentDelayMs; 59 | getStatsResult.internal.audio.prevGoogCurrentDelayMs = result.googCurrentDelayMs; 60 | 61 | getStatsResult.audio.latency = bytes.toFixed(1); 62 | 63 | if (getStatsResult.audio.latency < 0) { 64 | getStatsResult.audio.latency = 0; 65 | } 66 | } 67 | 68 | // calculate packetsLost 69 | if (!!result.packetsLost) { 70 | var kilobytes = 0; 71 | if (!getStatsResult.internal.audio.prevPacketsLost) { 72 | getStatsResult.internal.audio.prevPacketsLost = result.packetsLost; 73 | } 74 | 75 | var bytes = result.packetsLost - getStatsResult.internal.audio.prevPacketsLost; 76 | getStatsResult.internal.audio.prevPacketsLost = result.packetsLost; 77 | 78 | getStatsResult.audio.packetsLost = bytes.toFixed(1); 79 | 80 | if (getStatsResult.audio.packetsLost < 0) { 81 | getStatsResult.audio.packetsLost = 0; 82 | } 83 | } 84 | }; 85 | -------------------------------------------------------------------------------- /dev/googCodecName.video.js: -------------------------------------------------------------------------------- 1 | getStatsParser.checkVideoTracks = function(result) { 2 | if (result.mediaType !== 'video') return; 3 | 4 | var sendrecvType = result.id.split('_').pop(); 5 | if (result.isRemote === true) { 6 | sendrecvType = 'recv'; 7 | } 8 | if (result.isRemote === false) { 9 | sendrecvType = 'send'; 10 | } 11 | 12 | if (!sendrecvType) return; 13 | 14 | if (getStatsResult.video[sendrecvType].codecs.indexOf(result.googCodecName || 'VP8') === -1) { 15 | getStatsResult.video[sendrecvType].codecs.push(result.googCodecName || 'VP8'); 16 | } 17 | 18 | if (!!result.bytesSent) { 19 | var kilobytes = 0; 20 | if (!getStatsResult.internal.video[sendrecvType].prevBytesSent) { 21 | getStatsResult.internal.video[sendrecvType].prevBytesSent = result.bytesSent; 22 | } 23 | 24 | var bytes = result.bytesSent - getStatsResult.internal.video[sendrecvType].prevBytesSent; 25 | getStatsResult.internal.video[sendrecvType].prevBytesSent = result.bytesSent; 26 | 27 | kilobytes = bytes / 1024; 28 | 29 | getStatsResult.video[sendrecvType].availableBandwidth = kilobytes.toFixed(1); 30 | getStatsResult.video.bytesSent = kilobytes.toFixed(1); 31 | } 32 | 33 | if (!!result.bytesReceived) { 34 | var kilobytes = 0; 35 | if (!getStatsResult.internal.video[sendrecvType].prevBytesReceived) { 36 | getStatsResult.internal.video[sendrecvType].prevBytesReceived = result.bytesReceived; 37 | } 38 | 39 | var bytes = result.bytesReceived - getStatsResult.internal.video[sendrecvType].prevBytesReceived; 40 | getStatsResult.internal.video[sendrecvType].prevBytesReceived = result.bytesReceived; 41 | 42 | kilobytes = bytes / 1024; 43 | // getStatsResult.video[sendrecvType].availableBandwidth = kilobytes.toFixed(1); 44 | getStatsResult.video.bytesReceived = kilobytes.toFixed(1); 45 | } 46 | 47 | if (result.googFrameHeightReceived && result.googFrameWidthReceived) { 48 | getStatsResult.resolutions[sendrecvType].width = result.googFrameWidthReceived; 49 | getStatsResult.resolutions[sendrecvType].height = result.googFrameHeightReceived; 50 | } 51 | 52 | if (result.googFrameHeightSent && result.googFrameWidthSent) { 53 | getStatsResult.resolutions[sendrecvType].width = result.googFrameWidthSent; 54 | getStatsResult.resolutions[sendrecvType].height = result.googFrameHeightSent; 55 | } 56 | 57 | if (result.googTrackId && getStatsResult.video[sendrecvType].tracks.indexOf(result.googTrackId) === -1) { 58 | getStatsResult.video[sendrecvType].tracks.push(result.googTrackId); 59 | } 60 | 61 | if (result.framerateMean) { 62 | getStatsResult.bandwidth.framerateMean = result.framerateMean; 63 | var kilobytes = 0; 64 | if (!getStatsResult.internal.video[sendrecvType].prevFramerateMean) { 65 | getStatsResult.internal.video[sendrecvType].prevFramerateMean = result.bitrateMean; 66 | } 67 | 68 | var bytes = result.bytesSent - getStatsResult.internal.video[sendrecvType].prevFramerateMean; 69 | getStatsResult.internal.video[sendrecvType].prevFramerateMean = result.framerateMean; 70 | 71 | kilobytes = bytes / 1024; 72 | getStatsResult.video[sendrecvType].framerateMean = bytes.toFixed(1); 73 | } 74 | 75 | if (result.bitrateMean) { 76 | getStatsResult.bandwidth.bitrateMean = result.bitrateMean; 77 | var kilobytes = 0; 78 | if (!getStatsResult.internal.video[sendrecvType].prevBitrateMean) { 79 | getStatsResult.internal.video[sendrecvType].prevBitrateMean = result.bitrateMean; 80 | } 81 | 82 | var bytes = result.bytesSent - getStatsResult.internal.video[sendrecvType].prevBitrateMean; 83 | getStatsResult.internal.video[sendrecvType].prevBitrateMean = result.bitrateMean; 84 | 85 | kilobytes = bytes / 1024; 86 | getStatsResult.video[sendrecvType].bitrateMean = bytes.toFixed(1); 87 | } 88 | 89 | // calculate latency 90 | if (!!result.googCurrentDelayMs) { 91 | var kilobytes = 0; 92 | if (!getStatsResult.internal.video.prevGoogCurrentDelayMs) { 93 | getStatsResult.internal.video.prevGoogCurrentDelayMs = result.googCurrentDelayMs; 94 | } 95 | 96 | var bytes = result.googCurrentDelayMs - getStatsResult.internal.video.prevGoogCurrentDelayMs; 97 | getStatsResult.internal.video.prevGoogCurrentDelayMs = result.googCurrentDelayMs; 98 | 99 | getStatsResult.video.latency = bytes.toFixed(1); 100 | 101 | if (getStatsResult.video.latency < 0) { 102 | getStatsResult.video.latency = 0; 103 | } 104 | } 105 | 106 | // calculate packetsLost 107 | if (!!result.packetsLost) { 108 | var kilobytes = 0; 109 | if (!getStatsResult.internal.video.prevPacketsLost) { 110 | getStatsResult.internal.video.prevPacketsLost = result.packetsLost; 111 | } 112 | 113 | var bytes = result.packetsLost - getStatsResult.internal.video.prevPacketsLost; 114 | getStatsResult.internal.video.prevPacketsLost = result.packetsLost; 115 | 116 | getStatsResult.video.packetsLost = bytes.toFixed(1); 117 | 118 | if (getStatsResult.video.packetsLost < 0) { 119 | getStatsResult.video.packetsLost = 0; 120 | } 121 | } 122 | }; 123 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(grunt) { 4 | require('load-grunt-tasks')(grunt, { 5 | pattern: 'grunt-*', 6 | config: 'package.json', 7 | scope: 'devDependencies' 8 | }); 9 | 10 | var versionNumber = grunt.file.readJSON('package.json').version; 11 | 12 | var banner = '\'use strict\';\n\n'; 13 | 14 | banner += '// Last time updated: <%= grunt.template.today("UTC:yyyy-mm-dd h:MM:ss TT Z") %>\n\n'; 15 | 16 | banner += '// _______________\n'; 17 | banner += '// getStats v' + versionNumber + '\n\n'; 18 | 19 | banner += '// Open-Sourced: https://github.com/muaz-khan/getStats\n\n'; 20 | 21 | banner += '// --------------------------------------------------\n'; 22 | banner += '// Muaz Khan - www.MuazKhan.com\n'; 23 | banner += '// MIT License - www.WebRTC-Experiment.com/licence\n'; 24 | banner += '// --------------------------------------------------\n\n'; 25 | 26 | // configure project 27 | grunt.initConfig({ 28 | // make node configurations available 29 | pkg: grunt.file.readJSON('package.json'), 30 | concat: { 31 | options: { 32 | stripBanners: true, 33 | separator: '\n', 34 | banner: banner 35 | }, 36 | dist: { 37 | src: [ 38 | 'dev/head.js', 39 | 'dev/amd.js', 40 | 'dev/globals.js', 41 | 'dev/parameters.js', 42 | 'dev/getStats.js', 43 | 'dev/wrapper.js', 44 | 'dev/datachannel.js', 45 | 'dev/googCertificate.js', 46 | 'dev/googCodecName.audio.js', 47 | 'dev/googCodecName.video.js', 48 | 'dev/bweforvideo.js', 49 | 'dev/candidate-pair.js', 50 | 'dev/local-candidate.js', 51 | 'dev/remote-candidate.js', 52 | 'dev/dataSentReceived.js', 53 | 'dev/inbound-rtp.js', 54 | 'dev/outbound-rtp.js', 55 | 'dev/track.js', 56 | 'dev/ssrc.js', 57 | 'dev/tail.js' 58 | ], 59 | dest: 'getStats.js', 60 | }, 61 | }, 62 | uglify: { 63 | options: { 64 | mangle: false, 65 | banner: banner 66 | }, 67 | my_target: { 68 | files: { 69 | 'getStats.min.js': ['getStats.js'] 70 | } 71 | } 72 | }, 73 | jsbeautifier: { 74 | files: ['getStats.js', 'dev/*.js', 'server.js', 'Gruntfile.js'], 75 | options: { 76 | js: { 77 | braceStyle: "collapse", 78 | breakChainedMethods: false, 79 | e4x: false, 80 | evalCode: false, 81 | indentChar: " ", 82 | indentLevel: 0, 83 | indentSize: 4, 84 | indentWithTabs: false, 85 | jslintHappy: false, 86 | keepArrayIndentation: false, 87 | keepFunctionIndentation: false, 88 | maxPreserveNewlines: 10, 89 | preserveNewlines: true, 90 | spaceBeforeConditional: true, 91 | spaceInParen: false, 92 | unescapeStrings: false, 93 | wrapLineLength: 0 94 | }, 95 | html: { 96 | braceStyle: "collapse", 97 | indentChar: " ", 98 | indentScripts: "keep", 99 | indentSize: 4, 100 | maxPreserveNewlines: 10, 101 | preserveNewlines: true, 102 | unformatted: ["a", "sub", "sup", "b", "i", "u"], 103 | wrapLineLength: 0 104 | }, 105 | css: { 106 | indentChar: " ", 107 | indentSize: 4 108 | } 109 | } 110 | }, 111 | bump: { 112 | options: { 113 | files: ['package.json', 'bower.json'], 114 | updateConfigs: [], 115 | commit: true, 116 | commitMessage: 'v%VERSION%', 117 | commitFiles: ['package.json', 'bower.json'], 118 | createTag: true, 119 | tagName: '%VERSION%', 120 | tagMessage: '%VERSION%', 121 | push: false, 122 | pushTo: 'upstream', 123 | gitDescribeOptions: '--tags --always --abbrev=1 --dirty=-d' 124 | } 125 | }, 126 | watch: { 127 | scripts: { 128 | files: ['dev/*.js'], 129 | tasks: ['concat', 'jsbeautifier', 'uglify'], 130 | options: { 131 | spawn: false, 132 | }, 133 | } 134 | } 135 | }); 136 | 137 | // enable plugins 138 | 139 | // set default tasks to run when grunt is called without parameters 140 | // http://gruntjs.com/api/grunt.task 141 | grunt.registerTask('default', ['concat', 'jsbeautifier', 'uglify']); 142 | grunt.loadNpmTasks('grunt-contrib-watch'); 143 | }; 144 | -------------------------------------------------------------------------------- /dev/candidate-pair.js: -------------------------------------------------------------------------------- 1 | getStatsParser.candidatePair = function(result) { 2 | if (result.type !== 'googCandidatePair' && result.type !== 'candidate-pair' && result.type !== 'local-candidate' && result.type !== 'remote-candidate') return; 3 | 4 | // result.googActiveConnection means either STUN or TURN is used. 5 | 6 | if (result.googActiveConnection == 'true') { 7 | // id === 'Conn-audio-1-0' 8 | // localCandidateId, remoteCandidateId 9 | 10 | // bytesSent, bytesReceived 11 | 12 | Object.keys(getStatsResult.internal.candidates).forEach(function(cid) { 13 | var candidate = getStatsResult.internal.candidates[cid]; 14 | if (candidate.ipAddress.indexOf(result.googLocalAddress) !== -1) { 15 | getStatsResult.connectionType.local.candidateType = candidate.candidateType; 16 | getStatsResult.connectionType.local.ipAddress = candidate.ipAddress; 17 | getStatsResult.connectionType.local.networkType = candidate.networkType; 18 | getStatsResult.connectionType.local.transport = candidate.transport; 19 | } 20 | if (candidate.ipAddress.indexOf(result.googRemoteAddress) !== -1) { 21 | getStatsResult.connectionType.remote.candidateType = candidate.candidateType; 22 | getStatsResult.connectionType.remote.ipAddress = candidate.ipAddress; 23 | getStatsResult.connectionType.remote.networkType = candidate.networkType; 24 | getStatsResult.connectionType.remote.transport = candidate.transport; 25 | } 26 | }); 27 | 28 | getStatsResult.connectionType.transport = result.googTransportType; 29 | 30 | var localCandidate = getStatsResult.internal.candidates[result.localCandidateId]; 31 | if (localCandidate) { 32 | if (localCandidate.ipAddress) { 33 | getStatsResult.connectionType.systemIpAddress = localCandidate.ipAddress; 34 | } 35 | } 36 | 37 | var remoteCandidate = getStatsResult.internal.candidates[result.remoteCandidateId]; 38 | if (remoteCandidate) { 39 | if (remoteCandidate.ipAddress) { 40 | getStatsResult.connectionType.systemIpAddress = remoteCandidate.ipAddress; 41 | } 42 | } 43 | } 44 | 45 | if (result.type === 'candidate-pair') { 46 | if (result.selected === true && result.nominated === true && result.state === 'succeeded') { 47 | // remoteCandidateId, localCandidateId, componentId 48 | var localCandidate = getStatsResult.internal.candidates[result.remoteCandidateId]; 49 | var remoteCandidate = getStatsResult.internal.candidates[result.remoteCandidateId]; 50 | 51 | // Firefox used above two pairs for connection 52 | } 53 | } 54 | 55 | if (result.type === 'local-candidate') { 56 | getStatsResult.connectionType.local.candidateType = result.candidateType; 57 | getStatsResult.connectionType.local.ipAddress = result.ipAddress; 58 | getStatsResult.connectionType.local.networkType = result.networkType; 59 | getStatsResult.connectionType.local.transport = result.mozLocalTransport || result.transport; 60 | } 61 | 62 | if (result.type === 'remote-candidate') { 63 | getStatsResult.connectionType.remote.candidateType = result.candidateType; 64 | getStatsResult.connectionType.remote.ipAddress = result.ipAddress; 65 | getStatsResult.connectionType.remote.networkType = result.networkType; 66 | getStatsResult.connectionType.remote.transport = result.mozRemoteTransport || result.transport; 67 | } 68 | 69 | if (isSafari) { 70 | // result.remoteCandidateId 71 | // todo: below line will always force "send" on Safari; find a solution 72 | var sendrecvType = result.localCandidateId ? 'send' : 'recv'; 73 | 74 | if (!sendrecvType) return; 75 | 76 | if (!!result.bytesSent) { 77 | var kilobytes = 0; 78 | if (!getStatsResult.internal.video[sendrecvType].prevBytesSent) { 79 | getStatsResult.internal.video[sendrecvType].prevBytesSent = result.bytesSent; 80 | } 81 | 82 | var bytes = result.bytesSent - getStatsResult.internal.video[sendrecvType].prevBytesSent; 83 | getStatsResult.internal.video[sendrecvType].prevBytesSent = result.bytesSent; 84 | 85 | kilobytes = bytes / 1024; 86 | 87 | getStatsResult.video[sendrecvType].availableBandwidth = kilobytes.toFixed(1); 88 | getStatsResult.video.bytesSent = kilobytes.toFixed(1); 89 | } 90 | 91 | if (!!result.bytesReceived) { 92 | var kilobytes = 0; 93 | if (!getStatsResult.internal.video[sendrecvType].prevBytesReceived) { 94 | getStatsResult.internal.video[sendrecvType].prevBytesReceived = result.bytesReceived; 95 | } 96 | 97 | var bytes = result.bytesReceived - getStatsResult.internal.video[sendrecvType].prevBytesReceived; 98 | getStatsResult.internal.video[sendrecvType].prevBytesReceived = result.bytesReceived; 99 | 100 | kilobytes = bytes / 1024; 101 | // getStatsResult.video[sendrecvType].availableBandwidth = kilobytes.toFixed(1); 102 | getStatsResult.video.bytesReceived = kilobytes.toFixed(1); 103 | } 104 | 105 | if (!!result.availableOutgoingBitrate) { 106 | var kilobytes = 0; 107 | if (!getStatsResult.internal.video[sendrecvType].prevAvailableOutgoingBitrate) { 108 | getStatsResult.internal.video[sendrecvType].prevAvailableOutgoingBitrate = result.availableOutgoingBitrate; 109 | } 110 | 111 | var bytes = result.availableOutgoingBitrate - getStatsResult.internal.video[sendrecvType].prevAvailableOutgoingBitrate; 112 | getStatsResult.internal.video[sendrecvType].prevAvailableOutgoingBitrate = result.availableOutgoingBitrate; 113 | 114 | kilobytes = bytes / 1024; 115 | // getStatsResult.video[sendrecvType].availableBandwidth = kilobytes.toFixed(1); 116 | getStatsResult.video.availableOutgoingBitrate = kilobytes.toFixed(1); 117 | } 118 | 119 | if (!!result.availableIncomingBitrate) { 120 | var kilobytes = 0; 121 | if (!getStatsResult.internal.video[sendrecvType].prevAvailableIncomingBitrate) { 122 | getStatsResult.internal.video[sendrecvType].prevAvailableIncomingBitrate = result.availableIncomingBitrate; 123 | } 124 | 125 | var bytes = result.availableIncomingBitrate - getStatsResult.internal.video[sendrecvType].prevAvailableIncomingBitrate; 126 | getStatsResult.internal.video[sendrecvType].prevAvailableIncomingBitrate = result.availableIncomingBitrate; 127 | 128 | kilobytes = bytes / 1024; 129 | // getStatsResult.video[sendrecvType].availableBandwidth = kilobytes.toFixed(1); 130 | getStatsResult.video.availableIncomingBitrate = kilobytes.toFixed(1); 131 | } 132 | } 133 | }; 134 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [getStats.js](https://github.com/muaz-khan/getStats) | WebRTC getStats API 2 | 3 | # [Single Page Demo](https://www.webrtc-experiment.com/getStats/) or [Multi User P2P Demo](https://rtcmulticonnection.herokuapp.com/demos/getStats.html) 4 | 5 | [![npm](https://img.shields.io/npm/v/getstats.svg)](https://npmjs.org/package/getstats) [![downloads](https://img.shields.io/npm/dm/getstats.svg)](https://npmjs.org/package/getstats) [![Build Status: Linux](https://travis-ci.org/muaz-khan/getStats.png?branch=master)](https://travis-ci.org/muaz-khan/getStats) 6 | 7 | A tiny JavaScript library using [WebRTC getStats API](http://dev.w3.org/2011/webrtc/editor/webrtc.html#dom-peerconnection-getstats) to return peer connection stats i.e. bandwidth usage, packets lost, local/remote ip addresses and ports, type of connection etc. 8 | 9 | It is MIT Licenced, which means that you can use it in any commercial/non-commercial product, free of cost. 10 | 11 | ![getStats](https://www.webrtc-experiment.com/images/getStats.png) 12 | 13 | ``` 14 | npm install getstats 15 | 16 | cd node_modules 17 | cd getstats 18 | node server.js 19 | 20 | # and open: 21 | # http://localhost:9999/ 22 | ``` 23 | 24 | To use it: 25 | 26 | ```htm 27 | 28 | ``` 29 | 30 | # Link the library 31 | 32 | ```html 33 | 34 | ``` 35 | 36 | Or link specific build: 37 | 38 | * https://github.com/muaz-khan/getStats/releases 39 | 40 | Or: 41 | 42 | ```javascript 43 | const getStats = require('getstats'); 44 | import getStats from 'getstats'; 45 | ``` 46 | 47 | # Usage 48 | 49 | ```javascript 50 | var rtcPeerConnection = new RTCPeerConnection(rtcConfig); 51 | 52 | var repeatInterval = 2000; // 2000 ms == 2 seconds 53 | getStats(rtcPeerConnection, function(result) { 54 | result.connectionType.remote.ipAddress 55 | result.connectionType.remote.candidateType 56 | result.connectionType.transport 57 | 58 | result.bandwidth.speed // bandwidth download speed (bytes per second) 59 | 60 | // to access native "results" array 61 | result.results.forEach(function(item) { 62 | if (item.type === 'ssrc' && item.transportId === 'Channel-audio-1') { 63 | var packetsLost = item.packetsLost; 64 | var packetsSent = item.packetsSent; 65 | var audioInputLevel = item.audioInputLevel; 66 | var trackId = item.googTrackId; // media stream track id 67 | var isAudio = item.mediaType === 'audio'; // audio or video 68 | var isSending = item.id.indexOf('_send') !== -1; // sender or receiver 69 | 70 | console.log('SendRecv type', item.id.split('_send').pop()); 71 | console.log('MediaStream track type', item.mediaType); 72 | } 73 | }); 74 | }, repeatInterval); 75 | ``` 76 | 77 | # Safari? 78 | 79 | ```javascript 80 | var audioTrack = stream.getTracks().filter(function(t) { 81 | return t.kind === 'audio'; 82 | }); 83 | 84 | getStats(peer, audioTrack, function(results) { 85 | // rest goes here 86 | }, 5 * 1000); 87 | ``` 88 | 89 | # `result.datachannel` 90 | 91 | ```javascript 92 | // states => open or close 93 | alert(result.datachannel.state === 'open'); 94 | ``` 95 | 96 | # `result.isOfferer` 97 | 98 | Offerer is the person who invoked `createOffer` method. 99 | 100 | # `result.encryption` 101 | 102 | To detect which tech is used to encrypt your connections. 103 | 104 | ```javascript 105 | alert(result.encryption === 'sha-256'); 106 | ``` 107 | 108 | # `result.nomore()` 109 | 110 | This function can be used to ask to stop invoking getStats API. 111 | 112 | ```javascript 113 | btnStopGetStats.onclick = function() { 114 | getStatsResult.nomore(); 115 | }; 116 | ``` 117 | 118 | # `result.bandwidth` 119 | 120 | You can use `result.bandwidth.speed` to detect your system's available download speed. 121 | 122 | ```json 123 | { 124 | "speed": 25191, 125 | "systemBandwidth": 0, 126 | "sentPerSecond": 0, 127 | "encodedPerSecond": 0, 128 | "helper": { 129 | "audioBytesSent": 103053, 130 | "videoBytestSent": 0, 131 | "videoBytesSent": 4316619 132 | }, 133 | "availableSendBandwidth": "5181906", 134 | "googActualEncBitrate": "294608", 135 | "googAvailableSendBandwidth": "5181906", 136 | "googAvailableReceiveBandwidth": "0", 137 | "googRetransmitBitrate": "0", 138 | "googTargetEncBitrate": "1700000", 139 | "googBucketDelay": "0", 140 | "googTransmitBitrate": "198296" 141 | } 142 | ``` 143 | 144 | # `result.audio` 145 | 146 | ```json 147 | { 148 | "send": { 149 | "tracks": ["ab5be64a-00b0-4c69-9ffa-b934a3cdaf92"], 150 | "codecs": ["opus"], 151 | "availableBandwidth": "0.4", 152 | "streams": 1 153 | }, 154 | "recv": { 155 | "tracks": ["ab5be64a-00b0-4c69-9ffa-b934a3cdaf92"], 156 | "codecs": ["opus"], 157 | "availableBandwidth": "0.4", 158 | "streams": 1 159 | }, 160 | "bytesSent": 103053, 161 | "bytesReceived": 103053 162 | } 163 | ``` 164 | 165 | # `result.video` 166 | 167 | ```json 168 | { 169 | "send": { 170 | "tracks": ["c07bbd24-4e99-4b94-b429-5174370f8d12"], 171 | "codecs": ["VP9"], 172 | "availableBandwidth": "24.2", 173 | "streams": 1 174 | }, 175 | "recv": { 176 | "tracks": ["c07bbd24-4e99-4b94-b429-5174370f8d12"], 177 | "codecs": ["VP9"], 178 | "availableBandwidth": "25.5", 179 | "streams": 1 180 | }, 181 | "bytesSent": 4316619, 182 | "bytesReceived": 4294692 183 | } 184 | ``` 185 | 186 | # `result.connectionType` 187 | 188 | ```json 189 | { 190 | "systemNetworkType": "wifi", 191 | "systemIpAddress": ["192.168.1.2:66666"], 192 | "local": { 193 | "candidateType": ["host"], 194 | "transport": ["udp"], 195 | "ipAddress": ["192.168.1.2:66666"], 196 | "networkType": ["wlan"] 197 | }, 198 | "remote": { 199 | "candidateType": ["host"], 200 | "transport": ["udp"], 201 | "ipAddress": ["192.168.1.2:66666"], 202 | "networkType": [] 203 | }, 204 | "transport": "udp" 205 | } 206 | ``` 207 | 208 | # `result.resolutions` 209 | 210 | ```json 211 | { 212 | "send": { 213 | "width": "640", 214 | "height": "480" 215 | }, 216 | "recv": { 217 | "width": "640", 218 | "height": "480" 219 | } 220 | } 221 | ``` 222 | 223 | # `result.results` 224 | 225 | It is an array that is returned by browser's native PeerConnection API. 226 | 227 | ```javascript 228 | // "getStatsResult" is your "result" object 229 | getStatsResult.results.forEach(function(item) { 230 | if (item.type === 'ssrc' && item.transportId === 'Channel-audio-1') { 231 | var packetsLost = item.packetsLost; 232 | var packetsSent = item.packetsSent; 233 | var audioInputLevel = item.audioInputLevel; 234 | var trackId = item.googTrackId; // media stream track id 235 | var isAudio = item.mediaType === 'audio'; // audio or video 236 | var isSending = item.id.indexOf('_send') !== -1; // sender or receiver 237 | 238 | console.log('SendRecv type', item.id.split('_send').pop()); 239 | console.log('MediaStream track type', item.mediaType); 240 | } 241 | }); 242 | ``` 243 | 244 | **Above array looks like this:** 245 | 246 | ```json 247 | [{ 248 | "googTrackId": "ab5be64a-00b0-4c69-9ffa-b934a3cdaf92", 249 | "id": "googTrack_ab5be64a-00b0-4c69-9ffa-b934a3cdaf92", 250 | "type": "googTrack", 251 | "timestamp": "2017-11-21T04:10:10.905Z" 252 | }, { 253 | "googInitiator": "true", 254 | "id": "googLibjingleSession_2403774673032347671", 255 | "type": "googLibjingleSession", 256 | "timestamp": "2017-11-21T04:10:10.905Z" 257 | }, { 258 | "googActualEncBitrate": "294608", 259 | "googAvailableSendBandwidth": "5181906", 260 | "googRetransmitBitrate": "0", 261 | "googAvailableReceiveBandwidth": "0", 262 | "googTargetEncBitrate": "1700000", 263 | "googBucketDelay": "0", 264 | "googTransmitBitrate": "198296", 265 | "id": "bweforvideo", 266 | "type": "VideoBwe", 267 | "timestamp": "2017-11-21T04:10:10.905Z" 268 | }, { 269 | "googTrackId": "c07bbd24-4e99-4b94-b429-5174370f8d12", 270 | "id": "googTrack_c07bbd24-4e99-4b94-b429-5174370f8d12", 271 | "type": "googTrack", 272 | "timestamp": "2017-11-21T04:10:10.905Z" 273 | }, { 274 | "googFingerprint": "8B:---:70", 275 | "googFingerprintAlgorithm": "sha-256", 276 | "googDerBase64": "MII----FdhT", 277 | "id": "googCertificate_8B:---:70", 278 | "type": "googCertificate", 279 | "timestamp": "2017-11-21T04:10:10.905Z" 280 | }, { 281 | "googFingerprint": "EE:---:80", 282 | "googFingerprintAlgorithm": "sha-256", 283 | "googDerBase64": "MI----kr", 284 | "id": "googCertificate_EE:13:---:80", 285 | "type": "googCertificate", 286 | "timestamp": "2017-11-21T04:10:10.905Z" 287 | }, { 288 | "googComponent": "1", 289 | "remoteCertificateId": "googCertificate_EE:---:80", 290 | "selectedCandidatePairId": "Conn-audio-1-0", 291 | "dtlsCipher": "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", 292 | "localCertificateId": "googCertificate_8B:---:70", 293 | "srtpCipher": "AES_CM_128_HMAC_SHA1_32", 294 | "id": "Channel-audio-1", 295 | "type": "googComponent", 296 | "timestamp": "2017-11-21T04:10:10.905Z" 297 | }, { 298 | "responsesSent": "12", 299 | "requestsReceived": "12", 300 | "googRemoteCandidateType": "local", 301 | "googReadable": "true", 302 | "googLocalAddress": "192.168.1.2:61885", 303 | "consentRequestsSent": "1", 304 | "googTransportType": "udp", 305 | "googChannelId": "Channel-audio-1", 306 | "googLocalCandidateType": "local", 307 | "googWritable": "true", 308 | "requestsSent": "12", 309 | "googRemoteAddress": "192.168.1.2:60256", 310 | "googRtt": "0", 311 | "googActiveConnection": "true", 312 | "packetsDiscardedOnSend": "0", 313 | "bytesReceived": "4455306", 314 | "responsesReceived": "12", 315 | "remoteCandidateId": "Cand-aws75dws", 316 | "localCandidateId": "Cand-4hzYq0su", 317 | "bytesSent": "4466782", 318 | "packetsSent": "5437", 319 | "id": "Conn-audio-1-0", 320 | "type": "googCandidatePair", 321 | "timestamp": "2017-11-21T04:10:10.905Z" 322 | }, { 323 | "portNumber": "61885", 324 | "networkType": "wlan", 325 | "ipAddress": "192.168.1.2", 326 | "transport": "udp", 327 | "candidateType": "host", 328 | "priority": "2122260223", 329 | "id": "Cand-4hzYq0su", 330 | "type": "localcandidate", 331 | "timestamp": "2017-11-21T04:09:48.850Z" 332 | }, { 333 | "portNumber": "60256", 334 | "ipAddress": "192.168.1.2", 335 | "transport": "udp", 336 | "candidateType": "host", 337 | "priority": "2122260223", 338 | "id": "Cand-aws75dws", 339 | "type": "remotecandidate", 340 | "timestamp": "2017-11-21T04:09:48.850Z" 341 | }, { 342 | "googDecodingCTN": "2206", 343 | "packetsLost": "0", 344 | "googSecondaryDecodedRate": "0", 345 | "googDecodingPLC": "21", 346 | "packetsReceived": "1056", 347 | "googExpandRate": "0.900574", 348 | "googJitterReceived": "1", 349 | "googDecodingCNG": "0", 350 | "ssrc": "3719978459", 351 | "googPreferredJitterBufferMs": "20", 352 | "googSpeechExpandRate": "0.178711", 353 | "totalSamplesDuration": "22.06", 354 | "totalAudioEnergy": "0", 355 | "transportId": "Channel-audio-1", 356 | "mediaType": "audio", 357 | "googDecodingPLCCNG": "72", 358 | "googCodecName": "opus", 359 | "googSecondaryDiscardedRate": "0", 360 | "googDecodingNormal": "2113", 361 | "googTrackId": "ab5be64a-00b0-4c69-9ffa-b934a3cdaf92", 362 | "audioOutputLevel": "0", 363 | "googAccelerateRate": "0", 364 | "bytesReceived": "103053", 365 | "googCurrentDelayMs": "61", 366 | "googDecodingCTSG": "0", 367 | "googCaptureStartNtpTimeMs": "3720226188883", 368 | "googPreemptiveExpandRate": "0", 369 | "googJitterBufferMs": "5", 370 | "googDecodingMuted": "71", 371 | "id": "ssrc_3719978459_recv", 372 | "type": "ssrc", 373 | "timestamp": "2017-11-21T04:10:10.905Z" 374 | }, { 375 | "audioInputLevel": "38", 376 | "packetsLost": "0", 377 | "googTrackId": "ab5be64a-00b0-4c69-9ffa-b934a3cdaf92", 378 | "googRtt": "1", 379 | "googResidualEchoLikelihoodRecentMax": "0", 380 | "googEchoCancellationReturnLossEnhancement": "-100", 381 | "totalSamplesDuration": "0", 382 | "googCodecName": "opus", 383 | "transportId": "Channel-audio-1", 384 | "mediaType": "audio", 385 | "aecDivergentFilterFraction": "0", 386 | "googEchoCancellationReturnLoss": "-100", 387 | "googResidualEchoLikelihood": "0", 388 | "googEchoCancellationQualityMin": "0", 389 | "totalAudioEnergy": "0", 390 | "ssrc": "4057371623", 391 | "googJitterReceived": "1", 392 | "googTypingNoiseState": "false", 393 | "packetsSent": "1056", 394 | "bytesSent": "103053", 395 | "id": "ssrc_4057371623_send", 396 | "type": "ssrc", 397 | "timestamp": "2017-11-21T04:10:10.905Z" 398 | }, { 399 | "googContentType": "realtime", 400 | "googCaptureStartNtpTimeMs": "3720226188863", 401 | "googTargetDelayMs": "28", 402 | "packetsLost": "0", 403 | "googDecodeMs": "3", 404 | "googFrameHeightReceived": "480", 405 | "googFrameRateOutput": "2", 406 | "packetsReceived": "3843", 407 | "ssrc": "2926085704", 408 | "googRenderDelayMs": "10", 409 | "googMaxDecodeMs": "4", 410 | "googTrackId": "c07bbd24-4e99-4b94-b429-5174370f8d12", 411 | "googFrameWidthReceived": "640", 412 | "codecImplementationName": "libvpx", 413 | "transportId": "Channel-audio-1", 414 | "mediaType": "video", 415 | "googTimingFrameInfo": "2498355253,490771956,490771960,490771967,490771967,490771974,0,1", 416 | "googInterframeDelayMax": "67", 417 | "googCodecName": "VP9", 418 | "googFrameRateReceived": "2", 419 | "qpSum": "35281", 420 | "framesDecoded": "348", 421 | "googNacksSent": "0", 422 | "googFirsSent": "0", 423 | "bytesReceived": "4294692", 424 | "googCurrentDelayMs": "28", 425 | "googMinPlayoutDelayMs": "0", 426 | "googFrameRateDecoded": "2", 427 | "googJitterBufferMs": "14", 428 | "googPlisSent": "0", 429 | "id": "ssrc_2926085704_recv", 430 | "type": "ssrc", 431 | "timestamp": "2017-11-21T04:10:10.905Z" 432 | }, { 433 | "googFrameWidthSent": "640", 434 | "packetsLost": "0", 435 | "googRtt": "1", 436 | "googEncodeUsagePercent": "19", 437 | "googCpuLimitedResolution": "false", 438 | "googNacksReceived": "0", 439 | "googBandwidthLimitedResolution": "false", 440 | "googPlisReceived": "0", 441 | "googAvgEncodeMs": "7", 442 | "googTrackId": "c07bbd24-4e99-4b94-b429-5174370f8d12", 443 | "googFrameRateInput": "2", 444 | "framesEncoded": "348", 445 | "codecImplementationName": "libvpx", 446 | "transportId": "Channel-audio-1", 447 | "mediaType": "video", 448 | "googFrameHeightSent": "480", 449 | "googFrameRateSent": "18", 450 | "googCodecName": "VP9", 451 | "qpSum": "35291", 452 | "googAdaptationChanges": "0", 453 | "ssrc": "3181351409", 454 | "googFirsReceived": "0", 455 | "packetsSent": "3884", 456 | "bytesSent": "4316619", 457 | "id": "ssrc_3181351409_send", 458 | "type": "ssrc", 459 | "timestamp": "2017-11-21T04:10:10.905Z" 460 | }] 461 | ``` 462 | 463 | ## License 464 | 465 | [getStats.js](https://github.com/muaz-khan/getStats) is released under [MIT licence](https://github.com/muaz-khan/getStats/blob/master/LICENSE.md) . Copyright (c) [Muaz Khan](https://MuazKhan.com/). 466 | -------------------------------------------------------------------------------- /getStats.min.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Last time updated: 2019-02-20 3:31:30 PM UTC 4 | 5 | // _______________ 6 | // getStats v1.2.0 7 | 8 | // Open-Sourced: https://github.com/muaz-khan/getStats 9 | 10 | // -------------------------------------------------- 11 | // Muaz Khan - www.MuazKhan.com 12 | // MIT License - www.WebRTC-Experiment.com/licence 13 | // -------------------------------------------------- 14 | 15 | "use strict";var getStats=function(mediaStreamTrack,callback,interval){function getStatsLooper(){getStatsWrapper(function(results){if(results&&results.forEach){results.forEach(function(result){Object.keys(getStatsParser).forEach(function(key){if("function"==typeof getStatsParser[key])try{getStatsParser[key](result)}catch(e){console.error(e.message,e.stack,e)}})});try{peer.iceConnectionState.search(/failed|closed|disconnected/gi)!==-1&&(nomore=!0)}catch(e){nomore=!0}nomore===!0&&(getStatsResult.datachannel&&(getStatsResult.datachannel.state="close"),getStatsResult.ended=!0),getStatsResult.results=results,getStatsResult.audio&&getStatsResult.video&&(getStatsResult.bandwidth.speed=getStatsResult.audio.bytesSent-getStatsResult.bandwidth.helper.audioBytesSent+(getStatsResult.video.bytesSent-getStatsResult.bandwidth.helper.videoBytesSent),getStatsResult.bandwidth.helper.audioBytesSent=getStatsResult.audio.bytesSent,getStatsResult.bandwidth.helper.videoBytesSent=getStatsResult.video.bytesSent),callback(getStatsResult),nomore||void 0!=typeof interval&&interval&&setTimeout(getStatsLooper,interval||1e3)}})}function getStatsWrapper(cb){"undefined"!=typeof window.InstallTrigger||isSafari?peer.getStats(window.mediaStreamTrack||null).then(function(res){var items=[];res.forEach(function(r){items.push(r)}),cb(items)})["catch"](cb):peer.getStats(function(res){var items=[];res.result().forEach(function(res){var item={};res.names().forEach(function(name){item[name]=res.stat(name)}),item.id=res.id,item.type=res.type,item.timestamp=res.timestamp,items.push(item)}),cb(items)})}var browserFakeUserAgent="Fake/5.0 (FakeOS) AppleWebKit/123 (KHTML, like Gecko) Fake/12.3.4567.89 Fake/123.45";!function(that){that&&"undefined"==typeof window&&"undefined"!=typeof global&&(global.navigator={userAgent:browserFakeUserAgent,getUserMedia:function(){}},global.console||(global.console={}),"undefined"!=typeof global.console.log&&"undefined"!=typeof global.console.error||(global.console.error=global.console.log=global.console.log||function(){console.log(arguments)}),"undefined"==typeof document&&(that.document={documentElement:{appendChild:function(){return""}}},document.createElement=document.captureStream=document.mozCaptureStream=function(){var obj={getContext:function(){return obj},play:function(){},pause:function(){},drawImage:function(){},toDataURL:function(){return""}};return obj},that.HTMLVideoElement=function(){}),"undefined"==typeof location&&(that.location={protocol:"file:",href:"",hash:""}),"undefined"==typeof screen&&(that.screen={width:0,height:0}),"undefined"==typeof URL&&(that.URL={createObjectURL:function(){return""},revokeObjectURL:function(){return""}}),"undefined"==typeof MediaStreamTrack&&(that.MediaStreamTrack=function(){}),"undefined"==typeof RTCPeerConnection&&(that.RTCPeerConnection=function(){}),that.window=global)}("undefined"!=typeof global?global:null);var RTCPeerConnection=window.RTCPeerConnection||window.mozRTCPeerConnection||window.webkitRTCPeerConnection;"undefined"==typeof MediaStreamTrack&&(MediaStreamTrack={});var systemNetworkType=((navigator.connection||{}).type||"unknown").toString().toLowerCase(),getStatsResult={encryption:"sha-256",audio:{send:{tracks:[],codecs:[],availableBandwidth:0,streams:0,framerateMean:0,bitrateMean:0},recv:{tracks:[],codecs:[],availableBandwidth:0,streams:0,framerateMean:0,bitrateMean:0},bytesSent:0,bytesReceived:0,latency:0,packetsLost:0},video:{send:{tracks:[],codecs:[],availableBandwidth:0,streams:0,framerateMean:0,bitrateMean:0},recv:{tracks:[],codecs:[],availableBandwidth:0,streams:0,framerateMean:0,bitrateMean:0},bytesSent:0,bytesReceived:0,latency:0,packetsLost:0},bandwidth:{systemBandwidth:0,sentPerSecond:0,encodedPerSecond:0,helper:{audioBytesSent:0,videoBytestSent:0},speed:0},results:{},connectionType:{systemNetworkType:systemNetworkType,systemIpAddress:"192.168.1.2",local:{candidateType:[],transport:[],ipAddress:[],networkType:[]},remote:{candidateType:[],transport:[],ipAddress:[],networkType:[]}},resolutions:{send:{width:0,height:0},recv:{width:0,height:0}},internal:{audio:{send:{},recv:{}},video:{send:{},recv:{}},candidates:{}},nomore:function(){nomore=!0}},getStatsParser={checkIfOfferer:function(result){"googLibjingleSession"===result.type&&(getStatsResult.isOfferer=result.googInitiator)}},isSafari=/^((?!chrome|android).)*safari/i.test(navigator.userAgent),peer=this;if(!(arguments[0]instanceof RTCPeerConnection))throw"1st argument is not instance of RTCPeerConnection.";peer=arguments[0],arguments[1]instanceof MediaStreamTrack&&(mediaStreamTrack=arguments[1],callback=arguments[2],interval=arguments[3]);var nomore=!1;getStatsParser.datachannel=function(result){"datachannel"===result.type&&(getStatsResult.datachannel={state:result.state})},getStatsParser.googCertificate=function(result){"googCertificate"==result.type&&(getStatsResult.encryption=result.googFingerprintAlgorithm),"certificate"==result.type&&(getStatsResult.encryption=result.fingerprintAlgorithm)},getStatsParser.checkAudioTracks=function(result){if("audio"===result.mediaType){var sendrecvType=result.id.split("_").pop();if(result.isRemote===!0&&(sendrecvType="recv"),result.isRemote===!1&&(sendrecvType="send"),sendrecvType){if(getStatsResult.audio[sendrecvType].codecs.indexOf(result.googCodecName||"opus")===-1&&getStatsResult.audio[sendrecvType].codecs.push(result.googCodecName||"opus"),result.bytesSent){var kilobytes=0;getStatsResult.internal.audio[sendrecvType].prevBytesSent||(getStatsResult.internal.audio[sendrecvType].prevBytesSent=result.bytesSent);var bytes=result.bytesSent-getStatsResult.internal.audio[sendrecvType].prevBytesSent;getStatsResult.internal.audio[sendrecvType].prevBytesSent=result.bytesSent,kilobytes=bytes/1024,getStatsResult.audio[sendrecvType].availableBandwidth=kilobytes.toFixed(1),getStatsResult.audio.bytesSent=kilobytes.toFixed(1)}if(result.bytesReceived){var kilobytes=0;getStatsResult.internal.audio[sendrecvType].prevBytesReceived||(getStatsResult.internal.audio[sendrecvType].prevBytesReceived=result.bytesReceived);var bytes=result.bytesReceived-getStatsResult.internal.audio[sendrecvType].prevBytesReceived;getStatsResult.internal.audio[sendrecvType].prevBytesReceived=result.bytesReceived,kilobytes=bytes/1024,getStatsResult.audio.bytesReceived=kilobytes.toFixed(1)}if(result.googTrackId&&getStatsResult.audio[sendrecvType].tracks.indexOf(result.googTrackId)===-1&&getStatsResult.audio[sendrecvType].tracks.push(result.googTrackId),result.googCurrentDelayMs){var kilobytes=0;getStatsResult.internal.audio.prevGoogCurrentDelayMs||(getStatsResult.internal.audio.prevGoogCurrentDelayMs=result.googCurrentDelayMs);var bytes=result.googCurrentDelayMs-getStatsResult.internal.audio.prevGoogCurrentDelayMs;getStatsResult.internal.audio.prevGoogCurrentDelayMs=result.googCurrentDelayMs,getStatsResult.audio.latency=bytes.toFixed(1),getStatsResult.audio.latency<0&&(getStatsResult.audio.latency=0)}if(result.packetsLost){var kilobytes=0;getStatsResult.internal.audio.prevPacketsLost||(getStatsResult.internal.audio.prevPacketsLost=result.packetsLost);var bytes=result.packetsLost-getStatsResult.internal.audio.prevPacketsLost;getStatsResult.internal.audio.prevPacketsLost=result.packetsLost,getStatsResult.audio.packetsLost=bytes.toFixed(1),getStatsResult.audio.packetsLost<0&&(getStatsResult.audio.packetsLost=0)}}}},getStatsParser.checkVideoTracks=function(result){if("video"===result.mediaType){var sendrecvType=result.id.split("_").pop();if(result.isRemote===!0&&(sendrecvType="recv"),result.isRemote===!1&&(sendrecvType="send"),sendrecvType){if(getStatsResult.video[sendrecvType].codecs.indexOf(result.googCodecName||"VP8")===-1&&getStatsResult.video[sendrecvType].codecs.push(result.googCodecName||"VP8"),result.bytesSent){var kilobytes=0;getStatsResult.internal.video[sendrecvType].prevBytesSent||(getStatsResult.internal.video[sendrecvType].prevBytesSent=result.bytesSent);var bytes=result.bytesSent-getStatsResult.internal.video[sendrecvType].prevBytesSent;getStatsResult.internal.video[sendrecvType].prevBytesSent=result.bytesSent,kilobytes=bytes/1024,getStatsResult.video[sendrecvType].availableBandwidth=kilobytes.toFixed(1),getStatsResult.video.bytesSent=kilobytes.toFixed(1)}if(result.bytesReceived){var kilobytes=0;getStatsResult.internal.video[sendrecvType].prevBytesReceived||(getStatsResult.internal.video[sendrecvType].prevBytesReceived=result.bytesReceived);var bytes=result.bytesReceived-getStatsResult.internal.video[sendrecvType].prevBytesReceived;getStatsResult.internal.video[sendrecvType].prevBytesReceived=result.bytesReceived,kilobytes=bytes/1024,getStatsResult.video.bytesReceived=kilobytes.toFixed(1)}if(result.googFrameHeightReceived&&result.googFrameWidthReceived&&(getStatsResult.resolutions[sendrecvType].width=result.googFrameWidthReceived,getStatsResult.resolutions[sendrecvType].height=result.googFrameHeightReceived),result.googFrameHeightSent&&result.googFrameWidthSent&&(getStatsResult.resolutions[sendrecvType].width=result.googFrameWidthSent,getStatsResult.resolutions[sendrecvType].height=result.googFrameHeightSent),result.googTrackId&&getStatsResult.video[sendrecvType].tracks.indexOf(result.googTrackId)===-1&&getStatsResult.video[sendrecvType].tracks.push(result.googTrackId),result.framerateMean){getStatsResult.bandwidth.framerateMean=result.framerateMean;var kilobytes=0;getStatsResult.internal.video[sendrecvType].prevFramerateMean||(getStatsResult.internal.video[sendrecvType].prevFramerateMean=result.bitrateMean);var bytes=result.bytesSent-getStatsResult.internal.video[sendrecvType].prevFramerateMean;getStatsResult.internal.video[sendrecvType].prevFramerateMean=result.framerateMean,kilobytes=bytes/1024,getStatsResult.video[sendrecvType].framerateMean=bytes.toFixed(1)}if(result.bitrateMean){getStatsResult.bandwidth.bitrateMean=result.bitrateMean;var kilobytes=0;getStatsResult.internal.video[sendrecvType].prevBitrateMean||(getStatsResult.internal.video[sendrecvType].prevBitrateMean=result.bitrateMean);var bytes=result.bytesSent-getStatsResult.internal.video[sendrecvType].prevBitrateMean;getStatsResult.internal.video[sendrecvType].prevBitrateMean=result.bitrateMean,kilobytes=bytes/1024,getStatsResult.video[sendrecvType].bitrateMean=bytes.toFixed(1)}if(result.googCurrentDelayMs){var kilobytes=0;getStatsResult.internal.video.prevGoogCurrentDelayMs||(getStatsResult.internal.video.prevGoogCurrentDelayMs=result.googCurrentDelayMs);var bytes=result.googCurrentDelayMs-getStatsResult.internal.video.prevGoogCurrentDelayMs;getStatsResult.internal.video.prevGoogCurrentDelayMs=result.googCurrentDelayMs,getStatsResult.video.latency=bytes.toFixed(1),getStatsResult.video.latency<0&&(getStatsResult.video.latency=0)}if(result.packetsLost){var kilobytes=0;getStatsResult.internal.video.prevPacketsLost||(getStatsResult.internal.video.prevPacketsLost=result.packetsLost);var bytes=result.packetsLost-getStatsResult.internal.video.prevPacketsLost;getStatsResult.internal.video.prevPacketsLost=result.packetsLost,getStatsResult.video.packetsLost=bytes.toFixed(1),getStatsResult.video.packetsLost<0&&(getStatsResult.video.packetsLost=0)}}}},getStatsParser.bweforvideo=function(result){"VideoBwe"===result.type&&(getStatsResult.bandwidth.availableSendBandwidth=result.googAvailableSendBandwidth,getStatsResult.bandwidth.googActualEncBitrate=result.googActualEncBitrate,getStatsResult.bandwidth.googAvailableSendBandwidth=result.googAvailableSendBandwidth,getStatsResult.bandwidth.googAvailableReceiveBandwidth=result.googAvailableReceiveBandwidth,getStatsResult.bandwidth.googRetransmitBitrate=result.googRetransmitBitrate,getStatsResult.bandwidth.googTargetEncBitrate=result.googTargetEncBitrate,getStatsResult.bandwidth.googBucketDelay=result.googBucketDelay,getStatsResult.bandwidth.googTransmitBitrate=result.googTransmitBitrate)},getStatsParser.candidatePair=function(result){if("googCandidatePair"===result.type||"candidate-pair"===result.type||"local-candidate"===result.type||"remote-candidate"===result.type){if("true"==result.googActiveConnection){Object.keys(getStatsResult.internal.candidates).forEach(function(cid){var candidate=getStatsResult.internal.candidates[cid];candidate.ipAddress.indexOf(result.googLocalAddress)!==-1&&(getStatsResult.connectionType.local.candidateType=candidate.candidateType,getStatsResult.connectionType.local.ipAddress=candidate.ipAddress,getStatsResult.connectionType.local.networkType=candidate.networkType,getStatsResult.connectionType.local.transport=candidate.transport),candidate.ipAddress.indexOf(result.googRemoteAddress)!==-1&&(getStatsResult.connectionType.remote.candidateType=candidate.candidateType,getStatsResult.connectionType.remote.ipAddress=candidate.ipAddress,getStatsResult.connectionType.remote.networkType=candidate.networkType,getStatsResult.connectionType.remote.transport=candidate.transport)}),getStatsResult.connectionType.transport=result.googTransportType;var localCandidate=getStatsResult.internal.candidates[result.localCandidateId];localCandidate&&localCandidate.ipAddress&&(getStatsResult.connectionType.systemIpAddress=localCandidate.ipAddress);var remoteCandidate=getStatsResult.internal.candidates[result.remoteCandidateId];remoteCandidate&&remoteCandidate.ipAddress&&(getStatsResult.connectionType.systemIpAddress=remoteCandidate.ipAddress)}if("candidate-pair"===result.type&&result.selected===!0&&result.nominated===!0&&"succeeded"===result.state)var localCandidate=getStatsResult.internal.candidates[result.remoteCandidateId],remoteCandidate=getStatsResult.internal.candidates[result.remoteCandidateId];if("local-candidate"===result.type&&(getStatsResult.connectionType.local.candidateType=result.candidateType,getStatsResult.connectionType.local.ipAddress=result.ipAddress,getStatsResult.connectionType.local.networkType=result.networkType,getStatsResult.connectionType.local.transport=result.mozLocalTransport||result.transport),"remote-candidate"===result.type&&(getStatsResult.connectionType.remote.candidateType=result.candidateType,getStatsResult.connectionType.remote.ipAddress=result.ipAddress,getStatsResult.connectionType.remote.networkType=result.networkType,getStatsResult.connectionType.remote.transport=result.mozRemoteTransport||result.transport),isSafari){var sendrecvType=result.localCandidateId?"send":"recv";if(!sendrecvType)return;if(result.bytesSent){var kilobytes=0;getStatsResult.internal.video[sendrecvType].prevBytesSent||(getStatsResult.internal.video[sendrecvType].prevBytesSent=result.bytesSent);var bytes=result.bytesSent-getStatsResult.internal.video[sendrecvType].prevBytesSent;getStatsResult.internal.video[sendrecvType].prevBytesSent=result.bytesSent,kilobytes=bytes/1024,getStatsResult.video[sendrecvType].availableBandwidth=kilobytes.toFixed(1),getStatsResult.video.bytesSent=kilobytes.toFixed(1)}if(result.bytesReceived){var kilobytes=0;getStatsResult.internal.video[sendrecvType].prevBytesReceived||(getStatsResult.internal.video[sendrecvType].prevBytesReceived=result.bytesReceived);var bytes=result.bytesReceived-getStatsResult.internal.video[sendrecvType].prevBytesReceived;getStatsResult.internal.video[sendrecvType].prevBytesReceived=result.bytesReceived,kilobytes=bytes/1024,getStatsResult.video.bytesReceived=kilobytes.toFixed(1)}if(result.availableOutgoingBitrate){var kilobytes=0;getStatsResult.internal.video[sendrecvType].prevAvailableOutgoingBitrate||(getStatsResult.internal.video[sendrecvType].prevAvailableOutgoingBitrate=result.availableOutgoingBitrate);var bytes=result.availableOutgoingBitrate-getStatsResult.internal.video[sendrecvType].prevAvailableOutgoingBitrate;getStatsResult.internal.video[sendrecvType].prevAvailableOutgoingBitrate=result.availableOutgoingBitrate,kilobytes=bytes/1024,getStatsResult.video.availableOutgoingBitrate=kilobytes.toFixed(1)}if(result.availableIncomingBitrate){var kilobytes=0;getStatsResult.internal.video[sendrecvType].prevAvailableIncomingBitrate||(getStatsResult.internal.video[sendrecvType].prevAvailableIncomingBitrate=result.availableIncomingBitrate);var bytes=result.availableIncomingBitrate-getStatsResult.internal.video[sendrecvType].prevAvailableIncomingBitrate;getStatsResult.internal.video[sendrecvType].prevAvailableIncomingBitrate=result.availableIncomingBitrate,kilobytes=bytes/1024,getStatsResult.video.availableIncomingBitrate=kilobytes.toFixed(1)}}}};var LOCAL_candidateType={},LOCAL_transport={},LOCAL_ipAddress={},LOCAL_networkType={};getStatsParser.localcandidate=function(result){"localcandidate"!==result.type&&"local-candidate"!==result.type||result.id&&(LOCAL_candidateType[result.id]||(LOCAL_candidateType[result.id]=[]),LOCAL_transport[result.id]||(LOCAL_transport[result.id]=[]),LOCAL_ipAddress[result.id]||(LOCAL_ipAddress[result.id]=[]),LOCAL_networkType[result.id]||(LOCAL_networkType[result.id]=[]),result.candidateType&&LOCAL_candidateType[result.id].indexOf(result.candidateType)===-1&&LOCAL_candidateType[result.id].push(result.candidateType),result.transport&&LOCAL_transport[result.id].indexOf(result.transport)===-1&&LOCAL_transport[result.id].push(result.transport),result.ipAddress&&LOCAL_ipAddress[result.id].indexOf(result.ipAddress+":"+result.portNumber)===-1&&LOCAL_ipAddress[result.id].push(result.ipAddress+":"+result.portNumber),result.networkType&&LOCAL_networkType[result.id].indexOf(result.networkType)===-1&&LOCAL_networkType[result.id].push(result.networkType),getStatsResult.internal.candidates[result.id]={candidateType:LOCAL_candidateType[result.id],ipAddress:LOCAL_ipAddress[result.id],portNumber:result.portNumber,networkType:LOCAL_networkType[result.id],priority:result.priority,transport:LOCAL_transport[result.id],timestamp:result.timestamp,id:result.id,type:result.type},getStatsResult.connectionType.local.candidateType=LOCAL_candidateType[result.id],getStatsResult.connectionType.local.ipAddress=LOCAL_ipAddress[result.id],getStatsResult.connectionType.local.networkType=LOCAL_networkType[result.id],getStatsResult.connectionType.local.transport=LOCAL_transport[result.id])};var REMOTE_candidateType={},REMOTE_transport={},REMOTE_ipAddress={},REMOTE_networkType={};getStatsParser.remotecandidate=function(result){"remotecandidate"!==result.type&&"remote-candidate"!==result.type||result.id&&(REMOTE_candidateType[result.id]||(REMOTE_candidateType[result.id]=[]),REMOTE_transport[result.id]||(REMOTE_transport[result.id]=[]),REMOTE_ipAddress[result.id]||(REMOTE_ipAddress[result.id]=[]),REMOTE_networkType[result.id]||(REMOTE_networkType[result.id]=[]),result.candidateType&&REMOTE_candidateType[result.id].indexOf(result.candidateType)===-1&&REMOTE_candidateType[result.id].push(result.candidateType),result.transport&&REMOTE_transport[result.id].indexOf(result.transport)===-1&&REMOTE_transport[result.id].push(result.transport),result.ipAddress&&REMOTE_ipAddress[result.id].indexOf(result.ipAddress+":"+result.portNumber)===-1&&REMOTE_ipAddress[result.id].push(result.ipAddress+":"+result.portNumber),result.networkType&&REMOTE_networkType[result.id].indexOf(result.networkType)===-1&&REMOTE_networkType[result.id].push(result.networkType),getStatsResult.internal.candidates[result.id]={candidateType:REMOTE_candidateType[result.id],ipAddress:REMOTE_ipAddress[result.id],portNumber:result.portNumber,networkType:REMOTE_networkType[result.id],priority:result.priority,transport:REMOTE_transport[result.id],timestamp:result.timestamp,id:result.id,type:result.type},getStatsResult.connectionType.remote.candidateType=REMOTE_candidateType[result.id],getStatsResult.connectionType.remote.ipAddress=REMOTE_ipAddress[result.id],getStatsResult.connectionType.remote.networkType=REMOTE_networkType[result.id],getStatsResult.connectionType.remote.transport=REMOTE_transport[result.id])},getStatsParser.dataSentReceived=function(result){!result.googCodecName||"video"!==result.mediaType&&"audio"!==result.mediaType||(result.bytesSent&&(getStatsResult[result.mediaType].bytesSent=parseInt(result.bytesSent)),result.bytesReceived&&(getStatsResult[result.mediaType].bytesReceived=parseInt(result.bytesReceived)))},getStatsParser.inboundrtp=function(result){if(isSafari&&"inbound-rtp"===result.type){var mediaType=result.mediaType||"audio",sendrecvType=result.isRemote?"recv":"send";if(sendrecvType){if(result.bytesSent){var kilobytes=0;getStatsResult.internal[mediaType][sendrecvType].prevBytesSent||(getStatsResult.internal[mediaType][sendrecvType].prevBytesSent=result.bytesSent);var bytes=result.bytesSent-getStatsResult.internal[mediaType][sendrecvType].prevBytesSent;getStatsResult.internal[mediaType][sendrecvType].prevBytesSent=result.bytesSent,kilobytes=bytes/1024,getStatsResult[mediaType][sendrecvType].availableBandwidth=kilobytes.toFixed(1),getStatsResult[mediaType].bytesSent=kilobytes.toFixed(1)}if(result.bytesReceived){var kilobytes=0;getStatsResult.internal[mediaType][sendrecvType].prevBytesReceived||(getStatsResult.internal[mediaType][sendrecvType].prevBytesReceived=result.bytesReceived);var bytes=result.bytesReceived-getStatsResult.internal[mediaType][sendrecvType].prevBytesReceived;getStatsResult.internal[mediaType][sendrecvType].prevBytesReceived=result.bytesReceived,kilobytes=bytes/1024,getStatsResult[mediaType].bytesReceived=kilobytes.toFixed(1)}}}},getStatsParser.outboundrtp=function(result){if(isSafari&&"outbound-rtp"===result.type){var mediaType=result.mediaType||"audio",sendrecvType=result.isRemote?"recv":"send";if(sendrecvType){if(result.bytesSent){var kilobytes=0;getStatsResult.internal[mediaType][sendrecvType].prevBytesSent||(getStatsResult.internal[mediaType][sendrecvType].prevBytesSent=result.bytesSent);var bytes=result.bytesSent-getStatsResult.internal[mediaType][sendrecvType].prevBytesSent;getStatsResult.internal[mediaType][sendrecvType].prevBytesSent=result.bytesSent,kilobytes=bytes/1024,getStatsResult[mediaType][sendrecvType].availableBandwidth=kilobytes.toFixed(1),getStatsResult[mediaType].bytesSent=kilobytes.toFixed(1)}if(result.bytesReceived){var kilobytes=0;getStatsResult.internal[mediaType][sendrecvType].prevBytesReceived||(getStatsResult.internal[mediaType][sendrecvType].prevBytesReceived=result.bytesReceived);var bytes=result.bytesReceived-getStatsResult.internal[mediaType][sendrecvType].prevBytesReceived;getStatsResult.internal[mediaType][sendrecvType].prevBytesReceived=result.bytesReceived,kilobytes=bytes/1024,getStatsResult[mediaType].bytesReceived=kilobytes.toFixed(1)}}}},getStatsParser.track=function(result){if(isSafari&&"track"===result.type){var sendrecvType=result.remoteSource===!0?"send":"recv";result.frameWidth&&result.frameHeight&&(getStatsResult.resolutions[sendrecvType].width=result.frameWidth,getStatsResult.resolutions[sendrecvType].height=result.frameHeight)}};var SSRC={audio:{send:[],recv:[]},video:{send:[],recv:[]}};getStatsParser.ssrc=function(result){if(result.googCodecName&&("video"===result.mediaType||"audio"===result.mediaType)&&"ssrc"===result.type){var sendrecvType=result.id.split("_").pop();SSRC[result.mediaType][sendrecvType].indexOf(result.ssrc)===-1&&SSRC[result.mediaType][sendrecvType].push(result.ssrc),getStatsResult[result.mediaType][sendrecvType].streams=SSRC[result.mediaType][sendrecvType].length}},getStatsLooper()};"undefined"!=typeof module&&(module.exports=getStats),"function"==typeof define&&define.amd&&define("getStats",[],function(){return getStats}); -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | 12 | getStats.js Demo | WebRTC getStats API 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 |
131 |
132 |

133 | getStats.js Demo | WebRTC getStats API 134 |

135 |

136 | HOME 137 | © 138 | Muaz Khan . 139 | @WebRTCWeb . 140 | Github . 141 | Latest issues . 142 | What's New? 143 |

144 |
145 | 146 |
147 | 148 |
149 | 150 | 151 |
152 | 153 |
154 | 155 | 159 | 164 | 169 |
170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 183 | 186 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 203 | 207 | 208 | 209 | 210 | 211 | 215 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 233 | 237 | 238 | 239 | 240 | 241 | 245 | 249 | 250 | 251 | 252 | 253 | 257 | 261 | 262 | 263 | 264 | 265 | 268 | 271 | 272 | 273 | 274 | 275 | 278 | 281 | 282 | 283 | 284 | 285 | 289 | 293 | 294 | 295 | 296 | 297 | 301 | 305 | 306 | 307 |
First PeerSecond Peer
181 | Videos 182 | 184 | 185 | 187 | 188 |
Bandwidth Speed per second per second
STUN/TURN? 200 | ()
201 | ()
202 |
204 | ()
205 | ()
206 |
Codecs 212 |
213 |
214 |
216 |
217 |
218 |
Encryption
IP Address 230 |
231 | 232 |
234 |
235 | 236 |
Resolutions 242 |
243 | 244 |
246 |
247 | 248 |
Data 254 |
255 | 256 |
258 |
259 | 260 |
framerateMean 266 | 267 | 269 | 270 |
bitrateMean 276 | 277 | 279 | 280 |
latency 286 |
287 | 288 |
290 |
291 | 292 |
packetsLost 298 |
299 | 300 |
302 |
303 | 304 |
308 | 309 |
310 | Multi-user peer-to-peer demo: getStats using RTCMultiConnection 311 |
312 | 313 | 357 | 374 | 417 | 462 | 478 | 487 | 488 | 683 | 684 |
685 |

getStats Issues 686 |

687 |
688 |
689 | 690 |
691 |

Feedback

692 |
693 | 694 |
695 | 696 | Enter your email too; if you want "direct" reply! 697 |
698 | 699 |
700 |

Latest Updates 701 |

702 |
703 |
704 |
705 | 706 | 707 | 708 | 714 | 715 | 716 | 719 | 720 | 721 | 722 | 723 | -------------------------------------------------------------------------------- /getStats.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Last time updated: 2019-02-20 3:31:29 PM UTC 4 | 5 | // _______________ 6 | // getStats v1.2.0 7 | 8 | // Open-Sourced: https://github.com/muaz-khan/getStats 9 | 10 | // -------------------------------------------------- 11 | // Muaz Khan - www.MuazKhan.com 12 | // MIT License - www.WebRTC-Experiment.com/licence 13 | // -------------------------------------------------- 14 | 15 | var getStats = function(mediaStreamTrack, callback, interval) { 16 | 17 | var browserFakeUserAgent = 'Fake/5.0 (FakeOS) AppleWebKit/123 (KHTML, like Gecko) Fake/12.3.4567.89 Fake/123.45'; 18 | 19 | (function(that) { 20 | if (!that) { 21 | return; 22 | } 23 | 24 | if (typeof window !== 'undefined') { 25 | return; 26 | } 27 | 28 | if (typeof global === 'undefined') { 29 | return; 30 | } 31 | 32 | global.navigator = { 33 | userAgent: browserFakeUserAgent, 34 | getUserMedia: function() {} 35 | }; 36 | 37 | if (!global.console) { 38 | global.console = {}; 39 | } 40 | 41 | if (typeof global.console.log === 'undefined' || typeof global.console.error === 'undefined') { 42 | global.console.error = global.console.log = global.console.log || function() { 43 | console.log(arguments); 44 | }; 45 | } 46 | 47 | if (typeof document === 'undefined') { 48 | /*global document:true */ 49 | that.document = { 50 | documentElement: { 51 | appendChild: function() { 52 | return ''; 53 | } 54 | } 55 | }; 56 | 57 | document.createElement = document.captureStream = document.mozCaptureStream = function() { 58 | var obj = { 59 | getContext: function() { 60 | return obj; 61 | }, 62 | play: function() {}, 63 | pause: function() {}, 64 | drawImage: function() {}, 65 | toDataURL: function() { 66 | return ''; 67 | } 68 | }; 69 | return obj; 70 | }; 71 | 72 | that.HTMLVideoElement = function() {}; 73 | } 74 | 75 | if (typeof location === 'undefined') { 76 | /*global location:true */ 77 | that.location = { 78 | protocol: 'file:', 79 | href: '', 80 | hash: '' 81 | }; 82 | } 83 | 84 | if (typeof screen === 'undefined') { 85 | /*global screen:true */ 86 | that.screen = { 87 | width: 0, 88 | height: 0 89 | }; 90 | } 91 | 92 | if (typeof URL === 'undefined') { 93 | /*global screen:true */ 94 | that.URL = { 95 | createObjectURL: function() { 96 | return ''; 97 | }, 98 | revokeObjectURL: function() { 99 | return ''; 100 | } 101 | }; 102 | } 103 | 104 | if (typeof MediaStreamTrack === 'undefined') { 105 | /*global screen:true */ 106 | that.MediaStreamTrack = function() {}; 107 | } 108 | 109 | if (typeof RTCPeerConnection === 'undefined') { 110 | /*global screen:true */ 111 | that.RTCPeerConnection = function() {}; 112 | } 113 | 114 | /*global window:true */ 115 | that.window = global; 116 | })(typeof global !== 'undefined' ? global : null); 117 | 118 | var RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection; 119 | 120 | if (typeof MediaStreamTrack === 'undefined') { 121 | MediaStreamTrack = {}; // todo? 122 | } 123 | 124 | var systemNetworkType = ((navigator.connection || {}).type || 'unknown').toString().toLowerCase(); 125 | 126 | var getStatsResult = { 127 | encryption: 'sha-256', 128 | audio: { 129 | send: { 130 | tracks: [], 131 | codecs: [], 132 | availableBandwidth: 0, 133 | streams: 0, 134 | framerateMean: 0, 135 | bitrateMean: 0 136 | }, 137 | recv: { 138 | tracks: [], 139 | codecs: [], 140 | availableBandwidth: 0, 141 | streams: 0, 142 | framerateMean: 0, 143 | bitrateMean: 0 144 | }, 145 | bytesSent: 0, 146 | bytesReceived: 0, 147 | latency: 0, 148 | packetsLost: 0 149 | }, 150 | video: { 151 | send: { 152 | tracks: [], 153 | codecs: [], 154 | availableBandwidth: 0, 155 | streams: 0, 156 | framerateMean: 0, 157 | bitrateMean: 0 158 | }, 159 | recv: { 160 | tracks: [], 161 | codecs: [], 162 | availableBandwidth: 0, 163 | streams: 0, 164 | framerateMean: 0, 165 | bitrateMean: 0 166 | }, 167 | bytesSent: 0, 168 | bytesReceived: 0, 169 | latency: 0, 170 | packetsLost: 0 171 | }, 172 | bandwidth: { 173 | systemBandwidth: 0, 174 | sentPerSecond: 0, 175 | encodedPerSecond: 0, 176 | helper: { 177 | audioBytesSent: 0, 178 | videoBytestSent: 0 179 | }, 180 | speed: 0 181 | }, 182 | results: {}, 183 | connectionType: { 184 | systemNetworkType: systemNetworkType, 185 | systemIpAddress: '192.168.1.2', 186 | local: { 187 | candidateType: [], 188 | transport: [], 189 | ipAddress: [], 190 | networkType: [] 191 | }, 192 | remote: { 193 | candidateType: [], 194 | transport: [], 195 | ipAddress: [], 196 | networkType: [] 197 | } 198 | }, 199 | resolutions: { 200 | send: { 201 | width: 0, 202 | height: 0 203 | }, 204 | recv: { 205 | width: 0, 206 | height: 0 207 | } 208 | }, 209 | internal: { 210 | audio: { 211 | send: {}, 212 | recv: {} 213 | }, 214 | video: { 215 | send: {}, 216 | recv: {} 217 | }, 218 | candidates: {} 219 | }, 220 | nomore: function() { 221 | nomore = true; 222 | } 223 | }; 224 | 225 | var getStatsParser = { 226 | checkIfOfferer: function(result) { 227 | if (result.type === 'googLibjingleSession') { 228 | getStatsResult.isOfferer = result.googInitiator; 229 | } 230 | } 231 | }; 232 | 233 | var isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); 234 | 235 | var peer = this; 236 | 237 | if (!(arguments[0] instanceof RTCPeerConnection)) { 238 | throw '1st argument is not instance of RTCPeerConnection.'; 239 | } 240 | 241 | peer = arguments[0]; 242 | 243 | if (arguments[1] instanceof MediaStreamTrack) { 244 | mediaStreamTrack = arguments[1]; // redundant on non-safari 245 | callback = arguments[2]; 246 | interval = arguments[3]; 247 | } 248 | 249 | var nomore = false; 250 | 251 | function getStatsLooper() { 252 | getStatsWrapper(function(results) { 253 | if (!results || !results.forEach) return; 254 | 255 | results.forEach(function(result) { 256 | // console.error('result', result); 257 | Object.keys(getStatsParser).forEach(function(key) { 258 | if (typeof getStatsParser[key] === 'function') { 259 | try { 260 | getStatsParser[key](result); 261 | } catch (e) { 262 | console.error(e.message, e.stack, e); 263 | } 264 | } 265 | }); 266 | }); 267 | 268 | try { 269 | if (peer.iceConnectionState.search(/failed|closed|disconnected/gi) !== -1) { 270 | nomore = true; 271 | } 272 | } catch (e) { 273 | nomore = true; 274 | } 275 | 276 | if (nomore === true) { 277 | if (getStatsResult.datachannel) { 278 | getStatsResult.datachannel.state = 'close'; 279 | } 280 | getStatsResult.ended = true; 281 | } 282 | 283 | // allow users to access native results 284 | getStatsResult.results = results; 285 | 286 | if (getStatsResult.audio && getStatsResult.video) { 287 | getStatsResult.bandwidth.speed = (getStatsResult.audio.bytesSent - getStatsResult.bandwidth.helper.audioBytesSent) + (getStatsResult.video.bytesSent - getStatsResult.bandwidth.helper.videoBytesSent); 288 | getStatsResult.bandwidth.helper.audioBytesSent = getStatsResult.audio.bytesSent; 289 | getStatsResult.bandwidth.helper.videoBytesSent = getStatsResult.video.bytesSent; 290 | } 291 | 292 | callback(getStatsResult); 293 | 294 | // second argument checks to see, if target-user is still connected. 295 | if (!nomore) { 296 | typeof interval != undefined && interval && setTimeout(getStatsLooper, interval || 1000); 297 | } 298 | }); 299 | } 300 | 301 | // a wrapper around getStats which hides the differences (where possible) 302 | // following code-snippet is taken from somewhere on the github 303 | function getStatsWrapper(cb) { 304 | // if !peer or peer.signalingState == 'closed' then return; 305 | 306 | if (typeof window.InstallTrigger !== 'undefined' || isSafari) { // maybe "isEdge?" 307 | peer.getStats(window.mediaStreamTrack || null).then(function(res) { 308 | var items = []; 309 | res.forEach(function(r) { 310 | items.push(r); 311 | }); 312 | cb(items); 313 | }).catch(cb); 314 | } else { 315 | peer.getStats(function(res) { 316 | var items = []; 317 | res.result().forEach(function(res) { 318 | var item = {}; 319 | res.names().forEach(function(name) { 320 | item[name] = res.stat(name); 321 | }); 322 | item.id = res.id; 323 | item.type = res.type; 324 | item.timestamp = res.timestamp; 325 | items.push(item); 326 | }); 327 | cb(items); 328 | }); 329 | } 330 | }; 331 | 332 | getStatsParser.datachannel = function(result) { 333 | if (result.type !== 'datachannel') return; 334 | 335 | getStatsResult.datachannel = { 336 | state: result.state // open or connecting 337 | } 338 | }; 339 | 340 | getStatsParser.googCertificate = function(result) { 341 | if (result.type == 'googCertificate') { 342 | getStatsResult.encryption = result.googFingerprintAlgorithm; 343 | } 344 | 345 | // Safari-11 or higher 346 | if (result.type == 'certificate') { 347 | // todo: is it possible to have different encryption methods for senders and receivers? 348 | // if yes, then we need to set: 349 | // getStatsResult.encryption.local = value; 350 | // getStatsResult.encryption.remote = value; 351 | getStatsResult.encryption = result.fingerprintAlgorithm; 352 | } 353 | }; 354 | 355 | getStatsParser.checkAudioTracks = function(result) { 356 | if (result.mediaType !== 'audio') return; 357 | 358 | var sendrecvType = result.id.split('_').pop(); 359 | if (result.isRemote === true) { 360 | sendrecvType = 'recv'; 361 | } 362 | if (result.isRemote === false) { 363 | sendrecvType = 'send'; 364 | } 365 | 366 | if (!sendrecvType) return; 367 | 368 | if (getStatsResult.audio[sendrecvType].codecs.indexOf(result.googCodecName || 'opus') === -1) { 369 | getStatsResult.audio[sendrecvType].codecs.push(result.googCodecName || 'opus'); 370 | } 371 | 372 | if (!!result.bytesSent) { 373 | var kilobytes = 0; 374 | if (!getStatsResult.internal.audio[sendrecvType].prevBytesSent) { 375 | getStatsResult.internal.audio[sendrecvType].prevBytesSent = result.bytesSent; 376 | } 377 | 378 | var bytes = result.bytesSent - getStatsResult.internal.audio[sendrecvType].prevBytesSent; 379 | getStatsResult.internal.audio[sendrecvType].prevBytesSent = result.bytesSent; 380 | 381 | kilobytes = bytes / 1024; 382 | getStatsResult.audio[sendrecvType].availableBandwidth = kilobytes.toFixed(1); 383 | getStatsResult.audio.bytesSent = kilobytes.toFixed(1); 384 | } 385 | 386 | if (!!result.bytesReceived) { 387 | var kilobytes = 0; 388 | if (!getStatsResult.internal.audio[sendrecvType].prevBytesReceived) { 389 | getStatsResult.internal.audio[sendrecvType].prevBytesReceived = result.bytesReceived; 390 | } 391 | 392 | var bytes = result.bytesReceived - getStatsResult.internal.audio[sendrecvType].prevBytesReceived; 393 | getStatsResult.internal.audio[sendrecvType].prevBytesReceived = result.bytesReceived; 394 | 395 | kilobytes = bytes / 1024; 396 | 397 | // getStatsResult.audio[sendrecvType].availableBandwidth = kilobytes.toFixed(1); 398 | getStatsResult.audio.bytesReceived = kilobytes.toFixed(1); 399 | } 400 | 401 | if (result.googTrackId && getStatsResult.audio[sendrecvType].tracks.indexOf(result.googTrackId) === -1) { 402 | getStatsResult.audio[sendrecvType].tracks.push(result.googTrackId); 403 | } 404 | 405 | // calculate latency 406 | if (!!result.googCurrentDelayMs) { 407 | var kilobytes = 0; 408 | if (!getStatsResult.internal.audio.prevGoogCurrentDelayMs) { 409 | getStatsResult.internal.audio.prevGoogCurrentDelayMs = result.googCurrentDelayMs; 410 | } 411 | 412 | var bytes = result.googCurrentDelayMs - getStatsResult.internal.audio.prevGoogCurrentDelayMs; 413 | getStatsResult.internal.audio.prevGoogCurrentDelayMs = result.googCurrentDelayMs; 414 | 415 | getStatsResult.audio.latency = bytes.toFixed(1); 416 | 417 | if (getStatsResult.audio.latency < 0) { 418 | getStatsResult.audio.latency = 0; 419 | } 420 | } 421 | 422 | // calculate packetsLost 423 | if (!!result.packetsLost) { 424 | var kilobytes = 0; 425 | if (!getStatsResult.internal.audio.prevPacketsLost) { 426 | getStatsResult.internal.audio.prevPacketsLost = result.packetsLost; 427 | } 428 | 429 | var bytes = result.packetsLost - getStatsResult.internal.audio.prevPacketsLost; 430 | getStatsResult.internal.audio.prevPacketsLost = result.packetsLost; 431 | 432 | getStatsResult.audio.packetsLost = bytes.toFixed(1); 433 | 434 | if (getStatsResult.audio.packetsLost < 0) { 435 | getStatsResult.audio.packetsLost = 0; 436 | } 437 | } 438 | }; 439 | 440 | getStatsParser.checkVideoTracks = function(result) { 441 | if (result.mediaType !== 'video') return; 442 | 443 | var sendrecvType = result.id.split('_').pop(); 444 | if (result.isRemote === true) { 445 | sendrecvType = 'recv'; 446 | } 447 | if (result.isRemote === false) { 448 | sendrecvType = 'send'; 449 | } 450 | 451 | if (!sendrecvType) return; 452 | 453 | if (getStatsResult.video[sendrecvType].codecs.indexOf(result.googCodecName || 'VP8') === -1) { 454 | getStatsResult.video[sendrecvType].codecs.push(result.googCodecName || 'VP8'); 455 | } 456 | 457 | if (!!result.bytesSent) { 458 | var kilobytes = 0; 459 | if (!getStatsResult.internal.video[sendrecvType].prevBytesSent) { 460 | getStatsResult.internal.video[sendrecvType].prevBytesSent = result.bytesSent; 461 | } 462 | 463 | var bytes = result.bytesSent - getStatsResult.internal.video[sendrecvType].prevBytesSent; 464 | getStatsResult.internal.video[sendrecvType].prevBytesSent = result.bytesSent; 465 | 466 | kilobytes = bytes / 1024; 467 | 468 | getStatsResult.video[sendrecvType].availableBandwidth = kilobytes.toFixed(1); 469 | getStatsResult.video.bytesSent = kilobytes.toFixed(1); 470 | } 471 | 472 | if (!!result.bytesReceived) { 473 | var kilobytes = 0; 474 | if (!getStatsResult.internal.video[sendrecvType].prevBytesReceived) { 475 | getStatsResult.internal.video[sendrecvType].prevBytesReceived = result.bytesReceived; 476 | } 477 | 478 | var bytes = result.bytesReceived - getStatsResult.internal.video[sendrecvType].prevBytesReceived; 479 | getStatsResult.internal.video[sendrecvType].prevBytesReceived = result.bytesReceived; 480 | 481 | kilobytes = bytes / 1024; 482 | // getStatsResult.video[sendrecvType].availableBandwidth = kilobytes.toFixed(1); 483 | getStatsResult.video.bytesReceived = kilobytes.toFixed(1); 484 | } 485 | 486 | if (result.googFrameHeightReceived && result.googFrameWidthReceived) { 487 | getStatsResult.resolutions[sendrecvType].width = result.googFrameWidthReceived; 488 | getStatsResult.resolutions[sendrecvType].height = result.googFrameHeightReceived; 489 | } 490 | 491 | if (result.googFrameHeightSent && result.googFrameWidthSent) { 492 | getStatsResult.resolutions[sendrecvType].width = result.googFrameWidthSent; 493 | getStatsResult.resolutions[sendrecvType].height = result.googFrameHeightSent; 494 | } 495 | 496 | if (result.googTrackId && getStatsResult.video[sendrecvType].tracks.indexOf(result.googTrackId) === -1) { 497 | getStatsResult.video[sendrecvType].tracks.push(result.googTrackId); 498 | } 499 | 500 | if (result.framerateMean) { 501 | getStatsResult.bandwidth.framerateMean = result.framerateMean; 502 | var kilobytes = 0; 503 | if (!getStatsResult.internal.video[sendrecvType].prevFramerateMean) { 504 | getStatsResult.internal.video[sendrecvType].prevFramerateMean = result.bitrateMean; 505 | } 506 | 507 | var bytes = result.bytesSent - getStatsResult.internal.video[sendrecvType].prevFramerateMean; 508 | getStatsResult.internal.video[sendrecvType].prevFramerateMean = result.framerateMean; 509 | 510 | kilobytes = bytes / 1024; 511 | getStatsResult.video[sendrecvType].framerateMean = bytes.toFixed(1); 512 | } 513 | 514 | if (result.bitrateMean) { 515 | getStatsResult.bandwidth.bitrateMean = result.bitrateMean; 516 | var kilobytes = 0; 517 | if (!getStatsResult.internal.video[sendrecvType].prevBitrateMean) { 518 | getStatsResult.internal.video[sendrecvType].prevBitrateMean = result.bitrateMean; 519 | } 520 | 521 | var bytes = result.bytesSent - getStatsResult.internal.video[sendrecvType].prevBitrateMean; 522 | getStatsResult.internal.video[sendrecvType].prevBitrateMean = result.bitrateMean; 523 | 524 | kilobytes = bytes / 1024; 525 | getStatsResult.video[sendrecvType].bitrateMean = bytes.toFixed(1); 526 | } 527 | 528 | // calculate latency 529 | if (!!result.googCurrentDelayMs) { 530 | var kilobytes = 0; 531 | if (!getStatsResult.internal.video.prevGoogCurrentDelayMs) { 532 | getStatsResult.internal.video.prevGoogCurrentDelayMs = result.googCurrentDelayMs; 533 | } 534 | 535 | var bytes = result.googCurrentDelayMs - getStatsResult.internal.video.prevGoogCurrentDelayMs; 536 | getStatsResult.internal.video.prevGoogCurrentDelayMs = result.googCurrentDelayMs; 537 | 538 | getStatsResult.video.latency = bytes.toFixed(1); 539 | 540 | if (getStatsResult.video.latency < 0) { 541 | getStatsResult.video.latency = 0; 542 | } 543 | } 544 | 545 | // calculate packetsLost 546 | if (!!result.packetsLost) { 547 | var kilobytes = 0; 548 | if (!getStatsResult.internal.video.prevPacketsLost) { 549 | getStatsResult.internal.video.prevPacketsLost = result.packetsLost; 550 | } 551 | 552 | var bytes = result.packetsLost - getStatsResult.internal.video.prevPacketsLost; 553 | getStatsResult.internal.video.prevPacketsLost = result.packetsLost; 554 | 555 | getStatsResult.video.packetsLost = bytes.toFixed(1); 556 | 557 | if (getStatsResult.video.packetsLost < 0) { 558 | getStatsResult.video.packetsLost = 0; 559 | } 560 | } 561 | }; 562 | 563 | getStatsParser.bweforvideo = function(result) { 564 | if (result.type !== 'VideoBwe') return; 565 | 566 | getStatsResult.bandwidth.availableSendBandwidth = result.googAvailableSendBandwidth; 567 | 568 | getStatsResult.bandwidth.googActualEncBitrate = result.googActualEncBitrate; 569 | getStatsResult.bandwidth.googAvailableSendBandwidth = result.googAvailableSendBandwidth; 570 | getStatsResult.bandwidth.googAvailableReceiveBandwidth = result.googAvailableReceiveBandwidth; 571 | getStatsResult.bandwidth.googRetransmitBitrate = result.googRetransmitBitrate; 572 | getStatsResult.bandwidth.googTargetEncBitrate = result.googTargetEncBitrate; 573 | getStatsResult.bandwidth.googBucketDelay = result.googBucketDelay; 574 | getStatsResult.bandwidth.googTransmitBitrate = result.googTransmitBitrate; 575 | }; 576 | 577 | getStatsParser.candidatePair = function(result) { 578 | if (result.type !== 'googCandidatePair' && result.type !== 'candidate-pair' && result.type !== 'local-candidate' && result.type !== 'remote-candidate') return; 579 | 580 | // result.googActiveConnection means either STUN or TURN is used. 581 | 582 | if (result.googActiveConnection == 'true') { 583 | // id === 'Conn-audio-1-0' 584 | // localCandidateId, remoteCandidateId 585 | 586 | // bytesSent, bytesReceived 587 | 588 | Object.keys(getStatsResult.internal.candidates).forEach(function(cid) { 589 | var candidate = getStatsResult.internal.candidates[cid]; 590 | if (candidate.ipAddress.indexOf(result.googLocalAddress) !== -1) { 591 | getStatsResult.connectionType.local.candidateType = candidate.candidateType; 592 | getStatsResult.connectionType.local.ipAddress = candidate.ipAddress; 593 | getStatsResult.connectionType.local.networkType = candidate.networkType; 594 | getStatsResult.connectionType.local.transport = candidate.transport; 595 | } 596 | if (candidate.ipAddress.indexOf(result.googRemoteAddress) !== -1) { 597 | getStatsResult.connectionType.remote.candidateType = candidate.candidateType; 598 | getStatsResult.connectionType.remote.ipAddress = candidate.ipAddress; 599 | getStatsResult.connectionType.remote.networkType = candidate.networkType; 600 | getStatsResult.connectionType.remote.transport = candidate.transport; 601 | } 602 | }); 603 | 604 | getStatsResult.connectionType.transport = result.googTransportType; 605 | 606 | var localCandidate = getStatsResult.internal.candidates[result.localCandidateId]; 607 | if (localCandidate) { 608 | if (localCandidate.ipAddress) { 609 | getStatsResult.connectionType.systemIpAddress = localCandidate.ipAddress; 610 | } 611 | } 612 | 613 | var remoteCandidate = getStatsResult.internal.candidates[result.remoteCandidateId]; 614 | if (remoteCandidate) { 615 | if (remoteCandidate.ipAddress) { 616 | getStatsResult.connectionType.systemIpAddress = remoteCandidate.ipAddress; 617 | } 618 | } 619 | } 620 | 621 | if (result.type === 'candidate-pair') { 622 | if (result.selected === true && result.nominated === true && result.state === 'succeeded') { 623 | // remoteCandidateId, localCandidateId, componentId 624 | var localCandidate = getStatsResult.internal.candidates[result.remoteCandidateId]; 625 | var remoteCandidate = getStatsResult.internal.candidates[result.remoteCandidateId]; 626 | 627 | // Firefox used above two pairs for connection 628 | } 629 | } 630 | 631 | if (result.type === 'local-candidate') { 632 | getStatsResult.connectionType.local.candidateType = result.candidateType; 633 | getStatsResult.connectionType.local.ipAddress = result.ipAddress; 634 | getStatsResult.connectionType.local.networkType = result.networkType; 635 | getStatsResult.connectionType.local.transport = result.mozLocalTransport || result.transport; 636 | } 637 | 638 | if (result.type === 'remote-candidate') { 639 | getStatsResult.connectionType.remote.candidateType = result.candidateType; 640 | getStatsResult.connectionType.remote.ipAddress = result.ipAddress; 641 | getStatsResult.connectionType.remote.networkType = result.networkType; 642 | getStatsResult.connectionType.remote.transport = result.mozRemoteTransport || result.transport; 643 | } 644 | 645 | if (isSafari) { 646 | // result.remoteCandidateId 647 | // todo: below line will always force "send" on Safari; find a solution 648 | var sendrecvType = result.localCandidateId ? 'send' : 'recv'; 649 | 650 | if (!sendrecvType) return; 651 | 652 | if (!!result.bytesSent) { 653 | var kilobytes = 0; 654 | if (!getStatsResult.internal.video[sendrecvType].prevBytesSent) { 655 | getStatsResult.internal.video[sendrecvType].prevBytesSent = result.bytesSent; 656 | } 657 | 658 | var bytes = result.bytesSent - getStatsResult.internal.video[sendrecvType].prevBytesSent; 659 | getStatsResult.internal.video[sendrecvType].prevBytesSent = result.bytesSent; 660 | 661 | kilobytes = bytes / 1024; 662 | 663 | getStatsResult.video[sendrecvType].availableBandwidth = kilobytes.toFixed(1); 664 | getStatsResult.video.bytesSent = kilobytes.toFixed(1); 665 | } 666 | 667 | if (!!result.bytesReceived) { 668 | var kilobytes = 0; 669 | if (!getStatsResult.internal.video[sendrecvType].prevBytesReceived) { 670 | getStatsResult.internal.video[sendrecvType].prevBytesReceived = result.bytesReceived; 671 | } 672 | 673 | var bytes = result.bytesReceived - getStatsResult.internal.video[sendrecvType].prevBytesReceived; 674 | getStatsResult.internal.video[sendrecvType].prevBytesReceived = result.bytesReceived; 675 | 676 | kilobytes = bytes / 1024; 677 | // getStatsResult.video[sendrecvType].availableBandwidth = kilobytes.toFixed(1); 678 | getStatsResult.video.bytesReceived = kilobytes.toFixed(1); 679 | } 680 | 681 | if (!!result.availableOutgoingBitrate) { 682 | var kilobytes = 0; 683 | if (!getStatsResult.internal.video[sendrecvType].prevAvailableOutgoingBitrate) { 684 | getStatsResult.internal.video[sendrecvType].prevAvailableOutgoingBitrate = result.availableOutgoingBitrate; 685 | } 686 | 687 | var bytes = result.availableOutgoingBitrate - getStatsResult.internal.video[sendrecvType].prevAvailableOutgoingBitrate; 688 | getStatsResult.internal.video[sendrecvType].prevAvailableOutgoingBitrate = result.availableOutgoingBitrate; 689 | 690 | kilobytes = bytes / 1024; 691 | // getStatsResult.video[sendrecvType].availableBandwidth = kilobytes.toFixed(1); 692 | getStatsResult.video.availableOutgoingBitrate = kilobytes.toFixed(1); 693 | } 694 | 695 | if (!!result.availableIncomingBitrate) { 696 | var kilobytes = 0; 697 | if (!getStatsResult.internal.video[sendrecvType].prevAvailableIncomingBitrate) { 698 | getStatsResult.internal.video[sendrecvType].prevAvailableIncomingBitrate = result.availableIncomingBitrate; 699 | } 700 | 701 | var bytes = result.availableIncomingBitrate - getStatsResult.internal.video[sendrecvType].prevAvailableIncomingBitrate; 702 | getStatsResult.internal.video[sendrecvType].prevAvailableIncomingBitrate = result.availableIncomingBitrate; 703 | 704 | kilobytes = bytes / 1024; 705 | // getStatsResult.video[sendrecvType].availableBandwidth = kilobytes.toFixed(1); 706 | getStatsResult.video.availableIncomingBitrate = kilobytes.toFixed(1); 707 | } 708 | } 709 | }; 710 | 711 | var LOCAL_candidateType = {}; 712 | var LOCAL_transport = {}; 713 | var LOCAL_ipAddress = {}; 714 | var LOCAL_networkType = {}; 715 | 716 | getStatsParser.localcandidate = function(result) { 717 | if (result.type !== 'localcandidate' && result.type !== 'local-candidate') return; 718 | if (!result.id) return; 719 | 720 | if (!LOCAL_candidateType[result.id]) { 721 | LOCAL_candidateType[result.id] = []; 722 | } 723 | 724 | if (!LOCAL_transport[result.id]) { 725 | LOCAL_transport[result.id] = []; 726 | } 727 | 728 | if (!LOCAL_ipAddress[result.id]) { 729 | LOCAL_ipAddress[result.id] = []; 730 | } 731 | 732 | if (!LOCAL_networkType[result.id]) { 733 | LOCAL_networkType[result.id] = []; 734 | } 735 | 736 | if (result.candidateType && LOCAL_candidateType[result.id].indexOf(result.candidateType) === -1) { 737 | LOCAL_candidateType[result.id].push(result.candidateType); 738 | } 739 | 740 | if (result.transport && LOCAL_transport[result.id].indexOf(result.transport) === -1) { 741 | LOCAL_transport[result.id].push(result.transport); 742 | } 743 | 744 | if (result.ipAddress && LOCAL_ipAddress[result.id].indexOf(result.ipAddress + ':' + result.portNumber) === -1) { 745 | LOCAL_ipAddress[result.id].push(result.ipAddress + ':' + result.portNumber); 746 | } 747 | 748 | if (result.networkType && LOCAL_networkType[result.id].indexOf(result.networkType) === -1) { 749 | LOCAL_networkType[result.id].push(result.networkType); 750 | } 751 | 752 | getStatsResult.internal.candidates[result.id] = { 753 | candidateType: LOCAL_candidateType[result.id], 754 | ipAddress: LOCAL_ipAddress[result.id], 755 | portNumber: result.portNumber, 756 | networkType: LOCAL_networkType[result.id], 757 | priority: result.priority, 758 | transport: LOCAL_transport[result.id], 759 | timestamp: result.timestamp, 760 | id: result.id, 761 | type: result.type 762 | }; 763 | 764 | getStatsResult.connectionType.local.candidateType = LOCAL_candidateType[result.id]; 765 | getStatsResult.connectionType.local.ipAddress = LOCAL_ipAddress[result.id]; 766 | getStatsResult.connectionType.local.networkType = LOCAL_networkType[result.id]; 767 | getStatsResult.connectionType.local.transport = LOCAL_transport[result.id]; 768 | }; 769 | 770 | var REMOTE_candidateType = {}; 771 | var REMOTE_transport = {}; 772 | var REMOTE_ipAddress = {}; 773 | var REMOTE_networkType = {}; 774 | 775 | getStatsParser.remotecandidate = function(result) { 776 | if (result.type !== 'remotecandidate' && result.type !== 'remote-candidate') return; 777 | if (!result.id) return; 778 | 779 | if (!REMOTE_candidateType[result.id]) { 780 | REMOTE_candidateType[result.id] = []; 781 | } 782 | 783 | if (!REMOTE_transport[result.id]) { 784 | REMOTE_transport[result.id] = []; 785 | } 786 | 787 | if (!REMOTE_ipAddress[result.id]) { 788 | REMOTE_ipAddress[result.id] = []; 789 | } 790 | 791 | if (!REMOTE_networkType[result.id]) { 792 | REMOTE_networkType[result.id] = []; 793 | } 794 | 795 | if (result.candidateType && REMOTE_candidateType[result.id].indexOf(result.candidateType) === -1) { 796 | REMOTE_candidateType[result.id].push(result.candidateType); 797 | } 798 | 799 | if (result.transport && REMOTE_transport[result.id].indexOf(result.transport) === -1) { 800 | REMOTE_transport[result.id].push(result.transport); 801 | } 802 | 803 | if (result.ipAddress && REMOTE_ipAddress[result.id].indexOf(result.ipAddress + ':' + result.portNumber) === -1) { 804 | REMOTE_ipAddress[result.id].push(result.ipAddress + ':' + result.portNumber); 805 | } 806 | 807 | if (result.networkType && REMOTE_networkType[result.id].indexOf(result.networkType) === -1) { 808 | REMOTE_networkType[result.id].push(result.networkType); 809 | } 810 | 811 | getStatsResult.internal.candidates[result.id] = { 812 | candidateType: REMOTE_candidateType[result.id], 813 | ipAddress: REMOTE_ipAddress[result.id], 814 | portNumber: result.portNumber, 815 | networkType: REMOTE_networkType[result.id], 816 | priority: result.priority, 817 | transport: REMOTE_transport[result.id], 818 | timestamp: result.timestamp, 819 | id: result.id, 820 | type: result.type 821 | }; 822 | 823 | getStatsResult.connectionType.remote.candidateType = REMOTE_candidateType[result.id]; 824 | getStatsResult.connectionType.remote.ipAddress = REMOTE_ipAddress[result.id]; 825 | getStatsResult.connectionType.remote.networkType = REMOTE_networkType[result.id]; 826 | getStatsResult.connectionType.remote.transport = REMOTE_transport[result.id]; 827 | }; 828 | 829 | getStatsParser.dataSentReceived = function(result) { 830 | if (!result.googCodecName || (result.mediaType !== 'video' && result.mediaType !== 'audio')) return; 831 | 832 | if (!!result.bytesSent) { 833 | getStatsResult[result.mediaType].bytesSent = parseInt(result.bytesSent); 834 | } 835 | 836 | if (!!result.bytesReceived) { 837 | getStatsResult[result.mediaType].bytesReceived = parseInt(result.bytesReceived); 838 | } 839 | }; 840 | 841 | getStatsParser.inboundrtp = function(result) { 842 | if (!isSafari) return; 843 | if (result.type !== 'inbound-rtp') return; 844 | 845 | var mediaType = result.mediaType || 'audio'; 846 | var sendrecvType = result.isRemote ? 'recv' : 'send'; 847 | 848 | if (!sendrecvType) return; 849 | 850 | if (!!result.bytesSent) { 851 | var kilobytes = 0; 852 | if (!getStatsResult.internal[mediaType][sendrecvType].prevBytesSent) { 853 | getStatsResult.internal[mediaType][sendrecvType].prevBytesSent = result.bytesSent; 854 | } 855 | 856 | var bytes = result.bytesSent - getStatsResult.internal[mediaType][sendrecvType].prevBytesSent; 857 | getStatsResult.internal[mediaType][sendrecvType].prevBytesSent = result.bytesSent; 858 | 859 | kilobytes = bytes / 1024; 860 | 861 | getStatsResult[mediaType][sendrecvType].availableBandwidth = kilobytes.toFixed(1); 862 | getStatsResult[mediaType].bytesSent = kilobytes.toFixed(1); 863 | } 864 | 865 | if (!!result.bytesReceived) { 866 | var kilobytes = 0; 867 | if (!getStatsResult.internal[mediaType][sendrecvType].prevBytesReceived) { 868 | getStatsResult.internal[mediaType][sendrecvType].prevBytesReceived = result.bytesReceived; 869 | } 870 | 871 | var bytes = result.bytesReceived - getStatsResult.internal[mediaType][sendrecvType].prevBytesReceived; 872 | getStatsResult.internal[mediaType][sendrecvType].prevBytesReceived = result.bytesReceived; 873 | 874 | kilobytes = bytes / 1024; 875 | // getStatsResult[mediaType][sendrecvType].availableBandwidth = kilobytes.toFixed(1); 876 | getStatsResult[mediaType].bytesReceived = kilobytes.toFixed(1); 877 | } 878 | }; 879 | 880 | getStatsParser.outboundrtp = function(result) { 881 | if (!isSafari) return; 882 | if (result.type !== 'outbound-rtp') return; 883 | 884 | var mediaType = result.mediaType || 'audio'; 885 | var sendrecvType = result.isRemote ? 'recv' : 'send'; 886 | 887 | if (!sendrecvType) return; 888 | 889 | if (!!result.bytesSent) { 890 | var kilobytes = 0; 891 | if (!getStatsResult.internal[mediaType][sendrecvType].prevBytesSent) { 892 | getStatsResult.internal[mediaType][sendrecvType].prevBytesSent = result.bytesSent; 893 | } 894 | 895 | var bytes = result.bytesSent - getStatsResult.internal[mediaType][sendrecvType].prevBytesSent; 896 | getStatsResult.internal[mediaType][sendrecvType].prevBytesSent = result.bytesSent; 897 | 898 | kilobytes = bytes / 1024; 899 | 900 | getStatsResult[mediaType][sendrecvType].availableBandwidth = kilobytes.toFixed(1); 901 | getStatsResult[mediaType].bytesSent = kilobytes.toFixed(1); 902 | } 903 | 904 | if (!!result.bytesReceived) { 905 | var kilobytes = 0; 906 | if (!getStatsResult.internal[mediaType][sendrecvType].prevBytesReceived) { 907 | getStatsResult.internal[mediaType][sendrecvType].prevBytesReceived = result.bytesReceived; 908 | } 909 | 910 | var bytes = result.bytesReceived - getStatsResult.internal[mediaType][sendrecvType].prevBytesReceived; 911 | getStatsResult.internal[mediaType][sendrecvType].prevBytesReceived = result.bytesReceived; 912 | 913 | kilobytes = bytes / 1024; 914 | // getStatsResult[mediaType][sendrecvType].availableBandwidth = kilobytes.toFixed(1); 915 | getStatsResult[mediaType].bytesReceived = kilobytes.toFixed(1); 916 | } 917 | }; 918 | 919 | getStatsParser.track = function(result) { 920 | if (!isSafari) return; 921 | if (result.type !== 'track') return; 922 | 923 | var sendrecvType = result.remoteSource === true ? 'send' : 'recv'; 924 | 925 | if (result.frameWidth && result.frameHeight) { 926 | getStatsResult.resolutions[sendrecvType].width = result.frameWidth; 927 | getStatsResult.resolutions[sendrecvType].height = result.frameHeight; 928 | } 929 | 930 | // framesSent, framesReceived 931 | }; 932 | 933 | var SSRC = { 934 | audio: { 935 | send: [], 936 | recv: [] 937 | }, 938 | video: { 939 | send: [], 940 | recv: [] 941 | } 942 | }; 943 | 944 | getStatsParser.ssrc = function(result) { 945 | if (!result.googCodecName || (result.mediaType !== 'video' && result.mediaType !== 'audio')) return; 946 | if (result.type !== 'ssrc') return; 947 | var sendrecvType = result.id.split('_').pop(); 948 | 949 | if (SSRC[result.mediaType][sendrecvType].indexOf(result.ssrc) === -1) { 950 | SSRC[result.mediaType][sendrecvType].push(result.ssrc) 951 | } 952 | 953 | getStatsResult[result.mediaType][sendrecvType].streams = SSRC[result.mediaType][sendrecvType].length; 954 | }; 955 | 956 | getStatsLooper(); 957 | 958 | }; 959 | 960 | if (typeof module !== 'undefined' /* && !!module.exports*/ ) { 961 | module.exports = getStats; 962 | } 963 | 964 | if (typeof define === 'function' && define.amd) { 965 | define('getStats', [], function() { 966 | return getStats; 967 | }); 968 | } 969 | --------------------------------------------------------------------------------