Socket
Socket
Sign inDemoInstall

opentok-network-test-js

Package Overview
Dependencies
13
Maintainers
4
Versions
41
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 2.5.0 to 3.0.0

.nvmrc

2

dist/NetworkTest/errors/types.d.ts

@@ -68,2 +68,2 @@ /**

}
export declare const errorHasName: (error: OTError | null | undefined, name: OTErrorType) => boolean;
export declare const errorHasName: (error: OTError, name: OTErrorType) => boolean;
import { SubscriberStats } from '../../types/opentok/subscriber';
import { HasAudioVideo, QualityStats } from '../types/stats';
export default function calculateQualityStats(latestSamples: SubscriberStats[]): HasAudioVideo<QualityStats[]>;
import { HasAudioVideo, SubscriberQualityStats } from '../types/stats';
export default function calculateQualityStats(latestSamples: SubscriberStats[]): HasAudioVideo<SubscriberQualityStats[]>;

@@ -5,4 +5,4 @@ export interface AudioThreshold {

export interface VideoThreshold {
bps: number;
plr: number;
targetBitrate: number;
targetBitrateSimulcast: number;
recommendedSetting: string;

@@ -21,2 +21,3 @@ }

steadyStateAllowedDelta: number;
thresholdRatio: number;
qualityThresholds: {

@@ -23,0 +24,0 @@ audio: AudioThreshold[];

@@ -1,2 +0,2 @@

import { SubscriberStats } from '../../types/opentok/subscriber';
export default function getLatestSampleWindow(stats: SubscriberStats[]): SubscriberStats[];
import { PublisherStats } from '../../types/opentok/publisher';
export default function getLatestSampleWindow(stats: PublisherStats[]): PublisherStats[];

@@ -1,2 +0,8 @@

declare const _default: (publisher: import("../../../../../../../../Users/jswartz_1/src/opentok-network-test-js/src/NetworkTest/types/opentok/publisher").Publisher) => Promise<import("../../../../../../../../Users/jswartz_1/src/opentok-network-test-js/src/NetworkTest/types/opentok/publisher").PublisherRtcStatsReport[] | null>;
export default _default;
import { OT } from '../../types/opentok';
export interface PreviousStreamStats {
[ssrc: number]: {
timestamp: number;
bytesSent: number;
};
}
export declare function getPublisherStats(publisher: OT.Publisher, previousStats: OT.PublisherStats | undefined): Promise<OT.PublisherStats | null>;

@@ -8,2 +8,2 @@ import { AverageStatsBase } from '../types/stats';

}
export default function getVideoQualityEvaluation(stats: AverageStatsBase): QualityEvaluationResults;
export default function getVideoQualityEvaluationt(stats: AverageStatsBase): QualityEvaluationResults;
import { OT } from '../../types/opentok';
export default function isBitrateSteadyState(statsList: OT.SubscriberStats[]): boolean;
export default function isBitrateSteadyState(statsList: OT.PublisherStats[]): boolean;
import { OT } from '../../types/opentok';
import { AverageStats, Bandwidth, HasAudioVideo } from '../types/stats';
export default class MOSState {
statsLog: OT.SubscriberStats[];
subscriberStatsLog: OT.SubscriberStats[];
publisherStatsLog: OT.PublisherStats[];
audioScoresLog: number[];

@@ -16,2 +17,3 @@ videoScoresLog: number[];

readonly hasVideoTrack: () => boolean;
getLastPublisherStats: () => import("../../types/opentok/publisher").PublisherStats;
private audioScore;

@@ -18,0 +20,0 @@ private videoScore;

import MOSState from './MOSState';
import { OT } from '../../types/opentok';
export declare type StatsListener = (error?: OT.OTError, stats?: OT.SubscriberStats) => void;
export declare type StatsListener = (error?: OT.OTError, subStats?: OT.SubscriberStats, pubStats?: OT.PublisherStats) => void;
export default function subscriberMOS(mosState: MOSState, subscriber: OT.Subscriber, publisher: OT.Publisher, getStatsListener: StatsListener, callback: (state: MOSState) => void): MOSState;

@@ -13,3 +13,9 @@ export declare type AV = 'audio' | 'video';

}
export interface SubscriberQualityStats {
averageBitrate: number;
packetLossRatio: number;
frameRate?: number;
}
export interface QualityStats {
averageAvailableOutgoingBitrate: number;
averageBitrate: number;

@@ -30,4 +36,6 @@ packetLossRatio: number;

export interface AverageStatsBase {
simulcast: boolean;
availableOutgoingBitrate: number;
bitrate: number;
packetLossRatio: number;
}

@@ -1,5 +0,15 @@

import { OT } from './opentok';
export declare type UpdateCallback<A> = (stats: OT.SubscriberStats) => void;
export declare type UpdateCallbackStats = OT.SubscriberStats & {
export declare type UpdateCallback<A> = (stats: UpdateCallbackStats) => void;
export declare type UpdateCallbackStats = {
audio: CallbackTrackStats;
video: CallbackTrackStats & {
frameRate: number;
};
timestamp: number;
phase: string;
};
export interface CallbackTrackStats {
bytesSent: number;
bytesReceived: number;
packetsLost: number;
packetsReceived: number;
}

@@ -78,5 +78,4 @@ import * as OTSession from './session';

type OTError = OTError.OTError;
type PublisherRtcStatsReportArr = OTPublisher.PublisherRtcStatsReportArr;
type PublisherStats = OTPublisher.PublisherStats;
type PublisherRtcStatsReport = OTPublisher.PublisherRtcStatsReport;
type RTCStatsReport = OTPublisher.RTCStatsReport;
}

@@ -5,2 +5,3 @@ import { Stream } from './stream';

import { Dimensions, WidgetProperties, WidgetStyle } from './widget';
import { RTCStatsArray } from './rtcStats';
export interface OutgoingTrackStats {

@@ -11,7 +12,28 @@ bytesSent: number;

}
export interface VideoStats {
ssrc: number;
byteSent: number;
kbs: number;
qualityLimitationReason: string;
resolution: string;
framerate: number;
active: boolean;
pliCount: number;
nackCount: number;
currentTimestamp: number;
}
export interface AudioStats {
kbs: number;
byteSent: number;
currentTimestamp: number;
}
export interface PublisherStats {
audio: OutgoingTrackStats;
video: OutgoingTrackStats & {
frameRate: number;
};
videoStats: VideoStats[];
audioStats: AudioStats[];
availableOutgoingBitrate: number;
videoByteSent: number;
videoKbsSent: number;
simulcastEnabled: boolean;
transportProtocol: string;
currentRoundTripTime: number;
timestamp: number;

@@ -54,12 +76,5 @@ }

}
export interface RTCStatsReport {
forEach(callbackfn: (value: any, key: string, parent: RTCStatsReport) => void, thisArg?: any): void;
type: string;
roundTripTime: number;
kind: string;
}
export declare type PublisherRtcStatsReport = {
rtcStatsReport: RTCStatsReport;
rtcStatsReport: RTCStatsArray[];
};
export declare type PublisherRtcStatsReportArr = PublisherRtcStatsReport[];
export interface Publisher extends OTEventEmitter<{

@@ -106,3 +121,3 @@ accessAllowed: Event<'accessAllowed', Publisher>;

videoHeight(): number | undefined;
getRtcStatsReport(callback?: (error?: Error, stats?: PublisherRtcStatsReportArr) => void): Promise<PublisherRtcStatsReportArr> | undefined;
getRtcStatsReport(callback?: (error?: Error, stats?: PublisherRtcStatsReport) => void): Promise<PublisherRtcStatsReport> | undefined;
}

@@ -39,3 +39,3 @@ /**

*/
export declare const last: <T>(list: T[]) => T | undefined;
export declare const last: <T>(list: T[]) => T;
/**

@@ -45,6 +45,6 @@ * Returns the nth element of an array. If a negative value is passed, the nth element from the end

*/
export declare const nth: <T>(n: number, list: T[]) => T | undefined;
export declare const nth: <T>(n: number, list: T[]) => T;
/**
* Returns the first element from a list, or undefined if it doesn't exist
*/
export declare const head: <T>(list: T[]) => T | undefined;
export declare const head: <T>(list: T[]) => T;
{
"name": "opentok-network-test-js",
"version": "2.5.0",
"version": "3.0.0",
"description": "Precall network test for applications using the OpenTok platform.",

@@ -14,3 +14,4 @@ "main": "dist/NetworkTest/index.js",

"example": "npm run build && cd sample && npm run build && python -m SimpleHTTPServer",
"tslint": "./node_modules/tslint/bin/tslint -p tsconfig.json"
"tslint": "./node_modules/tslint/bin/tslint -p tsconfig.json",
"tslint-fix": "./node_modules/tslint/bin/tslint -p tsconfig.json --fix"
},

