diff --git a/src/page/sm-test/PubSubStreamManagerProxy/README.md b/src/page/sm-test/PubSubStreamManagerProxy/README.md new file mode 100644 index 00000000..7306eba5 --- /dev/null +++ b/src/page/sm-test/PubSubStreamManagerProxy/README.md @@ -0,0 +1,56 @@ +# Two Way Stream Manager Proxy Video Chat + +This example demonstrates two way communication using Red5 Pro. It also demonstrates using servlet requests on the server. + +> The Two-Way example requires access to a service that returns a stream listing. You may run into Cross-Origin Resource Sharing (**CORS**) issues if trying to use this example without the proper **CORS** settings provided by the server. + +It is recommended to view this example as part of the `webrtcexamples` webapp shipped with the [Red5 Pro Server](https://account.red5pro.com/download). + +More information on CORS can be found at: [https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS). +## Publisher Stream Manager Proxy + +**Please refer to the [Publisher Stream Manager Proxy Documentation](../publishStreamManagerProxy/README.md) to learn more about the setup of a publisher through a stream manager.** + +## Subscriber Stream Manager Proxy + +**Please refer to the [Subscriber Stream Manager Proxy Documentation](../subscribeStreamManagerProxy/README.md) to learn more about the setup of a subscriber through a stream manager.** + +## Basic Two Way + +**Please refer to the [Basic Two Way Documentation](../../test/twoWay/README.md) to learn more about the basic setup of a publisher.** + +## Example Code + +- **[index.html](index.html)** +- **[index.js](index.js)** + +> These examples use the WebRTC-based Subscriber implementation from the Red5 Pro HTML SDK. However, there is failover support to allow for Flash-based subscriber on unsupported browsers. + +# Setup + +Two way communication simply requires setting up a publish stream and a subscribe stream at the same time. You can test the example with two browser pages. On the second browser page, use the *Settings* page to swap the names of the stream. + +The subscriber portion will automatically connect when the second person begins streaming. + +# Getting Live Streams + +To get all the streams that a stream manager is handling on a cluster, use the `list` function of the stream manager api. + +```js +var baseUrl = isSecure ? protocol + '://' + host : protocol + '://' + host + portURI; +var url = baseUrl + '/streammanager/api/2.0/event/list'; +fetch(url) + .then(function (res) { + if (res.headers.get('content-type') && + res.headers.get('content-type').toLowerCase().indexOf('application/json') >= 0) { + return res.json(); + } + else { + return res.text(); + } + }) +``` + +[index #202](index.js#L202) + +After that, it's handled the same way that the returned data from the `streams` servlet would have been. For more information on this and other parts of the Stream Manager API, see our dcumentation [here](https://www.red5pro.com/docs/autoscale/streammanagerapi-v2.html) diff --git a/src/page/sm-test/PubSubStreamManagerProxy/index.html b/src/page/sm-test/PubSubStreamManagerProxy/index.html new file mode 100644 index 00000000..a78dbcaa --- /dev/null +++ b/src/page/sm-test/PubSubStreamManagerProxy/index.html @@ -0,0 +1,89 @@ + +{{> license}} + + + {{> meta title='Pub & Sub Stream Manager Test'}} + {{> header-scripts}} + {{> header-stylesheets}} + + + + {{> top-bar }} + {{> navigation isTestPage=true }} +
+ {{> settings-link}} +
+
+

The Pub&Sub example requires access to a service that returns a stream listing. You may run into Cross-Origin Resource Sharing (CORS) issues if trying to use this example without the proper CORS settings provided by the server.

+
+

It is recommended to view this example as part of the webrtcexamples webapp shipped with the Red5 Pro Server.

+
+
+ {{> test-title title='Stream Manager Proxy Pub&Sub Test'}} +
+
+

Publisher Stream: N/A

+

On hold.

+ {{> statistics-field packets_field='Packets Sent'}} + +
+
+

Subscriber Stream: N/A

+

On hold.

