diff --git a/README.md b/README.md
index 27164511..24d0d4e2 100644
--- a/README.md
+++ b/README.md
@@ -256,6 +256,8 @@ The `OTNetworkTest()` constructor includes the following parameters:
(`true`) or not (`false`, the default). Disabling scalable video
was added in OpenTok.js version 2.24.7.
+ * `fullHd` (Boolean) -- (Optional) Allows publishing with a resolution of 1920x1080. If the camera does not support 1920x1080 resolution, OTNetworkTest.testConnectivity() method is rejected with `UNSUPPORTED_RESOLUTION_ERROR` error.
+
The `options` parameter is optional.
The constructor throws an Error object with a `message` property and a `name` property. The
@@ -421,6 +423,9 @@ following properties:
* `reason` (String) -- A string describing the reason for an unsupported video recommendation.
For example, `'No camera was found.'`
+
+ * `qualityLimitationReason` (String) -- Indicates the reason behind
+ the highest resolution tested failing. It can have values: `'cpu'` for CPU overload, `'bandwidth'` for insufficient network bandwidth, or value is `'null'` if there is no limitation.
* `bitrate` (Number) -- The average number of video bits per second during the last
five seconds of the test. If the the test ran in audio-only mode (for example, because
@@ -567,6 +572,14 @@ method has a `name` property set to one of the following:
| `SUBSCRIBE_TO_SESSION_ERROR` | The test encountered an unknown error while attempting to subscribe to a test stream. |
| `SUBSCRIBER_GET_STATS_ERROR` | The test failed to get audio and video statistics for the test stream. |
+#### Errors thrown by the OTNetworkTest.checkCameraSupport() method
+
+| Error.name property set
to this property of
ErrorNames ... | Description |
+| ------------------------------------------------------------------ | ----------------- |
+| `PERMISSION_DENIED_ERROR` | The user denied access to the camera. |
+| `UNSUPPORTED_RESOLUTION_ERROR` | The camera does not support the requested resolution. |
+
+
## MOS estimates
The `testQuality()` results include MOS estimates for video (if supported) and audio (if supported).
diff --git a/package-lock.json b/package-lock.json
index 1f2498bb..b159e46d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "opentok-network-test-js",
- "version": "3.0.0",
+ "version": "3.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "opentok-network-test-js",
- "version": "3.0.0",
+ "version": "3.1.0",
"license": "MIT",
"dependencies": {
"axios": "^0.21.1",
diff --git a/package.json b/package.json
index 2dd1c4c3..b50a7c39 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "opentok-network-test-js",
- "version": "3.0.0",
+ "version": "3.1.0",
"description": "Precall network test for applications using the OpenTok platform.",
"main": "dist/NetworkTest/index.js",
"types": "dist/NetworkTest/index.d.ts",
diff --git a/sample/index.html b/sample/index.html
index 5e68a8d3..a47152c9 100644
--- a/sample/index.html
+++ b/sample/index.html
@@ -11,7 +11,9 @@
Quality:
+Video limitation: + +
Bitrate:
diff --git a/sample/package-lock.json b/sample/package-lock.json index 96342a67..b6431ef8 100644 --- a/sample/package-lock.json +++ b/sample/package-lock.json @@ -10,14 +10,14 @@ "license": "MIT", "dependencies": { "highcharts": "^9.0.0", - "opentok-network-test-js": "file://.." + "opentok-network-test-js": "../" }, "devDependencies": { "webpack": "^4.39.1" } }, "..": { - "version": "3.0.0", + "version": "3.1.0", "license": "MIT", "dependencies": { "axios": "^0.21.1", diff --git a/sample/package.json b/sample/package.json index 4566bda9..6709d736 100644 --- a/sample/package.json +++ b/sample/package.json @@ -17,6 +17,6 @@ }, "dependencies": { "highcharts": "^9.0.0", - "opentok-network-test-js": "file://.." + "opentok-network-test-js": "../" } } diff --git a/sample/src/.DS_Store b/sample/src/.DS_Store new file mode 100644 index 00000000..dee0e3ef Binary files /dev/null and b/sample/src/.DS_Store differ diff --git a/sample/src/js/connectivity-ui.js b/sample/src/js/connectivity-ui.js index 497f84ef..f2688381 100644 --- a/sample/src/js/connectivity-ui.js +++ b/sample/src/js/connectivity-ui.js @@ -119,6 +119,8 @@ export function displayTestQualityResults(error, results) { + ' (' + rateMosScore(videoMos) + ')'; resultsEl.querySelector('#video-bitrate').textContent = results.video.bitrate ? (results.video.bitrate / 1000).toFixed(2) + ' kbps' : '--'; + resultsEl.querySelector("#video-qualityLimitationReason").textContent = + results.video.qualityLimitationReason ? results.video.qualityLimitationReason : "none"; resultsEl.querySelector('#video-plr').textContent = results.video.packetLossRatio ? (results.video.packetLossRatio * 100).toFixed(2) + '%' : '0.00%'; resultsEl.querySelector('#video-recommendedResolution').textContent = diff --git a/sample/src/js/index.js b/sample/src/js/index.js index 5c54bd7a..d2b38a67 100644 --- a/sample/src/js/index.js +++ b/sample/src/js/index.js @@ -11,16 +11,23 @@ precallDiv.querySelector('#precall button').addEventListener('click', function ( document.getElementById('connectivity_status_container').style.display = 'block'; precallDiv.style.display = 'none'; startTest(); -}) +}); function startTest() { - audioOnly = precallDiv.querySelector('#precall input').checked; - var timeoutSelect = precallDiv.querySelector('select'); - var timeout = timeoutSelect.options[timeoutSelect.selectedIndex].text * 1000; - var options = { + const audioOnly = precallDiv.querySelector('#audioOnlyCheckbox').checked; + const scalableVideo = precallDiv.querySelector('#scalableCheckbox').checked; + const fullHd = precallDiv.querySelector('#fullHdCheckbox').checked; + + const timeoutSelect = precallDiv.querySelector('select'); + const timeout = timeoutSelect.options[timeoutSelect.selectedIndex].text * 1000; + + const options = { audioOnly: audioOnly, + scalableVideo: scalableVideo, + fullHd: fullHd, timeout: timeout }; + otNetworkTest = new NetworkTest(OT, sessionInfo, options); otNetworkTest.testConnectivity() .then(results => ConnectivityUI.displayTestConnectivityResults(results)) diff --git a/src/NetworkTest/errors/index.ts b/src/NetworkTest/errors/index.ts index 2624129b..824f8f94 100644 --- a/src/NetworkTest/errors/index.ts +++ b/src/NetworkTest/errors/index.ts @@ -43,3 +43,16 @@ export class InvalidOnUpdateCallback extends NetworkTestError { ErrorNames.INVALID_ON_UPDATE_CALLBACK); } } +export class PermissionDeniedError extends NetworkTestError { + constructor() { + super('Precall failed to acquire camera due to a permissions error.', + ErrorNames.PERMISSION_DENIED_ERROR); + } +} + +export class UnsupportedResolutionError extends NetworkTestError { + constructor() { + super('The camera does not support the given resolution.', + ErrorNames.UNSUPPORTED_RESOLUTION_ERROR); + } +} diff --git a/src/NetworkTest/errors/types.ts b/src/NetworkTest/errors/types.ts index 1b379049..f9a1b53c 100644 --- a/src/NetworkTest/errors/types.ts +++ b/src/NetworkTest/errors/types.ts @@ -38,6 +38,8 @@ export enum ErrorNames { UNSUPPORTED_BROWSER = 'UnsupportedBrowser', SUBSCRIBER_GET_STATS_ERROR = 'SubscriberGetStatsError', MISSING_SUBSCRIBER_ERROR = 'MissingSubscriberError', + PERMISSION_DENIED_ERROR = 'PermissionDeniedError', + UNSUPPORTED_RESOLUTION_ERROR = 'UnsupportedResolutionError', } export enum OTErrorType { diff --git a/src/NetworkTest/index.ts b/src/NetworkTest/index.ts index 9b80a7aa..31b4e9fa 100644 --- a/src/NetworkTest/index.ts +++ b/src/NetworkTest/index.ts @@ -36,6 +36,7 @@ export interface NetworkTestOptions { initSessionOptions?: OT.InitSessionOptions; proxyServerUrl?: string; scalableVideo?: boolean; + fullHd?: boolean; } export default class NetworkTest { diff --git a/src/NetworkTest/testQuality/helpers/calculateThroughput.ts b/src/NetworkTest/testQuality/helpers/calculateThroughput.ts index 24116d55..a406cfa2 100644 --- a/src/NetworkTest/testQuality/helpers/calculateThroughput.ts +++ b/src/NetworkTest/testQuality/helpers/calculateThroughput.ts @@ -35,6 +35,11 @@ function getAverageBitrateAndPlr(type: AV, publisherStats => publisherStats.simulcastEnabled, ); + const lastPublisherStats = publisherStatsList[publisherStatsList.length - 1]; + + const qualityLimitationReason = lastPublisherStats.videoStats.find( + videoStats => videoStats.qualityLimitationReason !== null)?.qualityLimitationReason || null; + const averageStats: AverageStatsBase = { availableOutgoingBitrate: publisherStatsList[publisherStatsList.length - 1].availableOutgoingBitrate, simulcast: isSimulcastEnabled, @@ -52,7 +57,7 @@ function getAverageBitrateAndPlr(type: AV, recommendedFrameRate, frameRate: sumFrameRate / subscriberStatsList.length, } : {}; - return { ...averageStats, supported, reason, ...videoStats }; + return { ...averageStats, supported, reason, qualityLimitationReason, ...videoStats }; } return { ...averageStats }; } diff --git a/src/NetworkTest/testQuality/helpers/getPublisherRtcStatsReport.ts b/src/NetworkTest/testQuality/helpers/getPublisherRtcStatsReport.ts index 06c033b4..17e13c8a 100644 --- a/src/NetworkTest/testQuality/helpers/getPublisherRtcStatsReport.ts +++ b/src/NetworkTest/testQuality/helpers/getPublisherRtcStatsReport.ts @@ -76,7 +76,7 @@ const extractOutboundRtpStats = ( const baseStats = { kbs, ssrc, byteSent, currentTimestamp }; videoStats.push({ ...baseStats, - qualityLimitationReason: stats.qualityLimitationReason || 'N/A', + qualityLimitationReason: stats.qualityLimitationReason, resolution: `${stats.frameWidth || 0}x${stats.frameHeight || 0}`, framerate: stats.framesPerSecond || 0, active: stats.active || false, @@ -131,13 +131,10 @@ const extractPublisherStats = ( 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("currentRoundTripTime: ", currentRoundTripTime); + console.info("simulcastEnabled: ", simulcastEnabled); + console.info("transportProtocol: ", transportProtocol); console.info("availableOutgoingBitrate: ", availableOutgoingBitrate); console.info("videoByteSent: ", videoByteSent); **/ diff --git a/src/NetworkTest/testQuality/helpers/getUpdateCallbackStats.ts b/src/NetworkTest/testQuality/helpers/getUpdateCallbackStats.ts index 50467aba..ef09575b 100644 --- a/src/NetworkTest/testQuality/helpers/getUpdateCallbackStats.ts +++ b/src/NetworkTest/testQuality/helpers/getUpdateCallbackStats.ts @@ -4,7 +4,7 @@ import { UpdateCallbackStats, CallbackTrackStats } from '../../types/callbacks'; const getUpdateCallbackStats = ( subscriberStats: OT.SubscriberStats, publisherStats: OT.PublisherStats, - phase: string + phase: string, ): UpdateCallbackStats => { const { audio: audioTrackStats, video: videoTrackStats } = subscriberStats; diff --git a/src/NetworkTest/testQuality/index.ts b/src/NetworkTest/testQuality/index.ts index f33a359a..db120742 100644 --- a/src/NetworkTest/testQuality/index.ts +++ b/src/NetworkTest/testQuality/index.ts @@ -27,6 +27,12 @@ import MOSState from './helpers/MOSState'; import config from './helpers/config'; import isSupportedBrowser from './helpers/isSupportedBrowser'; import getUpdateCallbackStats from './helpers/getUpdateCallbackStats'; +import { PermissionDeniedError, UnsupportedResolutionError } from '../errors'; + +const FULL_HD_WIDTH = 1920; +const FULL_HD_HEIGHT = 1080; +const FULL_HD_RESOLUTION = '1920x1080'; +const HD_RESOUTION = '1280x720'; interface QualityTestResultsBuilder { state: MOSState; @@ -49,7 +55,6 @@ let stopTest: Function | undefined; let stopTestTimeoutId: number; let stopTestTimeoutCompleted = false; let stopTestCalled = false; - /** * If not already connected, connect to the OpenTok Session */ @@ -75,31 +80,67 @@ function connectToSession(session: OT.Session, token: string): Promise