diff --git a/src/page/test/bandwidthDetection/README.md b/src/page/test/bandwidthDetection/README.md new file mode 100644 index 00000000..2552181c --- /dev/null +++ b/src/page/test/bandwidthDetection/README.md @@ -0,0 +1,27 @@ +# Bandwidth Detection using Red5 Pro + +This is an example of using the bandwidth detection webapp on a Red5 Pro server to determine the available bandwidth for streaming. + +Note that because this speed test is between the end-user's browser and the Red5 Pro server, the speed measured won't be the same as that from other sources. Not only is the user's connection speed tested, the specific speed that can be expected between the client and server is tested. Since all of the factors that will affect the connection - including distance, ssh encryption - if any, and server load - will also affect responses to this speed test, it can give you a better idea of a realistic streaming quality limit than a generic optimized test to a nearby server. + +**Please refer to the [Basic Publisher Documentation](../publisher/README.md) to learn more about the basic setup.** + +## Example Code +- **[index.html](index.html)** +- **[index.js](index.js)** +- **[bandwidthDetection.js](bandwidthDetection.js)** + +# Running The Speed Test +There are three functions available for testing different speeds: `checkDownloadSpeed`, `checkUploadSpeed`, and `checkSpeeds` which will call both in sequence and return the combined result. They each take a url, and a maximum time to spend on the test - in seconds. Note that for checkSpeeds, this time is split evenly between the upload and download tests. This returns a Promise which resolves with an object, holing the results as floats in its `upload` and/or `download` property, as appropriate. + +``` +checkSpeeds(config.host, 5.0) + .then( result => { + document.getElementById("speed-check-print").innerText = "Bandwidth Detection complete," + + "Uploading at: " + (Math.round( result.upload *100)/100.0) + "KbpS and downloading at: " + + (Math.round( result.download *100)/100.0) + "KbpS"; +``` + +[index.js #148](index.js#L148) + +From there, the results can be used to determine the target bandwidth for the stream, or to determine if the client has the bandwidth to successfully subscribe to a stream. Also be aware that this check should be done before starting any streams, as the concentration of data can conflict with any streams in progress. diff --git a/src/page/test/bandwidthDetection/bandwidthDetection.js b/src/page/test/bandwidthDetection/bandwidthDetection.js new file mode 100644 index 00000000..b4d78f45 --- /dev/null +++ b/src/page/test/bandwidthDetection/bandwidthDetection.js @@ -0,0 +1,204 @@ + +//***DOWNLOAD*** + +const CONCURRENT_CONNECTIONS = 4; + +function checkDownloadSpeed (baseURL, maxSeconds) { + + const isSecure = window.location.protocol.includes("https"); + baseURL = isSecure ? "https://" + baseURL : "http://" + baseURL + ":5080"; + + return new Promise( (resolve, reject) => { + const now = Date.now(); + const maxMillis = Math.floor(maxSeconds * 1000); + + const data = { + beganAt: now, + returnBy: now + maxMillis, + url: baseURL + "/bandwidthdetection/detect", + downloadResults: [], + requests:[], + resolve: resolve, + reject: reject + }; + + for (var i = 0; i < CONCURRENT_CONNECTIONS; i++) { + createDownloader(data); + } + }); +} + +function createDownloader(data) { + const request = new XMLHttpRequest(); + request.onreadystatechange = () => { + if(request.readyState === 4){ + if(request.status === 200){ //successful - add up the speed results as appropriate + data.downloadResults.push(request.response.size); + } else{ //unsuccesful, ignore the attempt, I guess? We'll try again plenty, I'm sure. + console.warn("Download detection failed with the following status: " + request.statusText); + } + removeRequest(data, request); + downloadLoop(data); + } + }; + request.ontimeout = () => { //Pencils down, time to give the data back. + console.warn("Download detection timed out"); + removeRequest(data, request); + downloadLoop(data); + }; + + data.requests.push(request); + + request.open("GET", data.url, true); + request.responseType = "blob"; + request.timeout = 5000; + request.send(null); +} + +function downloadLoop(data) { + const now = Date.now(); + + if (now < data.returnBy) { //We have more time, keep downloading + while(data.requests.length < CONCURRENT_CONNECTIONS){ + createDownloader(data); + } + } else { // Time's up. Once everything finishes, return the results + if(data.requests.length < 1){ + const totalSeconds = (Date.now() - data.beganAt) / 1000.0; + let totalBytes = 0; + for (var i = 0; i < data.downloadResults.length; i++) { + totalBytes += data.downloadResults[i]; + } + if(totalBytes == 0){ + data.reject("There was a problem with the download test, the server sent no data"); + } + console.log("Downloaded " + totalBytes + " bytes in " + totalSeconds + " seconds"); + const kbpS = ((totalBytes * 8) / 1024.0) / totalSeconds; + console.log("Download detection finished with speed result of " + kbpS + "KBpS"); + data.resolve({ download: kbpS }); + } + } +} + +//***UPLOAD*** + +function checkUploadSpeed (baseURL, maxSeconds) { + + const isSecure = window.location.protocol.includes("https"); + baseURL = isSecure ? "https://" + baseURL : "http://" + baseURL + ":5080"; + + return new Promise( (resolve, reject) => { + const now = Date.now(); + const maxMillis = Math.floor(maxSeconds * 1000); + + const data = { + beganAt: now, + returnBy: now + maxMillis, + url: baseURL + "/bandwidthdetection/detect", + uploadResults: [], + requests:[], + resolve: resolve, + reject: reject + }; + + for (var i = 0; i < CONCURRENT_CONNECTIONS; i++) { + createUploader(data); + } + }); +} + +function createUploader(data) { + const request = new XMLHttpRequest(); + request.onreadystatechange = () => { + if(request.readyState === 4){ + if(request.status === 200){ //successful - add up the speed results as appropriate + data.uploadResults.push(40 * 1024); //upload is always 40KB, might as well re-use logic from download + } else{ //unsuccesful, ignore the attempt, we'll try again plenty + console.warn("Upload detection failed with the following status: " + request.statusText); + } + removeRequest(data, request); + uploadLoop(data); + } + }; + request.ontimeout = () => { + console.warn("Upload detection timed out"); + removeRequest(data, request); + uploadLoop(data); + }; + + data.requests.push(request); + + request.open("POST", data.url, true); + request.timeout = 5000; + //send 40kB of random data + request.send( fortyKiloString() ); +} + +function uploadLoop(data) { + const now = Date.now(); + + if (now < data.returnBy) { //We have more time, keep uploading + while(data.requests.length < CONCURRENT_CONNECTIONS){ + createUploader(data); + } + } else { // Time's up. Once everything finishes, return the results + if(data.requests.length < 1){ + const totalSeconds = (Date.now() - data.beganAt) / 1000.0; + let totalBytes = 0; + for (var i = 0; i < data.uploadResults.length; i++) { + totalBytes += data.uploadResults[i]; + } + if(totalBytes == 0){ + data.reject("There was a problem with the upload test, no data reached the server"); + } + console.log("Uploaded " + totalBytes + " bytes in " + totalSeconds + " seconds"); + const kbpS = ((totalBytes * 8) / 1024.0) / totalSeconds; + console.log("Upload detection finished with speed result of " + kbpS + "KbpS"); + data.resolve({ upload: kbpS }); + } + } +} + +//***BOTH*** + +function checkSpeeds (baseURL, maxSeconds) { + + return new Promise(function(resolve, reject) { + + const halfMaxSeconds = maxSeconds / 2.0; + const ret = { upload: -1, download: -1 }; + + checkDownloadSpeed(baseURL, halfMaxSeconds) + .then(result => { + ret.download = result.download; + return checkUploadSpeed(baseURL, halfMaxSeconds); + }) + .then(result => { + ret.upload = result.upload; + resolve(ret); + }) + .catch(error => { + reject(error); + }); + }); +} + +//***SUPPORT*** + +function removeRequest(data, request) { + for (let i = 0; i < data.requests.length; i++) { + if(data.requests[i] === request){ + data.requests.splice(i, 1); + return; + } + } +} + +var allowedChar = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; +function fortyKiloString() { + const out = []; + for (let i = 0; i < 40960; i++){ + out.push( allowedChar.charAt(Math.floor(Math.random() * allowedChar.length)) ); + } + return out.join(''); +} diff --git a/src/page/test/bandwidthDetection/index.html b/src/page/test/bandwidthDetection/index.html new file mode 100644 index 00000000..6a871524 --- /dev/null +++ b/src/page/test/bandwidthDetection/index.html @@ -0,0 +1,24 @@ + + + + {{> meta title='Bandwidth Detection Test'}} + {{> header-scripts}} + {{> header-stylesheets}} + + +
+ {{> version }} + {{> settings-link}} + {{> test-info testTitle='Bandwidth Detection Test'}} + {{> status-field-publisher}} + {{> statistics-field}} +
+ +
+
+

