Skip to content

Commit

Permalink
Merge pull request #246 from opentok/develop
Browse files Browse the repository at this point in the history
v3.1.0 -- 1080p support
  • Loading branch information
jeffswartz authored Jun 30, 2023
2 parents c81be62 + 127d5d7 commit 6fad234
Show file tree
Hide file tree
Showing 18 changed files with 134 additions and 43 deletions.
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<br/>to this property of<br/>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).
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
8 changes: 7 additions & 1 deletion sample/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ <h1>OpenTok <small>Network test</small></h1>
<div class="options">
Options:
<span>
<input id="checkBox" type="checkbox"> <label>Audio-only</label>
<input id="audioOnlyCheckbox" type="checkbox"> <label>Audio-only</label>
<input id="scalableCheckbox" type="checkbox"> <label>Scalable-video</label>
<input id="fullHdCheckbox" type="checkbox"> <label>Full HD</label>
</span>
<span>
Maximum duration:
Expand All @@ -27,6 +29,7 @@ <h1>OpenTok <small>Network test</small></h1>
</div>
<button>Start test</button>
</div>


<div id="connectivity_status_container" class="prettyBox">
<h2>Testing connectivity</h2>
Expand Down Expand Up @@ -73,6 +76,9 @@ <h3>Video</h3>
<p>Quality:
<span id="video-mos"></span>
</p>
<p>Video limitation:
<span id="video-qualityLimitationReason"></span>
</p>
<p>Bitrate:
<span id="video-bitrate"></span>
</p>
Expand Down
4 changes: 2 additions & 2 deletions sample/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion sample/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@
},
"dependencies": {
"highcharts": "^9.0.0",
"opentok-network-test-js": "file://.."
"opentok-network-test-js": "../"
}
}
Binary file added sample/src/.DS_Store
Binary file not shown.
2 changes: 2 additions & 0 deletions sample/src/js/connectivity-ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
17 changes: 12 additions & 5 deletions sample/src/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
13 changes: 13 additions & 0 deletions src/NetworkTest/errors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
2 changes: 2 additions & 0 deletions src/NetworkTest/errors/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions src/NetworkTest/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export interface NetworkTestOptions {
initSessionOptions?: OT.InitSessionOptions;
proxyServerUrl?: string;
scalableVideo?: boolean;
fullHd?: boolean;
}

export default class NetworkTest {
Expand Down
7 changes: 6 additions & 1 deletion src/NetworkTest/testQuality/helpers/calculateThroughput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 };
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
**/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
84 changes: 63 additions & 21 deletions src/NetworkTest/testQuality/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
*/
Expand All @@ -75,31 +80,67 @@ function connectToSession(session: OT.Session, token: string): Promise<OT.Sessio
}
});
}

/**
* Checks for camera support for a given resolution.
*
* See the "API reference" section of the README.md file in the root of the
* opentok-network-test-js project for details.
*/
function checkCameraSupport(width: number, height: number): Promise<void> {
return new Promise((resolve, reject) => {
navigator.mediaDevices.getUserMedia({
video: {
width: { exact: width },
height: { exact: height },
},
audio: false,
}).then((mediaStream) => {
if (mediaStream) {
resolve();
}
}).catch((error) => {
switch (error.name) {
case 'OverconstrainedError':
reject(new UnsupportedResolutionError());
break;
case 'NotAllowedError':
reject(new PermissionDeniedError());
break;
default:
reject(error);
}
});
});
}
/**
* Ensure that audio and video devices are available
*/
function validateDevices(OT: OT.Client): Promise<AvailableDevices> {
function validateDevices(OT: OT.Client, options?: NetworkTestOptions): Promise<AvailableDevices> {
return new Promise((resolve, reject) => {
OT.getDevices((error?: OT.OTError, devices: OT.Device[] = []) => {

if (error) {
reject(new e.FailedToObtainMediaDevices());
} else {

const availableDevices: AvailableDevices = devices.reduce(
(acc: AvailableDevices, device: OT.Device) => {
const type: AV = device.kind === 'audioInput' ? 'audio' : 'video';
return { ...acc, [type]: { ...acc[type], [device.deviceId]: device } };
},
{ audio: {}, video: {} },
);
return;
}

if (!Object.keys(availableDevices.audio).length) {
reject(new e.NoAudioCaptureDevicesError());
} else {
resolve(availableDevices);
}
const availableDevices: AvailableDevices = devices.reduce(
(acc: AvailableDevices, device: OT.Device) => {
const type: AV = device.kind === 'audioInput' ? 'audio' : 'video';
return { ...acc, [type]: { ...acc[type], [device.deviceId]: device } };
},
{ audio: {}, video: {} },
);

if (!Object.keys(availableDevices.audio).length) {
reject(new e.NoAudioCaptureDevicesError());
return;
}
if (options?.fullHd) {
checkCameraSupport(FULL_HD_WIDTH, FULL_HD_HEIGHT)
.then(() => resolve(availableDevices))
.catch(reject);
} else {
resolve(availableDevices);
}
});
});
Expand All @@ -120,13 +161,14 @@ function publishAndSubscribe(OT: OT.Client, options?: NetworkTestOptions) {
containerDiv.style.opacity = '0';
document.body.appendChild(containerDiv);

validateDevices(OT)
validateDevices(OT, options)
.then((availableDevices: AvailableDevices) => {
if (!Object.keys(availableDevices.video).length) {
audioOnly = true;
}
const publisherOptions: OT.PublisherProperties = {
resolution: '1280x720',
resolution: options.fullHd ? FULL_HD_RESOLUTION : HD_RESOUTION,
scalableVideo: options.scalableVideo,
width: '100%',
height: '100%',
insertMode: 'append',
Expand Down Expand Up @@ -202,7 +244,7 @@ function buildResults(builder: QualityTestResultsBuilder): QualityTestResults {
}
return {
audio: pick(baseProps, builder.state.stats.audio),
video: pick(baseProps.concat(['frameRate', 'recommendedResolution', 'recommendedFrameRate']),
video: pick(baseProps.concat(['frameRate', 'qualityLimitationReason', 'recommendedResolution', 'recommendedFrameRate']),
builder.state.stats.video),
};
}
Expand Down
1 change: 1 addition & 0 deletions src/NetworkTest/testQuality/types/stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export interface AverageStats {
packetLossRatio?: number;
supported?: boolean;
reason?: string;
qualityLimitationReason? : string;
frameRate?: number;
recommendedFrameRate?: number;
recommendedResolution?: string;
Expand Down
2 changes: 2 additions & 0 deletions src/NetworkTest/types/opentok/publisher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export interface GetUserMediaProperties {
frameRate?: 30 | 15 | 7 | 1;
maxResolution?: Dimensions;
resolution?: (
'1920x1080' |
'1280x960' |
'1280x720' |
'640x480' |
Expand All @@ -82,6 +83,7 @@ export interface PublisherProperties extends WidgetProperties, GetUserMediaPrope
publishAudio?: boolean;
publishVideo?: boolean;
resolution?: (
'1920x1080' |
'1280x960' |
'1280x720' |
'640x480' |
Expand Down

0 comments on commit 6fad234

Please sign in to comment.