├── .gitignore
├── .idea
├── .name
├── codeStyleSettings.xml
├── compiler.xml
├── copyright
│ └── profiles_settings.xml
├── encodings.xml
├── jsLibraryMappings.xml
├── libraries
│ └── AppRTC_node_server_node_modules.xml
├── misc.xml
├── modules.xml
├── runConfigurations
│ └── bin_www.xml
├── scopes
│ └── scope_settings.xml
├── uiDesigner.xml
├── vcs.xml
└── workspace.xml
├── AppRTC-node-server.iml
├── README.md
├── app.js
├── bin
└── www
├── lib
└── rooms.js
├── package.json
├── public
├── css
│ └── main.css
├── html
│ ├── error.jade
│ ├── full_template.jade
│ ├── google1b7eb21c5b594ba0.html
│ ├── help.html
│ ├── index_template.jade
│ ├── index_template.json
│ ├── layout.jade
│ ├── manifest.json
│ └── params.html
├── images
│ ├── apprtc-128.png
│ ├── apprtc-16.png
│ ├── apprtc-22.png
│ ├── apprtc-32.png
│ ├── apprtc-48.png
│ └── webrtc-icon-192x192.png
└── js
│ ├── README.md
│ ├── adapter.js
│ ├── appcontroller.js
│ ├── appcontroller_test.js
│ ├── apprtc.debug.js
│ ├── appwindow.js
│ ├── background.js
│ ├── background_test.js
│ ├── call.js
│ ├── call_test.js
│ ├── constants.js
│ ├── infobox.js
│ ├── infobox_test.js
│ ├── loopback.js
│ ├── peerconnectionclient.js
│ ├── peerconnectionclient_test.js
│ ├── remotewebsocket.js
│ ├── remotewebsocket_test.js
│ ├── roomselection.js
│ ├── roomselection_test.js
│ ├── sdputils.js
│ ├── sdputils_test.js
│ ├── signalingchannel.js
│ ├── signalingchannel_test.js
│ ├── stats.js
│ ├── storage.js
│ ├── test_mocks.js
│ ├── testpolyfills.js
│ ├── util.js
│ ├── utils_test.js
│ └── windowport.js
└── routes
├── index.js
└── users.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | *.sw*
3 | *~
4 | /build/
5 | node_modules/
6 | webroot/
7 | /workspace/
8 |
--------------------------------------------------------------------------------
/.idea/.name:
--------------------------------------------------------------------------------
1 | AppRTC-node-server
--------------------------------------------------------------------------------
/.idea/codeStyleSettings.xml:
--------------------------------------------------------------------------------
1 |
2 |
149 |
A number of settings for the AppRTC video chat application can be changed by adding URL parameters.
56 | 57 |For example: https://apprtc.appspot.com/?hd=true&stereo=true&debug=loopback
58 | 59 |The file using the parameters is apprtc.py. More Google-specific parameters are available from the MediaConstraints interface.
60 | 61 |For more information see AppRTC : Google's WebRTC test app and its parameters.
62 | 63 |hd=true | Use HD camera resolution constraints, i.e. minWidth: 1280, minHeight: 720 |
stereo=true | Turn on stereo audio |
debug=loopback | Connect to yourself, e.g. to test firewalls |
ts=[turnserver] | Set TURN server different from the default |
audio=true&video=false | Audio only |
audio=false | Video only |
audio=googEchoCancellation=false,googAutoGainControl=true | Disable echo cancellation and enable gain control |
audio=googNoiseReduction=true | Enable noise reduction |
asc=ISAC/16000 | Set preferred audio send codec to be ISAC at 16kHz (use on Android) |
arc=opus/48000 | Set preferred audio receive codec Opus at 48kHz |
dtls=false | Disable Datagram Transport Layer Security |
dscp=true | Enable DSCP |
ipv6=true | Enable IPv6 |
arbr=[bitrate] | Set audio receive bitrate, kbps |
asbr=[bitrate] | Set audio send bitrate |
vsbr=[bitrate] | Set video receive bitrate |
vrbr=[bitrate] | Set video send bitrate |
'; 86 | 87 | if (this.stats_) { 88 | var states = this.call_.getPeerConnectionStates(); 89 | if (!states) { 90 | return; 91 | } 92 | // Build the display. 93 | contents += this.buildLine_('States'); 94 | contents += this.buildLine_('Signaling', states.signalingState); 95 | contents += this.buildLine_('Gathering', states.iceGatheringState); 96 | contents += this.buildLine_('Connection', states.iceConnectionState); 97 | for (var endpoint in this.iceCandidateTypes_) { 98 | var types = []; 99 | for (var type in this.iceCandidateTypes_[endpoint]) { 100 | types.push(type + ':' + this.iceCandidateTypes_[endpoint][type]); 101 | } 102 | contents += this.buildLine_(endpoint, types.join(' ')); 103 | } 104 | 105 | var activeCandPair = getStatsReport(this.stats_, 'googCandidatePair', 106 | 'googActiveConnection', 'true'); 107 | var localAddr; 108 | var remoteAddr; 109 | var localAddrType; 110 | var remoteAddrType; 111 | if (activeCandPair) { 112 | localAddr = activeCandPair.stat('googLocalAddress'); 113 | remoteAddr = activeCandPair.stat('googRemoteAddress'); 114 | localAddrType = activeCandPair.stat('googLocalCandidateType'); 115 | remoteAddrType = activeCandPair.stat('googRemoteCandidateType'); 116 | } 117 | if (localAddr && remoteAddr) { 118 | contents += this.buildLine_('LocalAddr', localAddr + 119 | ' (' + localAddrType + ')'); 120 | contents += this.buildLine_('RemoteAddr', remoteAddr + 121 | ' (' + remoteAddrType + ')'); 122 | } 123 | contents += this.buildLine_(); 124 | 125 | contents += this.buildStatsSection_(); 126 | } 127 | 128 | if (this.errorMessages_.length) { 129 | this.infoDiv_.classList.add('warning'); 130 | for (var i = 0; i !== this.errorMessages_.length; ++i) { 131 | contents += this.errorMessages_[i] + '\n'; 132 | } 133 | } else { 134 | this.infoDiv_.classList.remove('warning'); 135 | } 136 | 137 | if (this.versionInfo_) { 138 | contents += this.buildLine_(); 139 | contents += this.buildLine_('Version'); 140 | for (var key in this.versionInfo_) { 141 | contents += this.buildLine_(key, this.versionInfo_[key]); 142 | } 143 | } 144 | 145 | contents += ''; 146 | 147 | if (this.infoDiv_.innerHTML !== contents) { 148 | this.infoDiv_.innerHTML = contents; 149 | } 150 | }; 151 | 152 | InfoBox.prototype.buildStatsSection_ = function() { 153 | var contents = this.buildLine_('Stats'); 154 | 155 | // Obtain setup and latency this.stats_. 156 | var rtt = extractStatAsInt(this.stats_, 'ssrc', 'googRtt'); 157 | var captureStart = extractStatAsInt(this.stats_, 'ssrc', 158 | 'googCaptureStartNtpTimeMs'); 159 | var e2eDelay = computeE2EDelay(captureStart, this.remoteVideo_.currentTime); 160 | if (this.endTime_ !== null) { 161 | contents += this.buildLine_('Call time', 162 | InfoBox.formatInterval_(window.performance.now() - this.connectTime_)); 163 | contents += this.buildLine_('Setup time', 164 | InfoBox.formatMsec_(this.connectTime_ - this.startTime_)); 165 | } 166 | if (rtt !== null) { 167 | contents += this.buildLine_('RTT', InfoBox.formatMsec_(rtt)); 168 | } 169 | if (e2eDelay !== null) { 170 | contents += this.buildLine_('End to end', InfoBox.formatMsec_(e2eDelay)); 171 | } 172 | 173 | // Obtain resolution, framerate, and bitrate this.stats_. 174 | // TODO(juberti): find a better way to tell these apart. 175 | var txAudio = getStatsReport(this.stats_, 'ssrc', 'audioInputLevel'); 176 | var rxAudio = getStatsReport(this.stats_, 'ssrc', 'audioOutputLevel'); 177 | var txVideo = getStatsReport(this.stats_, 'ssrc', 'googFirsReceived'); 178 | var rxVideo = getStatsReport(this.stats_, 'ssrc', 'googFirsSent'); 179 | var txPrevAudio = getStatsReport(this.prevStats_, 'ssrc', 'audioInputLevel'); 180 | var rxPrevAudio = getStatsReport(this.prevStats_, 'ssrc', 'audioOutputLevel'); 181 | var txPrevVideo = getStatsReport(this.prevStats_, 'ssrc', 'googFirsReceived'); 182 | var rxPrevVideo = getStatsReport(this.prevStats_, 'ssrc', 'googFirsSent'); 183 | var txAudioCodec; 184 | var txAudioBitrate; 185 | var txAudioPacketRate; 186 | var rxAudioCodec; 187 | var rxAudioBitrate; 188 | var rxAudioPacketRate; 189 | var txVideoHeight; 190 | var txVideoFps; 191 | var txVideoCodec; 192 | var txVideoBitrate; 193 | var txVideoPacketRate; 194 | var rxVideoHeight; 195 | var rxVideoFps; 196 | var rxVideoCodec; 197 | var rxVideoBitrate; 198 | var rxVideoPacketRate; 199 | if (txAudio) { 200 | txAudioCodec = txAudio.stat('googCodecName'); 201 | txAudioBitrate = computeBitrate(txAudio, txPrevAudio, 'bytesSent'); 202 | txAudioPacketRate = computeRate(txAudio, txPrevAudio, 'packetsSent'); 203 | contents += this.buildLine_('Audio Tx', txAudioCodec + ', ' + 204 | InfoBox.formatBitrate_(txAudioBitrate) + ', ' + 205 | InfoBox.formatPacketRate_(txAudioPacketRate)); 206 | } 207 | if (rxAudio) { 208 | rxAudioCodec = rxAudio.stat('googCodecName'); 209 | rxAudioBitrate = computeBitrate(rxAudio, rxPrevAudio, 'bytesReceived'); 210 | rxAudioPacketRate = computeRate(rxAudio, rxPrevAudio, 'packetsReceived'); 211 | contents += this.buildLine_('Audio Rx', rxAudioCodec + ', ' + 212 | InfoBox.formatBitrate_(rxAudioBitrate) + ', ' + 213 | InfoBox.formatPacketRate_(rxAudioPacketRate)); 214 | } 215 | if (txVideo) { 216 | txVideoCodec = txVideo.stat('googCodecName'); 217 | txVideoHeight = txVideo.stat('googFrameHeightSent'); 218 | txVideoFps = txVideo.stat('googFrameRateSent'); 219 | txVideoBitrate = computeBitrate(txVideo, txPrevVideo, 'bytesSent'); 220 | txVideoPacketRate = computeRate(txVideo, txPrevVideo, 'packetsSent'); 221 | contents += this.buildLine_('Video Tx', 222 | txVideoCodec + ', ' + txVideoHeight.toString() + 'p' + 223 | txVideoFps.toString() + ', ' + 224 | InfoBox.formatBitrate_(txVideoBitrate) + ', ' + 225 | InfoBox.formatPacketRate_(txVideoPacketRate)); 226 | } 227 | if (rxVideo) { 228 | rxVideoCodec = 'TODO'; // rxVideo.stat('googCodecName'); 229 | rxVideoHeight = this.remoteVideo_.videoHeight; 230 | // TODO(juberti): this should ideally be obtained from the video element. 231 | rxVideoFps = rxVideo.stat('googFrameRateDecoded'); 232 | rxVideoBitrate = computeBitrate(rxVideo, rxPrevVideo, 'bytesReceived'); 233 | rxVideoPacketRate = computeRate(rxVideo, rxPrevVideo, 'packetsReceived'); 234 | contents += this.buildLine_('Video Rx', 235 | rxVideoCodec + ', ' + rxVideoHeight.toString() + 'p' + 236 | rxVideoFps.toString() + ', ' + 237 | InfoBox.formatBitrate_(rxVideoBitrate) + ', ' + 238 | InfoBox.formatPacketRate_(rxVideoPacketRate)); 239 | } 240 | return contents; 241 | }; 242 | 243 | InfoBox.prototype.buildLine_ = function(label, value) { 244 | var columnWidth = 12; 245 | var line = ''; 246 | if (label) { 247 | line += label + ':'; 248 | while (line.length < columnWidth) { 249 | line += ' '; 250 | } 251 | 252 | if (value) { 253 | line += value; 254 | } 255 | } 256 | line += '\n'; 257 | return line; 258 | }; 259 | 260 | // Convert a number of milliseconds into a '[HH:]MM:SS' string. 261 | InfoBox.formatInterval_ = function(value) { 262 | var result = ''; 263 | var seconds = Math.floor(value / 1000); 264 | var minutes = Math.floor(seconds / 60); 265 | var hours = Math.floor(minutes / 60); 266 | var formatTwoDigit = function(twodigit) { 267 | return ((twodigit < 10) ? '0' : '') + twodigit.toString(); 268 | }; 269 | 270 | if (hours > 0) { 271 | result += formatTwoDigit(hours) + ':'; 272 | } 273 | result += formatTwoDigit(minutes - hours * 60) + ':'; 274 | result += formatTwoDigit(seconds - minutes * 60); 275 | return result; 276 | }; 277 | 278 | // Convert a number of milliesconds into a 'XXX ms' string. 279 | InfoBox.formatMsec_ = function(value) { 280 | return value.toFixed(0).toString() + ' ms'; 281 | }; 282 | 283 | // Convert a bitrate into a 'XXX Xbps' string. 284 | InfoBox.formatBitrate_ = function(value) { 285 | if (!value) { 286 | return '- bps'; 287 | } 288 | 289 | var suffix; 290 | if (value < 1000) { 291 | suffix = 'bps'; 292 | } else if (value < 1000000) { 293 | suffix = 'kbps'; 294 | value /= 1000; 295 | } else { 296 | suffix = 'Mbps'; 297 | value /= 1000000; 298 | } 299 | 300 | var str = value.toPrecision(3) + ' ' + suffix; 301 | return str; 302 | }; 303 | 304 | // Convert a packet rate into a 'XXX pps' string. 305 | InfoBox.formatPacketRate_ = function(value) { 306 | if (!value) { 307 | return '- pps'; 308 | } 309 | return value.toPrecision(3) + ' ' + 'pps'; 310 | }; 311 | -------------------------------------------------------------------------------- /public/js/infobox_test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | 9 | /* More information about these options at jshint.com/docs/options */ 10 | 11 | /* globals TestCase, assertEquals, InfoBox */ 12 | 13 | 'use strict'; 14 | 15 | var InfoBoxTest = new TestCase('InfoBoxTest'); 16 | 17 | InfoBoxTest.prototype.testFormatBitrate = function() { 18 | assertEquals('Format bps.', '789 bps', InfoBox.formatBitrate_(789)); 19 | assertEquals('Format kbps.', '78.9 kbps', InfoBox.formatBitrate_(78912)); 20 | assertEquals('Format Mbps.', '7.89 Mbps', InfoBox.formatBitrate_(7891234)); 21 | }; 22 | 23 | InfoBoxTest.prototype.testFormatInterval = function() { 24 | assertEquals('Format 00:01', '00:01', InfoBox.formatInterval_(1999)); 25 | assertEquals('Format 00:12', '00:12', InfoBox.formatInterval_(12500)); 26 | assertEquals('Format 01:23', '01:23', InfoBox.formatInterval_(83123)); 27 | assertEquals('Format 12:34', '12:34', InfoBox.formatInterval_(754000)); 28 | assertEquals('Format 01:23:45', '01:23:45', 29 | InfoBox.formatInterval_(5025000)); 30 | assertEquals('Format 12:34:56', '12:34:56', 31 | InfoBox.formatInterval_(45296000)); 32 | assertEquals('Format 123:45:43', '123:45:43', 33 | InfoBox.formatInterval_(445543000)); 34 | }; 35 | -------------------------------------------------------------------------------- /public/js/loopback.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | 9 | /* More information about these options at jshint.com/docs/options */ 10 | 11 | /* exported setupLoopback */ 12 | 13 | 'use strict'; 14 | 15 | // We handle the loopback case by making a second connection to the WSS so that 16 | // we receive the same messages that we send out. When receiving an offer we 17 | // convert that offer into an answer message. When receiving candidates we 18 | // echo back the candidate. Answer is ignored because we should never receive 19 | // one while in loopback. Bye is ignored because there is no work to do. 20 | var loopbackWebSocket = null; 21 | var LOOPBACK_CLIENT_ID = 'loopback_client_id'; 22 | function setupLoopback(wssUrl, roomId) { 23 | if (loopbackWebSocket) { 24 | return; 25 | } 26 | // TODO(tkchin): merge duplicate code once SignalingChannel abstraction 27 | // exists. 28 | loopbackWebSocket = new WebSocket(wssUrl); 29 | 30 | var sendLoopbackMessage = function(message) { 31 | var msgString = JSON.stringify({ 32 | cmd: 'send', 33 | msg: JSON.stringify(message) 34 | }); 35 | loopbackWebSocket.send(msgString); 36 | }; 37 | 38 | loopbackWebSocket.onopen = function() { 39 | var registerMessage = { 40 | cmd: 'register', 41 | roomid: roomId, 42 | clientid: LOOPBACK_CLIENT_ID 43 | }; 44 | loopbackWebSocket.send(JSON.stringify(registerMessage)); 45 | }; 46 | 47 | loopbackWebSocket.onmessage = function(event) { 48 | var wssMessage; 49 | var message; 50 | try { 51 | wssMessage = JSON.parse(event.data); 52 | message = JSON.parse(wssMessage.msg); 53 | } catch (e) { 54 | trace('Error parsing JSON: ' + event.data); 55 | return; 56 | } 57 | if (wssMessage.error) { 58 | trace('WSS error: ' + wssMessage.error); 59 | return; 60 | } 61 | if (message.type === 'offer') { 62 | var loopbackAnswer = wssMessage.msg; 63 | loopbackAnswer = loopbackAnswer.replace('"offer"', '"answer"'); 64 | loopbackAnswer = 65 | loopbackAnswer.replace('a=ice-options:google-ice\\r\\n', ''); 66 | sendLoopbackMessage(JSON.parse(loopbackAnswer)); 67 | } else if (message.type === 'candidate') { 68 | sendLoopbackMessage(message); 69 | } 70 | }; 71 | 72 | loopbackWebSocket.onclose = function(event) { 73 | trace('Loopback closed with code:' + event.code + ' reason:' + 74 | event.reason); 75 | // TODO(tkchin): try to reconnect. 76 | loopbackWebSocket = null; 77 | }; 78 | } 79 | -------------------------------------------------------------------------------- /public/js/peerconnectionclient.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | 9 | /* More information about these options at jshint.com/docs/options */ 10 | 11 | /* globals trace, mergeConstraints, parseJSON, iceCandidateType, 12 | maybePreferAudioReceiveCodec, maybePreferVideoReceiveCodec, 13 | maybePreferAudioSendCodec, maybePreferVideoSendCodec, 14 | maybeSetAudioSendBitRate, maybeSetVideoSendBitRate, 15 | maybeSetAudioReceiveBitRate, maybeSetVideoSendInitialBitRate, 16 | maybeSetVideoReceiveBitRate, maybeSetVideoSendInitialBitRate, 17 | maybeSetOpusOptions */ 18 | 19 | /* exported PeerConnectionClient */ 20 | 21 | 'use strict'; 22 | 23 | var PeerConnectionClient = function(params, startTime) { 24 | this.params_ = params; 25 | this.startTime_ = startTime; 26 | 27 | trace('Creating RTCPeerConnnection with:\n' + 28 | ' config: \'' + JSON.stringify(params.peerConnectionConfig) + '\';\n' + 29 | ' constraints: \'' + JSON.stringify(params.peerConnectionConstraints) + 30 | '\'.'); 31 | 32 | // Create an RTCPeerConnection via the polyfill (adapter.js). 33 | this.pc_ = new RTCPeerConnection( 34 | params.peerConnectionConfig, params.peerConnectionConstraints); 35 | this.pc_.onicecandidate = this.onIceCandidate_.bind(this); 36 | this.pc_.onaddstream = this.onRemoteStreamAdded_.bind(this); 37 | this.pc_.onremovestream = trace.bind(null, 'Remote stream removed.'); 38 | this.pc_.onsignalingstatechange = this.onSignalingStateChanged_.bind(this); 39 | this.pc_.oniceconnectionstatechange = 40 | this.onIceConnectionStateChanged_.bind(this); 41 | 42 | this.hasRemoteSdp_ = false; 43 | this.messageQueue_ = []; 44 | this.isInitiator_ = false; 45 | this.started_ = false; 46 | 47 | // TODO(jiayl): Replace callbacks with events. 48 | // Public callbacks. Keep it sorted. 49 | this.onerror = null; 50 | this.oniceconnectionstatechange = null; 51 | this.onnewicecandidate = null; 52 | this.onremotehangup = null; 53 | this.onremotesdpset = null; 54 | this.onremotestreamadded = null; 55 | this.onsignalingmessage = null; 56 | this.onsignalingstatechange = null; 57 | }; 58 | 59 | // Set up audio and video regardless of what devices are present. 60 | // Disable comfort noise for maximum audio quality. 61 | PeerConnectionClient.DEFAULT_SDP_CONSTRAINTS_ = { 62 | 'mandatory': { 63 | 'OfferToReceiveAudio': true, 64 | 'OfferToReceiveVideo': true 65 | }, 66 | 'optional': [{ 67 | 'VoiceActivityDetection': false 68 | }] 69 | }; 70 | 71 | PeerConnectionClient.prototype.addStream = function(stream) { 72 | if (!this.pc_) { 73 | return; 74 | } 75 | this.pc_.addStream(stream); 76 | }; 77 | 78 | PeerConnectionClient.prototype.startAsCaller = function(offerConstraints) { 79 | if (!this.pc_) { 80 | return false; 81 | } 82 | 83 | if (this.started_) { 84 | return false; 85 | } 86 | 87 | this.isInitiator_ = true; 88 | this.started_ = true; 89 | var constraints = mergeConstraints( 90 | offerConstraints, PeerConnectionClient.DEFAULT_SDP_CONSTRAINTS_); 91 | trace('Sending offer to peer, with constraints: \n\'' + 92 | JSON.stringify(constraints) + '\'.'); 93 | this.pc_.createOffer(this.setLocalSdpAndNotify_.bind(this), 94 | this.onError_.bind(this, 'createOffer'), 95 | constraints); 96 | 97 | return true; 98 | }; 99 | 100 | PeerConnectionClient.prototype.startAsCallee = function(initialMessages) { 101 | if (!this.pc_) { 102 | return false; 103 | } 104 | 105 | if (this.started_) { 106 | return false; 107 | } 108 | 109 | this.isInitiator_ = false; 110 | this.started_ = true; 111 | 112 | if (initialMessages && initialMessages.length > 0) { 113 | // Convert received messages to JSON objects and add them to the message 114 | // queue. 115 | for (var i = 0, len = initialMessages.length; i < len; i++) { 116 | this.receiveSignalingMessage(initialMessages[i]); 117 | } 118 | return true; 119 | } 120 | 121 | // We may have queued messages received from the signaling channel before 122 | // started. 123 | if (this.messageQueue_.length > 0) { 124 | this.drainMessageQueue_(); 125 | } 126 | return true; 127 | }; 128 | 129 | PeerConnectionClient.prototype.receiveSignalingMessage = function(message) { 130 | var messageObj = parseJSON(message); 131 | if (!messageObj) { 132 | return; 133 | } 134 | if ((this.isInitiator_ && messageObj.type === 'answer') || 135 | (!this.isInitiator_ && messageObj.type === 'offer')) { 136 | this.hasRemoteSdp_ = true; 137 | // Always process offer before candidates. 138 | this.messageQueue_.unshift(messageObj); 139 | } else if (messageObj.type === 'candidate') { 140 | this.messageQueue_.push(messageObj); 141 | } else if (messageObj.type === 'bye') { 142 | if (this.onremotehangup) { 143 | this.onremotehangup(); 144 | } 145 | } 146 | this.drainMessageQueue_(); 147 | }; 148 | 149 | PeerConnectionClient.prototype.close = function() { 150 | if (!this.pc_) { 151 | return; 152 | } 153 | this.pc_.close(); 154 | this.pc_ = null; 155 | }; 156 | 157 | PeerConnectionClient.prototype.getPeerConnectionStates = function() { 158 | if (!this.pc_) { 159 | return null; 160 | } 161 | return { 162 | 'signalingState': this.pc_.signalingState, 163 | 'iceGatheringState': this.pc_.iceGatheringState, 164 | 'iceConnectionState': this.pc_.iceConnectionState 165 | }; 166 | }; 167 | 168 | PeerConnectionClient.prototype.getPeerConnectionStats = function(callback) { 169 | if (!this.pc_) { 170 | return; 171 | } 172 | this.pc_.getStats(callback); 173 | }; 174 | 175 | PeerConnectionClient.prototype.doAnswer_ = function() { 176 | trace('Sending answer to peer.'); 177 | this.pc_.createAnswer(this.setLocalSdpAndNotify_.bind(this), 178 | this.onError_.bind(this, 'createAnswer'), 179 | PeerConnectionClient.DEFAULT_SDP_CONSTRAINTS_); 180 | }; 181 | 182 | PeerConnectionClient.prototype.setLocalSdpAndNotify_ = 183 | function(sessionDescription) { 184 | sessionDescription.sdp = maybePreferAudioReceiveCodec( 185 | sessionDescription.sdp, 186 | this.params_); 187 | sessionDescription.sdp = maybePreferVideoReceiveCodec( 188 | sessionDescription.sdp, 189 | this.params_); 190 | sessionDescription.sdp = maybeSetAudioReceiveBitRate( 191 | sessionDescription.sdp, 192 | this.params_); 193 | sessionDescription.sdp = maybeSetVideoReceiveBitRate( 194 | sessionDescription.sdp, 195 | this.params_); 196 | this.pc_.setLocalDescription(sessionDescription, 197 | trace.bind(null, 'Set session description success.'), 198 | this.onError_.bind(this, 'setLocalDescription')); 199 | 200 | if (this.onsignalingmessage) { 201 | this.onsignalingmessage(sessionDescription); 202 | } 203 | }; 204 | 205 | PeerConnectionClient.prototype.setRemoteSdp_ = function(message) { 206 | message.sdp = maybeSetOpusOptions(message.sdp, this.params_); 207 | message.sdp = maybePreferAudioSendCodec(message.sdp, this.params_); 208 | message.sdp = maybePreferVideoSendCodec(message.sdp, this.params_); 209 | message.sdp = maybeSetAudioSendBitRate(message.sdp, this.params_); 210 | message.sdp = maybeSetVideoSendBitRate(message.sdp, this.params_); 211 | message.sdp = maybeSetVideoSendInitialBitRate(message.sdp, this.params_); 212 | this.pc_.setRemoteDescription(new RTCSessionDescription(message), 213 | this.onSetRemoteDescriptionSuccess_.bind(this), 214 | this.onError_.bind(this, 'setRemoteDescription')); 215 | }; 216 | 217 | PeerConnectionClient.prototype.onSetRemoteDescriptionSuccess_ = function() { 218 | trace('Set remote session description success.'); 219 | // By now all onaddstream events for the setRemoteDescription have fired, 220 | // so we can know if the peer has any remote video streams that we need 221 | // to wait for. Otherwise, transition immediately to the active state. 222 | var remoteStreams = this.pc_.getRemoteStreams(); 223 | if (this.onremotesdpset) { 224 | this.onremotesdpset(remoteStreams.length > 0 && 225 | remoteStreams[0].getVideoTracks().length > 0); 226 | } 227 | }; 228 | 229 | PeerConnectionClient.prototype.processSignalingMessage_ = function(message) { 230 | if (message.type === 'offer' && !this.isInitiator_) { 231 | if (this.pc_.signalingState !== 'stable') { 232 | trace('ERROR: remote offer received in unexpected state: ' + 233 | this.pc_.signalingState); 234 | return; 235 | } 236 | this.setRemoteSdp_(message); 237 | this.doAnswer_(); 238 | } else if (message.type === 'answer' && this.isInitiator_) { 239 | if (this.pc_.signalingState !== 'have-local-offer') { 240 | trace('ERROR: remote answer received in unexpected state: ' + 241 | this.pc_.signalingState); 242 | return; 243 | } 244 | this.setRemoteSdp_(message); 245 | } else if (message.type === 'candidate') { 246 | var candidate = new RTCIceCandidate({ 247 | sdpMLineIndex: message.label, 248 | candidate: message.candidate 249 | }); 250 | this.recordIceCandidate_('Remote', candidate); 251 | this.pc_.addIceCandidate(candidate, 252 | trace.bind(null, 'Remote candidate added successfully.'), 253 | this.onError_.bind(this, 'addIceCandidate')); 254 | } else { 255 | trace('WARNING: unexpected message: ' + JSON.stringify(message)); 256 | } 257 | }; 258 | 259 | // When we receive messages from GAE registration and from the WSS connection, 260 | // we add them to a queue and drain it if conditions are right. 261 | PeerConnectionClient.prototype.drainMessageQueue_ = function() { 262 | // It's possible that we finish registering and receiving messages from WSS 263 | // before our peer connection is created or started. We need to wait for the 264 | // peer connection to be created and started before processing messages. 265 | // 266 | // Also, the order of messages is in general not the same as the POST order 267 | // from the other client because the POSTs are async and the server may handle 268 | // some requests faster than others. We need to process offer before 269 | // candidates so we wait for the offer to arrive first if we're answering. 270 | // Offers are added to the front of the queue. 271 | if (!this.pc_ || !this.started_ || !this.hasRemoteSdp_) { 272 | return; 273 | } 274 | for (var i = 0, len = this.messageQueue_.length; i < len; i++) { 275 | this.processSignalingMessage_(this.messageQueue_[i]); 276 | } 277 | this.messageQueue_ = []; 278 | }; 279 | 280 | PeerConnectionClient.prototype.onIceCandidate_ = function(event) { 281 | if (event.candidate) { 282 | // Eat undesired candidates. 283 | if (this.filterIceCandidate_(event.candidate)) { 284 | var message = { 285 | type: 'candidate', 286 | label: event.candidate.sdpMLineIndex, 287 | id: event.candidate.sdpMid, 288 | candidate: event.candidate.candidate 289 | }; 290 | if (this.onsignalingmessage) { 291 | this.onsignalingmessage(message); 292 | } 293 | this.recordIceCandidate_('Local', event.candidate); 294 | } 295 | } else { 296 | trace('End of candidates.'); 297 | } 298 | }; 299 | 300 | PeerConnectionClient.prototype.onSignalingStateChanged_ = function() { 301 | if (!this.pc_) { 302 | return; 303 | } 304 | trace('Signaling state changed to: ' + this.pc_.signalingState); 305 | 306 | if (this.onsignalingstatechange) { 307 | this.onsignalingstatechange(); 308 | } 309 | }; 310 | 311 | PeerConnectionClient.prototype.onIceConnectionStateChanged_ = function() { 312 | if (!this.pc_) { 313 | return; 314 | } 315 | trace('ICE connection state changed to: ' + this.pc_.iceConnectionState); 316 | if (this.pc_.iceConnectionState === 'completed') { 317 | trace('ICE complete time: ' + 318 | (window.performance.now() - this.startTime_).toFixed(0) + 'ms.'); 319 | } 320 | 321 | if (this.oniceconnectionstatechange) { 322 | this.oniceconnectionstatechange(); 323 | } 324 | }; 325 | 326 | // Return false if the candidate should be dropped, true if not. 327 | PeerConnectionClient.prototype.filterIceCandidate_ = function(candidateObj) { 328 | var candidateStr = candidateObj.candidate; 329 | 330 | // Always eat TCP candidates. Not needed in this context. 331 | if (candidateStr.indexOf('tcp') !== -1) { 332 | return false; 333 | } 334 | 335 | // If we're trying to eat non-relay candidates, do that. 336 | if (this.params_.peerConnectionConfig.iceTransports === 'relay' && 337 | iceCandidateType(candidateStr) !== 'relay') { 338 | return false; 339 | } 340 | 341 | return true; 342 | }; 343 | 344 | PeerConnectionClient.prototype.recordIceCandidate_ = 345 | function(location, candidateObj) { 346 | if (this.onnewicecandidate) { 347 | this.onnewicecandidate(location, candidateObj.candidate); 348 | } 349 | }; 350 | 351 | PeerConnectionClient.prototype.onRemoteStreamAdded_ = function(event) { 352 | if (this.onremotestreamadded) { 353 | this.onremotestreamadded(event.stream); 354 | } 355 | }; 356 | 357 | PeerConnectionClient.prototype.onError_ = function(tag, error) { 358 | if (this.onerror) { 359 | this.onerror(tag + ': ' + error.toString()); 360 | } 361 | }; 362 | -------------------------------------------------------------------------------- /public/js/peerconnectionclient_test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | 9 | /* More information about these options at jshint.com/docs/options */ 10 | 11 | /* globals TestCase, assertEquals, assertNotNull, assertTrue, assertFalse, 12 | PeerConnectionClient */ 13 | 14 | 'use strict'; 15 | 16 | var FAKEPCCONFIG = { 17 | 'bar': 'foo' 18 | }; 19 | var FAKEPCCONSTRAINTS = { 20 | 'foo': 'bar' 21 | }; 22 | 23 | var peerConnections = []; 24 | var MockRTCPeerConnection = function(config, constraints) { 25 | this.config = config; 26 | this.constraints = constraints; 27 | this.streams = []; 28 | this.createSdpRequests = []; 29 | this.localDescriptions = []; 30 | this.remoteDescriptions = []; 31 | this.remoteIceCandidates = []; 32 | this.signalingState = 'stable'; 33 | 34 | peerConnections.push(this); 35 | }; 36 | MockRTCPeerConnection.prototype.addStream = function(stream) { 37 | this.streams.push(stream); 38 | }; 39 | MockRTCPeerConnection.prototype.createOffer = 40 | function(callback, errback, constraints) { 41 | this.createSdpRequests.push({ 42 | type: 'offer', 43 | callback: callback, 44 | errback: errback, 45 | constraints: constraints 46 | }); 47 | }; 48 | MockRTCPeerConnection.prototype.createAnswer = 49 | function(callback, errback, constraints) { 50 | this.createSdpRequests.push({ 51 | type: 'answer', 52 | callback: callback, 53 | errback: errback, 54 | constraints: constraints 55 | }); 56 | }; 57 | MockRTCPeerConnection.prototype.resolveLastCreateSdpRequest = function(sdp) { 58 | var request = this.createSdpRequests.pop(); 59 | assertNotNull(request); 60 | 61 | if (sdp) { 62 | request.callback({ 63 | 'type': request.type, 64 | 'sdp': sdp 65 | }); 66 | } else { 67 | request.errback(Error('MockCreateSdpError')); 68 | } 69 | }; 70 | MockRTCPeerConnection.prototype.setLocalDescription = 71 | function(localDescription, callback, errback) { 72 | if (localDescription.type === 'offer') { 73 | this.signalingState = 'have-local-offer'; 74 | } else { 75 | this.signalingState = 'stable'; 76 | } 77 | this.localDescriptions.push({ 78 | description: localDescription, 79 | callback: callback, 80 | errback: errback 81 | }); 82 | }; 83 | MockRTCPeerConnection.prototype.setRemoteDescription = 84 | function(remoteDescription, callback, errback) { 85 | if (remoteDescription.type === 'offer') { 86 | this.signalingState = 'have-remote-offer'; 87 | } else { 88 | this.signalingState = 'stable'; 89 | } 90 | this.remoteDescriptions.push({ 91 | description: remoteDescription, 92 | callback: callback, 93 | errback: errback 94 | }); 95 | }; 96 | MockRTCPeerConnection.prototype.addIceCandidate = function(candidate) { 97 | this.remoteIceCandidates.push(candidate); 98 | }; 99 | MockRTCPeerConnection.prototype.close = function() { 100 | this.signalingState = 'closed'; 101 | }; 102 | MockRTCPeerConnection.prototype.getRemoteStreams = function() { 103 | return [{ 104 | getVideoTracks: function() { return ['track']; } 105 | }]; 106 | }; 107 | 108 | function getParams(pcConfig, pcConstraints) { 109 | return { 110 | 'peerConnectionConfig': pcConfig, 111 | 'peerConnectionConstraints': pcConstraints 112 | }; 113 | } 114 | 115 | var PeerConnectionClientTest = new TestCase('PeerConnectionClientTest'); 116 | 117 | PeerConnectionClientTest.prototype.setUp = function() { 118 | window.params = {}; 119 | 120 | this.readlRTCPeerConnection = RTCPeerConnection; 121 | RTCPeerConnection = MockRTCPeerConnection; 122 | 123 | peerConnections.length = 0; 124 | this.pcClient = new PeerConnectionClient( 125 | getParams(FAKEPCCONFIG, FAKEPCCONSTRAINTS), window.performance.now()); 126 | }; 127 | 128 | PeerConnectionClientTest.prototype.tearDown = function() { 129 | RTCPeerConnection = this.readlRTCPeerConnection; 130 | }; 131 | 132 | PeerConnectionClientTest.prototype.testConstructor = function() { 133 | assertEquals(1, peerConnections.length); 134 | assertEquals(FAKEPCCONFIG, peerConnections[0].config); 135 | assertEquals(FAKEPCCONSTRAINTS, peerConnections[0].constraints); 136 | }; 137 | 138 | PeerConnectionClientTest.prototype.testAddStream = function() { 139 | var stream = {'foo': 'bar'}; 140 | this.pcClient.addStream(stream); 141 | assertEquals(1, peerConnections[0].streams.length); 142 | assertEquals(stream, peerConnections[0].streams[0]); 143 | }; 144 | 145 | PeerConnectionClientTest.prototype.testStartAsCaller = function() { 146 | var signalingMsgs = []; 147 | function onSignalingMessage(msg) { 148 | signalingMsgs.push(msg); 149 | } 150 | 151 | this.pcClient.onsignalingmessage = onSignalingMessage; 152 | assertTrue(this.pcClient.startAsCaller(null)); 153 | 154 | assertEquals(1, peerConnections[0].createSdpRequests.length); 155 | var request = peerConnections[0].createSdpRequests[0]; 156 | assertEquals('offer', request.type); 157 | 158 | var fakeSdp = 'fake sdp'; 159 | peerConnections[0].resolveLastCreateSdpRequest(fakeSdp); 160 | 161 | // Verify the input to setLocalDesciption. 162 | assertEquals(1, peerConnections[0].localDescriptions.length); 163 | assertEquals('offer', 164 | peerConnections[0].localDescriptions[0].description.type); 165 | assertEquals(fakeSdp, 166 | peerConnections[0].localDescriptions[0].description.sdp); 167 | 168 | // Verify the output signaling message for the offer. 169 | assertEquals(1, signalingMsgs.length); 170 | assertEquals('offer', signalingMsgs[0].type); 171 | assertEquals(fakeSdp, signalingMsgs[0].sdp); 172 | 173 | // Verify the output signaling messages for the ICE candidates. 174 | signalingMsgs.length = 0; 175 | var fakeCandidate = 'fake candidate'; 176 | var event = { 177 | candidate: { 178 | sdpMLineIndex: '0', 179 | sdpMid: '1', 180 | candidate: fakeCandidate 181 | } 182 | }; 183 | var expectedMessage = { 184 | type: 'candidate', 185 | label: event.candidate.sdpMLineIndex, 186 | id: event.candidate.sdpMid, 187 | candidate: event.candidate.candidate 188 | }; 189 | peerConnections[0].onicecandidate(event); 190 | assertEquals(1, signalingMsgs.length); 191 | assertEquals(expectedMessage, signalingMsgs[0]); 192 | }; 193 | 194 | PeerConnectionClientTest.prototype.testCallerReceiveSignalingMessage = 195 | function() { 196 | this.pcClient.startAsCaller(null); 197 | peerConnections[0].resolveLastCreateSdpRequest('fake offer'); 198 | var remoteAnswer = { 199 | type: 'answer', 200 | sdp: 'fake answer' 201 | }; 202 | 203 | var pc = peerConnections[0]; 204 | 205 | this.pcClient.receiveSignalingMessage(JSON.stringify(remoteAnswer)); 206 | assertEquals(1, pc.remoteDescriptions.length); 207 | assertEquals('answer', pc.remoteDescriptions[0].description.type); 208 | assertEquals(remoteAnswer.sdp, pc.remoteDescriptions[0].description.sdp); 209 | 210 | var candidate = { 211 | type: 'candidate', 212 | label: '0', 213 | candidate: 'fake candidate' 214 | }; 215 | this.pcClient.receiveSignalingMessage(JSON.stringify(candidate)); 216 | assertEquals(1, pc.remoteIceCandidates.length); 217 | assertEquals(candidate.label, pc.remoteIceCandidates[0].sdpMLineIndex); 218 | assertEquals(candidate.candidate, pc.remoteIceCandidates[0].candidate); 219 | }; 220 | 221 | PeerConnectionClientTest.prototype.testStartAsCallee = function() { 222 | var remoteOffer = { 223 | type: 'offer', 224 | sdp: 'fake sdp' 225 | }; 226 | var candidate = { 227 | type: 'candidate', 228 | label: '0', 229 | candidate: 'fake candidate' 230 | }; 231 | var initialMsgs = [ 232 | JSON.stringify(candidate), 233 | JSON.stringify(remoteOffer) 234 | ]; 235 | this.pcClient.startAsCallee(initialMsgs); 236 | 237 | var pc = peerConnections[0]; 238 | 239 | // Verify that remote offer and ICE candidates are set. 240 | assertEquals(1, pc.remoteDescriptions.length); 241 | assertEquals('offer', pc.remoteDescriptions[0].description.type); 242 | assertEquals(remoteOffer.sdp, pc.remoteDescriptions[0].description.sdp); 243 | assertEquals(1, pc.remoteIceCandidates.length); 244 | assertEquals(candidate.label, pc.remoteIceCandidates[0].sdpMLineIndex); 245 | assertEquals(candidate.candidate, pc.remoteIceCandidates[0].candidate); 246 | 247 | // Verify that createAnswer is called. 248 | assertEquals(1, pc.createSdpRequests.length); 249 | assertEquals('answer', pc.createSdpRequests[0].type); 250 | 251 | var fakeAnswer = 'fake answer'; 252 | pc.resolveLastCreateSdpRequest(fakeAnswer); 253 | 254 | // Verify that setLocalDescription is called. 255 | assertEquals(1, pc.localDescriptions.length); 256 | assertEquals('answer', pc.localDescriptions[0].description.type); 257 | assertEquals(fakeAnswer, pc.localDescriptions[0].description.sdp); 258 | }; 259 | 260 | PeerConnectionClient.prototype.testReceiveRemoteOfferBeforeStarted = 261 | function() { 262 | var remoteOffer = { 263 | type: 'offer', 264 | sdp: 'fake sdp' 265 | }; 266 | this.pcClient.receiveSignalingMessage(JSON.stringify(remoteOffer)); 267 | this.pcClient.startAsCallee(null); 268 | 269 | // Verify that the offer received before started is processed. 270 | var pc = peerConnections[0]; 271 | assertEquals(1, pc.remoteDescriptions.length); 272 | assertEquals('offer', pc.remoteDescriptions[0].description.type); 273 | assertEquals(remoteOffer.sdp, pc.remoteDescriptions[0].description.sdp); 274 | }; 275 | 276 | PeerConnectionClientTest.prototype.testRemoteHangup = function() { 277 | var remoteHangup = false; 278 | this.pcClient.onremotehangup = function() { 279 | remoteHangup = true; 280 | }; 281 | this.pcClient.receiveSignalingMessage(JSON.stringify({ 282 | type: 'bye' 283 | })); 284 | assertTrue(remoteHangup); 285 | }; 286 | 287 | PeerConnectionClientTest.prototype.testOnRemoteSdpSet = function() { 288 | var hasRemoteTrack = false; 289 | function onRemoteSdpSet(result) { 290 | hasRemoteTrack = result; 291 | } 292 | this.pcClient.onremotesdpset = onRemoteSdpSet; 293 | 294 | var remoteOffer = { 295 | type: 'offer', 296 | sdp: 'fake sdp' 297 | }; 298 | var initialMsgs = [JSON.stringify(remoteOffer)]; 299 | this.pcClient.startAsCallee(initialMsgs); 300 | 301 | var callback = peerConnections[0].remoteDescriptions[0].callback; 302 | assertNotNull(callback); 303 | callback(); 304 | assertTrue(hasRemoteTrack); 305 | }; 306 | 307 | PeerConnectionClientTest.prototype.testOnRemoteStreamAdded = function() { 308 | var stream = null; 309 | function onRemoteStreamAdded(s) { 310 | stream = s; 311 | } 312 | this.pcClient.onremotestreamadded = onRemoteStreamAdded; 313 | 314 | var event = { 315 | stream: 'stream' 316 | }; 317 | peerConnections[0].onaddstream(event); 318 | assertEquals(event.stream, stream); 319 | }; 320 | 321 | PeerConnectionClientTest.prototype.testOnSignalingStateChange = function() { 322 | var called = false; 323 | function callback() { 324 | called = true; 325 | } 326 | this.pcClient.onsignalingstatechange = callback; 327 | peerConnections[0].onsignalingstatechange(); 328 | assertTrue(called); 329 | }; 330 | 331 | PeerConnectionClientTest.prototype.testOnIceConnectionStateChange = function() { 332 | var called = false; 333 | function callback() { 334 | called = true; 335 | } 336 | this.pcClient.oniceconnectionstatechange = callback; 337 | peerConnections[0].oniceconnectionstatechange(); 338 | assertTrue(called); 339 | }; 340 | 341 | PeerConnectionClientTest.prototype.testStartAsCallerTwiceFailed = function() { 342 | assertTrue(this.pcClient.startAsCaller(null)); 343 | assertFalse(this.pcClient.startAsCaller(null)); 344 | }; 345 | 346 | PeerConnectionClientTest.prototype.testStartAsCalleeTwiceFailed = function() { 347 | assertTrue(this.pcClient.startAsCallee(null)); 348 | assertFalse(this.pcClient.startAsCallee(null)); 349 | }; 350 | 351 | PeerConnectionClientTest.prototype.testClose = function() { 352 | this.pcClient.close(); 353 | assertEquals('closed', peerConnections[0].signalingState); 354 | }; 355 | -------------------------------------------------------------------------------- /public/js/remotewebsocket.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | 9 | /* More information about these options at jshint.com/docs/options */ 10 | 11 | /* globals apprtc, Constants */ 12 | /* exported RemoteWebSocket */ 13 | 14 | 'use strict'; 15 | 16 | // This class is used as a proxy for the WebSocket owned by background.js. 17 | // This proxy class sends commands and receives events via a Port object 18 | // opened to communicate with background.js in a Chrome App. 19 | // The WebSocket object must be owned by background.js so the call can be 20 | // properly terminated when the app window is closed. 21 | var RemoteWebSocket = function(wssUrl, wssPostUrl) { 22 | this.wssUrl_ = wssUrl; 23 | apprtc.windowPort.addMessageListener(this.handleMessage_.bind(this)); 24 | this.sendMessage_({ 25 | action: Constants.WS_ACTION, 26 | wsAction: Constants.WS_CREATE_ACTION, 27 | wssUrl: wssUrl, 28 | wssPostUrl: wssPostUrl 29 | }); 30 | this.readyState = WebSocket.CONNECTING; 31 | }; 32 | 33 | RemoteWebSocket.prototype.sendMessage_ = function(message) { 34 | apprtc.windowPort.sendMessage(message); 35 | }; 36 | 37 | RemoteWebSocket.prototype.send = function(data) { 38 | if (this.readyState !== WebSocket.OPEN) { 39 | throw 'Web socket is not in OPEN state: ' + this.readyState; 40 | } 41 | this.sendMessage_({ 42 | action: Constants.WS_ACTION, 43 | wsAction: Constants.WS_SEND_ACTION, 44 | data: data 45 | }); 46 | }; 47 | 48 | RemoteWebSocket.prototype.close = function() { 49 | if (this.readyState === WebSocket.CLOSING || 50 | this.readyState === WebSocket.CLOSED) { 51 | return; 52 | } 53 | this.readyState = WebSocket.CLOSING; 54 | this.sendMessage_({ 55 | action: Constants.WS_ACTION, 56 | wsAction: Constants.WS_CLOSE_ACTION 57 | }); 58 | }; 59 | 60 | RemoteWebSocket.prototype.handleMessage_ = function(message) { 61 | if (message.action === Constants.WS_ACTION && 62 | message.wsAction === Constants.EVENT_ACTION) { 63 | if (message.wsEvent === Constants.WS_EVENT_ONOPEN) { 64 | this.readyState = WebSocket.OPEN; 65 | if (this.onopen) { 66 | this.onopen(); 67 | } 68 | } else if (message.wsEvent === Constants.WS_EVENT_ONCLOSE) { 69 | this.readyState = WebSocket.CLOSED; 70 | if (this.onclose) { 71 | this.onclose(message.data); 72 | } 73 | } else if (message.wsEvent === Constants.WS_EVENT_ONERROR) { 74 | if (this.onerror) { 75 | this.onerror(message.data); 76 | } 77 | } else if (message.wsEvent === Constants.WS_EVENT_ONMESSAGE) { 78 | if (this.onmessage) { 79 | this.onmessage(message.data); 80 | } 81 | } else if (message.wsEvent === Constants.WS_EVENT_SENDERROR) { 82 | if (this.onsenderror) { 83 | this.onsenderror(message.data); 84 | } 85 | trace('ERROR: web socket send failed: ' + message.data); 86 | } 87 | } 88 | }; 89 | -------------------------------------------------------------------------------- /public/js/remotewebsocket_test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | 9 | /* More information about these options at jshint.com/docs/options */ 10 | 11 | /* globals TestCase, assertEquals, Constants, FAKE_WSS_URL, apprtc, 12 | RemoteWebSocket, MockWindowPort */ 13 | 14 | 'use strict'; 15 | var TEST_MESSAGE = 'foobar'; 16 | 17 | var RemoteWebSocketTest = new TestCase('RemoteWebSocketTest'); 18 | 19 | RemoteWebSocketTest.prototype.setUp = function() { 20 | this.realWindowPort = apprtc.windowPort; 21 | apprtc.windowPort = new MockWindowPort(); 22 | 23 | this.rws_ = new RemoteWebSocket(FAKE_WSS_URL); 24 | // Should have an message to request create. 25 | assertEquals(1, apprtc.windowPort.messages.length); 26 | assertEquals(Constants.WS_ACTION, apprtc.windowPort.messages[0].action); 27 | assertEquals(Constants.WS_CREATE_ACTION, 28 | apprtc.windowPort.messages[0].wsAction); 29 | assertEquals(FAKE_WSS_URL, apprtc.windowPort.messages[0].wssUrl); 30 | assertEquals(WebSocket.CONNECTING, this.rws_.readyState); 31 | 32 | }; 33 | 34 | RemoteWebSocketTest.prototype.tearDown = function() { 35 | apprtc.windowPort = this.realWindowPort; 36 | }; 37 | 38 | RemoteWebSocketTest.prototype.testSendBeforeOpen = function() { 39 | var exception = false; 40 | try { 41 | this.rws_.send(TEST_MESSAGE); 42 | } catch (ex) { 43 | if (ex) { 44 | exception = true; 45 | } 46 | } 47 | 48 | assertEquals(true, exception); 49 | }; 50 | 51 | RemoteWebSocketTest.prototype.testSend = function() { 52 | apprtc.windowPort.simulateMessageFromBackground({ 53 | action: Constants.WS_ACTION, 54 | wsAction: Constants.EVENT_ACTION, 55 | wsEvent: Constants.WS_EVENT_ONOPEN, 56 | data: TEST_MESSAGE 57 | }); 58 | 59 | assertEquals(1, apprtc.windowPort.messages.length); 60 | this.rws_.send(TEST_MESSAGE); 61 | assertEquals(2, apprtc.windowPort.messages.length); 62 | assertEquals(Constants.WS_ACTION, apprtc.windowPort.messages[1].action); 63 | assertEquals(Constants.WS_SEND_ACTION, 64 | apprtc.windowPort.messages[1].wsAction); 65 | assertEquals(TEST_MESSAGE, apprtc.windowPort.messages[1].data); 66 | }; 67 | 68 | RemoteWebSocketTest.prototype.testClose = function() { 69 | var message = null; 70 | var called = false; 71 | this.rws_.onclose = function(e) { 72 | called = true; 73 | message = e; 74 | }; 75 | 76 | assertEquals(1, apprtc.windowPort.messages.length); 77 | this.rws_.close(); 78 | 79 | assertEquals(2, apprtc.windowPort.messages.length); 80 | assertEquals(Constants.WS_ACTION, apprtc.windowPort.messages[1].action); 81 | assertEquals(Constants.WS_CLOSE_ACTION, 82 | apprtc.windowPort.messages[1].wsAction); 83 | 84 | assertEquals(WebSocket.CLOSING, this.rws_.readyState); 85 | apprtc.windowPort.simulateMessageFromBackground({ 86 | action: Constants.WS_ACTION, 87 | wsAction: Constants.EVENT_ACTION, 88 | wsEvent: Constants.WS_EVENT_ONCLOSE, 89 | data: TEST_MESSAGE 90 | }); 91 | assertEquals(true, called); 92 | assertEquals(TEST_MESSAGE, message); 93 | assertEquals(WebSocket.CLOSED, this.rws_.readyState); 94 | }; 95 | 96 | RemoteWebSocketTest.prototype.testOnError = function() { 97 | var message = null; 98 | var called = false; 99 | this.rws_.onerror = function(e) { 100 | called = true; 101 | message = e; 102 | }; 103 | 104 | apprtc.windowPort.simulateMessageFromBackground({ 105 | action: Constants.WS_ACTION, 106 | wsAction: Constants.EVENT_ACTION, 107 | wsEvent: Constants.WS_EVENT_ONERROR, 108 | data: TEST_MESSAGE 109 | }); 110 | assertEquals(true, called); 111 | assertEquals(TEST_MESSAGE, message); 112 | }; 113 | 114 | RemoteWebSocketTest.prototype.testOnOpen = function() { 115 | var called = false; 116 | this.rws_.onopen = function() { 117 | called = true; 118 | }; 119 | 120 | apprtc.windowPort.simulateMessageFromBackground({ 121 | action: Constants.WS_ACTION, 122 | wsAction: Constants.EVENT_ACTION, 123 | wsEvent: Constants.WS_EVENT_ONOPEN, 124 | data: TEST_MESSAGE 125 | }); 126 | assertEquals(true, called); 127 | assertEquals(WebSocket.OPEN, this.rws_.readyState); 128 | }; 129 | 130 | RemoteWebSocketTest.prototype.testOnMessage = function() { 131 | var message = null; 132 | var called = false; 133 | this.rws_.onmessage = function(e) { 134 | called = true; 135 | message = e; 136 | }; 137 | 138 | apprtc.windowPort.simulateMessageFromBackground({ 139 | action: Constants.WS_ACTION, 140 | wsAction: Constants.EVENT_ACTION, 141 | wsEvent: Constants.WS_EVENT_ONMESSAGE, 142 | data: TEST_MESSAGE 143 | }); 144 | assertEquals(true, called); 145 | assertEquals(TEST_MESSAGE, message); 146 | }; 147 | 148 | RemoteWebSocketTest.prototype.testOnSendError = function() { 149 | var message = null; 150 | var called = false; 151 | this.rws_.onsenderror = function(e) { 152 | called = true; 153 | message = e; 154 | }; 155 | 156 | apprtc.windowPort.simulateMessageFromBackground({ 157 | action: Constants.WS_ACTION, 158 | wsAction: Constants.EVENT_ACTION, 159 | wsEvent: Constants.WS_EVENT_SENDERROR, 160 | data: TEST_MESSAGE 161 | }); 162 | assertEquals(true, called); 163 | assertEquals(TEST_MESSAGE, message); 164 | }; 165 | -------------------------------------------------------------------------------- /public/js/roomselection.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | 9 | /* More information about these options at jshint.com/docs/options */ 10 | 11 | /* globals randomString, Storage, parseJSON */ 12 | /* exported RoomSelection */ 13 | 14 | 'use strict'; 15 | 16 | var RoomSelection = function(roomSelectionDiv, 17 | uiConstants, recentRoomsKey, setupCompletedCallback) { 18 | this.roomSelectionDiv_ = roomSelectionDiv; 19 | 20 | this.setupCompletedCallback_ = setupCompletedCallback; 21 | 22 | this.roomIdInput_ = this.roomSelectionDiv_.querySelector( 23 | uiConstants.roomSelectionInput); 24 | this.roomIdInputLabel_ = this.roomSelectionDiv_.querySelector( 25 | uiConstants.roomSelectionInputLabel); 26 | this.roomJoinButton_ = this.roomSelectionDiv_.querySelector( 27 | uiConstants.roomSelectionJoinButton); 28 | this.roomRandomButton_ = this.roomSelectionDiv_.querySelector( 29 | uiConstants.roomSelectionRandomButton); 30 | this.roomRecentList_ = this.roomSelectionDiv_.querySelector( 31 | uiConstants.roomSelectionRecentList); 32 | 33 | this.roomIdInput_.value = randomString(9); 34 | // Call onRoomIdInput_ now to validate initial state of input box. 35 | this.onRoomIdInput_(); 36 | this.roomIdInput_.addEventListener('input', 37 | this.onRoomIdInput_.bind(this), false); 38 | this.roomIdInput_.addEventListener('keyup', 39 | this.onRoomIdKeyPress_.bind(this), false); 40 | this.roomRandomButton_.addEventListener('click', 41 | this.onRandomButton_.bind(this), false); 42 | this.roomJoinButton_.addEventListener('click', 43 | this.onJoinButton_.bind(this), false); 44 | 45 | // Public callbacks. Keep it sorted. 46 | this.onRoomSelected = null; 47 | 48 | this.recentlyUsedList_ = new RoomSelection.RecentlyUsedList(recentRoomsKey); 49 | this.startBuildingRecentRoomList_(); 50 | }; 51 | 52 | RoomSelection.matchRandomRoomPattern = function(input) { 53 | return input.match(/^\d{9}$/) !== null; 54 | }; 55 | 56 | RoomSelection.prototype.startBuildingRecentRoomList_ = function() { 57 | this.recentlyUsedList_.getRecentRooms().then(function(recentRooms) { 58 | this.buildRecentRoomList_(recentRooms); 59 | if (this.setupCompletedCallback_) { 60 | this.setupCompletedCallback_(); 61 | } 62 | }.bind(this)).catch(function(error) { 63 | trace('Error building recent rooms list: ' + error.message); 64 | }.bind(this)); 65 | }; 66 | 67 | RoomSelection.prototype.buildRecentRoomList_ = function(recentRooms) { 68 | var lastChild = this.roomRecentList_.lastChild; 69 | while (lastChild) { 70 | this.roomRecentList_.removeChild(lastChild); 71 | lastChild = this.roomRecentList_.lastChild; 72 | } 73 | 74 | for (var i = 0; i < recentRooms.length; ++i) { 75 | // Create link in recent list 76 | var li = document.createElement('li'); 77 | var href = document.createElement('a'); 78 | var linkText = document.createTextNode(recentRooms[i]); 79 | href.appendChild(linkText); 80 | href.href = location.origin + '/r/' + encodeURIComponent(recentRooms[i]); 81 | li.appendChild(href); 82 | this.roomRecentList_.appendChild(li); 83 | 84 | // Set up click handler to avoid browser navigation. 85 | href.addEventListener('click', 86 | this.makeRecentlyUsedClickHandler_(recentRooms[i]).bind(this), false); 87 | } 88 | }; 89 | 90 | RoomSelection.prototype.onRoomIdInput_ = function() { 91 | // Validate room id, enable/disable join button. 92 | // The server currently accepts only the \w character class. 93 | var room = this.roomIdInput_.value; 94 | var valid = room.length >= 5; 95 | var re = /^\w+$/; 96 | valid = valid && re.exec(room); 97 | if (valid) { 98 | this.roomJoinButton_.disabled = false; 99 | this.roomIdInput_.classList.remove('invalid'); 100 | this.roomIdInputLabel_.classList.add('hidden'); 101 | } else { 102 | this.roomJoinButton_.disabled = true; 103 | this.roomIdInput_.classList.add('invalid'); 104 | this.roomIdInputLabel_.classList.remove('hidden'); 105 | } 106 | }; 107 | 108 | RoomSelection.prototype.onRoomIdKeyPress_ = function(event) { 109 | if (event.which !== 13 || this.roomJoinButton_.disabled) { 110 | return; 111 | } 112 | this.onJoinButton_(); 113 | }; 114 | 115 | RoomSelection.prototype.onRandomButton_ = function() { 116 | this.roomIdInput_.value = randomString(9); 117 | this.onRoomIdInput_(); 118 | }; 119 | 120 | RoomSelection.prototype.onJoinButton_ = function() { 121 | this.loadRoom_(this.roomIdInput_.value); 122 | }; 123 | 124 | RoomSelection.prototype.makeRecentlyUsedClickHandler_ = function(roomName) { 125 | return function(e) { 126 | e.preventDefault(); 127 | this.loadRoom_(roomName); 128 | }; 129 | }; 130 | 131 | RoomSelection.prototype.loadRoom_ = function(roomName) { 132 | this.recentlyUsedList_.pushRecentRoom(roomName); 133 | if (this.onRoomSelected) { 134 | this.onRoomSelected(roomName); 135 | } 136 | }; 137 | 138 | RoomSelection.RecentlyUsedList = function(key) { 139 | // This is the length of the most recently used list. 140 | this.LISTLENGTH_ = 10; 141 | 142 | this.RECENTROOMSKEY_ = key || 'recentRooms'; 143 | this.storage_ = new Storage(); 144 | }; 145 | 146 | // Add a room to the recently used list and store to local storage. 147 | RoomSelection.RecentlyUsedList.prototype.pushRecentRoom = function(roomId) { 148 | // Push recent room to top of recent list, keep max of this.LISTLENGTH_ entries. 149 | return new Promise(function(resolve, reject) { 150 | if (!roomId) { 151 | resolve(); 152 | return; 153 | } 154 | 155 | this.getRecentRooms().then(function(recentRooms) { 156 | recentRooms = [roomId].concat(recentRooms); 157 | // Remove any duplicates from the list, leaving the first occurance. 158 | recentRooms = recentRooms.filter(function(value, index, self) { 159 | return self.indexOf(value) === index; 160 | }); 161 | recentRooms = recentRooms.slice(0, this.LISTLENGTH_); 162 | this.storage_.setStorage(this.RECENTROOMSKEY_, 163 | JSON.stringify(recentRooms), function() { 164 | resolve(); 165 | }); 166 | }.bind(this)).catch(function(err) { 167 | reject(err); 168 | }.bind(this)); 169 | }.bind(this)); 170 | }; 171 | 172 | // Get the list of recently used rooms from local storage. 173 | RoomSelection.RecentlyUsedList.prototype.getRecentRooms = function() { 174 | return new Promise(function(resolve) { 175 | this.storage_.getStorage(this.RECENTROOMSKEY_, function(value) { 176 | var recentRooms = parseJSON(value); 177 | if (!recentRooms) { 178 | recentRooms = []; 179 | } 180 | resolve(recentRooms); 181 | }); 182 | }.bind(this)); 183 | }; 184 | -------------------------------------------------------------------------------- /public/js/roomselection_test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | 9 | /* More information about these options at jshint.com/docs/options */ 10 | 11 | /* globals UI_CONSTANTS, RoomSelection, assertMatch, assertEquals, 12 | AsyncTestCase */ 13 | 14 | 'use strict'; 15 | 16 | var RoomSelectionTest = new AsyncTestCase('RoomSelectionTest'); 17 | 18 | RoomSelectionTest.prototype.setUp = function() { 19 | var key = 'testRecentRoomsKey'; 20 | localStorage.removeItem(key); 21 | localStorage.setItem(key, '["room1", "room2", "room3"]'); 22 | 23 | this.targetDiv_ = document.createElement('div'); 24 | this.targetDiv_.id = UI_CONSTANTS.roomSelectionDiv.substring(1); 25 | 26 | this.inputBox_ = document.createElement('input'); 27 | this.inputBox_.id = UI_CONSTANTS.roomSelectionInput.substring(1); 28 | this.inputBox_.type = 'text'; 29 | 30 | this.inputBoxLabel_ = document.createElement('label'); 31 | this.inputBoxLabel_.id = UI_CONSTANTS.roomSelectionInputLabel.substring(1); 32 | 33 | this.randomButton_ = document.createElement('button'); 34 | this.randomButton_.id = UI_CONSTANTS.roomSelectionRandomButton.substring(1); 35 | 36 | this.joinButton_ = document.createElement('button'); 37 | this.joinButton_.id = UI_CONSTANTS.roomSelectionJoinButton.substring(1); 38 | 39 | this.recentList_ = document.createElement('ul'); 40 | this.recentList_.id = UI_CONSTANTS.roomSelectionRecentList.substring(1); 41 | 42 | this.targetDiv_.appendChild(this.inputBox_); 43 | this.targetDiv_.appendChild(this.inputBoxLabel_); 44 | this.targetDiv_.appendChild(this.randomButton_); 45 | this.targetDiv_.appendChild(this.joinButton_); 46 | this.targetDiv_.appendChild(this.recentList_); 47 | 48 | this.roomSelectionSetupCompletedPromise_ = new Promise(function(resolve) { 49 | this.roomSelection_ = new RoomSelection(this.targetDiv_, 50 | UI_CONSTANTS, 51 | key, 52 | function() { 53 | resolve(); 54 | }.bind(this)); 55 | }.bind(this)); 56 | }; 57 | 58 | RoomSelectionTest.prototype.tearDown = function() { 59 | localStorage.removeItem('testRecentRoomsKey'); 60 | this.roomSelection_ = null; 61 | }; 62 | 63 | RoomSelectionTest.createUIEvent = function(type) { 64 | var event = document.createEvent('UIEvent'); 65 | event.initUIEvent(type, true, true); 66 | return event; 67 | }; 68 | 69 | RoomSelectionTest.prototype.testInputFilter = function() { 70 | var validInputs = [ 71 | '123123', 72 | 'asdfs3', 73 | 'room1', 74 | '3254234523452345234523452345asdfasfdasdf' 75 | ]; 76 | var invalidInputs = [ 77 | '', 78 | ' ', 79 | 'abcd', 80 | '123', 81 | '[5afasdf', 82 | 'ñsaer3' 83 | ]; 84 | 85 | var testInput = function(input, expectedResult) { 86 | this.inputBox_.value = input; 87 | this.inputBox_.dispatchEvent(RoomSelectionTest.createUIEvent('input')); 88 | 89 | assertEquals('Incorrect result with input: "' + input + '"', 90 | expectedResult, 91 | this.joinButton_.disabled); 92 | }.bind(this); 93 | 94 | for (var i = 0; i < validInputs.length; ++i) { 95 | testInput(validInputs[i], false); 96 | } 97 | 98 | for (i = 0; i < invalidInputs.length; ++i) { 99 | testInput(invalidInputs[i], true); 100 | } 101 | }; 102 | 103 | RoomSelectionTest.prototype.testRandomButton = function() { 104 | this.inputBox_.value = '123'; 105 | this.randomButton_.click(); 106 | assertMatch(/[0-9]{9}/, this.inputBox_.value); 107 | }; 108 | 109 | RoomSelectionTest.prototype.testRecentListHasChildren = function(queue) { 110 | queue.call('Step 1: wait for recent rooms list to be completed.', 111 | function(callbacks) { 112 | var onCompleted = callbacks.add(function() {}); 113 | this.roomSelectionSetupCompletedPromise_.then(function() { 114 | onCompleted(); 115 | }.bind(this)); 116 | }); 117 | 118 | queue.call('Step 2: validate recent rooms list.', function() { 119 | var children = this.recentList_.children; 120 | assertEquals('There should be 3 recent links.', 3, children.length); 121 | assertEquals('The text of the first should be room4.', 122 | 'room1', 123 | children[0].innerText); 124 | assertEquals('The first link should have 1 child.', 125 | 1, 126 | children[0].children.length); 127 | assertMatch('That child should be an href with a link containing room1.', /room1/, children[0].children[0].href); 128 | }); 129 | }; 130 | 131 | RoomSelectionTest.prototype.testJoinButton = function() { 132 | this.inputBox_.value = 'targetRoom'; 133 | var joinedRoom = null; 134 | this.roomSelection_.onRoomSelected = function(room) { 135 | joinedRoom = room; 136 | }; 137 | this.joinButton_.click(); 138 | 139 | assertEquals('targetRoom', joinedRoom); 140 | }; 141 | 142 | RoomSelectionTest.prototype.testMakeClickHandler = function(queue) { 143 | queue.call('Step 1: wait for recent rooms list to be completed.', 144 | function(callbacks) { 145 | var onCompleted = callbacks.add(function() {}); 146 | this.roomSelectionSetupCompletedPromise_.then(function() { 147 | onCompleted(); 148 | }.bind(this)); 149 | }); 150 | 151 | queue.call('Step 2: validate that click handler works.', function() { 152 | var children = this.recentList_.children; 153 | var link = children[0].children[0]; 154 | 155 | var joinedRoom = null; 156 | this.roomSelection_.onRoomSelected = function(room) { 157 | joinedRoom = room; 158 | }; 159 | 160 | link.dispatchEvent(RoomSelectionTest.createUIEvent('click')); 161 | 162 | assertEquals('room1', joinedRoom); 163 | }); 164 | }; 165 | 166 | RoomSelectionTest.prototype.testMatchRandomRoomPattern = function() { 167 | var testCases = [ 168 | 'abcdefghi', 169 | '1abcdefgh', 170 | '1abcdefg1', 171 | '12345678', 172 | '12345678a', 173 | 'a12345678', 174 | '123456789' 175 | ]; 176 | var expected = [ 177 | false, false, false, false, false, false, true 178 | ]; 179 | for (var i = 0; i < testCases.length; ++i) { 180 | assertEquals(expected[i], 181 | RoomSelection.matchRandomRoomPattern(testCases[i])); 182 | } 183 | }; 184 | 185 | RoomSelectionTest.prototype.testHitEnterInRoomIdInput = function() { 186 | var joinedRoom = null; 187 | this.roomSelection_.onRoomSelected = function(room) { 188 | joinedRoom = room; 189 | }; 190 | function createEnterKeyUpEvent() { 191 | var e = document.createEvent('Event'); 192 | e.initEvent('keyup'); 193 | e.keyCode = 13; 194 | e.which = 13; 195 | return e; 196 | } 197 | 198 | // Hitting ENTER when the room name is invalid should do nothing. 199 | this.inputBox_.value = '1'; 200 | this.inputBox_.dispatchEvent(RoomSelectionTest.createUIEvent('input')); 201 | this.inputBox_.dispatchEvent(createEnterKeyUpEvent()); 202 | assertEquals(null, joinedRoom); 203 | 204 | // Hitting ENTER when the room name is valid should select the room. 205 | this.inputBox_.value = '12345'; 206 | this.inputBox_.dispatchEvent(RoomSelectionTest.createUIEvent('input')); 207 | assertEquals(false, this.joinButton_.disabled); 208 | this.inputBox_.dispatchEvent(createEnterKeyUpEvent()); 209 | assertEquals(this.inputBox_.value, joinedRoom); 210 | 211 | joinedRoom = null; 212 | // Hitting other keys should not select the room. 213 | var e = document.createEvent('Event'); 214 | e.initEvent('keyup'); 215 | this.inputBox_.dispatchEvent(e); 216 | assertEquals(null, joinedRoom); 217 | }; 218 | 219 | var RecentlyUsedListTest = new AsyncTestCase('RecentlyUsedListTest'); 220 | 221 | RecentlyUsedListTest.prototype.setUp = function() { 222 | this.key_ = 'testRecentRoomsKey'; 223 | 224 | this.fullList_ = 225 | '["room4","room5","room6","room7","room8","room9",' + 226 | '"room10","room11","room12","room13"]'; 227 | this.tooManyList_ = 228 | '["room1","room2","room3","room4","room5","room6",' + 229 | '"room7","room8","room9","room10","room11","room12","room13"]'; 230 | this.duplicatesList_ = 231 | '["room4","room4","room6","room7","room6","room9",' + 232 | '"room10","room4","room6","room13"]'; 233 | this.noDuplicatesList_ = 234 | '["room4","room6","room7","room9","room10","room13"]'; 235 | this.emptyList_ = '[]'; 236 | this.notAList_ = 'asdasd'; 237 | 238 | this.recentlyUsedList_ = new RoomSelection.RecentlyUsedList(this.key_); 239 | }; 240 | 241 | RecentlyUsedListTest.prototype.tearDown = function() { 242 | localStorage.removeItem(this.key_); 243 | this.recentlyUsedList_ = null; 244 | }; 245 | 246 | RecentlyUsedListTest.prototype.testPushRecentlyUsedRoomDuplicateList = 247 | function(queue) { 248 | queue.call('Step 1: push new value.', function(callbacks) { 249 | var onCompleted = callbacks.add(function() {}); 250 | localStorage.removeItem(this.key_); 251 | localStorage.setItem(this.key_, this.duplicatesList_); 252 | this.recentlyUsedList_.pushRecentRoom('newRoom').then(function() { 253 | onCompleted(); 254 | }.bind(this)); 255 | }); 256 | queue.call('Step 2: verify results.', function() { 257 | var result = localStorage.getItem(this.key_); 258 | assertEquals( 259 | this.noDuplicatesList_ 260 | .replace('"room4"', '"newRoom","room4"'), 261 | result); 262 | }); 263 | }; 264 | 265 | RecentlyUsedListTest.prototype.testPushRecentlyUsedRoomTooManyList = 266 | function(queue) { 267 | queue.call('Step 1: push new value.', function(callbacks) { 268 | var onCompleted = callbacks.add(function() {}); 269 | localStorage.removeItem(this.key_); 270 | localStorage.setItem(this.key_, this.tooManyList_); 271 | this.recentlyUsedList_.pushRecentRoom('newRoom').then(function() { 272 | onCompleted(); 273 | }.bind(this)); 274 | }); 275 | queue.call('Step 2: verify results.', function() { 276 | var result = localStorage.getItem(this.key_); 277 | assertEquals( 278 | this.tooManyList_ 279 | .replace(',"room10","room11","room12","room13"', '') 280 | .replace('"room1"', '"newRoom","room1"'), 281 | result); 282 | }); 283 | }; 284 | 285 | RecentlyUsedListTest.prototype.testPushRecentlyUsedRoomFullList = 286 | function(queue) { 287 | queue.call('Step 1: push new value.', function(callbacks) { 288 | var onCompleted = callbacks.add(function() {}); 289 | localStorage.removeItem(this.key_); 290 | localStorage.setItem(this.key_, this.fullList_); 291 | this.recentlyUsedList_.pushRecentRoom('newRoom').then(function() { 292 | onCompleted(); 293 | }.bind(this)); 294 | }); 295 | queue.call('Step 2: verify results.', function() { 296 | var result = localStorage.getItem(this.key_); 297 | assertEquals( 298 | this.fullList_ 299 | .replace(',"room13"', '') 300 | .replace('"room4"', '"newRoom","room4"'), 301 | result); 302 | }); 303 | }; 304 | 305 | RecentlyUsedListTest.prototype.testPushRecentlyUsedRoomNoExisting = 306 | function(queue) { 307 | queue.call('Step 1: push new value.', function(callbacks) { 308 | var onCompleted = callbacks.add(function() {}); 309 | localStorage.removeItem(this.key_); 310 | 311 | this.recentlyUsedList_.pushRecentRoom('newRoom').then(function() { 312 | onCompleted(); 313 | }.bind(this)); 314 | }); 315 | queue.call('Step 2: verify results.', function() { 316 | var result = localStorage.getItem(this.key_); 317 | assertEquals('["newRoom"]', result); 318 | }); 319 | }; 320 | 321 | RecentlyUsedListTest.prototype.testPushRecentlyUsedRoomInvalidExisting = 322 | function(queue) { 323 | queue.call('Step 1: push new value.', function(callbacks) { 324 | var onCompleted = callbacks.add(function() {}); 325 | localStorage.removeItem(this.key_); 326 | localStorage.setItem(this.key_, this.notAList_); 327 | this.recentlyUsedList_.pushRecentRoom('newRoom').then(function() { 328 | onCompleted(); 329 | }.bind(this)); 330 | }); 331 | queue.call('Step 2: verify results.', function() { 332 | var result = localStorage.getItem(this.key_); 333 | assertEquals('["newRoom"]', result); 334 | }); 335 | }; 336 | -------------------------------------------------------------------------------- /public/js/sdputils.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | 9 | /* More information about these options at jshint.com/docs/options */ 10 | 11 | /* globals trace */ 12 | /* exported setCodecParam, iceCandidateType, maybeSetOpusOptions, 13 | maybePreferAudioReceiveCodec, maybePreferAudioSendCodec, 14 | maybeSetAudioReceiveBitRate, maybeSetAudioSendBitRate, 15 | maybePreferVideoReceiveCodec, maybePreferVideoSendCodec, 16 | maybeSetVideoReceiveBitRate, maybeSetVideoSendBitRate, 17 | maybeSetVideoSendInitialBitRate, mergeConstraints, removeCodecParam */ 18 | 19 | 'use strict'; 20 | 21 | function mergeConstraints(cons1, cons2) { 22 | if (!cons1 || !cons2) { 23 | return cons1 || cons2; 24 | } 25 | var merged = cons1; 26 | for (var name in cons2.mandatory) { 27 | merged.mandatory[name] = cons2.mandatory[name]; 28 | } 29 | merged.optional = merged.optional.concat(cons2.optional); 30 | return merged; 31 | } 32 | 33 | function iceCandidateType(candidateStr) { 34 | return candidateStr.split(' ')[7]; 35 | } 36 | 37 | function maybeSetOpusOptions(sdp, params) { 38 | // Set Opus in Stereo, if stereo is true, unset it, if stereo is false, and 39 | // do nothing if otherwise. 40 | if (params.opusStereo === 'true') { 41 | sdp = setCodecParam(sdp, 'opus/48000', 'stereo', '1'); 42 | } else if (params.opusStereo === 'false') { 43 | sdp = removeCodecParam(sdp, 'opus/48000', 'stereo'); 44 | } 45 | 46 | // Set Opus FEC, if opusfec is true, unset it, if opusfec is false, and 47 | // do nothing if otherwise. 48 | if (params.opusFec === 'true') { 49 | sdp = setCodecParam(sdp, 'opus/48000', 'useinbandfec', '1'); 50 | } else if (params.opusFec === 'false') { 51 | sdp = removeCodecParam(sdp, 'opus/48000', 'useinbandfec'); 52 | } 53 | 54 | // Set Opus maxplaybackrate, if requested. 55 | if (params.opusMaxPbr) { 56 | sdp = setCodecParam( 57 | sdp, 'opus/48000', 'maxplaybackrate', params.opusMaxPbr); 58 | } 59 | return sdp; 60 | } 61 | 62 | function maybeSetAudioSendBitRate(sdp, params) { 63 | if (!params.audioSendBitrate) { 64 | return sdp; 65 | } 66 | trace('Prefer audio send bitrate: ' + params.audioSendBitrate); 67 | return preferBitRate(sdp, params.audioSendBitrate, 'audio'); 68 | } 69 | 70 | function maybeSetAudioReceiveBitRate(sdp, params) { 71 | if (!params.audioRecvBitrate) { 72 | return sdp; 73 | } 74 | trace('Prefer audio receive bitrate: ' + params.audioRecvBitrate); 75 | return preferBitRate(sdp, params.audioRecvBitrate, 'audio'); 76 | } 77 | 78 | function maybeSetVideoSendBitRate(sdp, params) { 79 | if (!params.videoSendBitrate) { 80 | return sdp; 81 | } 82 | trace('Prefer video send bitrate: ' + params.videoSendBitrate); 83 | return preferBitRate(sdp, params.videoSendBitrate, 'video'); 84 | } 85 | 86 | function maybeSetVideoReceiveBitRate(sdp, params) { 87 | if (!params.videoRecvBitrate) { 88 | return sdp; 89 | } 90 | trace('Prefer video receive bitrate: ' + params.videoRecvBitrate); 91 | return preferBitRate(sdp, params.videoRecvBitrate, 'video'); 92 | } 93 | 94 | // Add a b=AS:bitrate line to the m=mediaType section. 95 | function preferBitRate(sdp, bitrate, mediaType) { 96 | var sdpLines = sdp.split('\r\n'); 97 | 98 | // Find m line for the given mediaType. 99 | var mLineIndex = findLine(sdpLines, 'm=', mediaType); 100 | if (mLineIndex === null) { 101 | trace('Failed to add bandwidth line to sdp, as no m-line found'); 102 | return sdp; 103 | } 104 | 105 | // Find next m-line if any. 106 | var nextMLineIndex = findLineInRange(sdpLines, mLineIndex + 1, -1, 'm='); 107 | if (nextMLineIndex === null) { 108 | nextMLineIndex = sdpLines.length; 109 | } 110 | 111 | // Find c-line corresponding to the m-line. 112 | var cLineIndex = findLineInRange(sdpLines, mLineIndex + 1, 113 | nextMLineIndex, 'c='); 114 | if (cLineIndex === null) { 115 | trace('Failed to add bandwidth line to sdp, as no c-line found'); 116 | return sdp; 117 | } 118 | 119 | // Check if bandwidth line already exists between c-line and next m-line. 120 | var bLineIndex = findLineInRange(sdpLines, cLineIndex + 1, 121 | nextMLineIndex, 'b=AS'); 122 | if (bLineIndex) { 123 | sdpLines.splice(bLineIndex, 1); 124 | } 125 | 126 | // Create the b (bandwidth) sdp line. 127 | var bwLine = 'b=AS:' + bitrate; 128 | // As per RFC 4566, the b line should follow after c-line. 129 | sdpLines.splice(cLineIndex + 1, 0, bwLine); 130 | sdp = sdpLines.join('\r\n'); 131 | return sdp; 132 | } 133 | 134 | // Add an a=fmtp: x-google-min-bitrate=kbps line, if videoSendInitialBitrate 135 | // is specified. We'll also add a x-google-min-bitrate value, since the max 136 | // must be >= the min. 137 | function maybeSetVideoSendInitialBitRate(sdp, params) { 138 | var initialBitrate = params.videoSendInitialBitrate; 139 | if (!initialBitrate) { 140 | return sdp; 141 | } 142 | 143 | // Validate the initial bitrate value. 144 | var maxBitrate = initialBitrate; 145 | var bitrate = params.videoSendBitrate; 146 | if (bitrate) { 147 | if (initialBitrate > bitrate) { 148 | trace('Clamping initial bitrate to max bitrate of ' + 149 | bitrate + ' kbps.'); 150 | initialBitrate = bitrate; 151 | params.videoSendInitialBitrate = initialBitrate; 152 | } 153 | maxBitrate = bitrate; 154 | } 155 | 156 | var sdpLines = sdp.split('\r\n'); 157 | 158 | // Search for m line. 159 | var mLineIndex = findLine(sdpLines, 'm=', 'video'); 160 | if (mLineIndex === null) { 161 | trace('Failed to find video m-line'); 162 | return sdp; 163 | } 164 | 165 | sdp = setCodecParam(sdp, 'VP8/90000', 'x-google-min-bitrate', 166 | params.videoSendInitialBitrate.toString()); 167 | sdp = setCodecParam(sdp, 'VP8/90000', 'x-google-max-bitrate', 168 | maxBitrate.toString()); 169 | 170 | return sdp; 171 | } 172 | 173 | // Promotes |audioSendCodec| to be the first in the m=audio line, if set. 174 | function maybePreferAudioSendCodec(sdp, params) { 175 | return maybePreferCodec(sdp, 'audio', 'send', params.audioSendCodec); 176 | } 177 | 178 | // Promotes |audioRecvCodec| to be the first in the m=audio line, if set. 179 | function maybePreferAudioReceiveCodec(sdp, params) { 180 | return maybePreferCodec(sdp, 'audio', 'receive', params.audioRecvCodec); 181 | } 182 | 183 | // Promotes |videoSendCodec| to be the first in the m=audio line, if set. 184 | function maybePreferVideoSendCodec(sdp, params) { 185 | return maybePreferCodec(sdp, 'video', 'send', params.videoSendCodec); 186 | } 187 | 188 | // Promotes |videoRecvCodec| to be the first in the m=audio line, if set. 189 | function maybePreferVideoReceiveCodec(sdp, params) { 190 | return maybePreferCodec(sdp, 'video', 'receive', params.videoRecvCodec); 191 | } 192 | 193 | // Sets |codec| as the default |type| codec if it's present. 194 | // The format of |codec| is 'NAME/RATE', e.g. 'opus/48000'. 195 | function maybePreferCodec(sdp, type, dir, codec) { 196 | var str = type + ' ' + dir + ' codec'; 197 | if (codec === '') { 198 | trace('No preference on ' + str + '.'); 199 | return sdp; 200 | } 201 | 202 | trace('Prefer ' + str + ': ' + codec); 203 | 204 | var sdpLines = sdp.split('\r\n'); 205 | 206 | // Search for m line. 207 | var mLineIndex = findLine(sdpLines, 'm=', type); 208 | if (mLineIndex === null) { 209 | return sdp; 210 | } 211 | 212 | // If the codec is available, set it as the default in m line. 213 | var payload = getCodecPayloadType(sdpLines, codec); 214 | if (payload) { 215 | sdpLines[mLineIndex] = setDefaultCodec(sdpLines[mLineIndex], payload); 216 | } 217 | 218 | sdp = sdpLines.join('\r\n'); 219 | return sdp; 220 | } 221 | 222 | // Set fmtp param to specific codec in SDP. If param does not exists, add it. 223 | function setCodecParam(sdp, codec, param, value) { 224 | var sdpLines = sdp.split('\r\n'); 225 | 226 | var fmtpLineIndex = findFmtpLine(sdpLines, codec); 227 | 228 | var fmtpObj = {}; 229 | if (fmtpLineIndex === null) { 230 | var index = findLine(sdpLines, 'a=rtpmap', codec); 231 | if (index === null) { 232 | return sdp; 233 | } 234 | var payload = getCodecPayloadTypeFromLine(sdpLines[index]); 235 | fmtpObj.pt = payload.toString(); 236 | fmtpObj.params = {}; 237 | fmtpObj.params[param] = value; 238 | sdpLines.splice(index + 1, 0, writeFmtpLine(fmtpObj)); 239 | } else { 240 | fmtpObj = parseFmtpLine(sdpLines[fmtpLineIndex]); 241 | fmtpObj.params[param] = value; 242 | sdpLines[fmtpLineIndex] = writeFmtpLine(fmtpObj); 243 | } 244 | 245 | sdp = sdpLines.join('\r\n'); 246 | return sdp; 247 | } 248 | 249 | // Remove fmtp param if it exists. 250 | function removeCodecParam(sdp, codec, param) { 251 | var sdpLines = sdp.split('\r\n'); 252 | 253 | var fmtpLineIndex = findFmtpLine(sdpLines, codec); 254 | if (fmtpLineIndex === null) { 255 | return sdp; 256 | } 257 | 258 | var map = parseFmtpLine(sdpLines[fmtpLineIndex]); 259 | delete map.params[param]; 260 | 261 | var newLine = writeFmtpLine(map); 262 | if (newLine === null) { 263 | sdpLines.splice(fmtpLineIndex, 1); 264 | } else { 265 | sdpLines[fmtpLineIndex] = newLine; 266 | } 267 | 268 | sdp = sdpLines.join('\r\n'); 269 | return sdp; 270 | } 271 | 272 | // Split an fmtp line into an object including 'pt' and 'params'. 273 | function parseFmtpLine(fmtpLine) { 274 | var fmtpObj = {}; 275 | var spacePos = fmtpLine.indexOf(' '); 276 | var keyValues = fmtpLine.substring(spacePos + 1).split('; '); 277 | 278 | var pattern = new RegExp('a=fmtp:(\\d+)'); 279 | var result = fmtpLine.match(pattern); 280 | if (result && result.length === 2) { 281 | fmtpObj.pt = result[1]; 282 | } else { 283 | return null; 284 | } 285 | 286 | var params = {}; 287 | for (var i = 0; i < keyValues.length; ++i) { 288 | var pair = keyValues[i].split('='); 289 | if (pair.length === 2) { 290 | params[pair[0]] = pair[1]; 291 | } 292 | } 293 | fmtpObj.params = params; 294 | 295 | return fmtpObj; 296 | } 297 | 298 | // Generate an fmtp line from an object including 'pt' and 'params'. 299 | function writeFmtpLine(fmtpObj) { 300 | if (!fmtpObj.hasOwnProperty('pt') || !fmtpObj.hasOwnProperty('params')) { 301 | return null; 302 | } 303 | var pt = fmtpObj.pt; 304 | var params = fmtpObj.params; 305 | var keyValues = []; 306 | var i = 0; 307 | for (var key in params) { 308 | keyValues[i] = key + '=' + params[key]; 309 | ++i; 310 | } 311 | if (i === 0) { 312 | return null; 313 | } 314 | return 'a=fmtp:' + pt.toString() + ' ' + keyValues.join('; '); 315 | } 316 | 317 | // Find fmtp attribute for |codec| in |sdpLines|. 318 | function findFmtpLine(sdpLines, codec) { 319 | // Find payload of codec. 320 | var payload = getCodecPayloadType(sdpLines, codec); 321 | // Find the payload in fmtp line. 322 | return payload ? findLine(sdpLines, 'a=fmtp:' + payload.toString()) : null; 323 | } 324 | 325 | // Find the line in sdpLines that starts with |prefix|, and, if specified, 326 | // contains |substr| (case-insensitive search). 327 | function findLine(sdpLines, prefix, substr) { 328 | return findLineInRange(sdpLines, 0, -1, prefix, substr); 329 | } 330 | 331 | // Find the line in sdpLines[startLine...endLine - 1] that starts with |prefix| 332 | // and, if specified, contains |substr| (case-insensitive search). 333 | function findLineInRange(sdpLines, startLine, endLine, prefix, substr) { 334 | var realEndLine = endLine !== -1 ? endLine : sdpLines.length; 335 | for (var i = startLine; i < realEndLine; ++i) { 336 | if (sdpLines[i].indexOf(prefix) === 0) { 337 | if (!substr || 338 | sdpLines[i].toLowerCase().indexOf(substr.toLowerCase()) !== -1) { 339 | return i; 340 | } 341 | } 342 | } 343 | return null; 344 | } 345 | 346 | // Gets the codec payload type from sdp lines. 347 | function getCodecPayloadType(sdpLines, codec) { 348 | var index = findLine(sdpLines, 'a=rtpmap', codec); 349 | return index ? getCodecPayloadTypeFromLine(sdpLines[index]) : null; 350 | } 351 | 352 | // Gets the codec payload type from an a=rtpmap:X line. 353 | function getCodecPayloadTypeFromLine(sdpLine) { 354 | var pattern = new RegExp('a=rtpmap:(\\d+) \\w+\\/\\d+'); 355 | var result = sdpLine.match(pattern); 356 | return (result && result.length === 2) ? result[1] : null; 357 | } 358 | 359 | // Returns a new m= line with the specified codec as the first one. 360 | function setDefaultCodec(mLine, payload) { 361 | var elements = mLine.split(' '); 362 | 363 | // Just copy the first three parameters; codec order starts on fourth. 364 | var newLine = elements.slice(0, 3); 365 | 366 | // Put target payload first and copy in the rest. 367 | newLine.push(payload); 368 | for (var i = 3; i < elements.length; i++) { 369 | if (elements[i] !== payload) { 370 | newLine.push(elements[i]); 371 | } 372 | } 373 | return newLine.join(' '); 374 | } 375 | -------------------------------------------------------------------------------- /public/js/sdputils_test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | 9 | /* More information about these options at jshint.com/docs/options */ 10 | 11 | /* globals TestCase, maybePreferCodec, removeCodecParam, setCodecParam, 12 | assertEquals */ 13 | 14 | 'use strict'; 15 | 16 | var SDP_WITH_AUDIO_CODECS = 17 | ['v=0', 18 | 'm=audio 9 RTP/SAVPF 111 103 104 0 9', 19 | 'a=rtcp-mux', 20 | 'a=rtpmap:111 opus/48000/2', 21 | 'a=fmtp:111 minptime=10', 22 | 'a=rtpmap:103 ISAC/16000', 23 | 'a=rtpmap:9 G722/8000', 24 | 'a=rtpmap:0 PCMU/8000', 25 | 'a=rtpmap:8 PCMA/8000', 26 | ].join('\r\n'); 27 | 28 | var SdpUtilsTest = new TestCase('SdpUtilsTest'); 29 | 30 | SdpUtilsTest.prototype.testMovesIsac16KToDefaultWhenPreferred = function() { 31 | var result = maybePreferCodec(SDP_WITH_AUDIO_CODECS, 'audio', 'send', 32 | 'iSAC/16000'); 33 | var audioLine = result.split('\r\n')[1]; 34 | assertEquals('iSAC 16K (of type 103) should be moved to front.', 35 | 'm=audio 9 RTP/SAVPF 103 111 104 0 9', 36 | audioLine); 37 | }; 38 | 39 | SdpUtilsTest.prototype.testDoesNothingIfPreferredCodecNotFound = function() { 40 | var result = maybePreferCodec(SDP_WITH_AUDIO_CODECS, 'audio', 'send', 41 | 'iSAC/123456'); 42 | var audioLine = result.split('\r\n')[1]; 43 | assertEquals('SDP should be unaffected since the codec does not exist.', 44 | SDP_WITH_AUDIO_CODECS.split('\r\n')[1], 45 | audioLine); 46 | }; 47 | 48 | SdpUtilsTest.prototype.testMovesCodecEvenIfPayloadTypeIsSameAsUdpPort = 49 | function() { 50 | var result = maybePreferCodec(SDP_WITH_AUDIO_CODECS, 51 | 'audio', 52 | 'send', 53 | 'G722/8000'); 54 | var audioLine = result.split('\r\n')[1]; 55 | assertEquals('G722/8000 (of type 9) should be moved to front.', 56 | 'm=audio 9 RTP/SAVPF 9 111 103 104 0', 57 | audioLine); 58 | }; 59 | 60 | SdpUtilsTest.prototype.testRemoveAndSetCodecParamModifyFmtpLine = 61 | function() { 62 | var result = setCodecParam(SDP_WITH_AUDIO_CODECS, 'opus/48000', 63 | 'minptime', '20'); 64 | var audioLine = result.split('\r\n')[4]; 65 | assertEquals('minptime=10 should be modified in a=fmtp:111 line.', 66 | 'a=fmtp:111 minptime=20', audioLine); 67 | 68 | result = setCodecParam(result, 'opus/48000', 'useinbandfec', '1'); 69 | audioLine = result.split('\r\n')[4]; 70 | assertEquals('useinbandfec=1 should be added to a=fmtp:111 line.', 71 | 'a=fmtp:111 minptime=20; useinbandfec=1', audioLine); 72 | 73 | result = removeCodecParam(result, 'opus/48000', 'minptime'); 74 | audioLine = result.split('\r\n')[4]; 75 | assertEquals('minptime should be removed from a=fmtp:111 line.', 76 | 'a=fmtp:111 useinbandfec=1', audioLine); 77 | 78 | var newResult = removeCodecParam(result, 'opus/48000', 'minptime'); 79 | assertEquals('removeCodecParam should not affect sdp ' + 80 | 'if param did not exist', result, newResult); 81 | }; 82 | 83 | SdpUtilsTest.prototype.testRemoveAndSetCodecParamRemoveAndAddFmtpLineIfNeeded = 84 | function() { 85 | var result = removeCodecParam(SDP_WITH_AUDIO_CODECS, 'opus/48000', 86 | 'minptime'); 87 | var audioLine = result.split('\r\n')[4]; 88 | assertEquals('a=fmtp:111 line should be deleted.', 89 | 'a=rtpmap:103 ISAC/16000', audioLine); 90 | result = setCodecParam(result, 'opus/48000', 'inbandfec', '1'); 91 | audioLine = result.split('\r\n')[4]; 92 | assertEquals('a=fmtp:111 line should be added.', 93 | 'a=fmtp:111 inbandfec=1', audioLine); 94 | }; 95 | -------------------------------------------------------------------------------- /public/js/signalingchannel.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | 9 | /* More information about these options at jshint.com/docs/options */ 10 | 11 | /* globals parseJSON, trace, sendUrlRequest, isChromeApp, RemoteWebSocket */ 12 | /* exported SignalingChannel */ 13 | 14 | 'use strict'; 15 | 16 | // This class implements a signaling channel based on WebSocket. 17 | var SignalingChannel = function(wssUrl, wssPostUrl) { 18 | this.wssUrl_ = wssUrl; 19 | this.wssPostUrl_ = wssPostUrl; 20 | this.roomId_ = null; 21 | this.clientId_ = null; 22 | this.websocket_ = null; 23 | this.registered_ = false; 24 | 25 | // Public callbacks. Keep it sorted. 26 | this.onerror = null; 27 | this.onmessage = null; 28 | }; 29 | 30 | SignalingChannel.prototype.open = function() { 31 | if (this.websocket_) { 32 | trace('ERROR: SignalingChannel has already opened.'); 33 | return; 34 | } 35 | 36 | trace('Opening signaling channel.'); 37 | return new Promise(function(resolve, reject) { 38 | if (isChromeApp()) { 39 | this.websocket_ = new RemoteWebSocket(this.wssUrl_, this.wssPostUrl_); 40 | } else { 41 | this.websocket_ = new WebSocket(this.wssUrl_); 42 | } 43 | 44 | this.websocket_.onopen = function() { 45 | trace('Signaling channel opened.'); 46 | 47 | this.websocket_.onerror = function() { 48 | trace('Signaling channel error.'); 49 | }; 50 | this.websocket_.onclose = function(event) { 51 | // TODO(tkchin): reconnect to WSS. 52 | trace('Channel closed with code:' + event.code + 53 | ' reason:' + event.reason); 54 | this.websocket_ = null; 55 | this.registered_ = false; 56 | }; 57 | 58 | if (this.clientId_ && this.roomId_) { 59 | this.register(this.roomId_, this.clientId_); 60 | } 61 | 62 | resolve(); 63 | }.bind(this); 64 | 65 | this.websocket_.onmessage = function(event) { 66 | trace('WSS->C: ' + event.data); 67 | 68 | var message = parseJSON(event.data); 69 | if (!message) { 70 | trace('Failed to parse WSS message: ' + event.data); 71 | return; 72 | } 73 | if (message.error) { 74 | trace('Signaling server error message: ' + message.error); 75 | return; 76 | } 77 | this.onmessage(message.msg); 78 | }.bind(this); 79 | 80 | this.websocket_.onerror = function() { 81 | reject(Error('WebSocket error.')); 82 | }; 83 | }.bind(this)); 84 | }; 85 | 86 | SignalingChannel.prototype.register = function(roomId, clientId) { 87 | if (this.registered_) { 88 | trace('ERROR: SignalingChannel has already registered.'); 89 | return; 90 | } 91 | 92 | this.roomId_ = roomId; 93 | this.clientId_ = clientId; 94 | 95 | if (!this.roomId_) { 96 | trace('ERROR: missing roomId.'); 97 | } 98 | if (!this.clientId_) { 99 | trace('ERROR: missing clientId.'); 100 | } 101 | if (!this.websocket_ || this.websocket_.readyState !== WebSocket.OPEN) { 102 | trace('WebSocket not open yet; saving the IDs to register later.'); 103 | return; 104 | } 105 | trace('Registering signaling channel.'); 106 | var registerMessage = { 107 | cmd: 'register', 108 | roomid: this.roomId_, 109 | clientid: this.clientId_ 110 | }; 111 | this.websocket_.send(JSON.stringify(registerMessage)); 112 | this.registered_ = true; 113 | 114 | // TODO(tkchin): Better notion of whether registration succeeded. Basically 115 | // check that we don't get an error message back from the socket. 116 | trace('Signaling channel registered.'); 117 | }; 118 | 119 | SignalingChannel.prototype.close = function(async) { 120 | if (this.websocket_) { 121 | this.websocket_.close(); 122 | this.websocket_ = null; 123 | } 124 | 125 | if (!this.clientId_ || !this.roomId_) { 126 | return; 127 | } 128 | // Tell WSS that we're done. 129 | var path = this.getWssPostUrl(); 130 | 131 | return sendUrlRequest('DELETE', path, async).catch(function(error) { 132 | trace('Error deleting web socket connection: ' + error.message); 133 | }.bind(this)).then(function() { 134 | this.clientId_ = null; 135 | this.roomId_ = null; 136 | this.registered_ = false; 137 | }.bind(this)); 138 | }; 139 | 140 | SignalingChannel.prototype.send = function(message) { 141 | if (!this.roomId_ || !this.clientId_) { 142 | trace('ERROR: SignalingChannel has not registered.'); 143 | return; 144 | } 145 | trace('C->WSS: ' + message); 146 | 147 | var wssMessage = { 148 | cmd: 'send', 149 | msg: message 150 | }; 151 | var msgString = JSON.stringify(wssMessage); 152 | 153 | if (this.websocket_ && this.websocket_.readyState === WebSocket.OPEN) { 154 | this.websocket_.send(msgString); 155 | } else { 156 | var path = this.getWssPostUrl(); 157 | var xhr = new XMLHttpRequest(); 158 | xhr.open('POST', path, true); 159 | xhr.send(wssMessage.msg); 160 | } 161 | }; 162 | 163 | SignalingChannel.prototype.getWssPostUrl = function() { 164 | return this.wssPostUrl_ + '/' + this.roomId_ + '/' + this.clientId_; 165 | }; 166 | -------------------------------------------------------------------------------- /public/js/signalingchannel_test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | 9 | /* More information about these options at jshint.com/docs/options */ 10 | 11 | /* globals TestCase, assertEquals, assertNotNull, assertTrue, assertFalse, 12 | WebSocket:true, XMLHttpRequest:true, SignalingChannel, webSockets:true, 13 | xhrs:true, FAKE_WSS_URL, FAKE_WSS_POST_URL, FAKE_ROOM_ID, FAKE_CLIENT_ID, 14 | MockXMLHttpRequest, MockWebSocket */ 15 | 16 | 'use strict'; 17 | 18 | var SignalingChannelTest = new TestCase('SignalingChannelTest'); 19 | 20 | SignalingChannelTest.prototype.setUp = function() { 21 | webSockets = []; 22 | xhrs = []; 23 | 24 | this.realWebSocket = WebSocket; 25 | WebSocket = MockWebSocket; 26 | 27 | this.channel = 28 | new SignalingChannel(FAKE_WSS_URL, FAKE_WSS_POST_URL); 29 | }; 30 | 31 | SignalingChannelTest.prototype.tearDown = function() { 32 | WebSocket = this.realWebSocket; 33 | }; 34 | 35 | SignalingChannelTest.prototype.testOpenSuccess = function() { 36 | var promise = this.channel.open(); 37 | assertEquals(1, webSockets.length); 38 | 39 | var resolved = false; 40 | var rejected = false; 41 | promise.then(function() { 42 | resolved = true; 43 | }).catch (function() { 44 | rejected = true; 45 | }); 46 | 47 | var socket = webSockets[0]; 48 | socket.simulateOpenResult(true); 49 | assertTrue(resolved); 50 | assertFalse(rejected); 51 | }; 52 | 53 | SignalingChannelTest.prototype.testReceiveMessage = function() { 54 | this.channel.open(); 55 | var socket = webSockets[0]; 56 | socket.simulateOpenResult(true); 57 | 58 | assertNotNull(socket.onmessage); 59 | 60 | var msgs = []; 61 | this.channel.onmessage = function(msg) { 62 | msgs.push(msg); 63 | }; 64 | 65 | var expectedMsg = 'hi'; 66 | var event = { 67 | 'data': JSON.stringify({'msg': expectedMsg}) 68 | }; 69 | socket.onmessage(event); 70 | assertEquals(1, msgs.length); 71 | assertEquals(expectedMsg, msgs[0]); 72 | }; 73 | 74 | SignalingChannelTest.prototype.testOpenFailure = function() { 75 | var promise = this.channel.open(); 76 | assertEquals(1, webSockets.length); 77 | 78 | var resolved = false; 79 | var rejected = false; 80 | promise.then(function() { 81 | resolved = true; 82 | }).catch (function() { 83 | rejected = true; 84 | }); 85 | 86 | var socket = webSockets[0]; 87 | socket.simulateOpenResult(false); 88 | assertFalse(resolved); 89 | assertTrue(rejected); 90 | }; 91 | 92 | SignalingChannelTest.prototype.testRegisterBeforeOpen = function() { 93 | this.channel.open(); 94 | this.channel.register(FAKE_ROOM_ID, FAKE_CLIENT_ID); 95 | 96 | var socket = webSockets[0]; 97 | socket.simulateOpenResult(true); 98 | 99 | assertEquals(1, socket.messages.length); 100 | 101 | var registerMessage = { 102 | cmd: 'register', 103 | roomid: FAKE_ROOM_ID, 104 | clientid: FAKE_CLIENT_ID 105 | }; 106 | assertEquals(JSON.stringify(registerMessage), socket.messages[0]); 107 | }; 108 | 109 | SignalingChannelTest.prototype.testRegisterAfterOpen = function() { 110 | this.channel.open(); 111 | var socket = webSockets[0]; 112 | socket.simulateOpenResult(true); 113 | this.channel.register(FAKE_ROOM_ID, FAKE_CLIENT_ID); 114 | 115 | assertEquals(1, socket.messages.length); 116 | 117 | var registerMessage = { 118 | cmd: 'register', 119 | roomid: FAKE_ROOM_ID, 120 | clientid: FAKE_CLIENT_ID 121 | }; 122 | assertEquals(JSON.stringify(registerMessage), socket.messages[0]); 123 | }; 124 | 125 | SignalingChannelTest.prototype.testSendBeforeOpen = function() { 126 | // Stubbing XMLHttpRequest cannot be done in setUp since it caused PhantomJS 127 | // to hang. 128 | var realXMLHttpRequest = XMLHttpRequest; 129 | XMLHttpRequest = MockXMLHttpRequest; 130 | 131 | this.channel.open(); 132 | this.channel.register(FAKE_ROOM_ID, FAKE_CLIENT_ID); 133 | 134 | var message = 'hello'; 135 | this.channel.send(message); 136 | 137 | assertEquals(1, xhrs.length); 138 | assertEquals(2, xhrs[0].readyState); 139 | assertEquals(FAKE_WSS_POST_URL + '/' + FAKE_ROOM_ID + '/' + FAKE_CLIENT_ID, 140 | xhrs[0].url); 141 | assertEquals('POST', xhrs[0].method); 142 | assertEquals(message, xhrs[0].body); 143 | 144 | XMLHttpRequest = realXMLHttpRequest; 145 | }; 146 | 147 | SignalingChannelTest.prototype.testSendAfterOpen = function() { 148 | this.channel.open(); 149 | var socket = webSockets[0]; 150 | socket.simulateOpenResult(true); 151 | this.channel.register(FAKE_ROOM_ID, FAKE_CLIENT_ID); 152 | 153 | var message = 'hello'; 154 | var wsMessage = { 155 | cmd: 'send', 156 | msg: message 157 | }; 158 | this.channel.send(message); 159 | assertEquals(2, socket.messages.length); 160 | assertEquals(JSON.stringify(wsMessage), socket.messages[1]); 161 | }; 162 | 163 | SignalingChannelTest.prototype.testCloseAfterRegister = function() { 164 | var realXMLHttpRequest = XMLHttpRequest; 165 | XMLHttpRequest = MockXMLHttpRequest; 166 | 167 | this.channel.open(); 168 | var socket = webSockets[0]; 169 | socket.simulateOpenResult(true); 170 | this.channel.register(FAKE_ROOM_ID, FAKE_CLIENT_ID); 171 | 172 | assertEquals(WebSocket.OPEN, socket.readyState); 173 | this.channel.close(); 174 | assertEquals(WebSocket.CLOSED, socket.readyState); 175 | 176 | assertEquals(1, xhrs.length); 177 | assertEquals(4, xhrs[0].readyState); 178 | assertEquals(FAKE_WSS_POST_URL + '/' + FAKE_ROOM_ID + '/' + FAKE_CLIENT_ID, 179 | xhrs[0].url); 180 | assertEquals('DELETE', xhrs[0].method); 181 | 182 | XMLHttpRequest = realXMLHttpRequest; 183 | }; 184 | 185 | SignalingChannelTest.prototype.testCloseBeforeRegister = function() { 186 | var realXMLHttpRequest = XMLHttpRequest; 187 | XMLHttpRequest = MockXMLHttpRequest; 188 | 189 | this.channel.open(); 190 | this.channel.close(); 191 | 192 | assertEquals(0, xhrs.length); 193 | XMLHttpRequest = realXMLHttpRequest; 194 | }; 195 | -------------------------------------------------------------------------------- /public/js/stats.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | 9 | /* More information about these options at jshint.com/docs/options */ 10 | 11 | /* exported computeBitrate, computeE2EDelay, computeRate, 12 | extractStatAsInt, refreshStats */ 13 | 14 | 'use strict'; 15 | 16 | // Return the integer stat |statName| from the object with type |statObj| in 17 | // |stats|, or null if not present. 18 | function extractStatAsInt(stats, statObj, statName) { 19 | // Ignore stats that have a 'nullish' value. 20 | // The correct fix is indicated in 21 | // https://code.google.com/p/webrtc/issues/detail?id=3377. 22 | var str = extractStat(stats, statObj, statName); 23 | if (str) { 24 | var val = parseInt(str); 25 | if (val !== -1) { 26 | return val; 27 | } 28 | } 29 | return null; 30 | } 31 | 32 | // Return the stat |statName| from the object with type |statObj| in |stats| 33 | // as a string, or null if not present. 34 | function extractStat(stats, statObj, statName) { 35 | var report = getStatsReport(stats, statObj, statName); 36 | if (report && report.names().indexOf(statName) !== -1) { 37 | return report.stat(statName); 38 | } 39 | return null; 40 | } 41 | 42 | // Return the stats report with type |statObj| in |stats|, with the stat 43 | // |statName| (if specified), and value |statVal| (if specified). Return 44 | // undef if not present. 45 | function getStatsReport(stats, statObj, statName, statVal) { 46 | if (stats) { 47 | for (var i = 0; i < stats.length; ++i) { 48 | var report = stats[i]; 49 | if (report.type === statObj) { 50 | var found = true; 51 | // If |statName| is present, ensure |report| has that stat. 52 | // If |statVal| is present, ensure the value matches. 53 | if (statName) { 54 | var val = report.stat(statName); 55 | found = (statVal !== undefined) ? (val === statVal) : val; 56 | } 57 | if (found) { 58 | return report; 59 | } 60 | } 61 | } 62 | } 63 | } 64 | 65 | // Takes two stats reports and determines the rate based on two counter readings 66 | // and the time between them (which is in units of milliseconds). 67 | function computeRate(newReport, oldReport, statName) { 68 | var newVal = newReport.stat(statName); 69 | var oldVal = (oldReport) ? oldReport.stat(statName) : null; 70 | if (newVal === null || oldVal === null) { 71 | return null; 72 | } 73 | return (newVal - oldVal) / (newReport.timestamp - oldReport.timestamp) * 1000; 74 | } 75 | 76 | // Convert a byte rate to a bit rate. 77 | function computeBitrate(newReport, oldReport, statName) { 78 | return computeRate(newReport, oldReport, statName) * 8; 79 | } 80 | 81 | // Computes end to end delay based on the capture start time (in NTP format) 82 | // and the current render time (in seconds since start of render). 83 | function computeE2EDelay(captureStart, remoteVideoCurrentTime) { 84 | if (!captureStart) { 85 | return null; 86 | } 87 | 88 | // Adding offset (milliseconds between 1900 and 1970) to get NTP time. 89 | var nowNTP = Date.now() + 2208988800000; 90 | return nowNTP - captureStart - remoteVideoCurrentTime * 1000; 91 | } 92 | -------------------------------------------------------------------------------- /public/js/storage.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | 9 | /* More information about these options at jshint.com/docs/options */ 10 | 11 | /* exported Storage */ 12 | /* globals isChromeApp, chrome */ 13 | 14 | 'use strict'; 15 | 16 | var Storage = function() {}; 17 | 18 | // Get a value from local browser storage. Calls callback with value. 19 | // Handles variation in API between localStorage and Chrome app storage. 20 | Storage.prototype.getStorage = function(key, callback) { 21 | if (isChromeApp()) { 22 | // Use chrome.storage.local. 23 | chrome.storage.local.get(key, function(values) { 24 | // Unwrap key/value pair. 25 | if (callback) { 26 | window.setTimeout(function() { 27 | callback(values[key]); 28 | }, 0); 29 | } 30 | }); 31 | } else { 32 | // Use localStorage. 33 | var value = localStorage.getItem(key); 34 | if (callback) { 35 | window.setTimeout(function() { 36 | callback(value); 37 | }, 0); 38 | } 39 | } 40 | }; 41 | 42 | // Set a value in local browser storage. Calls callback after completion. 43 | // Handles variation in API between localStorage and Chrome app storage. 44 | Storage.prototype.setStorage = function(key, value, callback) { 45 | if (isChromeApp()) { 46 | // Use chrome.storage.local. 47 | var data = {}; 48 | data[key] = value; 49 | chrome.storage.local.set(data, callback); 50 | } else { 51 | // Use localStorage. 52 | localStorage.setItem(key, value); 53 | if (callback) { 54 | window.setTimeout(callback, 0); 55 | } 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /public/js/test_mocks.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | 9 | /* More information about these options at jshint.com/docs/options */ 10 | 11 | /* globals assertEquals */ 12 | /* exported FAKE_WSS_POST_URL, FAKE_WSS_URL, FAKE_WSS_POST_URL, FAKE_ROOM_ID, 13 | FAKE_CLIENT_ID, MockWebSocket, MockXMLHttpRequest, webSockets, xhrs, 14 | MockWindowPort, FAKE_SEND_EXCEPTION */ 15 | 16 | 'use strict'; 17 | 18 | var FAKE_WSS_URL = 'wss://foo.com'; 19 | var FAKE_WSS_POST_URL = 'https://foo.com'; 20 | var FAKE_ROOM_ID = 'bar'; 21 | var FAKE_CLIENT_ID = 'barbar'; 22 | var FAKE_SEND_EXCEPTION = 'Send exception'; 23 | 24 | var webSockets = []; 25 | var MockWebSocket = function(url) { 26 | assertEquals(FAKE_WSS_URL, url); 27 | 28 | this.url = url; 29 | this.messages = []; 30 | this.readyState = WebSocket.CONNECTING; 31 | 32 | this.onopen = null; 33 | this.onclose = null; 34 | this.onerror = null; 35 | this.onmessage = null; 36 | 37 | webSockets.push(this); 38 | }; 39 | 40 | MockWebSocket.CONNECTING = WebSocket.CONNECTING; 41 | MockWebSocket.OPEN = WebSocket.OPEN; 42 | MockWebSocket.CLOSED = WebSocket.CLOSED; 43 | 44 | MockWebSocket.prototype.simulateOpenResult = function(success) { 45 | if (success) { 46 | this.readyState = WebSocket.OPEN; 47 | if (this.onopen) { 48 | this.onopen(); 49 | } 50 | } else { 51 | this.readyState = WebSocket.CLOSED; 52 | if (this.onerror) { 53 | this.onerror(Error('Mock open error')); 54 | } 55 | } 56 | }; 57 | 58 | MockWebSocket.prototype.send = function(msg) { 59 | if (this.readyState !== WebSocket.OPEN) { 60 | throw 'Send called when the connection is not open'; 61 | } 62 | 63 | if (this.throwOnSend) { 64 | throw FAKE_SEND_EXCEPTION; 65 | } 66 | 67 | this.messages.push(msg); 68 | }; 69 | 70 | MockWebSocket.prototype.close = function() { 71 | this.readyState = WebSocket.CLOSED; 72 | }; 73 | 74 | var xhrs = []; 75 | var MockXMLHttpRequest = function() { 76 | this.url = null; 77 | this.method = null; 78 | this.async = true; 79 | this.body = null; 80 | this.readyState = 0; 81 | 82 | xhrs.push(this); 83 | }; 84 | MockXMLHttpRequest.prototype.open = function(method, path, async) { 85 | this.url = path; 86 | this.method = method; 87 | this.async = async; 88 | this.readyState = 1; 89 | }; 90 | MockXMLHttpRequest.prototype.send = function(body) { 91 | this.body = body; 92 | if (this.async) { 93 | this.readyState = 2; 94 | } else { 95 | this.readyState = 4; 96 | } 97 | }; 98 | 99 | var MockWindowPort = function() { 100 | this.messages = []; 101 | this.onMessage_ = null; 102 | }; 103 | 104 | MockWindowPort.prototype.addMessageListener = function(callback) { 105 | this.onMessage_ = callback; 106 | }; 107 | 108 | MockWindowPort.prototype.sendMessage = function(message) { 109 | this.messages.push(message); 110 | }; 111 | 112 | MockWindowPort.prototype.simulateMessageFromBackground = function(message) { 113 | if (this.onMessage_) { 114 | this.onMessage_(message); 115 | } 116 | }; 117 | -------------------------------------------------------------------------------- /public/js/testpolyfills.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | 9 | /* More information about these options at jshint.com/docs/options */ 10 | 11 | 'use strict'; 12 | 13 | Function.prototype.bind = Function.prototype.bind || function(thisp) { 14 | var fn = this; 15 | var suppliedArgs = Array.prototype.slice.call(arguments, 1); 16 | return function() { 17 | return fn.apply(thisp, 18 | suppliedArgs.concat(Array.prototype.slice.call(arguments))); 19 | }; 20 | }; 21 | 22 | if (!window.performance) { 23 | window.performance = function() {}; 24 | window.performance.now = function() { return 0; }; 25 | } 26 | 27 | window.RTCSessionDescription = window.RTCSessionDescription || function(input) { 28 | this.type = input.type; 29 | this.sdp = input.sdp; 30 | }; 31 | 32 | window.RTCIceCandidate = window.RTCIceCandidate || function(candidate) { 33 | this.sdpMLineIndex = candidate.sdpMLineIndex; 34 | this.candidate = candidate.candidate; 35 | }; 36 | 37 | var PROMISE_STATE = { 38 | PENDING: 0, 39 | FULLFILLED: 1, 40 | REJECTED: 2 41 | }; 42 | 43 | var MyPromise = function(executor) { 44 | this.state_ = PROMISE_STATE.PENDING; 45 | this.resolveCallback_ = null; 46 | this.rejectCallback_ = null; 47 | 48 | this.value_ = null; 49 | this.reason_ = null; 50 | executor(this.onResolve_.bind(this), this.onReject_.bind(this)); 51 | }; 52 | 53 | MyPromise.all = function(promises) { 54 | var values = new Array(promises.length); 55 | return new MyPromise(function(values, resolve, reject) { 56 | function onResolve(values, index, value) { 57 | values[index] = value || null; 58 | 59 | for (var i = 0; i < values.length; ++i) { 60 | if (values[i] === undefined) { 61 | return; 62 | } 63 | } 64 | resolve(values); 65 | } 66 | for (var i = 0; i < promises.length; ++i) { 67 | promises[i].then(onResolve.bind(null, values, i), reject); 68 | } 69 | }.bind(null, values)); 70 | }; 71 | 72 | MyPromise.resolve = function(value) { 73 | return new MyPromise(function(resolve) { 74 | resolve(value); 75 | }); 76 | }; 77 | 78 | MyPromise.reject = function(error) { 79 | // JSHint flags the unused variable resolve. 80 | return new MyPromise(function(resolve, reject) { // jshint ignore:line 81 | reject(error); 82 | }); 83 | }; 84 | 85 | MyPromise.prototype.then = function(onResolve, onReject) { 86 | switch (this.state_) { 87 | case PROMISE_STATE.PENDING: 88 | this.resolveCallback_ = onResolve; 89 | this.rejectCallback_ = onReject; 90 | break; 91 | case PROMISE_STATE.FULLFILLED: 92 | onResolve(this.value_); 93 | break; 94 | case PROMISE_STATE.REJECTED: 95 | if (onReject) { 96 | onReject(this.reason_); 97 | } 98 | break; 99 | } 100 | return this; 101 | }; 102 | 103 | MyPromise.prototype.catch = function(onReject) { 104 | switch (this.state_) { 105 | case PROMISE_STATE.PENDING: 106 | this.rejectCallback_ = onReject; 107 | break; 108 | case PROMISE_STATE.FULLFILLED: 109 | break; 110 | case PROMISE_STATE.REJECTED: 111 | onReject(this.reason_); 112 | break; 113 | } 114 | return this; 115 | }; 116 | 117 | MyPromise.prototype.onResolve_ = function(value) { 118 | if (this.state_ !== PROMISE_STATE.PENDING) { 119 | return; 120 | } 121 | this.state_ = PROMISE_STATE.FULLFILLED; 122 | if (this.resolveCallback_) { 123 | this.resolveCallback_(value); 124 | } else { 125 | this.value_ = value; 126 | } 127 | }; 128 | 129 | MyPromise.prototype.onReject_ = function(reason) { 130 | if (this.state_ !== PROMISE_STATE.PENDING) { 131 | return; 132 | } 133 | this.state_ = PROMISE_STATE.REJECTED; 134 | if (this.rejectCallback_) { 135 | this.rejectCallback_(reason); 136 | } else { 137 | this.reason_ = reason; 138 | } 139 | }; 140 | 141 | window.Promise = window.Promise || MyPromise; 142 | 143 | // Provide a shim for phantomjs, where chrome is not defined. 144 | var myChrome = (function() { 145 | var onConnectCallback_; 146 | return { 147 | app: { 148 | runtime: { 149 | onLaunched: { 150 | addListener: function(callback) { 151 | console.log( 152 | 'chrome.app.runtime.onLaunched.addListener called:' + callback); 153 | } 154 | } 155 | }, 156 | window: { 157 | create: function(fileName, callback) { 158 | console.log( 159 | 'chrome.window.create called: ' + 160 | fileName + ', ' + callback); 161 | } 162 | } 163 | }, 164 | runtime: { 165 | onConnect: { 166 | addListener: function(callback) { 167 | console.log( 168 | 'chrome.runtime.onConnect.addListener called: ' + callback); 169 | onConnectCallback_ = callback; 170 | } 171 | } 172 | }, 173 | callOnConnect: function(port) { 174 | if (onConnectCallback_) { 175 | onConnectCallback_(port); 176 | } 177 | } 178 | }; 179 | })(); 180 | 181 | window.chrome = window.chrome || myChrome; 182 | -------------------------------------------------------------------------------- /public/js/util.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | 9 | /* More information about these options at jshint.com/docs/options */ 10 | 11 | /* exported setUpFullScreen, fullScreenElement, isFullScreen, 12 | requestTurnServers, sendAsyncUrlRequest, sendSyncUrlRequest, randomString, $, 13 | queryStringToDictionary */ 14 | /* globals chrome */ 15 | 16 | 'use strict'; 17 | 18 | function $(selector) { 19 | return document.querySelector(selector); 20 | } 21 | 22 | // Returns the URL query key-value pairs as a dictionary object. 23 | function queryStringToDictionary(queryString) { 24 | var pairs = queryString.slice(1).split('&'); 25 | 26 | var result = {}; 27 | pairs.forEach(function(pair) { 28 | if (pair) { 29 | pair = pair.split('='); 30 | if (pair[0]) { 31 | result[pair[0]] = decodeURIComponent(pair[1] || ''); 32 | } 33 | } 34 | }); 35 | return result; 36 | } 37 | 38 | // Sends the URL request and returns a Promise as the result. 39 | function sendAsyncUrlRequest(method, url, body) { 40 | return sendUrlRequest(method, url, true, body); 41 | } 42 | 43 | // If async is true, returns a Promise and executes the xhr request 44 | // async. If async is false, the xhr will be executed sync and a 45 | // resolved promise is returned. 46 | function sendUrlRequest(method, url, async, body) { 47 | return new Promise(function(resolve, reject) { 48 | var xhr; 49 | var reportResults = function() { 50 | if (xhr.status !== 200) { 51 | reject( 52 | Error('Status=' + xhr.status + ', response=' + 53 | xhr.responseText)); 54 | return; 55 | } 56 | resolve(xhr.responseText); 57 | }; 58 | 59 | xhr = new XMLHttpRequest(); 60 | if (async) { 61 | xhr.onreadystatechange = function() { 62 | if (xhr.readyState !== 4) { 63 | return; 64 | } 65 | reportResults(); 66 | }; 67 | } 68 | xhr.open(method, url, async); 69 | xhr.send(body); 70 | 71 | if (!async) { 72 | reportResults(); 73 | } 74 | }); 75 | } 76 | 77 | // Returns a list of turn servers after requesting it from CEOD. 78 | function requestTurnServers(turnRequestUrl, turnTransports) { 79 | return new Promise(function(resolve, reject) { 80 | // Chrome apps don't send origin header for GET requests, but 81 | // do send it for POST requests. Origin header is required for 82 | // access to turn request url. 83 | var method = isChromeApp() ? 'POST' : 'GET'; 84 | sendAsyncUrlRequest(method, turnRequestUrl).then(function(response) { 85 | var turnServerResponse = parseJSON(response); 86 | if (!turnServerResponse) { 87 | reject(Error('Error parsing response JSON: ' + response)); 88 | return; 89 | } 90 | // Filter the TURN URLs to only use the desired transport, if specified. 91 | if (turnTransports.length > 0) { 92 | filterTurnUrls(turnServerResponse.uris, turnTransports); 93 | } 94 | 95 | // Create the RTCIceServer objects from the response. 96 | var turnServers = createIceServers(turnServerResponse.uris, 97 | turnServerResponse.username, turnServerResponse.password); 98 | if (!turnServers) { 99 | reject(Error('Error creating ICE servers from response.')); 100 | return; 101 | } 102 | trace('Retrieved TURN server information.'); 103 | resolve(turnServers); 104 | }).catch(function(error) { 105 | reject(Error('TURN server request error: ' + error.message)); 106 | return; 107 | }); 108 | }); 109 | } 110 | 111 | // Parse the supplied JSON, or return null if parsing fails. 112 | function parseJSON(json) { 113 | try { 114 | return JSON.parse(json); 115 | } catch (e) { 116 | trace('Error parsing json: ' + json); 117 | } 118 | return null; 119 | } 120 | 121 | // Filter a list of TURN urls to only contain those with transport=|protocol|. 122 | function filterTurnUrls(urls, protocol) { 123 | for (var i = 0; i < urls.length;) { 124 | var parts = urls[i].split('?'); 125 | if (parts.length > 1 && parts[1] !== ('transport=' + protocol)) { 126 | urls.splice(i, 1); 127 | } else { 128 | ++i; 129 | } 130 | } 131 | } 132 | 133 | // Start shims for fullscreen 134 | function setUpFullScreen() { 135 | if (isChromeApp()) { 136 | document.cancelFullScreen = function() { 137 | chrome.app.window.current().restore(); 138 | }; 139 | } else { 140 | document.cancelFullScreen = document.webkitCancelFullScreen || 141 | document.mozCancelFullScreen || document.cancelFullScreen; 142 | } 143 | 144 | if (isChromeApp()) { 145 | document.body.requestFullScreen = function() { 146 | chrome.app.window.current().fullscreen(); 147 | }; 148 | } else { 149 | document.body.requestFullScreen = document.body.webkitRequestFullScreen || 150 | document.body.mozRequestFullScreen || document.body.requestFullScreen; 151 | } 152 | 153 | document.onfullscreenchange = document.onfullscreenchange || 154 | document.onwebkitfullscreenchange || document.onmozfullscreenchange; 155 | } 156 | 157 | function isFullScreen() { 158 | if (isChromeApp()) { 159 | return chrome.app.window.current().isFullscreen(); 160 | } 161 | 162 | return !!(document.webkitIsFullScreen || document.mozFullScreen || 163 | document.isFullScreen); // if any defined and true 164 | } 165 | 166 | function fullScreenElement() { 167 | return document.webkitFullScreenElement || 168 | document.webkitCurrentFullScreenElement || 169 | document.mozFullScreenElement || 170 | document.fullScreenElement; 171 | } 172 | 173 | // End shims for fullscreen 174 | 175 | // Return a random numerical string. 176 | function randomString(strLength) { 177 | var result = []; 178 | strLength = strLength || 5; 179 | var charSet = '0123456789'; 180 | while (strLength--) { 181 | result.push(charSet.charAt(Math.floor(Math.random() * charSet.length))); 182 | } 183 | return result.join(''); 184 | } 185 | 186 | // Returns true if the code is running in a packaged Chrome App. 187 | function isChromeApp() { 188 | return (typeof chrome !== 'undefined' && 189 | typeof chrome.storage !== 'undefined' && 190 | typeof chrome.storage.local !== 'undefined'); 191 | } 192 | -------------------------------------------------------------------------------- /public/js/utils_test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | 9 | /* More information about these options at jshint.com/docs/options */ 10 | 11 | /* globals TestCase, filterTurnUrls, assertEquals, randomString, 12 | queryStringToDictionary */ 13 | 14 | 'use strict'; 15 | 16 | var TURN_URLS = [ 17 | 'turn:turn.example.com?transport=tcp', 18 | 'turn:turn.example.com?transport=udp', 19 | 'turn:turn.example.com:8888?transport=udp', 20 | 'turn:turn.example.com:8888?transport=tcp' 21 | ]; 22 | 23 | var TURN_URLS_UDP = [ 24 | 'turn:turn.example.com?transport=udp', 25 | 'turn:turn.example.com:8888?transport=udp', 26 | ]; 27 | 28 | var TURN_URLS_TCP = [ 29 | 'turn:turn.example.com?transport=tcp', 30 | 'turn:turn.example.com:8888?transport=tcp' 31 | ]; 32 | 33 | var UtilsTest = new TestCase('UtilsTest'); 34 | 35 | UtilsTest.prototype.testFilterTurnUrlsUdp = function() { 36 | var urls = TURN_URLS.slice(0); // make a copy 37 | filterTurnUrls(urls, 'udp'); 38 | assertEquals('Only transport=udp URLs should remain.', TURN_URLS_UDP, urls); 39 | }; 40 | 41 | UtilsTest.prototype.testFilterTurnUrlsTcp = function() { 42 | var urls = TURN_URLS.slice(0); // make a copy 43 | filterTurnUrls(urls, 'tcp'); 44 | assertEquals('Only transport=tcp URLs should remain.', TURN_URLS_TCP, urls); 45 | }; 46 | 47 | UtilsTest.prototype.testRandomReturnsCorrectLength = function() { 48 | assertEquals('13 length string', 13, randomString(13).length); 49 | assertEquals('5 length string', 5, randomString(5).length); 50 | assertEquals('10 length string', 10, randomString(10).length); 51 | }; 52 | 53 | UtilsTest.prototype.testRandomReturnsCorrectCharacters = function() { 54 | var str = randomString(500); 55 | 56 | // randromString should return only the digits 0-9. 57 | var positiveRe = /^[0-9]+$/; 58 | var negativeRe = /[^0-9]/; 59 | 60 | var positiveResult = positiveRe.exec(str); 61 | var negativeResult = negativeRe.exec(str); 62 | 63 | assertEquals( 64 | 'Number only regular expression should match.', 65 | 0, positiveResult.index); 66 | assertEquals( 67 | 'Anything other than digits regular expression should not match.', 68 | null, negativeResult); 69 | }; 70 | 71 | UtilsTest.prototype.testQueryStringToDictionary = function() { 72 | var dictionary = { 73 | 'foo': 'a', 74 | 'baz': '', 75 | 'bar': 'b', 76 | 'tee': '', 77 | }; 78 | 79 | var buildQuery = function(data, includeEqualsOnEmpty) { 80 | var queryString = '?'; 81 | for (var key in data) { 82 | queryString += key; 83 | if (data[key] || includeEqualsOnEmpty) { 84 | queryString += '='; 85 | } 86 | queryString += data[key] + '&'; 87 | } 88 | queryString = queryString.slice(0, -1); 89 | return queryString; 90 | }; 91 | 92 | // Build query where empty value is formatted as &tee=&. 93 | var query = buildQuery(dictionary, true); 94 | var result = queryStringToDictionary(query); 95 | assertEquals(JSON.stringify(dictionary), JSON.stringify(result)); 96 | 97 | // Build query where empty value is formatted as &tee&. 98 | query = buildQuery(dictionary, false); 99 | result = queryStringToDictionary(query); 100 | assertEquals(JSON.stringify(dictionary), JSON.stringify(result)); 101 | 102 | result = queryStringToDictionary('?'); 103 | assertEquals(0, Object.keys(result).length); 104 | 105 | result = queryStringToDictionary('?='); 106 | assertEquals(0, Object.keys(result).length); 107 | 108 | result = queryStringToDictionary('?&='); 109 | assertEquals(0, Object.keys(result).length); 110 | 111 | result = queryStringToDictionary(''); 112 | assertEquals(0, Object.keys(result).length); 113 | 114 | result = queryStringToDictionary('?=abc'); 115 | assertEquals(0, Object.keys(result).length); 116 | }; 117 | -------------------------------------------------------------------------------- /public/js/windowport.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved. 3 | * 4 | * Use of this source code is governed by a BSD-style license 5 | * that can be found in the LICENSE file in the root of the source 6 | * tree. 7 | */ 8 | 9 | /* More information about these options at jshint.com/docs/options */ 10 | 11 | /* globals trace, chrome */ 12 | /* exported apprtc, apprtc.windowPort */ 13 | 14 | 'use strict'; 15 | 16 | // This is used to communicate from the Chrome App window to background.js. 17 | // It opens a Port object to send and receive messages. When the Chrome 18 | // App window is closed, background.js receives notification and can 19 | // handle clean up tasks. 20 | var apprtc = apprtc || {}; 21 | apprtc.windowPort = apprtc.windowPort || {}; 22 | (function() { 23 | var port_; 24 | 25 | apprtc.windowPort.sendMessage = function(message) { 26 | var port = getPort_(); 27 | try { 28 | port.postMessage(message); 29 | } 30 | catch (ex) { 31 | trace('Error sending message via port: ' + ex); 32 | } 33 | }; 34 | 35 | apprtc.windowPort.addMessageListener = function(listener) { 36 | var port = getPort_(); 37 | port.onMessage.addListener(listener); 38 | }; 39 | 40 | var getPort_ = function() { 41 | if (!port_) { 42 | port_ = chrome.runtime.connect(); 43 | } 44 | return port_; 45 | }; 46 | })(); 47 | -------------------------------------------------------------------------------- /routes/users.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | 4 | /* GET users listing. */ 5 | router.get('/', function(req, res, next) { 6 | res.send('respond with a resource'); 7 | }); 8 | 9 | module.exports = router; 10 | --------------------------------------------------------------------------------