Speed test in progress...

+ {{> body-scripts}} + + + + diff --git a/src/page/test/bandwidthDetection/index.js b/src/page/test/bandwidthDetection/index.js new file mode 100644 index 00000000..3091187b --- /dev/null +++ b/src/page/test/bandwidthDetection/index.js @@ -0,0 +1,187 @@ +(function(window, document, red5prosdk) { + 'use strict'; + + var serverSettings = (function() { + var settings = sessionStorage.getItem('r5proServerSettings'); + try { + return JSON.parse(settings); + } + catch (e) { + console.error('Could not read server settings from sessionstorage: ' + e.message); + } + return {}; + })(); + + var configuration = (function () { + var conf = sessionStorage.getItem('r5proTestBed'); + try { + return JSON.parse(conf); + } + catch (e) { + console.error('Could not read testbed configuration from sessionstorage: ' + e.message); + } + return {} + })(); + + red5prosdk.setLogLevel(configuration.verboseLogging ? red5prosdk.LOG_LEVELS.TRACE : red5prosdk.LOG_LEVELS.WARN); + + + var targetPublisher; + + var updateStatusFromEvent = window.red5proHandlePublisherEvent; // defined in src/template/partial/status-field-publisher.hbs + var streamTitle = document.getElementById('stream-title'); + var statisticsField = document.getElementById('statistics-field'); + + var protocol = serverSettings.protocol; + var isSecure = protocol == 'https'; + function getSocketLocationFromProtocol () { + return !isSecure + ? {protocol: 'ws', port: serverSettings.wsport} + : {protocol: 'wss', port: serverSettings.wssport}; + } + + function onBitrateUpdate (bitrate, packetsSent) { + statisticsField.innerText = 'Bitrate: ' + Math.floor(bitrate) + '. Packets Sent: ' + packetsSent + '.'; + } + + function onPublisherEvent (event) { + console.log('[Red5ProPublisher] ' + event.type + '.'); + updateStatusFromEvent(event); + } + function onPublishFail (message) { + console.error('[Red5ProPublisher] Publish Error :: ' + message); + } + function onPublishSuccess (publisher) { + console.log('[Red5ProPublisher] Publish Complete.'); + try { + window.trackBitrate(publisher.getPeerConnection(), onBitrateUpdate); + } + catch (e) { + // no tracking for you! + } + } + function onUnpublishFail (message) { + console.error('[Red5ProPublisher] Unpublish Error :: ' + message); + } + function onUnpublishSuccess () { + console.log('[Red5ProPublisher] Unpublish Complete.'); + } + + function getAuthenticationParams () { + var auth = configuration.authentication; + return auth && auth.enabled + ? { + connectionParams: { + username: auth.username, + password: auth.password + } + } + : {}; + } + + function getUserMediaConfiguration () { + return { + mediaConstraints: { + audio: configuration.useAudio ? configuration.mediaConstraints.audio : false, + video: configuration.useVideo ? configuration.mediaConstraints.video : false + } + }; + } + + function getRTMPMediaConfiguration () { + return { + mediaConstraints: { + audio: configuration.useAudio ? configuration.mediaConstraints.audio : false, + video: configuration.useVideo ? { + width: configuration.cameraWidth, + height: configuration.cameraHeight + } : false + } + } + } + + function unpublish () { + return new Promise(function (resolve, reject) { + var publisher = targetPublisher; + publisher.unpublish() + .then(function () { + onUnpublishSuccess(); + resolve(); + }) + .catch(function (error) { + var jsonError = typeof error === 'string' ? error : JSON.stringify(error, 2, null); + onUnpublishFail('Unmount Error ' + jsonError); + reject(error); + }); + }); + } + + var config = Object.assign({}, + configuration, + getAuthenticationParams(), + getUserMediaConfiguration()); + + var rtcConfig = Object.assign({}, config, { + protocol: getSocketLocationFromProtocol().protocol, + port: getSocketLocationFromProtocol().port, + streamName: config.stream1, + }); + var rtmpConfig = Object.assign({}, config, { + protocol: 'rtmp', + port: serverSettings.rtmpport, + streamName: config.stream1, + backgroundColor: '#000000', + swf: '../../lib/red5pro/red5pro-publisher.swf', + swfobjectURL: '../../lib/swfobject/swfobject.js', + productInstallURL: '../../lib/swfobject/playerProductInstall.swf' + }, getRTMPMediaConfiguration()); + var publishOrder = config.publisherFailoverOrder + .split(',') + .map(function (item) { + return item.trim() + }); + + if (window.query('view')) { + publishOrder = [window.query('view')]; + } + + checkSpeeds(config.host, 5.0) + .then( result => { + document.getElementById("speed-check-print").innerText = "Bandwidth Detection complete," + + "Uploading at: " + (Math.round(result.upload*100)/100.0) + "KbpS and downloading at: " + + (Math.round(result.download*100)/100.0) + "KbpS"; + + var publisher = new red5prosdk.Red5ProPublisher(); + publisher.setPublishOrder(publishOrder) + .init({ + rtc: rtcConfig, + rtmp: rtmpConfig + }) + .then(function (publisherImpl) { + streamTitle.innerText = configuration.stream1; + targetPublisher = publisherImpl; + targetPublisher.on('*', onPublisherEvent); + return targetPublisher.publish(); + }) + .then(function () { + onPublishSuccess(targetPublisher); + }) + .catch(function (error) { + var jsonError = typeof error === 'string' ? error : JSON.stringify(error, null, 2); + console.error('[Red5ProPublisher] :: Error in publishing - ' + jsonError); + onPublishFail(jsonError); + }); + }); + + window.addEventListener('beforeunload', function() { + function clearRefs () { + if (targetPublisher) { + targetPublisher.off('*', onPublisherEvent); + } + targetPublisher = undefined; + } + unpublish().then(clearRefs).catch(clearRefs); + window.untrackBitrate(); + }); + +})(this, document, window.red5prosdk); diff --git a/src/page/testbed-menu.html b/src/page/testbed-menu.html index 11475de8..38424671 100644 --- a/src/page/testbed-menu.html +++ b/src/page/testbed-menu.html @@ -38,6 +38,7 @@

Red5 Pro HTML Testbed

  • Publish - Image Capture (WebRTC)
  • Publish - ScreenShare (WebRTC)
  • Publish - Server Call (WebRTC)
  • +
  • Publish - Bandwidth Detection
  • Subscribe
  • Subscribe - Authentication
  • Subscribe - RoundTrip Authentication