diff --git a/package-lock.json b/package-lock.json
index 44267a0..ef92525 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "visualizer-server",
- "version": "0.3.5",
+ "version": "0.3.8",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "visualizer-server",
- "version": "0.3.5",
+ "version": "0.3.8",
"license": "ISC",
"dependencies": {
"@reduxjs/toolkit": "^1.9.0",
diff --git a/package.json b/package.json
index 678e647..8200fdc 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "visualizer-server",
- "version": "0.3.5",
+ "version": "0.3.8",
"description": "Visualize RTC stats data",
"repository": "https://github.com/8x8Cloud/rtc-visualizer",
"author": "8x8",
diff --git a/src/client/app/files/formatter.js b/src/client/app/files/formatter.js
index 2eb8a67..5d4044f 100644
--- a/src/client/app/files/formatter.js
+++ b/src/client/app/files/formatter.js
@@ -9,7 +9,7 @@ export const parseData = input => {
lines.forEach(line => {
if (line.length) {
const data = JSON.parse(line)
- const time = new Date(data.time || data[data.length - 1])
+ const time = new Date(data.time || data[3])
delete data.time
switch (data[0]) {
diff --git a/src/client/app/raw/legacy.js b/src/client/app/raw/legacy.js
index 9d01d2a..3965af5 100644
--- a/src/client/app/raw/legacy.js
+++ b/src/client/app/raw/legacy.js
@@ -172,11 +172,18 @@ function createSpecCandidateTable (container, allGroupedStats) {
function createContainers (connid, url) {
let signalingState, iceConnectionState, connectionState, candidates
const container = document.createElement('details')
- container.open = true
+ container.open = false
container.style.margin = '10px'
-
+ let innerText = '';
let summary = document.createElement('summary')
- summary.innerText = 'Connection:' + connid + ' URL: ' + url
+ if (connid !== 'null') {
+ innerText = 'Connection:' + connid// + ' URL: ' + url
+ } else {
+ innerText = 'Meeting events';
+ }
+
+ summary.innerText = innerText;
+
container.appendChild(summary)
if (connid !== 'null') {
@@ -228,9 +235,34 @@ function createContainers (connid, url) {
return container
}
+function convertTotalToRateSeries(timeSeries) {
+ return timeSeries.reduce(
+ (accumulator, currentValue) => {
+
+ const {prevValue} = accumulator;
+ accumulator.prevValue = currentValue;
+
+ if (!prevValue.length) {
+ return accumulator;
+ }
+
+ const [timestamp = 0, totalBytesSent = 0] = currentValue;
+ const [prevTimestamp = 0, prevTotalBytesSent = 0] = prevValue;
+
+ const sampleRateSeconds = (timestamp - prevTimestamp) / 1000;
+ const bitRate = (totalBytesSent - prevTotalBytesSent) * 8;
+ const bitRatePerSecond = Math.round(bitRate / sampleRateSeconds);
+
+ accumulator.bitRate.push([timestamp, bitRatePerSecond]);
+ return accumulator;
+ },
+ { bitRate: [], prevValue: []},
+ ).bitRate;
+}
+
function processGUM (data) {
const container = document.createElement('details')
- container.open = true
+ container.open = false
container.style.margin = '10px'
const summary = document.createElement('summary')
@@ -440,23 +472,21 @@ function processConnections (connectionIds, data) {
container.appendChild(chartContainer)
const ignoredSeries = ['type', 'ssrc']
- const hiddenSeries = [
- 'bytesReceived', 'bytesSent',
- 'headerBytesReceived', 'headerBytesSent',
- 'packetsReceived', 'packetsSent',
- 'qpSum', 'estimatedPlayoutTimestamp',
- 'framesEncoded', 'framesDecoded',
- 'lastPacketReceivedTimestamp', 'lastPacketSentTimestamp',
- 'remoteTimestamp',
- 'audioInputLevel', 'audioOutputLevel',
- 'totalSamplesDuration',
- 'totalSamplesReceived', 'jitterBufferEmittedCount',
- // legacy
- 'googDecodingCTN', 'googDecodingCNG', 'googDecodingNormal',
- 'googDecodingPLCCNG', 'googDecodingCTSG', 'googDecodingMuted',
- 'googEchoCancellationEchoDelayStdDev',
- 'googCaptureStartNtpTimeMs'
+ const visibleSeries = [
+ 'bytesReceivedInBits/S', 'bytesSentInBits/S',
+ 'targetBitrate', 'packetsLost', 'jitter',
+ 'availableOutgoingBitrate', 'roundTripTime'
]
+ const rateSeriesWhitelist = ['bytesSent', 'bytesReceived'];
+
+ // Calculate bitrate per second for time series that contain cumulated values
+ // over time, time total bytes sent or total bytes received
+ Object.keys(series[reportname])
+ .filter(name => rateSeriesWhitelist.includes(name))
+ .map(name => {
+ const rateSeries = convertTotalToRateSeries(series[reportname][name]);
+ series[reportname][`${name}InBits/S`] = rateSeries;
+ })
const traces = Object.keys(series[reportname])
.filter(name => !ignoredSeries.includes(name))
@@ -467,11 +497,11 @@ function processConnections (connectionIds, data) {
return {
mode: 'lines+markers',
name: name,
- visible: hiddenSeries.includes(name) ? 'legendonly' : true,
+ visible: visibleSeries.includes(name) ? true: 'legendonly',
x: data.map(d => new Date(d[0])),
y: data.map(d => d[1])
}
- })
+ })
// expand the graph when opening
container.ontoggle = () => container.open && Plotly.react(chartContainer, traces)
@@ -503,7 +533,7 @@ export const clearStats = () => {
export const showStats = data => {
clearStats()
- document.getElementById('userAgent').innerHTML = `User Agent: ${data.userAgent}`
+ // TODO Add user agent support document.getElementById('userAgent').innerHTML = `User Agent: ${data.userAgent}`
processGUM(data.getUserMedia)
window.setTimeout(processConnections, 0, Object.keys(data.peerConnections), data)
}
diff --git a/src/server/jwt-auth.mjs b/src/server/jwt-auth.mjs
index 7c6d30b..49f2afa 100644
--- a/src/server/jwt-auth.mjs
+++ b/src/server/jwt-auth.mjs
@@ -5,24 +5,53 @@ function addPEMHeaders (headerlessPEMKey) {
return `-----BEGIN CERTIFICATE-----\n${headerlessPEMKey}\n-----END CERTIFICATE-----`
}
+function addRSAPublicKeyPEMHeaders(headerlessPEMKey) {
+ const nlHeaderlessPEMKey = headerlessPEMKey.replace(/(.{64})/g, '$1\n');
+
+ return `-----BEGIN PUBLIC KEY-----\n${nlHeaderlessPEMKey}\n-----END PUBLIC KEY-----`;
+}
+
const { RTCSTATS_JWT_PUBLIC_KEY } = process.env
-const formattedKey = addPEMHeaders(RTCSTATS_JWT_PUBLIC_KEY)
+const { RTCSTATS_JWT_EGHT_PUBLIC_KEY } = process.env
+
+const formattedKeyJaaS = addPEMHeaders(RTCSTATS_JWT_PUBLIC_KEY)
+const formattedKeyEGHT = addRSAPublicKeyPEMHeaders(RTCSTATS_JWT_EGHT_PUBLIC_KEY)
+
+
+function isValidJaaSToken(authorization) {
+ try {
+ const bearerToken = authorization.substring(7)
+ const decodedToken = jwt.verify(bearerToken, formattedKeyJaaS)
+
+ return decodedToken;
+ } catch (error) {
+ log.error(`JAAS Bearer authorization failed: ${JSON.stringify(error)}`)
+ }
+}
+
+function isValidEGHTToken(authorization) {
+ try {
+ const bearerToken = authorization.substring(7)
+ const decodedToken = jwt.verify(bearerToken, formattedKeyEGHT)
+
+ return decodedToken;
+ } catch (error) {
+ log.error(`EGHT Bearer authorization failed: ${JSON.stringify(error)}`)
+ }
+}
export default (req, res, next) => {
const { headers: { authorization = '' } = {} } = req
if (authorization.startsWith('Bearer ')) {
- try {
- const bearerToken = authorization.substring(7)
- const decodedToken = jwt.verify(bearerToken, formattedKey)
-
+ const decodedToken = isValidJaaSToken(authorization) || isValidEGHTToken(authorization);
+
+ if (decodedToken) {
req.user = decodedToken
-
- next()
- } catch (error) {
- log.error(`Bearer authorization failed: ${JSON.stringify(error)}`)
- res.status(401).json({ error: 'Unauthorized' })
+ return next()
}
+
+ res.status(401).json({ error: 'Unauthorized' })
} else {
log.warn(`Bearer authorization token not present, found: ${authorization}`)