@@ -51,3 +52,3 @@ "repository": {

"@types/promise": "^7.1.30",
"@types/webrtc": "0.0.22",
"@types/webrtc": "^0.0.34",
"dotenv": "^4.0.0",

@@ -73,3 +74,3 @@ "fs-extra": "^4.0.3",

"tslint-config-airbnb": "^5.9.2",
"typescript": "2.9.2",
"typescript": "3.7.2",
"webpack": "^4.12.0",

@@ -76,0 +77,0 @@ "webpack-cli": "^3.0.4",

@@ -143,6 +143,8 @@

The `OTNetworkTest.testQuality()` method is supported in Chrome, Firefox, Safari,
The `OTNetworkTest.testQuality()` method is supported in Chrome, Safari,
Opera, and Chromium-based versions of Edge (versions 79+).
It is not supported in non-Chromium-based versions of Edge.
It is *not* supported in Firefox and non-Chromium-based versions of Edge.
Update: **Firefox** has been removed from the list of supported browsers for the testQuality feature since version **2.6.0** due to missing some important statistics. This could lead to inaccurate results.
## API reference

@@ -376,2 +378,3 @@

timestamp: 1509747314,
bytesSent:534349,// The total number of video bytes received, cumulative
bytesReceived: 434349, // The total number of audio bytes received, cumulative

@@ -384,2 +387,3 @@ packetsReceived: 24234, // The total number of audio packets received, cumulative

bytesReceived: 434349, // The total number of video bytes received, cumulative
bytesSent:434349, // The total number of video bytes sent
frameRate: 15, // The video frame rate

@@ -400,2 +404,4 @@ packetsReceived: 24234, // The total number of video packets received, cumulative

Update: We advise using 'bytesSent' to provide a more accurate result, especially if scalable video is enabled for the API key
#### Promise returned

@@ -402,0 +408,0 @@

@@ -147,7 +147,7 @@ // Utility functions to display test results in the sample app UI

}
const bitsReceived = mediaStats && mediaStats.bytesReceived ? mediaStats.bytesReceived * 8 : 0;
const bitsSent = mediaStats && mediaStats.bytesSent ? mediaStats.bytesSent * 8 : 0;
resultCount[mediaType]++;
charts[mediaType].series[0].addPoint({
x: resultCount[mediaType],
y: bitsReceived - prevBitsReceived[mediaType]
y: bitsSent - prevBitsReceived[mediaType]
}, true, false);

