diff --git a/package.json b/package.json index 4742226c..9f1cd3f1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "red5pro-html-sdk-testbed", - "version": "10.8.0-RC1", + "version": "10.8.0-RC2", "description": "Testbed examples for Red5 Pro HTML SDK", "main": "src/js/index.js", "repository": { diff --git a/src/page/sm-test/publishStreamManagerProxyMute/README.md b/src/page/sm-test/publishStreamManagerProxyMute/README.md new file mode 100644 index 00000000..3d222026 --- /dev/null +++ b/src/page/sm-test/publishStreamManagerProxyMute/README.md @@ -0,0 +1,98 @@ +# Publishing and Muting Audio & Video using Red5 Pro + +This example shows how to "mute" the audio and video of a publisher while streaming; essentially, turning off the microphone and camera feeds for the stream, respectively. + +**Please refer to the [Basic Publisher Documentation](../publishStreamManagerProxy/README.md) to learn more about the basic setup.** + +## Example Code + +- **[index.html](index.html)** +- **[index.js](index.js)** + +> These examples use the WebRTC-based Publisher implementation from the Red5 Pro HTML SDK. + +# Important Note + +A server configuration related to WebRTC plugin is required in order for this example to work properly: + +In the server distribution, under `conf`, update the following line in `webrtc-plugin.properties`: + +```sh +# Idle checking for transport layer +idle.check.enabled=false +``` + +The reason it needs to be switched from the default of `true` to `false` is to properly notify the server to not consider a subscriber who is not receiving video data (in the case of a publisher having muted their audio and video) to be considered a lost connection. + +# Mute & Unmute + +## Mute API in the SDK + +The WebRTC-based publisher exposes the following API to mute and unmute audio and video: + +| Method Name | Description | +| :-- | :-- | +| `muteAudio` | Turns off sending audio data during a live broadcast. | +| `unmuteAudio` | Turns on sending audio data during a live broadcast. | +| `muteVideo` | Turns off sending video data during a live broadcast. | +| `unmuteVideo` | Turns on sending video data during a live broadcast. | + +These methods are used to communicate to the server that the broadcaster is going to be shutting off their audio and/or video stream(s), respectively. + +This notification is first made prior to actually stopping the sending of packets to the server using the `active` encoding parameter of the `RTCRtspSender` of a `RTCPeerConnection`. + +## active Encoding in RTCRtspSender + +After notifying the server that packets will stop being sent, we utilize the `active` encoding parameter of each `RTCRtspSender` of the underlying connection to toggle on or off the audio and/or video stream. + +For example, the following will stop sending audio packets out on the target publisher: + +```js +var pc = publisher.getPeerConnection(); +var sender = pc.getSenders()[0]; // Assuming Audio is first in list. +var params = sender.getParameters(); +params.encodings[0].active = wasMuted ? true : false; +sender.setParameters(params); +``` + +> More information: [https://developer.mozilla.org/en-US/docs/Web/API/RTCRtpSendParameters/encodings](https://developer.mozilla.org/en-US/docs/Web/API/RTCRtpSendParameters/encodings) + +## UI + +Included on the page are buttons that allow you to mute and unmute both audio and video during a broadcast. Upon initialization and broadcast of a publisher, the button handlers are set to toggle these stream states: + +``` +function addMuteListener (publisher) { + muteAudioButton.addEventListener('click', function () { + var wasMuted = muteAudioButton.innerText === 'Unmute Audio (Client)'; + muteAudioButton.innerText = wasMuted ? 'Mute Audio (Client)' : 'Unmute Audio (Client)'; + if (wasMuted) { + publisher.unmuteAudio(); + } + else { + publisher.muteAudio(); + } + var pc = publisher.getPeerConnection(); + var sender = pc.getSenders()[0]; // Assuming Audio is first in list. + var params = sender.getParameters(); + params.encodings[0].active = wasMuted ? true : false; + sender.setParameters(params); + }); + muteVideoButton.addEventListener('click', function () { + var wasMuted = muteVideoButton.innerText === 'Unmute Video (Client)'; + muteVideoButton.innerText = wasMuted ? 'Mute Video (Client)' : 'Unmute Video (Client)'; + if (wasMuted) { + publisher.unmuteVideo(); + } + else { + publisher.muteVideo(); + } + var pc = publisher.getPeerConnection(); + var sender = pc.getSenders()[1]; // Assuming Video is second in list. + var params = sender.getParameters(); + params.encodings[0].active = wasMuted ? true : false; + sender.setParameters(params); + }); + } +} +``` diff --git a/src/page/sm-test/publishStreamManagerProxyMute/index.html b/src/page/sm-test/publishStreamManagerProxyMute/index.html new file mode 100644 index 00000000..8b2c9775 --- /dev/null +++ b/src/page/sm-test/publishStreamManagerProxyMute/index.html @@ -0,0 +1,51 @@ + +{{> license}} + + + {{> meta title='Publish Stream Manager Mute API Test'}} + {{> header-scripts}} + {{> header-stylesheets}} + + + + {{> top-bar }} + {{> navigation isTestPage=true }} +
+ {{> settings-link}} +
+
+

In order to properly run the Stream Manager examples, you will need to configure you server for cluster infrastructure as described in the following documentation:

+

https://www.red5pro.com/docs/server/autoscale/

+
+
+ {{> test-info testTitle='Publish Stream Manager Mute API Test'}} +
+ {{> status-field-publisher}} + {{> statistics-field packets_field='Packets Sent'}} +

Origin Address: N/A

+
+ +
+
+

+ + +

+
+
+
+ {{> body-scripts}} + + + diff --git a/src/page/sm-test/publishStreamManagerProxyMute/index.js b/src/page/sm-test/publishStreamManagerProxyMute/index.js new file mode 100644 index 00000000..5a0f9021 --- /dev/null +++ b/src/page/sm-test/publishStreamManagerProxyMute/index.js @@ -0,0 +1,429 @@ +/* +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 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 muteAudioButton = document.getElementById('mute-audio-button'); + var muteVideoButton = document.getElementById('mute-video-button'); + var addressField = document.getElementById('address-field'); + var bitrateField = document.getElementById('bitrate-field'); + var packetsField = document.getElementById('packets-field'); + var resolutionField = document.getElementById('resolution-field'); + + var protocol = serverSettings.protocol; + var isSecure = protocol == 'https'; + function getSocketLocationFromProtocol () { + return !isSecure + ? {protocol: 'ws', port: serverSettings.wsport} + : {protocol: 'wss', port: serverSettings.wssport}; + } + + streamTitle.innerText = configuration.stream1; + var defaultConfiguration = { + protocol: getSocketLocationFromProtocol().protocol, + port: getSocketLocationFromProtocol().port, + streamMode: configuration.recordBroadcast ? 'record' : 'live' + }; + + streamTitle.innerText = configuration.stream1; + + function getAuthenticationParams () { + var auth = configuration.authentication; + return auth && auth.enabled + ? { + connectionParams: { + username: auth.username, + password: auth.password, + token: auth.token + } + } + : {}; + } + + function displayServerAddress (serverAddress, proxyAddress) { + proxyAddress = (typeof proxyAddress === 'undefined') ? 'N/A' : proxyAddress; + addressField.innerText = ' Proxy Address: ' + proxyAddress + ' | ' + ' Origin Address: ' + serverAddress; + } + + var bitrate = 0; + var packetsSent = 0; + var frameWidth = 0; + var frameHeight = 0; + + function updateStatistics (b, p, w, h) { + statisticsField.classList.remove('hidden'); + bitrateField.innerText = b === 0 ? 'N/A' : Math.floor(b); + packetsField.innerText = p; + resolutionField.innerText = (w || 0) + 'x' + (h || 0); + } + + function onBitrateUpdate (b, p) { + bitrate = b; + packetsSent = p; + updateStatistics(bitrate, packetsSent, frameWidth, frameHeight); + } + + function onResolutionUpdate (w, h) { + frameWidth = w; + frameHeight = h; + updateStatistics(bitrate, packetsSent, frameWidth, frameHeight); + } + + 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 { + var pc = publisher.getPeerConnection(); + var stream = publisher.getMediaStream(); + window.trackBitrate(pc, onBitrateUpdate, onResolutionUpdate); + statisticsField.classList.remove('hidden'); + stream.getVideoTracks().forEach(function (track) { + var settings = track.getSettings(); + onResolutionUpdate(settings.width, settings.height); + }); + } + catch (e) { + // no tracking for you! + } + } + function onUnpublishFail (message) { + console.error('[Red5ProPublisher] Unpublish Error :: ' + message); + } + function onUnpublishSuccess () { + console.log('[Red5ProPublisher] Unpublish Complete.'); + } + + function getRegionIfDefined () { + var region = configuration.streamManagerRegion; + if (typeof region === 'string' && region.length > 0 && region !== 'undefined') { + return region; + } + return undefined + } + + function addMuteListener (publisher) { + muteAudioButton.addEventListener('click', function () { + var wasMuted = muteAudioButton.innerText === 'Unmute Audio (Client)'; + if (wasMuted) { + publisher.unmuteAudio(); + } + else { + publisher.muteAudio(); + } + var pc = publisher.getPeerConnection(); + var sender = pc.getSenders()[0]; // Assuming Audio is first in list. + var params = sender.getParameters(); + params.encodings[0].active = wasMuted ? true : false; + sender.setParameters(params); + muteAudioButton.innerText = wasMuted ? 'Mute Audio (Client)' : 'Unmute Audio (Client)'; + }); + muteVideoButton.addEventListener('click', function () { + var wasMuted = muteVideoButton.innerText === 'Unmute Video (Client)'; + if (wasMuted) { + publisher.unmuteVideo(); + } + else { + publisher.muteVideo(); + } + var pc = publisher.getPeerConnection(); + var sender = pc.getSenders()[1]; // Assuming Video is second in list. + var params = sender.getParameters(); + params.encodings[0].active = wasMuted ? true : false; + sender.setParameters(params); + muteVideoButton.innerText = wasMuted ? 'Mute Video (Client)' : 'Unmute Video (Client)'; + }); + } + + function requestOrigin (configuration) { + var host = configuration.host; + var app = configuration.app; + var streamName = configuration.stream1; + 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=broadcast'; + var region = getRegionIfDefined(); + if (region) { + url += '®ion=' + region; + } + return new Promise(function (resolve, reject) { + fetch(url) + .then(function (res) { + if(res.status == 200){ + 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.'); + } + } + else{ + var msg = ""; + if(res.status == 400) + { + msg = "An invalid request was detected"; + } + else if(res.status == 404) + { + msg = "Data for the request could not be located/provided."; + } + else if(res.status == 500) + { + msg = "Improper server state error was detected."; + } + else + { + msg = "Unknown error"; + } + + + throw new TypeError(msg); + } + }) + .then(function (json) { + resolve(json); + }) + .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 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 determinePublisher (jsonResponse) { + var host = jsonResponse.serverAddress; + var app = jsonResponse.scope; + var name = jsonResponse.name; + var config = Object.assign({}, + configuration, + defaultConfiguration, + getUserMediaConfiguration()); + var rtcConfig = Object.assign({}, config, { + protocol: getSocketLocationFromProtocol().protocol, + port: getSocketLocationFromProtocol().port, + streamName: name, + app: configuration.proxy, + connectionParams: { + host: host, + app: app + } + }); + var rtmpConfig = Object.assign({}, config, { + host: host, + app: app, + protocol: 'rtmp', + port: serverSettings.rtmpport, + streamName: name, + backgroundColor: '#000000', + swf: '../../lib/red5pro/red5pro-publisher.swf', + swfobjectURL: '../../lib/swfobject/swfobject.js', + productInstallURL: '../../lib/swfobject/playerProductInstall.swf' + }, + getAuthenticationParams(), + getRTMPMediaConfiguration()); + var publishOrder = config.publisherFailoverOrder + .split(',') + .map(function (item) { + return item.trim() + }); + + // Merge in possible authentication params. + rtcConfig.connectionParams = Object.assign({}, + getAuthenticationParams().connectionParams, + rtcConfig.connectionParams); + + if(window.query('view')) { + publishOrder = [window.query('view')]; + } + + var publisher = new red5prosdk.Red5ProPublisher(); + return publisher.setPublishOrder(publishOrder) + .init({ + rtc: rtcConfig, + rtmp: rtmpConfig + }); + } + + function showAddress (publisher) { + var config = publisher.getOptions(); + console.log("Host = " + config.host + " | " + "app = " + config.app); + if (publisher.getType().toLowerCase() === 'rtc') { + displayServerAddress(config.connectionParams.host, config.host); + console.log("Using streammanager proxy for rtc"); + console.log("Proxy target = " + config.connectionParams.host + " | " + "Proxy app = " + config.connectionParams.app) + if(isSecure) { + console.log("Operating over secure connection | protocol: " + config.protocol + " | port: " + config.port); + } + else { + console.log("Operating over unsecure connection | protocol: " + config.protocol + " | port: " + config.port); + } + } + else { + displayServerAddress(config.host); + } + } + + 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 retryCount = 0; + var retryLimit = 3; + + function respondToOrigin (response) { + determinePublisher(response) + .then(function (publisherImpl) { + streamTitle.innerText = configuration.stream1; + targetPublisher = publisherImpl; + targetPublisher.on('*', onPublisherEvent); + showAddress(targetPublisher); + return targetPublisher.publish(); + }) + .then(function () { + addMuteListener(targetPublisher); + onPublishSuccess(targetPublisher); + }) + .catch(function (error) { + var jsonError = typeof error === 'string' ? error : JSON.stringify(error, null, 2); + console.error('[Red5ProPublisher] :: Error in access of Origin IP: ' + jsonError); + updateStatusFromEvent({ + type: red5prosdk.PublisherEventTypes.CONNECT_FAILURE + }); + onPublishFail(jsonError); + }); + } + + function respondToOriginFailure (error) { + if (retryCount++ < retryLimit) { + var retryTimer = setTimeout(function () { + clearTimeout(retryTimer); + startup(); + }, 1000); + } + else { + var jsonError = typeof error === 'string' ? error : JSON.stringify(error, null, 2); + updateStatusFromEvent({ + type: red5prosdk.PublisherEventTypes.CONNECT_FAILURE + }); + console.error('[Red5ProPublisher] :: Retry timeout in publishing - ' + jsonError); + } + } + + function startup () { + // Kick off. + requestOrigin(configuration) + .then(respondToOrigin) + .catch(respondToOriginFailure); + } + startup(); + + var shuttingDown = false; + function shutdown() { + if (shuttingDown) return; + shuttingDown = true; + function clearRefs () { + if (targetPublisher) { + targetPublisher.off('*', onPublisherEvent); + } + targetPublisher = undefined; + } + unpublish().then(clearRefs).catch(clearRefs); + window.untrackBitrate(); + } + window.addEventListener('pagehide', shutdown); + window.addEventListener('beforeunload', shutdown); + +})(this, document, window.red5prosdk); + + diff --git a/src/page/testbed-menu-sm.html b/src/page/testbed-menu-sm.html index 5e12c611..afddec1b 100644 --- a/src/page/testbed-menu-sm.html +++ b/src/page/testbed-menu-sm.html @@ -30,6 +30,7 @@

Red5 Pro HTML Stream Manager Testbed

  • Publish - Stream Manager Proxy w/ Camera Selection
  • Publish - Stream Manager Proxy w/ Custom Video Settings
  • Publish - Stream Manager Proxy w/ Custom Video and Audio Settings
  • +
  • Publish - Stream Manager Proxy Mute API
  • Publish - Stream Manager Proxy RoundTrip Authentication
  • Publish - Stream Manager Proxy Screen Share
  • Publish - Stream Manager Shared Object