+ {{> statistics-field packets_field='Packets Received'}} + +
+
+
+ {{> body-scripts}} + {{> mobile-subscriber-util}} + + + + + diff --git a/src/page/sm-test/PubSubStreamManagerProxy/index.js b/src/page/sm-test/PubSubStreamManagerProxy/index.js new file mode 100644 index 00000000..6b7d87e9 --- /dev/null +++ b/src/page/sm-test/PubSubStreamManagerProxy/index.js @@ -0,0 +1,522 @@ +/* +Copyright © 2015 Infrared5, Inc. All rights reserved. + +The accompanying code comprising examples for use solely in conjunction with Red5 Pro (the "Example Code") +is licensed to you by Infrared5 Inc. in consideration of your agreement to the following +license terms and conditions. Access, use, modification, or redistribution of the accompanying +code constitutes your acceptance of the following license terms and conditions. + +Permission is hereby granted, free of charge, to you to use the Example Code and associated documentation +files (collectively, the "Software") without restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, subject to the following conditions: + +The Software shall be used solely in conjunction with Red5 Pro. Red5 Pro is licensed under a separate end +user license agreement (the "EULA"), which must be executed with Infrared5, Inc. +An example of the EULA can be found on our website at: https://account.red5pro.com/assets/LICENSE.txt. + +The above copyright notice and this license shall be included in all copies or portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL INFRARED5, INC. BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +(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 pubStatusField = document.getElementById('pub-status-field'); + var pubStreamTitle = document.getElementById('pub-stream-title'); + + var targetSubscriber; + var subStatusField = document.getElementById('sub-status-field'); + var subStreamTitle = document.getElementById('sub-stream-title'); + var statisticsFields = document.getElementsByClassName('statistics-field'); + + var timeout = 0 + var instanceId = Math.floor(Math.random() * 0x10000).toString(16); + var protocol = serverSettings.protocol; + var isSecure = protocol === 'https'; + function getSocketLocationFromProtocol () { + return !isSecure + ? {protocol: 'ws', port: serverSettings.wsport} + : {protocol: 'wss', port: serverSettings.wssport}; + } + + var defaultSubscriberConfiguration = (function(useVideo, useAudio) { + var c = { + protocol: getSocketLocationFromProtocol().protocol, + port: getSocketLocationFromProtocol().port, + streamMode: configuration.recordBroadcast ? 'record' : 'live' + }; + if (!useVideo) { + c.videoEncoding = red5prosdk.PlaybackVideoEncoder.NONE; + } + if (!useAudio) { + c.audioEncoding = red5prosdk.PlaybackAudioEncoder.NONE; + } + return c; + })(configuration.useVideo, configuration.useAudio); + + var updateStatusFromPublishEvent = window.red5proHandlePublisherEvent; // defined in src/template/partial/status-field-publisher.hbs + var updateStatusFromSubscribeEvent = window.red5proHandleSubscriberEvent; // defined in src/template/partial/status-field-subscriber.hbs + + function onPublisherEvent (event) { + console.log('[Red5ProPublisher] ' + event.type + '.'); + updateStatusFromPublishEvent(event, pubStatusField); + } + function onPublishFail (message) { + console.error('[Red5ProPublisher] Publish Error :: ' + message); + } + function onPublishSuccess (publisher) { + console.log('[Red5ProPublisher] Publish Complete.'); + if (window.exposePublisherGlobally) { + window.exposePublisherGlobally(publisher); + } + (function (pub, index) { + if (pub.getType().toLowerCase() === 'rtc') { + try { + var bitrate = 0; + var packets = 0; + var frameWidth = 0; + var frameHeight = 0; + var bitrateField = statisticsFields[index].getElementsByClassName('bitrate-field')[0]; + var packetsField = statisticsFields[index].getElementsByClassName('packets-field')[0]; + var resolutionField = statisticsFields[index].getElementsByClassName('resolution-field')[0]; + + var updateStatisticsField = function (b, p, w, h) { + statisticsFields[index].classList.remove('hidden'); + bitrateField.innerText = Math.floor(b); + packetsField.innerText = p; + resolutionField.innerText = w + 'x' + h; + } + var onBitrateUpdate = function (b, p) { + bitrate = b; + packets = p + updateStatisticsField(bitrate, packets, frameWidth, frameHeight); + } + var onResolutionUpdate = function (w, h) { + frameWidth = w; + frameHeight = h; + updateStatisticsField(bitrate, packets, frameWidth, frameHeight); + } + var pc = pub.getPeerConnection(); + var stream = pub.getMediaStream(); + window.trackBitrate(pc, onBitrateUpdate, null, false, true); + stream.getVideoTracks().forEach(function (track) { + var settings = track.getSettings(); + onResolutionUpdate(settings.width, settings.height); + }); + } catch (e) { + // + } + } + })(publisher, 0); + } + function onUnpublishFail (message) { + console.error('[Red5ProPublisher] Unpublish Error :: ' + message); + } + function onUnpublishSuccess () { + console.log('[Red5ProPublisher] Unpublish Complete.'); + } + function onSubscriberEvent (event) { + console.log('[Red5ProSubsriber] ' + event.type + '.'); + updateStatusFromSubscribeEvent(event, subStatusField); + if (event.type === 'Subscribe.VideoDimensions.Change') { + var resolutionField = statisticsFields[1].getElementsByClassName('resolution-field')[0]; + resolutionField.text = event.data.width + 'x' + event.data.height; + } + } + function onSubscribeFail (message) { + console.error('[Red5ProSubsriber] Subscribe Error :: ' + message); + } + function onSubscribeSuccess (subscriber) { + console.log('[Red5ProSubsriber] Subscribe Complete.'); + if (window.exposeSubscriberGlobally) { + window.exposeSubscriberGlobally(subscriber); + } + (function (sub, index) { + if (sub.getType().toLowerCase() === 'rtc') { + try { + var bitrate = 0; + var packets = 0; + var frameWidth = 0; + var frameHeight = 0; + var bitrateField = statisticsFields[index].getElementsByClassName('bitrate-field')[0]; + var packetsField = statisticsFields[index].getElementsByClassName('packets-field')[0]; + var resolutionField = statisticsFields[index].getElementsByClassName('resolution-field')[0]; + + var updateStatisticsField = function (b, p, w, h) { + statisticsFields[index].classList.remove('hidden'); + bitrateField.innerText = Math.floor(b); + packetsField.innerText = p; + resolutionField.innerText = w + 'x' + h; + } + var onBitrateUpdate = function (b, p) { + bitrate = b; + packets = p + updateStatisticsField(bitrate, packets, frameWidth, frameHeight); + } + var onResolutionUpdate = function (w, h) { + frameWidth = w; + frameHeight = h; + updateStatisticsField(bitrate, packets, frameWidth, frameHeight); + } + window.trackBitrate(sub.getPeerConnection(), onBitrateUpdate, onResolutionUpdate, true, true); + } catch (e) { + // + } + } + })(subscriber, 1); + } + function onUnsubscribeFail (message) { + console.error('[Red5ProSubsriber] Unsubscribe Error :: ' + message); + } + function onUnsubscribeSuccess () { + console.log('[Red5ProSubsriber] Unsubscribe Complete.'); + } + + function getUserMediaConfiguration () { + return { + mediaConstraints: { + audio: configuration.useAudio ? configuration.mediaConstraints.audio : false, + video: configuration.useVideo ? configuration.mediaConstraints.video : false, + frameRate: configuration.frameRate + } + }; + } + + function getRegionIfDefined () { + var region = configuration.streamManagerRegion; + if (typeof region === 'string' && region.length > 0 && region !== 'undefined') { + return region; + } + return undefined + } + + function requestOrigin(streamName, action){ + var host = configuration.host; + var app = configuration.app; + var port = serverSettings.httpport; + var baseUrl = protocol + '://' + host + ':' + port; + var apiVersion = configuration.streamManagerAPI || '4.0'; + var url = baseUrl + '/streammanager/api/' + apiVersion + '/event/' + app + '/' + streamName + '?action=' + action; + var region = getRegionIfDefined(); + if (region) { + url += '®ion=' + region; + } + return new Promise(function (resolve, reject) { + fetch(url) + .then(function (res) { + if (res.headers.get("content-type") && + res.headers.get("content-type").toLowerCase().indexOf("application/json") >= 0) { + return res.json(); + } + else { + throw new TypeError('Could not properly parse response.'); + } + }) + .then(function (json) { + resolve(json.serverAddress); + }) + .catch(function (error) { + var jsonError = typeof error === 'string' ? error : JSON.stringify(error, null, 2) + console.error('[PublisherStreamManagerTest] :: Error - Could not request Origin IP from Stream Manager. ' + jsonError) + reject(error) + }); + }); + } + + function determinePublisher (serverAddress) { + + var config = Object.assign({}, + configuration, + getUserMediaConfiguration()); + var rtcConfig = Object.assign({}, config, { + protocol: getSocketLocationFromProtocol().protocol, + port: getSocketLocationFromProtocol().port, + streamName: config.stream1, + app: configuration.proxy, + connectionParams: { + host: serverAddress, + app: configuration.app + } + }); + var rtmpConfig = Object.assign({}, config, { + host: serverAddress, + protocol: 'rtmp', + port: serverSettings.rtmpport, + streamName: config.stream1, + swf: '../../lib/red5pro/red5pro-publisher.swf', + swfobjectURL: '../../lib/swfobject/swfobject.js', + productInstallURL: '../../lib/swfobject/playerProductInstall.swf', + mediaConstraint: { + video: { + width: config.cameraWidth, + height: config.cameraHeight, + } + } + }); + var publishOrder = config.publisherFailoverOrder + .split(',') + .map(function (item) { + return item.trim() + }); + + if (window.query('view')) { + publishOrder = [window.query('view')]; + } + + var publisher = new red5prosdk.Red5ProPublisher(); + return publisher.setPublishOrder(publishOrder) + .init({ + rtc: rtcConfig, + rtmp: rtmpConfig + }); + } + + 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); + }); + }); + } + + function beginStreamListCall () { + var host = configuration.host; + var port = serverSettings.httpport; + var baseUrl = protocol + '://' + host + ':' + port; + var url = baseUrl + '/streammanager/api/4.0/event/list'; + fetch(url) + .then(function (res) { + if (res.headers.get('content-type') && + res.headers.get('content-type').toLowerCase().indexOf('application/json') >= 0) { + return res.json(); + } + else { + return res.text(); + } + }) + .then(function (jsonOrString) { + var json = jsonOrString; + if (typeof jsonOrString === 'string') { + try { + json = JSON.parse(json); + } + catch(e) { + throw new TypeError('Could not properly parse response: ' + e.message); + } + } + recieveList(json); + }) + .catch(function (error) { + var jsonError = typeof error === 'string' ? error : JSON.stringify(error, null, 2); + console.error('[Two-Way] :: Error - Could not request Stream List. ' + jsonError); + listError(error); + }); + } + + function recieveList (listIn) { + var found = false; + var address = undefined + for (var i = listIn.length - 1; i >= 0; i--) { + found = listIn[i].name === configuration.stream1; + address = listIn[i].serverAddress + if(found) break; + } + + if (found) { + startSubscribing(address); + } + else { + setWaitTime(); + } + } + + function listError (err) { + console.log( "Error recieved on streamListCall - " + err ); + setWaitTime(); + } + + function setWaitTime () { + clearTimeout(timeout) + timeout = setTimeout(function() { + clearTimeout(timeout) + beginStreamListCall() + }, 1000); + } + + function startSubscribing (address) { + // Kick off. + determineSubscriber(address) + .then(function(subscriberImpl) { + subStreamTitle.innerText = configuration.stream1; + targetSubscriber = subscriberImpl; + // Subscribe to events. + targetSubscriber.on('*', onSubscriberEvent); + return targetSubscriber.subscribe(); + }) + .then(function() { + onSubscribeSuccess(targetSubscriber); + }) + .catch(function (error) { + var jsonError = typeof error === 'string' ? error : JSON.stringify(error, null, 2); + console.error('[Red5ProSubscriber] :: Error in subscribing - ' + jsonError); + onSubscribeFail(jsonError); + }); + } + + function determineSubscriber (serverAddress) { + var config = Object.assign({}, configuration, defaultSubscriberConfiguration); + var rtcConfig = Object.assign({}, config, { + host: configuration.host, + protocol: getSocketLocationFromProtocol().protocol, + port: getSocketLocationFromProtocol().port, + app: configuration.proxy, + connectionParams: { + host: serverAddress, + app: configuration.app + }, + subscriptionId: 'subscriber-' + instanceId, + streamName: config.stream1 + }) + var rtmpConfig = Object.assign({}, config, { + host: serverAddress, + protocol: 'rtmp', + port: serverSettings.rtmpport, + streamName: config.stream1, + mimeType: 'rtmp/flv', + useVideoJS: false, + width: config.cameraWidth, + height: config.cameraHeight, + swf: '../../lib/red5pro/red5pro-subscriber.swf', + swfobjectURL: '../../lib/swfobject/swfobject.js', + productInstallURL: '../../lib/swfobject/playerProductInstall.swf' + }) + var hlsConfig = Object.assign({}, config, { + host: serverAddress, + protocol: protocol, + port: isSecure ? serverSettings.hlssport : serverSettings.hlsport, + streamName: config.stream1, + mimeType: 'application/x-mpegURL' + }) + + if (!config.useVideo) { + rtcConfig.videoEncoding = 'NONE'; + } + if (!config.useAudio) { + rtcConfig.audioEncoding = 'NONE'; + } + + var subscribeOrder = config.subscriberFailoverOrder + .split(',').map(function (item) { + return item.trim(); + }); + + if (window.query('view')) { + subscribeOrder = [window.query('view')]; + } + + var subscriber = new red5prosdk.Red5ProSubscriber(); + return subscriber.setPlaybackOrder(subscribeOrder) + .init({ + rtc: rtcConfig, + rtmp: rtmpConfig, + hls: hlsConfig + }); + } + + // Request to unsubscribe. + function unsubscribe () { + return new Promise(function(resolve, reject) { + var subscriber = targetSubscriber; + subscriber.unsubscribe() + .then(function () { + targetSubscriber.off('*', onSubscriberEvent); + targetSubscriber = undefined; + onUnsubscribeSuccess(); + resolve(); + }) + .catch(function (error) { + var jsonError = typeof error === 'string' ? error : JSON.stringify(error, null, 2); + onUnsubscribeFail(jsonError); + reject(error); + }); + }); + } + + // Kick off. + + requestOrigin(configuration.stream1, "broadcast") + .then(function(serverAddress) { + return determinePublisher(serverAddress) + }) + .then(function (publisherImpl) { + pubStreamTitle.innerText = configuration.stream1; + targetPublisher = publisherImpl; + targetPublisher.on('*', onPublisherEvent); + return targetPublisher.publish(); + }) + .then(function () { + onPublishSuccess(targetPublisher); + beginStreamListCall(); + }) + .catch(function (error) { + var jsonError = typeof error === 'string' ? error : JSON.stringify(error, null, 2); + console.error('[Red5ProPublisher] :: Error in publishing - ' + jsonError); + onPublishFail(jsonError); + }); + + var shuttingDown = false; + function shutdown() { + if (shuttingDown) return; + shuttingDown = true; + function clearRefs () { + if (targetPublisher) { + targetPublisher.off('*', onPublisherEvent); + } + if (targetSubscriber) { + targetSubscriber.off('*', onSubscriberEvent); + } + targetPublisher = undefined; + targetSubscriber = undefined; + } + unpublish().then(unsubscribe).then(clearRefs).catch(clearRefs); + } + window.addEventListener('pagehide', shutdown); + window.addEventListener('beforeunload', shutdown); +})(this, document, window.red5prosdk); diff --git a/src/page/sm-test/TwoWayStreamManagerProxy/index.js b/src/page/sm-test/TwoWayStreamManagerProxy/index.js index ee91d44d..dc639b1b 100644 --- a/src/page/sm-test/TwoWayStreamManagerProxy/index.js +++ b/src/page/sm-test/TwoWayStreamManagerProxy/index.js @@ -58,6 +58,7 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. var subStreamTitle = document.getElementById('sub-stream-title'); var statisticsFields = document.getElementsByClassName('statistics-field'); + var timeout = 0 var instanceId = Math.floor(Math.random() * 0x10000).toString(16); var protocol = serverSettings.protocol; var isSecure = protocol === 'https'; @@ -351,13 +352,15 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. function recieveList (listIn) { var found = false; + var address = undefined for (var i = listIn.length - 1; i >= 0; i--) { - found = listIn[i].name == configuration.stream2; + found = listIn[i].name === configuration.stream2; + address = listIn[i].serverAddress if(found) break; } if (found) { - startSubscribing(); + startSubscribing(address); } else { setWaitTime(); @@ -370,15 +373,16 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. } function setWaitTime () { - setTimeout(beginStreamListCall, 2000); + clearTimeout(timeout) + timeout = setTimeout(function() { + clearTimeout(timeout) + beginStreamListCall() + }, 1000); } - function startSubscribing () { + function startSubscribing (address) { // Kick off. - requestOrigin(configuration.stream2, "subscribe") - .then(function(serverAddress) { - return determineSubscriber(serverAddress) - }) + determineSubscriber(address) .then(function(subscriberImpl) { subStreamTitle.innerText = configuration.stream2; targetSubscriber = subscriberImpl; @@ -414,7 +418,7 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. host: serverAddress, protocol: 'rtmp', port: serverSettings.rtmpport, - streamName: config.stream1, + streamName: config.stream2, mimeType: 'rtmp/flv', useVideoJS: false, width: config.cameraWidth, @@ -427,7 +431,7 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. host: serverAddress, protocol: protocol, port: isSecure ? serverSettings.hlssport : serverSettings.hlsport, - streamName: config.stream1, + streamName: config.stream2, mimeType: 'application/x-mpegURL' }) diff --git a/src/page/testbed-menu-sm.html b/src/page/testbed-menu-sm.html index a7d39ac2..cd653361 100644 --- a/src/page/testbed-menu-sm.html +++ b/src/page/testbed-menu-sm.html @@ -35,7 +35,8 @@

Red5 Pro HTML Stream Manager Testbed

  • Publish - Stream Manager Transcoder Proxy w/ Provision & Authentication
  • Publish - Stream Manager Proxy Validation
  • - +
  • Two Way - Stream Manager Proxy
  • +
  • Pub & Sub - Stream Manager Proxy
  • Conference - Stream Manager Proxy