@@ -158,3 +158,3 @@ const chartTitle = (stats.phase === 'audio-only') && (mediaType === 'video') ?

charts[mediaType].setTitle(null, { text: chartTitle});
prevBitsReceived[mediaType] = bitsReceived;
prevBitsReceived[mediaType] = bitsSent;
}
import { SubscriberStats } from '../../types/opentok/subscriber';
import { AV, HasAudioVideo, QualityStats } from '../types/stats';
import { AV, HasAudioVideo, SubscriberQualityStats } from '../types/stats';
function calculateStats(type: AV, samples: SubscriberStats[]): QualityStats[] {
function calculateStats(type: AV, samples: SubscriberStats[]): SubscriberQualityStats[] {
const qualityStats: QualityStats[] = [];
const qualityStats: SubscriberQualityStats[] = [];

@@ -37,3 +37,5 @@ for (let i = 1; i < samples.length; i += 1) {

export default function calculateQualityStats(latestSamples: SubscriberStats[]): HasAudioVideo<QualityStats[]> {
export default function calculateQualityStats(
latestSamples: SubscriberStats[],
): HasAudioVideo<SubscriberQualityStats[]> {
if (latestSamples.length < 2) {

@@ -40,0 +42,0 @@ throw new Error('Cannot calculate bitrate with less than two data points.');

import getLatestSampleWindow from './getLatestSampleWindow';
import calculateQualityStats from './calculateQualityStats';
import getVideoQualityEvaluation from './getVideoQualityEvaluation';
import { AV, AverageStats, AverageStatsBase, HasAudioVideo, QualityStats } from '../types/stats';
import { AV, AverageStats, AverageStatsBase, HasAudioVideo, SubscriberQualityStats } from '../types/stats';
import config from './config';
import MOSState from './MOSState';
import { PublisherStats } from '../../types/opentok/publisher';
import { getOr } from '../../util';
function getAverageBitrateAndPlr(type: AV, statsList: QualityStats[]): AverageStats {
function getAverageBitrateAndPlr(type: AV,
subscriberStatsList: SubscriberQualityStats[],
publisherStatsList: PublisherStats[]): AverageStats {
let sumBps = 0;

@@ -14,4 +18,12 @@ let sumPlr = 0;

statsList.forEach((stat) => {
sumBps += stat.averageBitrate;
publisherStatsList.forEach((stat) => {
sumPlr += 0;
if (type === 'video') {
sumBps += stat.videoKbsSent * 1000;
} else {
sumBps += stat.audioStats[0].kbs * 1000;
}
});
subscriberStatsList.forEach((stat) => {
sumPlr += stat.packetLossRatio;

@@ -23,5 +35,11 @@ if (type === 'video') {

const isSimulcastEnabled = publisherStatsList.some(
publisherStats => publisherStats.simulcastEnabled,
);
const averageStats: AverageStatsBase = {
bitrate: sumBps / statsList.length,
packetLossRatio: sumPlr / statsList.length,
availableOutgoingBitrate: publisherStatsList[publisherStatsList.length - 1].availableOutgoingBitrate,
simulcast: isSimulcastEnabled,
bitrate: sumBps / publisherStatsList.length,
packetLossRatio: sumPlr / subscriberStatsList.length,
};

@@ -31,3 +49,3 @@

const { supported, reason, recommendedResolution, recommendedFrameRate } =
getVideoQualityEvaluation(averageStats);
getVideoQualityEvaluation(averageStats);

@@ -38,3 +56,3 @@ const videoStats =

recommendedFrameRate,
frameRate: sumFrameRate / statsList.length,
frameRate: sumFrameRate / subscriberStatsList.length,
} : {};

@@ -48,4 +66,4 @@ return { ...averageStats, supported, reason, ...videoStats };

const sampleWindow = getLatestSampleWindow(state.statsLog);
const qualityStats = calculateQualityStats(sampleWindow);
const sampleWindow = getLatestSampleWindow(state.publisherStatsLog);
const subscriberQualityStats = calculateQualityStats(state.subscriberStatsLog);

@@ -59,3 +77,3 @@ const averageAudioStats = () => {

}
return getAverageBitrateAndPlr('audio', qualityStats.audio);
return getAverageBitrateAndPlr('audio', subscriberQualityStats.audio, sampleWindow);
};

@@ -76,3 +94,3 @@

}
return getAverageBitrateAndPlr('video', qualityStats.video);
return getAverageBitrateAndPlr('video', subscriberQualityStats.video, sampleWindow);
};

@@ -79,0 +97,0 @@

export interface AudioThreshold { minMos: number; }
export interface VideoThreshold { bps: number; plr: number; recommendedSetting: string; }
export interface VideoThreshold { targetBitrate: number; targetBitrateSimulcast: number; recommendedSetting: string; }

@@ -16,2 +16,3 @@ export type QualityTestConfig = {

steadyStateAllowedDelta: number,
thresholdRatio: number,
qualityThresholds: {

@@ -39,33 +40,34 @@ audio: AudioThreshold[],

steadyStateAllowedDelta: 0.05, // 1 = 100%, from point to point
thresholdRatio: 0.75,
qualityThresholds: {
video: [
{
bps: 1000000,
plr: 0.005,
recommendedSetting: '1280x720 @ 30FPS',
targetBitrate: 4000000,
targetBitrateSimulcast: 5550000,
recommendedSetting: '1920x1080 @ 30FPS',
},
{
bps: 600000,
plr: 0.005,
recommendedSetting: '640x480 @ 30FPS',
targetBitrate: 2500000,
targetBitrateSimulcast: 3150000,
recommendedSetting: '1280x720 @ 30FPS',
},
{
bps: 300000,
plr: 0.005,
recommendedSetting: '320x240 @ 30FPS',
targetBitrate: 1200000,
targetBitrateSimulcast: 1550000,
recommendedSetting: '960x540 @ 30FPS',
},
{
bps: 350000,
plr: 0.03,
recommendedSetting: '1280x720 @ 30FPS',
targetBitrate: 500000,
targetBitrateSimulcast: 650000,
recommendedSetting: '640x360 @ 30FPS',
},
{
bps: 250000,
plr: 0.03,
recommendedSetting: '640x480 @ 30FPS',
targetBitrate: 300000,
targetBitrateSimulcast: 350000,
recommendedSetting: '480x270 @ 30FPS',
},
{
bps: 150000,
plr: 0.03,
recommendedSetting: '320x240 @ 30FPS',
targetBitrate: 150000,
targetBitrateSimulcast: 150000,
recommendedSetting: '320x180 @ 30FPS',
},

@@ -79,2 +81,3 @@ ],

},
strings: {

@@ -81,0 +84,0 @@ bandwidthLow: 'Bandwidth too low.',

import config from './config';
import { SubscriberStats } from '../../types/opentok/subscriber';
import { getOr, last } from '../../util';
import { PublisherStats } from '../../types/opentok/publisher';
export default function getLatestSampleWindow(stats: SubscriberStats[]): SubscriberStats[] {
export default function getLatestSampleWindow(stats: PublisherStats[]): PublisherStats[] {
const mostRecentTimestamp: number = getOr(0, 'timestamp', last(stats));
const oldestAllowedTime: number = mostRecentTimestamp - config.steadyStateSampleWindow;
return stats.filter((stat: SubscriberStats) => stat.timestamp >= oldestAllowedTime);
return stats.filter((stat: PublisherStats) => stat.timestamp >= oldestAllowedTime);
}
import { OT } from '../../types/opentok';
import {
RTCTransportStats,
RTCOutboundRtpStreamStats,
RTCIceCandidatePairStats,
RTCStatsInternal,
RTCandidateStatsInternal,
} from '../../types/opentok/rtcStats';
export default (publisher: OT.Publisher): Promise<OT.PublisherRtcStatsReportArr | null> =>
new Promise((resolve) => {
// If getRtcStatsReport is not a function, that means OT version is < 2.17.6
// In this case we just return null.
if (typeof publisher.getRtcStatsReport !== 'function') {
return resolve(null);
export interface PreviousStreamStats {
[ssrc: number]: {
timestamp: number;
bytesSent: number;
};
}
export async function getPublisherStats(
publisher: OT.Publisher,
previousStats: OT.PublisherStats | undefined,
): Promise<OT.PublisherStats | null> {
if (typeof publisher.getRtcStatsReport !== 'function') {
return null;
}
try {
const publisherStatsReport = await publisher.getRtcStatsReport();
return extractPublisherStats(publisherStatsReport, previousStats);
} catch (error) {
return null;
}
}
const calculateAudioBitrate = (
stats: RTCOutboundRtpStreamStats,
previousStats: OT.PublisherStats | undefined,
): number => {
const previousSsrcFrameData = previousStats?.audioStats[0];
if (!previousSsrcFrameData) {
return 0;
}
const { currentTimestamp: previousTimestamp, byteSent: previousByteSent } = previousSsrcFrameData;
const byteSent = stats.bytesSent - previousByteSent;
const timeDiff = (stats.timestamp - previousTimestamp) / 1000; // Convert to seconds
return Math.round((byteSent * 8) / (1000 * timeDiff)); // Convert to bits per second
};
const calculateVideoBitrate = (
stats: RTCOutboundRtpStreamStats,
previousStats: OT.PublisherStats | undefined,
): number => {
const previousSsrcFrameData = previousStats?.videoStats.find(videoStats => videoStats.ssrc === stats.ssrc);
if (!previousSsrcFrameData) {
return 0;
}
const { currentTimestamp: previousTimestamp, byteSent: previousByteSent } = previousSsrcFrameData;
const byteSent = stats.bytesSent - previousByteSent;
const timeDiff = (stats.timestamp - previousTimestamp) / 1000; // Convert to seconds
return Math.round((byteSent * 8) / (1000 * timeDiff)); // Convert to kbit per second
};
const extractOutboundRtpStats = (
outboundRtpStats: RTCOutboundRtpStreamStats[],
previousStats?: OT.PublisherStats,
) => {
const videoStats = [];
const audioStats = [];
for (const stats of outboundRtpStats) {
if (stats.mediaType === 'video') {
const kbs = calculateVideoBitrate(stats, previousStats);
const { ssrc, bytesSent: byteSent, timestamp: currentTimestamp } = stats;
const baseStats = { kbs, ssrc, byteSent, currentTimestamp };
videoStats.push({
...baseStats,
qualityLimitationReason: stats.qualityLimitationReason || 'N/A',
resolution: `${stats.frameWidth || 0}x${stats.frameHeight || 0}`,
framerate: stats.framesPerSecond || 0,
active: stats.active || false,
pliCount: stats.pliCount || 0,
nackCount: stats.nackCount || 0,
});
} else if (stats.mediaType === 'audio') {
const kbs = calculateAudioBitrate(stats, previousStats);
const { ssrc, bytesSent: byteSent, timestamp: currentTimestamp } = stats;
const baseStats = { kbs, ssrc, byteSent, currentTimestamp };
audioStats.push(baseStats);
}
}
// Need to evaluate the result of getRtcStatsReport() to determine
// whether it's the promise or callback version of the API.
const getRtcStatsReportPromise = publisher.getRtcStatsReport();
if (!getRtcStatsReportPromise) {
publisher.getRtcStatsReport((err, stats) => { resolve(stats); });
} else {
(getRtcStatsReportPromise as Promise<OT.PublisherRtcStatsReportArr>)
.then(resolve)
.catch(() => resolve(null));
}
});
return { videoStats, audioStats };
};
const extractPublisherStats = (
publisherRtcStatsReport?: OT.PublisherRtcStatsReport,
previousStats?: OT.PublisherStats,
): OT.PublisherStats | null => {
if (!publisherRtcStatsReport) {
return null;
}
const { rtcStatsReport } = publisherRtcStatsReport[0];
const rtcStatsArray: RTCStatsInternal[] = Array.from(rtcStatsReport.values());
const transportStats = rtcStatsArray.find(
stats => stats.type === 'transport') as RTCTransportStats;
const outboundRtpStats = rtcStatsArray.filter(
stats => stats.type === 'outbound-rtp') as RTCOutboundRtpStreamStats[];
const iceCandidatePairStats = rtcStatsArray.find(
stats => stats.type === 'candidate-pair' && stats.nominated) as RTCIceCandidatePairStats;
const findCandidateById = (type: string, id: string) => {
return rtcStatsArray.find(stats => stats.type === type && stats.id === id) as RTCandidateStatsInternal | null;
};
const localCandidate = findCandidateById('local-candidate', iceCandidatePairStats.localCandidateId);
const remoteCandidate = findCandidateById('remote-candidate', iceCandidatePairStats.remoteCandidateId);
const { videoStats, audioStats } = extractOutboundRtpStats(outboundRtpStats, previousStats);
const availableOutgoingBitrate = iceCandidatePairStats?.availableOutgoingBitrate || -1;
const currentRoundTripTime = iceCandidatePairStats?.currentRoundTripTime || -1;
const videoKbsSent = videoStats.reduce((sum, stats) => sum + stats.kbs, 0);
const videoByteSent = videoStats.reduce((sum, stats) => sum + stats.byteSent, 0);
const simulcastEnabled = videoStats.length > 1;
const transportProtocol = localCandidate?.protocol || 'N/A';
const timestamp = localCandidate?.timestamp || 0;
/**
console.trace("videoStats: ", videoStats);
console.trace("audioStats: ", audioStats);
console.trace("availableOutgoingBitrate: ", availableOutgoingBitrate);
console.trace("currentRoundTripTime: ", currentRoundTripTime);
console.trace("videoSentKbs: ", videoSentKbs);
console.trace("simulcastEnabled: ", simulcastEnabled);
console.trace("transportProtocol: ", transportProtocol);
console.info("availableOutgoingBitrate: ", availableOutgoingBitrate);
console.info("videoByteSent: ", videoByteSent);
**/
return {
videoStats,
audioStats,
availableOutgoingBitrate,
videoByteSent,
videoKbsSent,
simulcastEnabled,
transportProtocol,
currentRoundTripTime,
timestamp,
};
};

@@ -12,6 +12,7 @@ import config from './config';

export default function getVideoQualityEvaluation(stats: AverageStatsBase): QualityEvaluationResults {
export default function getVideoQualityEvaluationt(stats: AverageStatsBase): QualityEvaluationResults {
const thresholds = config.qualityThresholds.video;
const bitrate = stats.bitrate;
const packetLoss = stats.packetLossRatio;
const thresholdRatio = config.thresholdRatio;
const bitrate = stats.availableOutgoingBitrate;
let supported = false;

@@ -24,3 +25,4 @@ let recommendedFrameRate : number = 30;

const threshold = thresholds[i];
if (bitrate >= threshold.bps && packetLoss <= threshold.plr) {
const targetBitrate = stats.simulcast ? threshold.targetBitrateSimulcast : threshold.targetBitrate;
if (bitrate >= targetBitrate * thresholdRatio) {
supported = true;

@@ -27,0 +29,0 @@ recommendedSetting = get('recommendedSetting', threshold);

import getLatestSampleWindow from './getLatestSampleWindow';
import calculateQualityStats from './calculateQualityStats';
import config from './config';
import { AV } from '../types/stats';
import { OT } from '../../types/opentok';
export default function isBitrateSteadyState(statsList: OT.SubscriberStats[]): boolean {
export default function isBitrateSteadyState(statsList: OT.PublisherStats[]): boolean {
const latestSamples = getLatestSampleWindow(statsList);
const steadyStateAllowedDelta = config.steadyStateAllowedDelta;
let isSteadyState = true;

@@ -16,19 +13,19 @@

const statsBitrates = calculateQualityStats(latestSamples);
const avTypes: AV[] = ['video', 'audio'];
avTypes.forEach((avType: 'audio' | 'video') => {
for (let i = 1; i < statsBitrates[avType].length; i += 1) {
const currBitrate = statsBitrates[avType][i].averageBitrate;
const prevBitrate = statsBitrates[avType][i - 1].averageBitrate;
const bitrateDelta = currBitrate - prevBitrate;
const allowableBitrateDelta = (prevBitrate * steadyStateAllowedDelta);
if (bitrateDelta > allowableBitrateDelta) {
for (const [i, currSample] of latestSamples.entries()) {
if (i === 0) { continue; }
const prevSample = latestSamples[i - 1];
if (currSample.videoKbsSent > 0){
const currBitrate = currSample.videoKbsSent;
const prevBitrate = prevSample.videoKbsSent;
if ((currBitrate - prevBitrate) > (prevBitrate * config.steadyStateAllowedDelta)) {
isSteadyState = false;
}
}
});
const currBitrateAudio = currSample.audioStats[0].kbs;
const prevBitrateAudio = prevSample.audioStats[0].kbs;
if ((currBitrateAudio - prevBitrateAudio) > (prevBitrateAudio * config.steadyStateAllowedDelta)) {
isSteadyState = false;
}
}
return isSteadyState;
}

@@ -63,3 +63,3 @@ import { get } from '../../util';

export default function isSupportedBrowser(): { supported: boolean, browser: Browser } {
const supportedBrowsers = ['Chrome', 'Firefox', 'Safari', 'Edge', 'Opera'];
const supportedBrowsers = ['Chrome', 'Safari', 'Edge', 'Opera'];
const browser = detectBrowser();

@@ -66,0 +66,0 @@ const supported = supportedBrowsers.indexOf(browser) > -1;

@@ -6,3 +6,4 @@ import { OT } from '../../types/opentok';

export default class MOSState {
statsLog: OT.SubscriberStats[];
subscriberStatsLog: OT.SubscriberStats[];
publisherStatsLog: OT.PublisherStats[];
audioScoresLog: number[];

@@ -16,3 +17,4 @@ videoScoresLog: number[];

constructor(audioOnly?: boolean) {
this.statsLog = [];
this.subscriberStatsLog = [];
this.publisherStatsLog = [];
this.audioScoresLog = [];

@@ -26,5 +28,8 @@ this.videoScoresLog = [];

readonly hasAudioTrack = (): boolean => this.statsLog[0] && !!this.statsLog[0].audio;
readonly hasVideoTrack = (): boolean => this.statsLog[0] && !!this.statsLog[0].video;
readonly hasAudioTrack = (): boolean => this.subscriberStatsLog[0] && !!this.subscriberStatsLog[0].audio;
readonly hasVideoTrack = (): boolean => this.subscriberStatsLog[0] && !!this.subscriberStatsLog[0].video;
public getLastPublisherStats = (): OT.PublisherStats | undefined =>
this.publisherStatsLog[this.publisherStatsLog.length - 1] ?? undefined
private audioScore(): number {

@@ -31,0 +36,0 @@ return this.audioScoresLog.reduce((acc, score) => acc + score, 0) / this.audioScoresLog.length;

@@ -7,5 +7,9 @@ import isBitrateSteadyState from './isBitrateSteadyState';

import { getOr, last, nth } from '../../util';
import getPublisherRtcStatsReport from '../helpers/getPublisherRtcStatsReport';
import { getPublisherStats } from '../helpers/getPublisherRtcStatsReport';
export type StatsListener = (error?: OT.OTError, stats?: OT.SubscriberStats) => void;
export type StatsListener = (
error?: OT.OTError,
subStats?: OT.SubscriberStats,
pubStats?: OT.PublisherStats,
) => void;

@@ -22,3 +26,3 @@ const getPacketsLost = (ts: OT.TrackStats): number => getOr(0, 'packetsLost', ts);

};
const MS_PER_SEC = 1000;
const MS_PER_SEC = 10000;
const DEFAULT_DELAY = 150; // expressed in ms

@@ -56,24 +60,6 @@

function calculateAudioScore(
subscriber: OT.Subscriber, publisherStats: OT.PublisherRtcStatsReportArr | null,
subscriber: OT.Subscriber, publisherStats: OT.PublisherStats | null,
stats: OT.SubscriberStats[]): number {
/**
* Get publisher raw stats directly from the Peer Connection in order
* to get the roundTripTime on type=remote-inbound-rtp and kind=audio
* We can get this only using the standard getStats API. For legacy API
* we will return 0.
*/
const getRoundTripTime = () => {
const roundTripTime = 0;
if (publisherStats) {
const { rtcStatsReport } = publisherStats[0];
let roundTripTime = 0;
rtcStatsReport.forEach((stat: any) => {
if (stat.type === 'remote-inbound-rtp' && stat.kind === 'audio') {
roundTripTime = !isNaN(stat.roundTripTime) ? stat.roundTripTime : 0;
}
});
}
return roundTripTime;
};
const getRoundTripTime = () => publisherStats?.currentRoundTripTime || 0;

@@ -83,3 +69,4 @@ const getDelay = (): number => {

const delay = (roundTripTime * MS_PER_SEC) / 2;
return delay || DEFAULT_DELAY;
// Return default delay until proper calculation
return DEFAULT_DELAY;
};

@@ -143,25 +130,13 @@

getStatsListener: StatsListener,
callback: (state: MOSState) => void) {
mosState.intervalId = window.setInterval(
() => {
subscriber.getStats(async (error?: OT.OTError, stats?: OT.SubscriberStats) => {
if (!stats) {
callback: (state: MOSState) => void,
) {
mosState.intervalId = window.setInterval(() => {
subscriber.getStats(
async (error?: OT.OTError, subscriberStats?: OT.SubscriberStats) => {
if (!subscriberStats) {
return null;
}
let publisherStats = null;
try {
publisherStats = await getPublisherRtcStatsReport(publisher);
} catch {}
/**
* We occasionally start to receive faulty stat during long-running
* tests. If this occurs, let's end the test early and report the
* results as they are, as we should have sufficient data to
* calculate a score at this point.
*
* We know that we're receiving "faulty" stats when we see a negative
* value for bytesReceived.
*/
if (stats.audio.bytesReceived < 0 || getOr(1, 'video.bytesReceived', stats) < 0) {
// Check for faulty stats
if (subscriberStats.audio.bytesReceived < 0 || getOr(1, 'video.bytesReceived', subscriberStats) < 0) {
mosState.clearInterval();

@@ -171,33 +146,44 @@ return callback(mosState);

stats && mosState.statsLog.push(stats);
// Get publisher stats and push to MOSState statsLog array
const publisherStats = await getPublisherStats(publisher, mosState.getLastPublisherStats());
// Push subscriber stats to MOSState statsLog array
subscriberStats && mosState.subscriberStatsLog.push(subscriberStats);
publisherStats && mosState.publisherStatsLog.push(publisherStats);
// Call getStatsListener if it exists
if (getStatsListener && typeof getStatsListener === 'function') {
getStatsListener(error, stats);
getStatsListener(error, subscriberStats, publisherStats);
}
if (mosState.statsLog.length < 2) {
return null;
}
// Calculate MOSState stats and push to appropriate logs
if (mosState.publisherStatsLog.length && mosState.publisherStatsLog.length >= 2) {
mosState.stats = calculateThroughput(mosState);
const videoScore = calculateVideoScore(subscriber, mosState.subscriberStatsLog);
mosState.videoScoresLog.push(videoScore);
mosState.stats = calculateThroughput(mosState);
const videoScore = calculateVideoScore(subscriber, mosState.statsLog);
mosState.videoScoresLog.push(videoScore);
const audioScore = calculateAudioScore(
subscriber,
publisherStats,
mosState.subscriberStatsLog,
);
mosState.audioScoresLog.push(audioScore);
mosState.pruneScores();
const audioScore = calculateAudioScore(subscriber, publisherStats, mosState.statsLog);
mosState.audioScoresLog.push(audioScore);
mosState.pruneScores();
// If bandwidth has reached a steady state, end the test early
if (isBitrateSteadyState(mosState.statsLog)) {
mosState.clearInterval();
return callback(mosState);
// Check if bitrate has reached a steady state, if yes end the test early
if (isBitrateSteadyState(mosState.publisherStatsLog)) {
mosState.clearInterval();
return callback(mosState);
}
}
return null;
},
);
}, MOSState.scoreInterval);
});
}, MOSState.scoreInterval);
subscriber.on('destroyed', mosState.clearInterval.bind(mosState));
// Fix: Incorrect statistics were sent because the publisher disconnected before the subscriber.
publisher.on('destroyed', mosState.clearInterval.bind(mosState));
return mosState;
}

@@ -29,2 +29,3 @@ /**

import isSupportedBrowser from './helpers/isSupportedBrowser';
import getUpdateCallbackStats from './helpers/getUpdateCallbackStats';

@@ -122,2 +123,3 @@ interface QualityTestResultsBuilder {

document.body.appendChild(containerDiv);
validateDevices(OT)

@@ -166,3 +168,3 @@ .then((availableDevices: AvailableDevices) => {

containerDiv,
{ testNetwork: true, insertMode: 'append' },
{ testNetwork: true, insertMode: 'append', subscribeToAudio: true, subscribeToVideo: true },
(subscribeError?: OT.OTError) => {

@@ -236,3 +238,3 @@ return subscribeError ?

*/
function cleanPublisher(publisher: OT.Publisher) {
function cleanPublisher(session: OT.Session, publisher: OT.Publisher) {
return new Promise((resolve, reject) => {

@@ -245,3 +247,3 @@ publisher.on('destroyed', () => {

}
publisher.destroy();
session.unpublish(publisher);
});

@@ -255,3 +257,3 @@ }

options?: NetworkTestOptions,
onUpdate?: UpdateCallback<OT.SubscriberStats>,
onUpdate?: UpdateCallback<UpdateCallbackStats>,
audioOnlyFallback?: boolean,

@@ -275,8 +277,11 @@ ): Promise<QualityTestResults> {

const getStatsListener = (error?: OT.OTError, stats?: OT.SubscriberStats) => {
const updateStats = (subscriberStats: OT.SubscriberStats): UpdateCallbackStats => ({
...subscriberStats,
phase: audioOnly ? 'audio-only' : 'audio-video',
});
stats && onUpdate && onUpdate(updateStats(stats));
const getStatsListener = (
error?: OT.OTError,
subscriberStats?: OT.SubscriberStats,
publisherStats?: OT.PublisherStats,
) => {
if (subscriberStats && publisherStats && onUpdate) {
const updateStats = getUpdateCallbackStats(subscriberStats, publisherStats, audioOnly ? 'audio-only' : 'audio-video');
onUpdate(updateStats);
}
};

@@ -301,3 +306,3 @@

cleanSubscriber(session, subscriber)
.then(() => cleanPublisher(publisher))
.then(() => cleanPublisher(session, publisher))
.then(() => session.disconnect());

@@ -308,2 +313,3 @@ }

stopTest = () => {
clearTimeout(mosEstimatorTimeoutId);
processResults();

@@ -319,4 +325,3 @@ };

// We add +1 to the testTimeout value in order to consider the last stats snapshot.
mosEstimatorTimeoutId = window.setTimeout(processResults, testTimeout + 1);
mosEstimatorTimeoutId = window.setTimeout(processResults, testTimeout);

@@ -363,3 +368,2 @@ window.clearTimeout(stopTestTimeoutId);

return new Promise((resolve, reject) => {
audioOnly = !!(options && options.audioOnly);

@@ -366,0 +370,0 @@ testTimeout = audioOnly ? config.getStatsAudioOnlyDuration :

@@ -12,3 +12,10 @@ export type AV = 'audio' | 'video';

export interface SubscriberQualityStats {
averageBitrate: number;
packetLossRatio: number;
frameRate?: number;
}
export interface QualityStats {
averageAvailableOutgoingBitrate: number;
averageBitrate: number;

@@ -31,4 +38,6 @@ packetLossRatio: number;

export interface AverageStatsBase {
simulcast : boolean;
availableOutgoingBitrate: number;
bitrate: number;
packetLossRatio: number;
}
import { OT } from './opentok';
export type UpdateCallback<A> = (stats: OT.SubscriberStats) => void;
export type UpdateCallbackStats = OT.SubscriberStats & { phase: string; };
export type UpdateCallback<A> = (stats: UpdateCallbackStats) => void;
export type UpdateCallbackStats = {
audio: CallbackTrackStats;
video: CallbackTrackStats & { frameRate: number; };
timestamp: number;
phase: string;
};
export interface CallbackTrackStats {
bytesSent: number;
bytesReceived: number;
packetsLost: number;
packetsReceived: number;
}

@@ -1,2 +0,1 @@

import * as OTSession from './session';

@@ -105,5 +104,4 @@ import * as OTStream from './stream';

export type OTError = OTError.OTError;
export type PublisherRtcStatsReportArr = OTPublisher.PublisherRtcStatsReportArr;
export type PublisherStats = OTPublisher.PublisherStats;
export type PublisherRtcStatsReport = OTPublisher.PublisherRtcStatsReport;
export type RTCStatsReport = OTPublisher.RTCStatsReport;
}

@@ -5,2 +5,3 @@ import { Stream } from './stream';

import { Dimensions, WidgetProperties, WidgetStyle } from './widget';
import { RTCStatsArray } from './rtcStats';

@@ -13,5 +14,30 @@ export interface OutgoingTrackStats {

export interface VideoStats {
ssrc: number;
byteSent: number;
kbs: number;
qualityLimitationReason: string;
resolution: string;
framerate: number;
active: boolean;
pliCount: number;
nackCount: number;
currentTimestamp: number;
}
export interface AudioStats {
kbs: number;
byteSent: number;
currentTimestamp: number;
}
export interface PublisherStats {
audio: OutgoingTrackStats;
video: OutgoingTrackStats & { frameRate: number; };
videoStats: VideoStats[];
audioStats: AudioStats[];
availableOutgoingBitrate: number;
videoByteSent:number;
videoKbsSent: number;
simulcastEnabled: boolean;
transportProtocol: string;
currentRoundTripTime: number;
timestamp: number;

@@ -74,15 +100,6 @@ }

export interface RTCStatsReport {
forEach(callbackfn: (value: any, key: string, parent: RTCStatsReport) => void, thisArg?: any): void;
type: string;
roundTripTime: number;
kind: string;
}
export type PublisherRtcStatsReport = {
rtcStatsReport: RTCStatsReport,
rtcStatsReport: RTCStatsArray[],
};
export type PublisherRtcStatsReportArr = PublisherRtcStatsReport[];
export interface Publisher extends OTEventEmitter<{

@@ -138,4 +155,4 @@ accessAllowed: Event<'accessAllowed', Publisher>;

getRtcStatsReport(
callback?: (error?: Error, stats?: PublisherRtcStatsReportArr) => void,
): Promise<PublisherRtcStatsReportArr> | undefined;
callback?: (error?: Error, stats?: PublisherRtcStatsReport) => void,
): Promise<PublisherRtcStatsReport> | undefined;
}

@@ -25,3 +25,5 @@ /**

const valForKey = get(key, obj);
const base: Object = (!!valForKey && typeof valForKey === 'object') ? valForKey : { ...obj, [key]: {} };
const base: Object = (!!valForKey && typeof valForKey === 'object')
? valForKey
: (!!valForKey ? { ...obj, [key]: {} } : obj);
const update = assoc(key, assocPath(keys.slice(1).join('.'), value, get(key, base)), obj);

@@ -28,0 +30,0 @@ return { ...obj, ...update };

@@ -6,3 +6,3 @@ {

"module": "commonjs", /* Specify module code generation: 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
"lib": ["es2016", "es2017.object", "dom"], /* Specify library files to be included in the compilation: */
"lib": ["es2018", "es2017.object", "dom"], /* Specify library files to be included in the compilation: */
"allowJs": false, /* Allow javascript files to be compiled. */

@@ -13,6 +13,2 @@ "declaration": true, /* Generates corresponding '.d.ts' file. */

"rootDir": "src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
/* Module Resolution Options */

@@ -19,0 +15,0 @@ "types": [

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is too big to display

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc