/*
* Copyright (c) 2015 The WebRTC project authors. All Rights Reserved.
*
* Use of this source code is governed by a BSD-style license
* that can be found in the LICENSE file in the root of the source
* tree.
*/
'use strict';
const MAX_CHUNK_SIZE = 262144;
let localConnection;
let remoteConnection;
let sendChannel;
let receiveChannel;
let chunkSize;
let lowWaterMark;
let highWaterMark;
let dataString;
let timeoutHandle = null;
const megsToSend = document.querySelector('input#megsToSend');
const sendButton = document.querySelector('button#sendTheData');
const orderedCheckbox = document.querySelector('input#ordered');
const sendProgress = document.querySelector('progress#sendProgress');
const receiveProgress = document.querySelector('progress#receiveProgress');
const errorMessage = document.querySelector('div#errorMsg');
const transferStatus = document.querySelector('span#transferStatus');
let bytesToSend = 0;
let totalTimeUsedInSend = 0;
let numberOfSendCalls = 0;
let maxTimeUsedInSend = 0;
let sendStartTime = 0;
let currentThroughput = 0;
sendButton.addEventListener('click', createConnection);
// Prevent data sent to be set to 0.
megsToSend.addEventListener('change', function() {
const number = this.value;
if (Number.isNaN(number)) {
errorMessage.innerHTML = `Invalid value for MB to send: ${number}`;
} else if (number <= 0) {
sendButton.disabled = true;
errorMessage.innerHTML = '<p>Please enter a number greater than zero.</p>';
} else if (number > 64) {
sendButton.disabled = true;
errorMessage.innerHTML = '<p>Please enter a number lower or equal than 64.</p>';
} else {
errorMessage.innerHTML = '';
sendButton.disabled = false;
}
});
async function createConnection() {
sendButton.disabled = true;
megsToSend.disabled = true;
const servers = null;
const number = Number.parseInt(megsToSend.value);
bytesToSend = number * 1024 * 1024;
localConnection = new RTCPeerConnection(servers);
// Let's make a data channel!
const dataChannelParams = {ordered: false};
if (orderedCheckbox.checked) {
dataChannelParams.ordered = true;
}
sendChannel = localConnection.createDataChannel('sendDataChannel', dataChannelParams);
sendChannel.addEventListener('open', onSendChannelOpen);
sendChannel.addEventListener('close', onSendChannelClosed);
console.log('Created send data channel: ', sendChannel);
console.log('Created local peer connection object localConnection: ', localConnection);
localConnection.addEventListener('icecandidate', e => onIceCandidate(localConnection, e));
remoteConnection = new RTCPeerConnection(servers);
remoteConnection.addEventListener('icecandidate', e => onIceCandidate(remoteConnection, e));
remoteConnection.addEventListener('datachannel', receiveChannelCallback);
try {
const localOffer = await localConnection.createOffer();
await handleLocalDescription(localOffer);
} catch (e) {
console.error('Failed to create session description: ', e);
}
transferStatus.innerHTML = 'Peer connection setup complete.';
}
function sendData() {
// Stop scheduled timer if any (part of the workaround introduced below)
if (timeoutHandle !== null) {
clearTimeout(timeoutHandle);
timeoutHandle = null;
}
let bufferedAmount = sendChannel.bufferedAmount;
while (sendProgress.value < sendProgress.max) {
transferStatus.innerText = 'Sending data...';
const timeBefore = performance.now();
sendChannel.send(dataString);
const timeUsed = performance.now() - timeBefore;
if (timeUsed > maxTimeUsedInSend) {
maxTimeUsedInSend = timeUsed;
totalTimeUsedInSend += timeUsed;
}
numberOfSendCalls += 1;
bufferedAmount += chunkSize;
sendProgress.value += chunkSize;
// Pause sending if we reach the high water mark
if (bufferedAmount >= highWaterMark) {
// This is a workaround due to the bug that all browsers are incorrectly calculating the
// amount of buffered data. Therefore, the 'bufferedamountlow' event would not fire.
if (sendChannel.bufferedAmount < lowWaterMark) {
timeoutHandle = setTimeout(() => sendData(), 0);
}
console.log(`Paused sending, buffered amount: ${bufferedAmount} (announced: ${sendChannel.bufferedAmount})`);
break;
}
}
if (sendProgress.value === sendProgress.max) {
transferStatus.innerHTML = 'Data transfer completed successfully!';
}
}
function startSendingData() {
transferStatus.innerHTML = 'Start sending data.';
sendProgress.max = bytesToSend;
receiveProgress.max = sendProgress.max;
sendProgress.value = 0;
receiveProgress.value = 0;
sendStartTime = performance.now();
maxTimeUsedInSend = 0;
totalTimeUsedInSend = 0;
numberOfSendCalls = 0;
sendData();
}
function maybeReset() {
if (localConnection === null && remoteConnection === null) {
sendButton.disabled = false;
megsToSend.disabled = false;
}
}
async function handleLocalDescription(desc) {
localConnection.setLocalDescription(desc);
console.log('Offer from localConnection:\n', desc.sdp);
remoteConnection.setRemoteDescription(desc);
try {
const remoteAnswer = await remoteConnection.createAnswer();
handleRemoteAnswer(remoteAnswer);
} catch (e) {
console.error('Error when creating remote answer: ', e);
}
}
function handleRemoteAnswer(desc) {
remoteConnection.setLocalDescription(desc);
console.log('Answer from remoteConnection:\n', desc.sdp);
localConnection.setRemoteDescription(desc);
}
function getOtherPc(pc) {
return (pc === localConnection) ? remoteConnection : localConnection;
}
async function onIceCandidate(pc, event) {
const candidate = event.candidate;
if (candidate === null) {
return;
} // Ignore null candidates
try {
await getOtherPc(pc).addIceCandidate(candidate);
console.log('AddIceCandidate successful: ', candidate);
} catch (e) {
console.error('Failed to add Ice Candidate: ', e);
}
}
function receiveChannelCallback(event) {
console.log('Receive Channel Callback');
receiveChannel = event.channel;
receiveChannel.binaryType = 'arraybuffer';
receiveChannel.addEventListener('close', onReceiveChannelClosed);
receiveChannel.addEventListener('message', onReceiveMessageCallback);
}
function onReceiveMessageCallback(event) {
receiveProgress.value += event.data.length;
currentThroughput = receiveProgress.value / (performance.now() - sendStartTime);
console.log('Current Throughput is:', currentThroughput, 'bytes/sec');
// Workaround for a bug in Chrome which prevents the closing event from being raised by the
// remote side. Also a workaround for Firefox which does not send all pending data when closing
// the channel.
if (receiveProgress.value === receiveProgress.max) {
sendChannel.close();
receiveChannel.close();
}
}
function onSendChannelOpen() {
console.log('Send channel is open');
chunkSize = Math.min(localConnection.sctp.maxMessageSize, MAX_CHUNK_SIZE);
console.log('Determined chunk size: ', chunkSize);
dataString = new Array(chunkSize).fill('X').join('');
lowWaterMark = chunkSize; // A single chunk
highWaterMark = Math.max(chunkSize * 8, 1048576); // 8 chunks or at least 1 MiB
console.log('Send buffer low water threshold: ', lowWaterMark);
console.log('Send buffer high water threshold: ', highWaterMark);
sendChannel.bufferedAmountLowThreshold = lowWaterMark;
sendChannel.addEventListener('bufferedamountlow', (e) => {
console.log('BufferedAmountLow event:', e);
sendData();
});
startSendingData();
}
function onSendChannelClosed() {
console.log('Send channel is closed');
localConnection.close();
localConnection = null;
console.log('Closed local peer connection');
maybeReset();
console.log('Average time spent in send() (ms): ' +
totalTimeUsedInSend / numberOfSendCalls);
console.log('Max time spent in send() (ms): ' + maxTimeUsedInSend);
const spentTime = performance.now() - sendStartTime;
console.log('Total time spent: ' + spentTime);
console.log('MBytes/Sec: ' + (bytesToSend / 1000) / spentTime);
}
function onReceiveChannelClosed() {
console.log('Receive channel is closed');
remoteConnection.close();
remoteConnection = null;
console.log('Closed remote peer connection');
maybeReset();
}