├── .gitignore ├── LICENSE ├── README.md ├── cyber_mega_phone.css ├── cyber_mega_phone.js ├── index.html ├── lib ├── jssip-3.0.13.js └── sdp-interop-sl-1.4.0.js └── utils.js /.gitignore: -------------------------------------------------------------------------------- 1 | # git ls-files --others --exclude-from=.git/info/exclude 2 | # Lines that start with '#' are comments. 3 | # For a project mostly in C, the following would be a good set of 4 | # exclude patterns (uncomment them if you want to use them): 5 | # *.[oa] 6 | # *~ 7 | 8 | # See .gitignore in subdirectories for more ignored files 9 | 10 | *~ 11 | *.gz 12 | 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Digium, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cyber Mega Phone 2K 2 | 3 | Cyber Mega Phone 2K Ultimate Dynamic Edition is a simple browser side client 4 | application that was created for testing of [Asterisk's](https://github.com/asterisk) 5 | (15+) multistream capabilities. Firefox and Chrome based browsers are supported. 6 | 7 | ### Dependencies 8 | 9 | Currently, Cyber Mega Phone 2K utilizes [JsSIP](http://www.jssip.net/) (v3.0.13) for 10 | SIP support and [sdp-interop-sl](https://github.com/StarLeafRob/sdp-interop-sl) (v1.4.0) 11 | for SDP Plan B support that is currently needed for Chrome based browsers. Both of these 12 | libraries can be found under the 'lib' directory within the project so there should be 13 | no need for further download. 14 | 15 | As mentioned multistream support is only supported in Asterisk 15+. Also, the pjsip 16 | channel driver is currently the **only** channel driver that is multistream enabled. 17 | 18 | ### Usage 19 | 20 | Build and [install](https://wiki.asterisk.org/wiki/display/AST/Installing+Asterisk) Asterisk. 21 | Once installed configure Asterisk to listen for webrtc connections. See the 22 | [WebRTC tutorial](https://wiki.asterisk.org/wiki/display/AST/WebRTC+tutorial+using+SIPML5) 23 | on the Asterisk wiki. The configuration should be similar. 24 | 25 | You'll need to add a few additional settings to your configured pjsip endpoint. 26 | `max_audio_streams` and `max_video_streams` need to be set to a number greater than one 27 | (the default) in order for Asterisk to allow more than one of each stream type. 28 | ``` 29 | max_audio_streams= 30 | max_video_streams= 31 | webrtc=yes 32 | ``` 33 | 34 | You will also need to configure an extension to dial. You should be able to dial out to another 35 | endpoint, but the easiest way to check out the multistream capabilities is to dial into a 36 | [confbridge](https://wiki.asterisk.org/wiki/display/AST/ConfBridge) 37 | or use app_stream_echo. For instance, to use the Asterisk stream echo dialplan application create 38 | an extension with the following (be sure to set 'max_video_streams' to at least 4 then): 39 | ``` 40 | exten => stream_echo,1,Answer() 41 | same => n,StreamEcho(4) 42 | same => n,Hangup() 43 | ``` 44 | Calling the above should result in your browser showing five video streams. One local and four 45 | remote streams. If you have configured an extension for a confbridge then, when dialed, you may 46 | initially see a single video stream (if you are the first to join) and then other video elements 47 | are added and removed as others join or leave the confbridge. 48 | 49 | Once Asterisk is configured and running open either a Firefox or Chrome based browser. 50 | In all likelyhood you'll need to register your cert first, so enter the following address: 51 | 52 | https://[ip of asterisk server]:8089/ws 53 | 54 | And manually confirm the security exception. Go to File->Open, navigate to where you downloaded 55 | Cyber Mega Phone 2K, and then open the 'index.html' file. In your browser you should see some 56 | fancy side scrolling text and three buttons. Click the 'Account' button and enter the endpoint 57 | credentials you configured in Asterisk (Note, 'ID' is the endpoint name). Also enter the extension 58 | you would like to dial. Close the box and then press the 'Connect' button. This should connect you 59 | to Asterisk and register the endpoint if configured to do that. Now the 'Call' button should be 60 | enabled. Press it to dial the set extension. Depending on the extension you dialed, and if you 61 | allowed your browser access, you should now see one or more video elements displayed. 'Hangup' or 62 | 'Disconnect' to end. 63 | 64 | ### Recommendations 65 | 66 | If you experience audio issues, it may be a good idea to turn on the jitterbuffer. This can cause 67 | the audio to be slightly delayed, but will also eliminate problems such as bursty audio packets 68 | causing disruptions. You can enable this option in confbridge.conf for a user, or you can do it 69 | through the dialplan before placing the user in the conference by using the JITTERBUFFER dialplan 70 | function for a more fine tuned experience. 71 | 72 | ### License 73 | 74 | Cyber Mega Phone 2K is released under the [MIT License](LICENSE) Copyright (C) 2017 Digium, Inc. 75 | -------------------------------------------------------------------------------- /cyber_mega_phone.css: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | * Cyber Mega Phone 2K 3 | * Copyright (C) 2017 Digium, Inc. 4 | * 5 | * This program is free software, distributed under the terms of the 6 | * MIT License. See the LICENSE file at the top of the source tree. 7 | ******************************************************************************/ 8 | 9 | body { 10 | font-family: sans-serif; 11 | } 12 | 13 | header { 14 | font-size: 1.55rem; 15 | text-shadow: 0 2px 2px #b6701e; 16 | text-align: center; 17 | } 18 | 19 | .footer { 20 | position: fixed; 21 | width: 100%; 22 | bottom: 0; 23 | left: 0; 24 | padding: 0.2rem; 25 | background-color: #efefef; 26 | text-align: center; 27 | } 28 | 29 | /* Checkbox switch */ 30 | .switches { 31 | display: block; 32 | } 33 | 34 | .switch { 35 | position: relative; 36 | display: inline-block; 37 | width: 70px; 38 | height: 20px; 39 | } 40 | 41 | .switch input { 42 | display: none; 43 | } 44 | 45 | .slider { 46 | position: absolute; 47 | cursor: pointer; 48 | top: 0; 49 | left: 0; 50 | right: 0; 51 | bottom: 0; 52 | background-color: #ccc; 53 | -webkit-transition: .4s; 54 | transition: .4s; 55 | } 56 | 57 | .slider:before { 58 | position: absolute; 59 | content: ""; 60 | height: 12px; 61 | width: 16px; 62 | left: 4px; 63 | bottom: 4px; 64 | background-color: white; 65 | -webkit-transition: .4s; 66 | transition: .4s; 67 | } 68 | 69 | input:checked + .slider { 70 | background-color: #0068b2; 71 | } 72 | 73 | input:focus + .slider { 74 | box-shadow: 0 0 1px #0068b2; 75 | } 76 | 77 | input:checked + .slider:before { 78 | -webkit-transform: translateX(26px); 79 | -ms-transform: translateX(26px); 80 | transform: translateX(46px); 81 | } 82 | 83 | .slider:after { 84 | content: 'no'; 85 | color: white; 86 | display: block; 87 | position: absolute; 88 | transform: translate(-50%,-50%); 89 | top: 50%; 90 | left: 47%; 91 | font-size: 10px; 92 | font-family: Verdana, sans-serif; 93 | } 94 | 95 | input:checked + .slider:after { 96 | content: 'yes'; 97 | } 98 | 99 | /* Account and connection */ 100 | 101 | .connection { 102 | text-align: left; 103 | padding: 10px; 104 | } 105 | 106 | .connection input { 107 | vertical-align: middle; 108 | } 109 | 110 | .account-modal { 111 | text-align: center; 112 | display: none; 113 | position: fixed; 114 | z-index: 1; 115 | left: 0; 116 | top: 0; 117 | width: 100%; 118 | height: 100%; 119 | overflow: auto; 120 | background-color: rgb(0,0,0); 121 | background-color: rgba(0,0,0,0.4); 122 | } 123 | 124 | .account-content { 125 | margin: 10% auto; 126 | padding: 10px; 127 | border: 1px solid #888; 128 | width: 250px; 129 | position: relative; 130 | -webkit-animation-name: animatetop; 131 | -webkit-animation-duration: 0.4s; 132 | background-color: #0c2959; 133 | } 134 | 135 | @-webkit-keyframes animatetop { 136 | from {top: -300px; opacity: 0} 137 | to {top: 0; opacity: 1} 138 | } 139 | 140 | @keyframes animatetop { 141 | from {top: -300px; opacity: 0} 142 | to {top: 0; opacity: 1} 143 | } 144 | 145 | .account-content label { 146 | color: white; 147 | display: block; 148 | text-align: left; 149 | margin-top: 5px; 150 | } 151 | 152 | .account-close { 153 | color: #aaa; 154 | float: right; 155 | font-size: 20px; 156 | font-weight: bold; 157 | } 158 | 159 | .account-close:hover, .account-close:focus { 160 | color: black; 161 | text-decoration: none; 162 | cursor: pointer; 163 | } 164 | 165 | /* Media view and video */ 166 | 167 | video { 168 | height: 360px; 169 | width: 704px; 170 | } 171 | 172 | .media-view { 173 | border: 1px solid black; 174 | float: left; 175 | height: auto; 176 | padding: 0.5%; 177 | background-color: #F5F5F5; 178 | } 179 | 180 | .media-overlay { 181 | width: 100%; 182 | height: 100%; 183 | position: absolute; 184 | } 185 | 186 | .media-controls { 187 | width: 100%; 188 | position: relative; 189 | } 190 | 191 | .media-controls button:hover, .media-controls button:focus { 192 | opacity: 0.5; 193 | } 194 | -------------------------------------------------------------------------------- /cyber_mega_phone.js: -------------------------------------------------------------------------------- 1 | /////////////////////////////////////////////////////////////////////////////// 2 | // Cyber Mega Phone 2K 3 | // Copyright (C) 2017 Digium, Inc. 4 | // 5 | // This program is free software, distributed under the terms of the 6 | // MIT License. See the LICENSE file at the top of the source tree. 7 | /////////////////////////////////////////////////////////////////////////////// 8 | 9 | 'use_strict'; 10 | 11 | // Turn on jssip debugging by un-commenting the below: 12 | //JsSIP.debug.enable('JsSIP:*'); 13 | 14 | let isFirefox = typeof InstallTrigger !== 'undefined'; 15 | let isChrome = !!window.chrome && !!window.chrome; 16 | 17 | function CyberMegaPhone(id, name, password, host, register, audio=true, video=true) { 18 | EasyEvent.call(this); 19 | this.id = id; 20 | this.name = name; 21 | this.password = password; 22 | this.host = host; 23 | this.register = register; 24 | this.audio = audio; 25 | this.video = video; 26 | 27 | // If either video or audio isn't available, then disable so we don't fail. 28 | navigator.mediaDevices.enumerateDevices() 29 | .then(devices => { 30 | const mics = devices.filter(device => device.kind == "audioinput"); 31 | const cams = devices.filter(device => device.kind == "videoinput"); 32 | 33 | if (!mics.length > 0) { 34 | console.log("Microphone not available"); 35 | this.audio = false; 36 | } 37 | if (!cams.length > 0) { 38 | console.log("Camera not available"); 39 | this.video = false; 40 | } 41 | }) 42 | 43 | this._locals = new Streams(); 44 | this._locals.bubble("streamAdded", this); 45 | this._locals.bubble("streamRemoved", this); 46 | 47 | this._remotes = new Streams(); 48 | this._remotes.bubble("streamAdded", this); 49 | this._remotes.bubble("streamRemoved", this); 50 | }; 51 | 52 | CyberMegaPhone.prototype = Object.create(EasyEvent.prototype); 53 | CyberMegaPhone.prototype.constructor = CyberMegaPhone; 54 | 55 | // This was taken from the WebRTC unified transition guide located at 56 | // https://docs.google.com/document/d/1-ZfikoUtoJa9k-GZG1daN0BU3IjIanQ_JSscHxQesvU/edit 57 | function isUnifiedPlanDefault() { 58 | // Safari supports addTransceiver() but not Unified Plan when 59 | // currentDirection is not defined. 60 | if (!('currentDirection' in RTCRtpTransceiver.prototype)) 61 | return false; 62 | 63 | // If Unified Plan is supported, addTransceiver() should not throw. 64 | const tempPc = new RTCPeerConnection(); 65 | let canAddTransceiver = false; 66 | try { 67 | tempPc.addTransceiver('audio'); 68 | canAddTransceiver = true; 69 | } catch (e) { 70 | } 71 | 72 | tempPc.close(); 73 | return canAddTransceiver; 74 | } 75 | 76 | CyberMegaPhone.prototype.connect = function () { 77 | if (this._ua) { 78 | this._ua.start(); // Just reconnect 79 | return; 80 | } 81 | 82 | let that = this; 83 | 84 | let socket = new JsSIP.WebSocketInterface('wss://' + this.host + ':8089/ws'); 85 | let uri = 'sip:' + this.id + '@' + this.host; 86 | 87 | let config = { 88 | sockets: [ socket ], 89 | uri: uri, 90 | contact_uri: uri, 91 | username: this.name ? this.name : this.id, 92 | password: this.password, 93 | register: this.register, 94 | register_expires : 300 95 | }; 96 | 97 | this._unified = isUnifiedPlanDefault(); 98 | 99 | this._ua = new JsSIP.UA(config); 100 | 101 | function bubble (obj, name) { 102 | obj.on(name, function (data) { 103 | that.raise(name, data); 104 | }); 105 | }; 106 | 107 | bubble(this._ua, 'connected'); 108 | bubble(this._ua, 'disconnected'); 109 | bubble(this._ua, 'registered'); 110 | bubble(this._ua, 'unregistered'); 111 | bubble(this._ua, 'registrationFailed'); 112 | 113 | this._ua.on('newRTCSession', function (data) { 114 | let rtc = data.session; 115 | rtc.interop = new SdpInterop.InteropChrome(); 116 | 117 | console.log('new session - ' + rtc.direction + ' - ' + rtc); 118 | 119 | rtc.on("confirmed", function () { 120 | // ACK was received 121 | let streams = rtc.connection.getLocalStreams(); 122 | for (let i = 0; i < streams.length; ++i) { 123 | console.log('confirmed: adding local stream ' + streams[i].id); 124 | streams[i].local = true; 125 | that._locals.add(streams[i]); 126 | } 127 | }); 128 | 129 | rtc.on("sdp", function (data) { 130 | if (isFirefox && data.originator === 'remote') { 131 | data.sdp = data.sdp.replace(/actpass/g, 'active'); 132 | } else if (isChrome && !that._unified) { 133 | let desc = new RTCSessionDescription({type:data.type, sdp:data.sdp}); 134 | if (data.originator === 'local') { 135 | converted = rtc.interop.toUnifiedPlan(desc); 136 | } else { 137 | converted = rtc.interop.toPlanB(desc); 138 | } 139 | 140 | data.sdp = converted.sdp; 141 | } 142 | }); 143 | 144 | bubble(rtc, 'muted'); 145 | bubble(rtc, 'unmuted'); 146 | bubble(rtc, 'failed'); 147 | bubble(rtc, 'ended'); 148 | 149 | rtc.connection.ontrack = function (event) { 150 | console.log('ontrack: ' + event.track.kind + ' - ' + event.track.id + 151 | ' stream ' + event.streams[0].id); 152 | if (event.track.kind == 'video') { 153 | event.track.enabled = false; 154 | } 155 | for (let i = 0; i < event.streams.length; ++i) { 156 | event.streams[i].local = false; 157 | that._remotes.add(event.streams[i]); 158 | } 159 | }; 160 | 161 | rtc.connection.onremovestream = function (event) { 162 | console.log('onremovestream: ' + event.stream.id); 163 | that._remotes.remove(event.stream); 164 | }; 165 | 166 | if (data.originator === "remote") { 167 | that.raise('incoming', data.request.ruri.toAor()); 168 | } 169 | }); 170 | 171 | this._ua.start(); 172 | }; 173 | 174 | CyberMegaPhone.prototype.disconnect = function () { 175 | this._locals.removeAll(); 176 | this._remotes.removeAll(); 177 | if (this._ua) { 178 | this._ua.stop(); 179 | } 180 | }; 181 | 182 | CyberMegaPhone.prototype.answer = function () { 183 | if (!this._ua) { 184 | return; 185 | } 186 | 187 | let options = { 188 | 'mediaConstraints': { 'audio': this.audio, 'video': this.video } 189 | }; 190 | 191 | this._rtc.answer(options); 192 | }; 193 | 194 | CyberMegaPhone.prototype.call = function (exten) { 195 | if (!this._ua || !exten) { 196 | return; 197 | } 198 | 199 | let options = { 200 | 'mediaConstraints': { 'audio': this.audio, 'video': this.video } 201 | }; 202 | 203 | if (exten.startsWith('sip:')) { 204 | this._rtc = this._ua.call(exten); 205 | } else { 206 | this._rtc = this._ua.call('sip:' + exten + '@' + this.host, options); 207 | } 208 | }; 209 | 210 | CyberMegaPhone.prototype.terminate = function () { 211 | this._locals.removeAll(); 212 | this._remotes.removeAll(); 213 | if (this._ua) { 214 | this._rtc.terminate(); 215 | } 216 | }; 217 | 218 | /////////////////////////////////////////////////////////////////////////////// 219 | 220 | function mute(stream, options) { 221 | 222 | function setTracks(tracks, val) { 223 | if (!tracks) { 224 | return; 225 | } 226 | 227 | for (let i = 0; i < tracks.length; ++i) { 228 | if (tracks[i].enabled == val) { 229 | tracks[i].enabled = !val; 230 | } 231 | } 232 | }; 233 | 234 | options = options || { audio: true, video: true }; 235 | 236 | if (typeof options.audio != 'undefined') { 237 | setTracks(stream.getAudioTracks(), options.audio); 238 | } 239 | 240 | if (typeof options.video != 'undefined') { 241 | setTracks(stream.getVideoTracks(), options.video); 242 | } 243 | } 244 | 245 | function unmute(stream, options) { 246 | let opts = options || { audio: false, video: false }; 247 | mute(stream, opts); 248 | } 249 | 250 | /////////////////////////////////////////////////////////////////////////////// 251 | 252 | function Streams () { 253 | EasyEvent.call(this); 254 | this._streams = []; 255 | }; 256 | 257 | Streams.prototype = Object.create(EasyEvent.prototype); 258 | Streams.prototype.constructor = Streams; 259 | 260 | Streams.prototype.add = function (stream) { 261 | if (this._streams.indexOf(stream) == -1) { 262 | this._streams.push(stream); 263 | console.log('Streams: added ' + stream.id); 264 | this.raise('streamAdded', stream); 265 | } 266 | }; 267 | 268 | Streams.prototype.remove = function (stream) { 269 | let index = typeof stream == 'number' ? stream : this._streams.indexOf(stream); 270 | 271 | if (index == -1) { 272 | return; 273 | } 274 | 275 | let removed = this._streams.splice(index, 1); 276 | for (let i = 0; i < removed.length; ++i) { 277 | console.log('Streams: removed ' + removed[i].id); 278 | this.raise('streamRemoved', removed[i]); 279 | } 280 | }; 281 | 282 | Streams.prototype.removeAll = function () { 283 | for (let i = this._streams.length - 1; i >= 0 ; --i) { 284 | this.remove(i); 285 | } 286 | }; 287 | 288 | /////////////////////////////////////////////////////////////////////////////// 289 | 290 | function EasyEvent () { 291 | this._events = {}; 292 | }; 293 | 294 | EasyEvent.prototype.handle = function (name, fun) { 295 | if (name in this._events) { 296 | this._events[name].push(fun); 297 | } else { 298 | this._events[name] = [fun]; 299 | } 300 | }; 301 | 302 | EasyEvent.prototype.raise = function (name) { 303 | if (name in this._events) { 304 | for (let i = 0; i < this._events[name].length; ++i) { 305 | this._events[name][i].apply(this, 306 | Array.prototype.slice.call(arguments, 1)); 307 | } 308 | } 309 | }; 310 | 311 | EasyEvent.prototype.bubble = function (name, obj) { 312 | this.handle(name, function (data) { 313 | obj.raise(name, data); 314 | }); 315 | }; 316 | 317 | EasyEvent.prototype.raiseForEach = function (name, array) { 318 | if (name in this._events) { 319 | for (let i = 0; i < array.length; ++i) { 320 | this.raise(name, array[i], i); 321 | } 322 | } 323 | }; 324 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | Cyber Mega Phone 2K 14 | 15 | 16 | 17 | 18 | 19 | 283 | 284 | 285 |
286 |

Welcome to Cyber Mega Phone 2K Ultimate Dynamic Edition

287 |
288 |
289 | 290 | 291 | 292 |
293 | 313 |
314 |
315 | 316 | 352 | 353 | 354 | -------------------------------------------------------------------------------- /lib/sdp-interop-sl-1.4.0.js: -------------------------------------------------------------------------------- 1 | (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.SdpInterop = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o= len) { 491 | return x; // missing argument 492 | } 493 | var arg = args[i]; 494 | i += 1; 495 | switch (x) { 496 | case '%%': 497 | return '%'; 498 | case '%s': 499 | return String(arg); 500 | case '%d': 501 | return Number(arg); 502 | case '%v': 503 | return ''; 504 | } 505 | }); 506 | // NB: we discard excess arguments - they are typically undefined from makeLine 507 | }; 508 | 509 | var makeLine = function (type, obj, location) { 510 | var str = obj.format instanceof Function ? 511 | (obj.format(obj.push ? location : location[obj.name])) : 512 | obj.format; 513 | 514 | var args = [type + '=' + str]; 515 | if (obj.names) { 516 | for (var i = 0; i < obj.names.length; i += 1) { 517 | var n = obj.names[i]; 518 | if (obj.name) { 519 | args.push(location[obj.name][n]); 520 | } 521 | else { // for mLine and push attributes 522 | args.push(location[obj.names[i]]); 523 | } 524 | } 525 | } 526 | else { 527 | args.push(location[obj.name]); 528 | } 529 | return format.apply(null, args); 530 | }; 531 | 532 | // RFC specified order 533 | // TODO: extend this with all the rest 534 | var defaultOuterOrder = [ 535 | 'v', 'o', 's', 'i', 536 | 'u', 'e', 'p', 'c', 537 | 'b', 't', 'r', 'z', 'a' 538 | ]; 539 | var defaultInnerOrder = ['i', 'c', 'b', 'a']; 540 | 541 | 542 | module.exports = function (session, opts) { 543 | opts = opts || {}; 544 | // ensure certain properties exist 545 | if (session.version == null) { 546 | session.version = 0; // 'v=0' must be there (only defined version atm) 547 | } 548 | if (session.name == null) { 549 | session.name = ' '; // 's= ' must be there if no meaningful name set 550 | } 551 | session.media.forEach(function (mLine) { 552 | if (mLine.payloads == null) { 553 | mLine.payloads = ''; 554 | } 555 | }); 556 | 557 | var outerOrder = opts.outerOrder || defaultOuterOrder; 558 | var innerOrder = opts.innerOrder || defaultInnerOrder; 559 | var sdp = []; 560 | 561 | // loop through outerOrder for matching properties on session 562 | outerOrder.forEach(function (type) { 563 | grammar[type].forEach(function (obj) { 564 | if (obj.name in session && session[obj.name] != null) { 565 | sdp.push(makeLine(type, obj, session)); 566 | } 567 | else if (obj.push in session && session[obj.push] != null) { 568 | session[obj.push].forEach(function (el) { 569 | sdp.push(makeLine(type, obj, el)); 570 | }); 571 | } 572 | }); 573 | }); 574 | 575 | // then for each media line, follow the innerOrder 576 | session.media.forEach(function (mLine) { 577 | sdp.push(makeLine('m', grammar.m[0], mLine)); 578 | 579 | innerOrder.forEach(function (type) { 580 | grammar[type].forEach(function (obj) { 581 | if (obj.name in mLine && mLine[obj.name] != null) { 582 | sdp.push(makeLine(type, obj, mLine)); 583 | } 584 | else if (obj.push in mLine && mLine[obj.push] != null) { 585 | mLine[obj.push].forEach(function (el) { 586 | sdp.push(makeLine(type, obj, el)); 587 | }); 588 | } 589 | }); 590 | }); 591 | }); 592 | 593 | return sdp.join('\r\n') + '\r\n'; 594 | }; 595 | 596 | },{"./grammar":1}],5:[function(require,module,exports){ 597 | /* Copyright @ 2015 Atlassian Pty Ltd 598 | * 599 | * Licensed under the Apache License, Version 2.0 (the "License"); 600 | * you may not use this file except in compliance with the License. 601 | * You may obtain a copy of the License at 602 | * 603 | * http://www.apache.org/licenses/LICENSE-2.0 604 | * 605 | * Unless required by applicable law or agreed to in writing, software 606 | * distributed under the License is distributed on an "AS IS" BASIS, 607 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 608 | * See the License for the specific language governing permissions and 609 | * limitations under the License. 610 | */ 611 | 612 | var SdpInterop = module.exports = { 613 | InteropFF: require('./interop_on_ff'), 614 | InteropChrome: require('./interop_on_chrome'), 615 | transform: require('./transform') 616 | }; 617 | 618 | },{"./interop_on_chrome":7,"./interop_on_ff":8,"./transform":11}],6:[function(require,module,exports){ 619 | /* Copyright @ 2015 Atlassian Pty Ltd 620 | * 621 | * Licensed under the Apache License, Version 2.0 (the "License"); 622 | * you may not use this file except in compliance with the License. 623 | * You may obtain a copy of the License at 624 | * 625 | * http://www.apache.org/licenses/LICENSE-2.0 626 | * 627 | * Unless required by applicable law or agreed to in writing, software 628 | * distributed under the License is distributed on an "AS IS" BASIS, 629 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 630 | * See the License for the specific language governing permissions and 631 | * limitations under the License. 632 | */ 633 | 634 | module.exports = function arrayEquals(array) { 635 | // if the other array is a falsy value, return 636 | if (!array) 637 | return false; 638 | 639 | // compare lengths - can save a lot of time 640 | if (this.length != array.length) 641 | return false; 642 | 643 | for (var i = 0, l = this.length; i < l; i++) { 644 | // Check if we have nested arrays 645 | if (this[i] instanceof Array && array[i] instanceof Array) { 646 | // recurse into the nested arrays 647 | if (!arrayEquals.apply(this[i], [array[i]])) 648 | return false; 649 | } else if (this[i] != array[i]) { 650 | // Warning - two different object instances will never be equal: 651 | // {x:20} != {x:20} 652 | return false; 653 | } 654 | } 655 | return true; 656 | }; 657 | 658 | 659 | },{}],7:[function(require,module,exports){ 660 | /** 661 | * Copyright(c) Starleaf Ltd. 2016. 662 | */ 663 | 664 | 665 | "use strict"; 666 | 667 | 668 | //Small library for plan b interop - Designed to be run on chrome. 669 | //Assumes you will do the following - convert unified plan received on the wire into plan B 670 | //before setting the remote description 671 | //Convert plan b generated by chrome into unified plan prior to sending. 672 | 673 | var Interop = function () { 674 | var cache = {}; 675 | 676 | var copyObj = function (obj) { 677 | return JSON.parse(JSON.stringify(obj)); 678 | }; 679 | 680 | var toUnifiedPlan = function (desc) { 681 | var uplan = require('./on_chrome/to-unified-plan')(desc, cache); 682 | //cache a copy 683 | cache.local = copyObj(uplan.sdp); 684 | return uplan; 685 | }; 686 | 687 | var toPlanB = function (desc) { 688 | //cache the last unified plan we received on the wire 689 | cache.remote = copyObj(desc.sdp); 690 | return require('./on_chrome/to-plan-b')(desc, cache); 691 | }; 692 | 693 | 694 | var that = {}; 695 | that.toUnifiedPlan = toUnifiedPlan; 696 | that.toPlanB = toPlanB; 697 | return that; 698 | }; 699 | 700 | module.exports = Interop; 701 | },{"./on_chrome/to-plan-b":9,"./on_chrome/to-unified-plan":10}],8:[function(require,module,exports){ 702 | /* Copyright @ 2015 Atlassian Pty Ltd 703 | * 704 | * Licensed under the Apache License, Version 2.0 (the "License"); 705 | * you may not use this file except in compliance with the License. 706 | * You may obtain a copy of the License at 707 | * 708 | * http://www.apache.org/licenses/LICENSE-2.0 709 | * 710 | * Unless required by applicable law or agreed to in writing, software 711 | * distributed under the License is distributed on an "AS IS" BASIS, 712 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 713 | * See the License for the specific language governing permissions and 714 | * limitations under the License. 715 | */ 716 | 717 | /* global RTCSessionDescription */ 718 | /* jshint -W097 */ 719 | "use strict"; 720 | 721 | var transform = require('./transform'); 722 | var arrayEquals = require('./array-equals'); 723 | 724 | function Interop() { 725 | 726 | /** 727 | * This map holds the most recent Unified Plan offer/answer SDP that was 728 | * converted to Plan B, with the SDP type ('offer' or 'answer') as keys and 729 | * the SDP string as values. 730 | * 731 | * @type {{}} 732 | */ 733 | this.cache = {}; 734 | } 735 | 736 | module.exports = Interop; 737 | 738 | /** 739 | * Returns the index of the first m-line with the given media type and with a 740 | * direction which allows sending, in the last Unified Plan description with 741 | * type "answer" converted to Plan B. Returns {null} if there is no saved 742 | * answer, or if none of its m-lines with the given type allow sending. 743 | * @param type the media type ("audio" or "video"). 744 | * @returns {*} 745 | */ 746 | Interop.prototype.getFirstSendingIndexFromAnswer = function (type) { 747 | if (!this.cache.answer) { 748 | return null; 749 | } 750 | 751 | var session = transform.parse(this.cache.answer); 752 | if (session && session.media && Array.isArray(session.media)) { 753 | for (var i = 0; i < session.media.length; i++) { 754 | if (session.media[i].type == type && 755 | (!session.media[i].direction /* default to sendrecv */ || 756 | session.media[i].direction === 'sendrecv' || 757 | session.media[i].direction === 'sendonly')) { 758 | return i; 759 | } 760 | } 761 | } 762 | 763 | return null; 764 | }; 765 | 766 | /** 767 | * This method transforms a Unified Plan SDP to an equivalent Plan B SDP. A 768 | * PeerConnection wrapper transforms the SDP to Plan B before passing it to the 769 | * application. 770 | * 771 | * @param desc 772 | * @returns {*} 773 | */ 774 | Interop.prototype.toPlanB = function (desc) { 775 | var self = this; 776 | //#region Preliminary input validation. 777 | 778 | if (typeof desc !== 'object' || desc === null || 779 | typeof desc.sdp !== 'string') { 780 | console.warn('An empty description was passed as an argument.'); 781 | return desc; 782 | } 783 | 784 | // Objectify the SDP for easier manipulation. 785 | var session = transform.parse(desc.sdp); 786 | 787 | // If the SDP contains no media, there's nothing to transform. 788 | if (typeof session.media === 'undefined' || !Array.isArray(session.media) || session.media.length === 0) { 789 | console.warn('The description has no media.'); 790 | return desc; 791 | } 792 | 793 | // Try some heuristics to "make sure" this is a Unified Plan SDP. Plan B 794 | // SDP has a video, an audio and a data "channel" at most. 795 | if (session.media.length <= 3 && session.media.every(function (m) { 796 | return ['video', 'audio', 'data'].indexOf(m.mid) !== -1; 797 | } 798 | )) { 799 | console.warn('This description does not look like Unified Plan.'); 800 | return desc; 801 | } 802 | 803 | //#endregion 804 | 805 | // HACK https://bugzilla.mozilla.org/show_bug.cgi?id=1113443 806 | var sdp = desc.sdp; 807 | var rewrite = false; 808 | for (var i = 0; i < session.media.length; i++) { 809 | var uLine = session.media[i]; 810 | uLine.rtp.forEach(function (rtp) { 811 | if (rtp.codec === 'NULL') { 812 | rewrite = true; 813 | var offer = transform.parse(self.cache.offer); 814 | rtp.codec = offer.media[i].rtp[0].codec; 815 | } 816 | } 817 | ); 818 | } 819 | if (rewrite) { 820 | sdp = transform.write(session); 821 | } 822 | 823 | // Unified Plan SDP is our "precious". Cache it for later use in the Plan B 824 | // -> Unified Plan transformation. 825 | this.cache[desc.type] = sdp; 826 | 827 | //#region Convert from Unified Plan to Plan B. 828 | 829 | // We rebuild the session.media array. 830 | var media = session.media; 831 | session.media = []; 832 | 833 | // Associative array that maps channel types to channel objects for fast 834 | // access to channel objects by their type, e.g. type2bl['audio']->channel 835 | // obj. 836 | var type2bl = {}; 837 | 838 | // Used to build the group:BUNDLE value after the channels construction 839 | // loop. 840 | var types = []; 841 | 842 | // Implode the Unified Plan m-lines/tracks into Plan B channels. 843 | media.forEach(function (uLine) { 844 | 845 | // rtcp-mux is required in the Plan B SDP. 846 | if ((typeof uLine.rtcpMux !== 'string' || 847 | uLine.rtcpMux !== 'rtcp-mux') && 848 | uLine.direction !== 'inactive') { 849 | throw new Error('Cannot convert to Plan B because m-lines ' + 850 | 'without the rtcp-mux attribute were found.' 851 | ); 852 | } 853 | 854 | if (uLine.type === 'application') { 855 | session.media.push(uLine); 856 | types.push(uLine.mid); 857 | return; 858 | } 859 | 860 | // If we don't have a channel for this uLine.type, then use this 861 | // uLine as the channel basis. 862 | if (typeof type2bl[uLine.type] === 'undefined') { 863 | type2bl[uLine.type] = uLine; 864 | } 865 | 866 | // Add sources to the channel and handle a=msid. 867 | if (typeof uLine.sources === 'object') { 868 | Object.keys(uLine.sources).forEach(function (ssrc) { 869 | if (typeof type2bl[uLine.type].sources !== 'object') 870 | type2bl[uLine.type].sources = {}; 871 | 872 | // Assign the sources to the channel. 873 | type2bl[uLine.type].sources[ssrc] = 874 | uLine.sources[ssrc]; 875 | 876 | if (typeof uLine.msid !== 'undefined') { 877 | // In Plan B the msid is an SSRC attribute. Also, we don't 878 | // care about the obsolete label and mslabel attributes. 879 | // 880 | // Note that it is not guaranteed that the uLine will 881 | // have an msid. recvonly channels in particular don't have 882 | // one. 883 | type2bl[uLine.type].sources[ssrc].msid = 884 | uLine.msid; 885 | } 886 | // NOTE ssrcs in ssrc groups will share msids, as 887 | // draft-uberti-rtcweb-plan-00 mandates. 888 | } 889 | ); 890 | } 891 | 892 | // Add ssrc groups to the channel. 893 | if (typeof uLine.ssrcGroups !== 'undefined' && 894 | Array.isArray(uLine.ssrcGroups)) { 895 | // Create the ssrcGroups array, if it's not defined. 896 | if (typeof type2bl[uLine.type].ssrcGroups === 'undefined' || !Array.isArray(type2bl[uLine.type].ssrcGroups 897 | )) { 898 | type2bl[uLine.type].ssrcGroups = []; 899 | } 900 | 901 | type2bl[uLine.type].ssrcGroups = 902 | type2bl[uLine.type].ssrcGroups.concat( 903 | uLine.ssrcGroups 904 | ); 905 | } 906 | 907 | if (type2bl[uLine.type] === uLine) { 908 | // Copy ICE related stuff from the principal media line. 909 | uLine.candidates = media[0].candidates; 910 | uLine.iceUfrag = media[0].iceUfrag; 911 | uLine.icePwd = media[0].icePwd; 912 | uLine.fingerprint = media[0].fingerprint; 913 | 914 | // Plan B mids are in ['audio', 'video', 'data'] 915 | uLine.mid = uLine.type; 916 | 917 | // Plan B doesn't support/need the bundle-only attribute. 918 | delete uLine.bundleOnly; 919 | 920 | // In Plan B the msid is an SSRC attribute. 921 | delete uLine.msid; 922 | 923 | // Used to build the group:BUNDLE value after this loop. 924 | types.push(uLine.type); 925 | 926 | // Add the channel to the new media array. 927 | session.media.push(uLine); 928 | } 929 | } 930 | ); 931 | 932 | // We regenerate the BUNDLE group with the new mids. 933 | session.groups.some(function (group) { 934 | if (group.type === 'BUNDLE') { 935 | group.mids = types.join(' '); 936 | return true; 937 | } 938 | } 939 | ); 940 | 941 | // msid semantic 942 | session.msidSemantic = { 943 | semantic: 'WMS', 944 | token: '*' 945 | }; 946 | 947 | var resStr = transform.write(session); 948 | 949 | return new RTCSessionDescription({ 950 | type: desc.type, 951 | sdp: resStr 952 | } 953 | ); 954 | 955 | //#endregion 956 | }; 957 | 958 | /** 959 | * This method transforms a Plan B SDP to an equivalent Unified Plan SDP. A 960 | * PeerConnection wrapper transforms the SDP to Unified Plan before passing it 961 | * to FF. 962 | * 963 | * @param desc 964 | * @returns {*} 965 | */ 966 | Interop.prototype.toUnifiedPlan = function (desc) { 967 | var self = this; 968 | //#region Preliminary input validation. 969 | 970 | if (typeof desc !== 'object' || desc === null || 971 | typeof desc.sdp !== 'string') { 972 | console.warn('An empty description was passed as an argument.'); 973 | return desc; 974 | } 975 | 976 | var session = transform.parse(desc.sdp); 977 | 978 | // If the SDP contains no media, there's nothing to transform. 979 | if (typeof session.media === 'undefined' || !Array.isArray(session.media) || session.media.length === 0) { 980 | console.warn('The description has no media.'); 981 | return desc; 982 | } 983 | 984 | // Try some heuristics to "make sure" this is a Plan B SDP. Plan B SDP has 985 | // a video, an audio and a data "channel" at most. 986 | if (session.media.length > 3 || !session.media.every(function (m) { 987 | return ['video', 'audio', 'data'].indexOf(m.mid) !== -1; 988 | } 989 | )) { 990 | console.warn('This description does not look like Plan B.'); 991 | return desc; 992 | } 993 | 994 | // Make sure this Plan B SDP can be converted to a Unified Plan SDP. 995 | var mids = []; 996 | session.media.forEach(function (m) { 997 | mids.push(m.mid); 998 | } 999 | ); 1000 | 1001 | var hasBundle = false; 1002 | if (typeof session.groups !== 'undefined' && 1003 | Array.isArray(session.groups)) { 1004 | hasBundle = session.groups.every(function (g) { 1005 | return g.type !== 'BUNDLE' || 1006 | arrayEquals.apply(g.mids.sort(), [mids.sort()]); 1007 | } 1008 | ); 1009 | } 1010 | 1011 | if (!hasBundle) { 1012 | throw new Error("Cannot convert to Unified Plan because m-lines that" + 1013 | " are not bundled were found." 1014 | ); 1015 | } 1016 | 1017 | //#endregion 1018 | 1019 | 1020 | //#region Convert from Plan B to Unified Plan. 1021 | 1022 | // Unfortunately, a Plan B offer/answer doesn't have enough information to 1023 | // rebuild an equivalent Unified Plan offer/answer. 1024 | // 1025 | // For example, if this is a local answer (in Unified Plan style) that we 1026 | // convert to Plan B prior to handing it over to the application (the 1027 | // PeerConnection wrapper called us, for instance, after a successful 1028 | // createAnswer), we want to remember the m-line at which we've seen the 1029 | // (local) SSRC. That's because when the application wants to do call the 1030 | // SLD method, forcing us to do the inverse transformation (from Plan B to 1031 | // Unified Plan), we need to know to which m-line to assign the (local) 1032 | // SSRC. We also need to know all the other m-lines that the original 1033 | // answer had and include them in the transformed answer as well. 1034 | // 1035 | // Another example is if this is a remote offer that we convert to Plan B 1036 | // prior to giving it to the application, we want to remember the mid at 1037 | // which we've seen the (remote) SSRC. 1038 | // 1039 | // In the iteration that follows, we use the cached Unified Plan (if it 1040 | // exists) to assign mids to ssrcs. 1041 | 1042 | var cached; 1043 | if (typeof this.cache[desc.type] !== 'undefined') { 1044 | cached = transform.parse(this.cache[desc.type]); 1045 | } 1046 | 1047 | var recvonlySsrcs = { 1048 | audio: {}, 1049 | video: {} 1050 | }; 1051 | 1052 | // A helper map that sends mids to m-line objects. We use it later to 1053 | // rebuild the Unified Plan style session.media array. 1054 | var mid2ul = {}; 1055 | session.media.forEach(function (bLine) { 1056 | if ((typeof bLine.rtcpMux !== 'string' || 1057 | bLine.rtcpMux !== 'rtcp-mux') && 1058 | bLine.direction !== 'inactive') { 1059 | throw new Error("Cannot convert to Unified Plan because m-lines " + 1060 | "without the rtcp-mux attribute were found." 1061 | ); 1062 | } 1063 | 1064 | if (bLine.type === 'application') { 1065 | mid2ul[bLine.mid] = bLine; 1066 | return; 1067 | } 1068 | 1069 | // With rtcp-mux and bundle all the channels should have the same ICE 1070 | // stuff. 1071 | var sources = bLine.sources; 1072 | var ssrcGroups = bLine.ssrcGroups; 1073 | var candidates = bLine.candidates; 1074 | var iceUfrag = bLine.iceUfrag; 1075 | var icePwd = bLine.icePwd; 1076 | var fingerprint = bLine.fingerprint; 1077 | var port = bLine.port; 1078 | 1079 | // We'll use the "bLine" object as a prototype for each new "mLine" 1080 | // that we create, but first we need to clean it up a bit. 1081 | delete bLine.sources; 1082 | delete bLine.ssrcGroups; 1083 | delete bLine.candidates; 1084 | delete bLine.iceUfrag; 1085 | delete bLine.icePwd; 1086 | delete bLine.fingerprint; 1087 | delete bLine.port; 1088 | delete bLine.mid; 1089 | 1090 | // inverted ssrc group map 1091 | var ssrc2group = {}; 1092 | if (typeof ssrcGroups !== 'undefined' && Array.isArray(ssrcGroups)) { 1093 | ssrcGroups.forEach(function (ssrcGroup) { 1094 | 1095 | // TODO(gp) find out how to receive simulcast with FF. For the 1096 | // time being, hide it. 1097 | if (ssrcGroup.semantics === 'SIM') { 1098 | return; 1099 | } 1100 | 1101 | // XXX This might brake if an SSRC is in more than one group 1102 | // for some reason. 1103 | if (typeof ssrcGroup.ssrcs !== 'undefined' && 1104 | Array.isArray(ssrcGroup.ssrcs)) { 1105 | ssrcGroup.ssrcs.forEach(function (ssrc) { 1106 | if (typeof ssrc2group[ssrc] === 'undefined') { 1107 | ssrc2group[ssrc] = []; 1108 | } 1109 | 1110 | ssrc2group[ssrc].push(ssrcGroup); 1111 | } 1112 | ); 1113 | } 1114 | } 1115 | ); 1116 | } 1117 | 1118 | // ssrc to m-line index. 1119 | var ssrc2ml = {}; 1120 | 1121 | if (typeof sources === 'object') { 1122 | 1123 | // Explode the Plan B channel sources with one m-line per source. 1124 | Object.keys(sources).forEach(function (ssrc) { 1125 | 1126 | // The (unified) m-line for this SSRC. We either create it from 1127 | // scratch or, if it's a grouped SSRC, we re-use a related 1128 | // mline. In other words, if the source is grouped with another 1129 | // source, put the two together in the same m-line. 1130 | var uLine; 1131 | 1132 | // We assume here that we are the answerer in the O/A, so any 1133 | // offers which we translate come from the remote side, while 1134 | // answers are local. So the check below is to make that we 1135 | // handle receive-only SSRCs in a special way only if they come 1136 | // from the remote side. 1137 | if (desc.type === 'offer') { 1138 | // We want to detect SSRCs which are used by a remote peer 1139 | // in an m-line with direction=recvonly (i.e. they are 1140 | // being used for RTCP only). 1141 | // This information would have gotten lost if the remote 1142 | // peer used Unified Plan and their local description was 1143 | // translated to Plan B. So we use the lack of an MSID 1144 | // attribute to deduce a "receive only" SSRC. 1145 | if (!sources[ssrc].msid) { 1146 | recvonlySsrcs[bLine.type][ssrc] = sources[ssrc]; 1147 | // Receive-only SSRCs must not create new m-lines. We 1148 | // will assign them to an existing m-line later. 1149 | return; 1150 | } 1151 | } 1152 | 1153 | if (typeof ssrc2group[ssrc] !== 'undefined' && 1154 | Array.isArray(ssrc2group[ssrc])) { 1155 | ssrc2group[ssrc].some(function (ssrcGroup) { 1156 | // ssrcGroup.ssrcs *is* an Array, no need to check 1157 | // again here. 1158 | return ssrcGroup.ssrcs.some(function (related) { 1159 | if (typeof ssrc2ml[related] === 'object') { 1160 | uLine = ssrc2ml[related]; 1161 | return true; 1162 | } 1163 | } 1164 | ); 1165 | } 1166 | ); 1167 | } 1168 | 1169 | if (typeof uLine === 'object') { 1170 | // the m-line already exists. Just add the source. 1171 | uLine.sources[ssrc] = sources[ssrc]; 1172 | delete sources[ssrc].msid; 1173 | } else { 1174 | // Use the "bLine" as a prototype for the "uLine". 1175 | uLine = Object.create(bLine); 1176 | ssrc2ml[ssrc] = uLine; 1177 | 1178 | if (typeof sources[ssrc].msid !== 'undefined') { 1179 | // Assign the msid of the source to the m-line. Note 1180 | // that it is not guaranteed that the source will have 1181 | // msid. In particular "recvonly" sources don't have an 1182 | // msid. Note that "recvonly" is a term only defined 1183 | // for m-lines. 1184 | uLine.msid = sources[ssrc].msid; 1185 | uLine.direction = 'sendrecv'; 1186 | delete sources[ssrc].msid; 1187 | } 1188 | 1189 | // We assign one SSRC per media line. 1190 | uLine.sources = {}; 1191 | uLine.sources[ssrc] = sources[ssrc]; 1192 | uLine.ssrcGroups = ssrc2group[ssrc]; 1193 | 1194 | // Use the cached Unified Plan SDP (if it exists) to assign 1195 | // SSRCs to mids. 1196 | if (typeof cached !== 'undefined' && 1197 | typeof cached.media !== 'undefined' && 1198 | Array.isArray(cached.media)) { 1199 | 1200 | cached.media.forEach(function (m) { 1201 | if (typeof m.sources === 'object') { 1202 | Object.keys(m.sources).forEach(function (s) { 1203 | if (s === ssrc) { 1204 | uLine.mid = m.mid; 1205 | } 1206 | } 1207 | ); 1208 | } 1209 | } 1210 | ); 1211 | } 1212 | 1213 | if (typeof uLine.mid === 'undefined') { 1214 | 1215 | // If this is an SSRC that we see for the first time 1216 | // assign it a new mid. This is typically the case when 1217 | // this method is called to transform a remote 1218 | // description for the first time or when there is a 1219 | // new SSRC in the remote description because a new 1220 | // peer has joined the conference. Local SSRCs should 1221 | // have already been added to the map in the toPlanB 1222 | // method. 1223 | // 1224 | // Because FF generates answers in Unified Plan style, 1225 | // we MUST already have a cached answer with all the 1226 | // local SSRCs mapped to some m-line/mid. 1227 | 1228 | if (desc.type === 'answer') { 1229 | throw new Error("An unmapped SSRC was found."); 1230 | } 1231 | 1232 | uLine.mid = [bLine.type, '-', ssrc].join(''); 1233 | } 1234 | 1235 | // Include the candidates in the 1st media line. 1236 | uLine.candidates = candidates; 1237 | uLine.iceUfrag = iceUfrag; 1238 | uLine.icePwd = icePwd; 1239 | uLine.fingerprint = fingerprint; 1240 | uLine.port = port; 1241 | 1242 | mid2ul[uLine.mid] = uLine; 1243 | } 1244 | } 1245 | ); 1246 | } 1247 | } 1248 | ); 1249 | 1250 | // Rebuild the media array in the right order and add the missing mLines 1251 | // (missing from the Plan B SDP). 1252 | session.media = []; 1253 | mids = []; // reuse 1254 | 1255 | if (desc.type === 'answer') { 1256 | 1257 | // The media lines in the answer must match the media lines in the 1258 | // offer. The order is important too. Here we assume that Firefox is 1259 | // the answerer, so we merely have to use the reconstructed (unified) 1260 | // answer to update the cached (unified) answer accordingly. 1261 | // 1262 | // In the general case, one would have to use the cached (unified) 1263 | // offer to find the m-lines that are missing from the reconstructed 1264 | // answer, potentially grabbing them from the cached (unified) answer. 1265 | // One has to be careful with this approach because inactive m-lines do 1266 | // not always have an mid, making it tricky (impossible?) to find where 1267 | // exactly and which m-lines are missing from the reconstructed answer. 1268 | 1269 | for (var i = 0; i < cached.media.length; i++) { 1270 | var uLine = cached.media[i]; 1271 | 1272 | if (typeof mid2ul[uLine.mid] === 'undefined') { 1273 | 1274 | // The mid isn't in the reconstructed (unified) answer. 1275 | // This is either a (unified) m-line containing a remote 1276 | // track only, or a (unified) m-line containing a remote 1277 | // track and a local track that has been removed. 1278 | // In either case, it MUST exist in the cached 1279 | // (unified) answer. 1280 | // 1281 | // In case this is a removed local track, clean-up 1282 | // the (unified) m-line and make sure it's 'recvonly' or 1283 | // 'inactive'. 1284 | 1285 | delete uLine.msid; 1286 | delete uLine.sources; 1287 | delete uLine.ssrcGroups; 1288 | if (!uLine.direction 1289 | || uLine.direction === 'sendrecv') 1290 | uLine.direction = 'recvonly'; 1291 | else if (uLine.direction === 'sendonly') 1292 | uLine.direction = 'inactive'; 1293 | } else { 1294 | // This is an (unified) m-line/channel that contains a local 1295 | // track (sendrecv or sendonly channel) or it's a unified 1296 | // recvonly m-line/channel. In either case, since we're 1297 | // going from PlanB -> Unified Plan this m-line MUST 1298 | // exist in the cached answer. 1299 | } 1300 | 1301 | session.media.push(uLine); 1302 | 1303 | if (typeof uLine.mid === 'string') { 1304 | // inactive lines don't/may not have an mid. 1305 | mids.push(uLine.mid); 1306 | } 1307 | } 1308 | } else { 1309 | 1310 | // SDP offer/answer (and the JSEP spec) forbids removing an m-section 1311 | // under any circumstances. If we are no longer interested in sending a 1312 | // track, we just remove the msid and ssrc attributes and set it to 1313 | // either a=recvonly (as the reofferer, we must use recvonly if the 1314 | // other side was previously sending on the m-section, but we can also 1315 | // leave the possibility open if it wasn't previously in use), or 1316 | // a=inactive. 1317 | 1318 | if (typeof cached !== 'undefined' && 1319 | typeof cached.media !== 'undefined' && 1320 | Array.isArray(cached.media)) { 1321 | cached.media.forEach(function (uLine) { 1322 | mids.push(uLine.mid); 1323 | if (typeof mid2ul[uLine.mid] !== 'undefined') { 1324 | session.media.push(mid2ul[uLine.mid]); 1325 | } else { 1326 | delete uLine.msid; 1327 | delete uLine.sources; 1328 | delete uLine.ssrcGroups; 1329 | if (!uLine.direction 1330 | || uLine.direction === 'sendrecv') 1331 | uLine.direction = 'recvonly'; 1332 | if (!uLine.direction 1333 | || uLine.direction === 'sendonly') 1334 | uLine.direction = 'inactive'; 1335 | session.media.push(uLine); 1336 | } 1337 | } 1338 | ); 1339 | } 1340 | 1341 | // Add all the remaining (new) m-lines of the transformed SDP. 1342 | Object.keys(mid2ul).forEach(function (mid) { 1343 | if (mids.indexOf(mid) === -1) { 1344 | mids.push(mid); 1345 | if (mid2ul[mid].direction === 'recvonly') { 1346 | // This is a remote recvonly channel. Add its SSRC to the 1347 | // appropriate sendrecv or sendonly channel. 1348 | // TODO(gp) what if we don't have sendrecv/sendonly 1349 | // channel? 1350 | 1351 | session.media.some(function (uLine) { 1352 | if ((uLine.direction === 'sendrecv' || 1353 | uLine.direction === 'sendonly') && 1354 | uLine.type === mid2ul[mid].type) { 1355 | 1356 | // mid2ul[mid] shouldn't have any ssrc-groups 1357 | Object.keys(mid2ul[mid].sources).forEach( 1358 | function (ssrc) { 1359 | uLine.sources[ssrc] = 1360 | mid2ul[mid].sources[ssrc]; 1361 | } 1362 | ); 1363 | 1364 | return true; 1365 | } 1366 | } 1367 | ); 1368 | } else { 1369 | session.media.push(mid2ul[mid]); 1370 | } 1371 | } 1372 | } 1373 | ); 1374 | } 1375 | 1376 | // After we have constructed the Plan Unified m-lines we can figure out 1377 | // where (in which m-line) to place the 'recvonly SSRCs'. 1378 | // Note: we assume here that we are the answerer in the O/A, so any offers 1379 | // which we translate come from the remote side, while answers are local 1380 | // (and so our last local description is cached as an 'answer'). 1381 | ["audio", "video"].forEach(function (type) { 1382 | if (!session || !session.media || !Array.isArray(session.media)) 1383 | return; 1384 | 1385 | var idx = null; 1386 | if (Object.keys(recvonlySsrcs[type]).length > 0) { 1387 | idx = self.getFirstSendingIndexFromAnswer(type); 1388 | if (idx === null) { 1389 | // If this is the first offer we receive, we don't have a 1390 | // cached answer. Assume that we will be sending media using 1391 | // the first m-line for each media type. 1392 | 1393 | for (var i = 0; i < session.media.length; i++) { 1394 | if (session.media[i].type === type) { 1395 | idx = i; 1396 | break; 1397 | } 1398 | } 1399 | } 1400 | } 1401 | 1402 | if (idx && session.media.length > idx) { 1403 | var mLine = session.media[idx]; 1404 | Object.keys(recvonlySsrcs[type]).forEach(function (ssrc) { 1405 | if (mLine.sources && mLine.sources[ssrc]) { 1406 | console.warn("Replacing an existing SSRC."); 1407 | } 1408 | if (!mLine.sources) { 1409 | mLine.sources = {}; 1410 | } 1411 | 1412 | mLine.sources[ssrc] = recvonlySsrcs[type][ssrc]; 1413 | } 1414 | ); 1415 | } 1416 | } 1417 | ); 1418 | 1419 | // We regenerate the BUNDLE group (since we regenerated the mids) 1420 | session.groups.some(function (group) { 1421 | if (group.type === 'BUNDLE') { 1422 | group.mids = mids.join(' '); 1423 | return true; 1424 | } 1425 | } 1426 | ); 1427 | 1428 | // msid semantic 1429 | session.msidSemantic = { 1430 | semantic: 'WMS', 1431 | token: '*' 1432 | }; 1433 | 1434 | var resStr = transform.write(session); 1435 | 1436 | // Cache the transformed SDP (Unified Plan) for later re-use in this 1437 | // function. 1438 | this.cache[desc.type] = resStr; 1439 | 1440 | return new RTCSessionDescription({ 1441 | type: desc.type, 1442 | sdp: resStr 1443 | } 1444 | ); 1445 | 1446 | //#endregion 1447 | }; 1448 | 1449 | },{"./array-equals":6,"./transform":11}],9:[function(require,module,exports){ 1450 | /** 1451 | * Copyright(c) Starleaf Ltd. 2016. 1452 | */ 1453 | 1454 | 1455 | "use strict"; 1456 | 1457 | var transform = require('../transform'); 1458 | 1459 | module.exports = function (desc, cache) { 1460 | if (typeof desc !== 'object' || desc === null || 1461 | typeof desc.sdp !== 'string') { 1462 | console.warn('An empty description was passed as an argument.'); 1463 | return desc; 1464 | } 1465 | 1466 | // Objectify the SDP for easier manipulation. 1467 | var session = transform.parse(desc.sdp); 1468 | 1469 | // If the SDP contains no media, there's nothing to transform. 1470 | if (typeof session.media === 'undefined' || !Array.isArray(session.media) || session.media.length === 0) { 1471 | console.warn('The description has no media.'); 1472 | return desc; 1473 | } 1474 | 1475 | // Try some heuristics to "make sure" this is a Unified Plan SDP. Plan B 1476 | // SDP has a video, an audio and a data "channel" at most. 1477 | if (session.media.length <= 3 && session.media.every(function (m) { 1478 | return ['video', 'audio', 'data'].indexOf(m.mid) !== -1; 1479 | })) { 1480 | console.warn('This description does not look like Unified Plan.'); 1481 | return desc; 1482 | } 1483 | 1484 | //#endregion 1485 | 1486 | // HACK https://bugzilla.mozilla.org/show_bug.cgi?id=1113443 1487 | var rewrite = false; 1488 | for (var i = 0; i < session.media.length; i++) { 1489 | var uLine = session.media[i]; 1490 | uLine.rtp.forEach(function (rtp) { 1491 | if (rtp.codec === 'NULL') { 1492 | rewrite = true; 1493 | var offer = transform.parse(cache.local); 1494 | rtp.codec = offer.media[i].rtp[0].codec; 1495 | } 1496 | }); 1497 | } 1498 | 1499 | if (rewrite) { 1500 | desc.sdp = transform.write(session); 1501 | } 1502 | 1503 | // Unified Plan SDP is our "precious". Cache it for later use in the Plan B 1504 | // -> Unified Plan transformation. 1505 | 1506 | //#region Convert from Unified Plan to Plan B. 1507 | 1508 | // We rebuild the session.media array. 1509 | var media = session.media; 1510 | session.media = []; 1511 | 1512 | // Associative array that maps channel types to channel objects for fast 1513 | // access to channel objects by their type, e.g. type2bl['audio']->channel 1514 | // obj. 1515 | var type2bl = {}; 1516 | 1517 | // Used to build the group:BUNDLE value after the channels construction 1518 | // loop. 1519 | var types = []; 1520 | 1521 | // Implode the Unified Plan m-lines/tracks into Plan B channels. 1522 | media.forEach(function (uLine, index) { 1523 | 1524 | // If we don't have a channel for this uLine.type, then use this 1525 | // uLine as the channel basis. 1526 | if (typeof type2bl[uLine.type] === 'undefined') { 1527 | type2bl[uLine.type] = uLine; 1528 | } 1529 | 1530 | if (uLine.port === 0) { 1531 | if (index > 1 && uLine.type !== 'data') { //it's a secondary video stream - drop without further ado 1532 | return; 1533 | } 1534 | else { 1535 | delete uLine.mid; 1536 | uLine.mid = uLine.type; 1537 | //types.push(uLine.type); 1538 | session.media.push(uLine); 1539 | return; 1540 | } 1541 | } 1542 | 1543 | if (uLine.type === 'application') { 1544 | session.media.push(uLine); 1545 | types.push(uLine.mid); 1546 | return; 1547 | } 1548 | // Add sources to the channel and handle a=msid. 1549 | if (typeof uLine.sources === 'object') { 1550 | Object.keys(uLine.sources).forEach(function (ssrc) { 1551 | if (typeof type2bl[uLine.type].sources !== 'object') 1552 | type2bl[uLine.type].sources = {}; 1553 | 1554 | // Assign the sources to the channel. 1555 | type2bl[uLine.type].sources[ssrc] = 1556 | uLine.sources[ssrc]; 1557 | 1558 | if (typeof uLine.msid !== 'undefined') { 1559 | // In Plan B the msid is an SSRC attribute. Also, we don't 1560 | // care about the obsolete label and mslabel attributes. 1561 | // 1562 | // Note that it is not guaranteed that the uLine will 1563 | // have an msid. recvonly channels in particular don't have 1564 | // one. 1565 | type2bl[uLine.type].sources[ssrc].msid = 1566 | uLine.msid; 1567 | } 1568 | // NOTE ssrcs in ssrc groups will share msids, as 1569 | // draft-uberti-rtcweb-plan-00 mandates. 1570 | }); 1571 | } 1572 | 1573 | // Add ssrc groups to the channel. 1574 | if (typeof uLine.ssrcGroups !== 'undefined' && 1575 | Array.isArray(uLine.ssrcGroups)) { 1576 | 1577 | // Create the ssrcGroups array, if it's not defined. 1578 | if (typeof type2bl[uLine.type].ssrcGroups === 'undefined' || !Array.isArray( 1579 | type2bl[uLine.type].ssrcGroups)) { 1580 | type2bl[uLine.type].ssrcGroups = []; 1581 | } 1582 | 1583 | type2bl[uLine.type].ssrcGroups = 1584 | type2bl[uLine.type].ssrcGroups.concat( 1585 | uLine.ssrcGroups); 1586 | } 1587 | 1588 | if (type2bl[uLine.type] === uLine) { 1589 | // Copy ICE related stuff from the principal media line. 1590 | uLine.candidates = media[0].candidates; 1591 | uLine.iceUfrag = media[0].iceUfrag; 1592 | uLine.icePwd = media[0].icePwd; 1593 | uLine.fingerprint = media[0].fingerprint; 1594 | 1595 | // Plan B mids are in ['audio', 'video', 'data'] 1596 | uLine.mid = uLine.type; 1597 | 1598 | // Plan B doesn't support/need the bundle-only attribute. 1599 | delete uLine.bundleOnly; 1600 | 1601 | // In Plan B the msid is an SSRC attribute. 1602 | delete uLine.msid; 1603 | 1604 | // Used to build the group:BUNDLE value after this loop. 1605 | types.push(uLine.type); 1606 | 1607 | // Add the channel to the new media array. 1608 | session.media.push(uLine); 1609 | } 1610 | }); 1611 | 1612 | // We regenerate the BUNDLE group with the new mids. 1613 | session.groups.some(function (group) { 1614 | if (group.type === 'BUNDLE') { 1615 | group.mids = types.join(' '); 1616 | return true; 1617 | } 1618 | }); 1619 | 1620 | // msid semantic 1621 | session.msidSemantic = { 1622 | semantic: 'WMS', 1623 | token: '*' 1624 | }; 1625 | 1626 | var resStr = transform.write(session); 1627 | 1628 | return new window.RTCSessionDescription({ 1629 | type: desc.type, 1630 | sdp: resStr 1631 | }); 1632 | }; 1633 | },{"../transform":11}],10:[function(require,module,exports){ 1634 | /** 1635 | * Copyright(c) Starleaf Ltd. 2016. 1636 | */ 1637 | 1638 | 1639 | "use strict"; 1640 | 1641 | 1642 | var transform = require('../transform'); 1643 | var arrayEquals = require('../array-equals'); 1644 | 1645 | var copyObj = function (obj) { 1646 | return JSON.parse(JSON.stringify(obj)); 1647 | }; 1648 | 1649 | module.exports = function (desc, cache) { 1650 | 1651 | if (typeof desc !== 'object' || desc === null || 1652 | typeof desc.sdp !== 'string') { 1653 | console.warn('An empty description was passed as an argument.'); 1654 | return desc; 1655 | } 1656 | 1657 | var session = transform.parse(desc.sdp); 1658 | 1659 | // If the SDP contains no media, there's nothing to transform. 1660 | if (typeof session.media === 'undefined' || !Array.isArray(session.media) || session.media.length === 0) { 1661 | console.warn('The description has no media.'); 1662 | return desc; 1663 | } 1664 | 1665 | // Try some heuristics to "make sure" this is a Plan B SDP. Plan B SDP has 1666 | // a video, an audio and a data "channel" at most. 1667 | if (session.media.length > 3 || !session.media.every(function (m) { 1668 | return ['video', 'audio', 'data'].indexOf(m.mid) !== -1; 1669 | } 1670 | )) { 1671 | console.warn('This description does not look like Plan B.'); 1672 | return desc; 1673 | } 1674 | 1675 | // Make sure this Plan B SDP can be converted to a Unified Plan SDP. 1676 | var bmids = []; 1677 | session.media.forEach(function (m) { 1678 | if(m.port !== 0) { //ignore disabled streams, these can be removed from the bundle 1679 | bmids.push(m.mid); 1680 | } 1681 | } 1682 | ); 1683 | 1684 | var hasBundle = false; 1685 | if (typeof session.groups !== 'undefined' && 1686 | Array.isArray(session.groups)) { 1687 | hasBundle = session.groups.every(function (g) { 1688 | return g.type !== 'BUNDLE' || 1689 | arrayEquals.apply(g.mids.sort(), [bmids.sort()]); 1690 | } 1691 | ); 1692 | } 1693 | 1694 | if (!hasBundle) { 1695 | throw new Error("Cannot convert to Unified Plan because m-lines that" + 1696 | " are not bundled were found." 1697 | ); 1698 | } 1699 | 1700 | var localRef = null; 1701 | if (typeof cache.local !== 'undefined') 1702 | localRef = transform.parse(cache.local); 1703 | 1704 | var remoteRef = null; 1705 | if (typeof cache.remote !== 'undefined') 1706 | remoteRef = transform.parse(cache.remote); 1707 | 1708 | 1709 | var mLines = []; 1710 | 1711 | session.media.forEach(function (bLine, index, lines) { 1712 | 1713 | var uLine; 1714 | var ssrc; 1715 | 1716 | /*if ((typeof bLine.rtcpMux !== 'string' || 1717 | bLine.rtcpMux !== 'rtcp-mux') && 1718 | bLine.direction !== 'inactive') { 1719 | throw new Error("Cannot convert to Unified Plan because m-lines " + 1720 | "without the rtcp-mux attribute were found."); 1721 | }*/ 1722 | if(bLine.port === 0) { 1723 | // change the mid to the last used mid for this media type, for consistency 1724 | if(localRef !== null && localRef.media.length > index) { 1725 | bLine.mid = localRef.media[index].mid; 1726 | } 1727 | mLines.push(bLine); 1728 | return; 1729 | } 1730 | 1731 | // if we're offering to recv-only on chrome, we won't have any ssrcs at all 1732 | if (!bLine.sources) { 1733 | uLine = copyObj(bLine); 1734 | uLine.sources = {}; 1735 | uLine.mid = uLine.type + "-" + 1; 1736 | mLines.push(uLine); 1737 | return; 1738 | } 1739 | 1740 | var sources = bLine.sources || null; 1741 | 1742 | if (!sources) { 1743 | throw new Error("can't convert to unified plan - each m-line must have an ssrc"); 1744 | } 1745 | 1746 | var ssrcGroups = bLine.ssrcGroups || []; 1747 | bLine.rtcp.port = bLine.port; 1748 | 1749 | var sourcesKeys = Object.keys(sources); 1750 | if (sourcesKeys.length === 0) { 1751 | return; 1752 | } 1753 | else if (sourcesKeys.length == 1) { 1754 | ssrc = sourcesKeys[0]; 1755 | uLine = copyObj(bLine); 1756 | uLine.mid = uLine.type + "-" + ssrc; 1757 | mLines.push(uLine); 1758 | } 1759 | else { 1760 | //we might need to split this line 1761 | delete bLine.sources; 1762 | delete bLine.ssrcGroups; 1763 | 1764 | ssrcGroups.forEach(function (ssrcGroup) { 1765 | //update in use ssrcs so we don't accidentally override it 1766 | var primary = ssrcGroup.ssrcs[0]; 1767 | //use the first ssrc as the main ssrc for this m-line; 1768 | var copyLine = copyObj(bLine); 1769 | copyLine.sources = {}; 1770 | copyLine.sources[primary] = sources[primary]; 1771 | copyLine.mid = copyLine.type + "-" + primary; 1772 | mLines.push(copyLine); 1773 | }); 1774 | } 1775 | }); 1776 | 1777 | if (desc.type === 'offer') { 1778 | if (localRef) { 1779 | // you can never remove media streams from SDP. 1780 | while (mLines.length < localRef.media.length) { 1781 | var copyline = localRef.media[mLines.length]; 1782 | copyline.port = 0; 1783 | mLines.push(copyline); 1784 | } 1785 | } 1786 | } 1787 | else { 1788 | //if we're answering, if the browser accepted the transformed plan b we passed it, 1789 | //then we're implicitly accepting every stream. 1790 | //Check all the offers mlines - if we're missing one, we need to add it to our unified plan in recvOnly. 1791 | //in this case the far end will need to dynamically determine our real SSRC for the RTCP stream, 1792 | //as chrome won't tell us! 1793 | 1794 | if (remoteRef === undefined) { 1795 | throw Error("remote cache required to generate answer?"); 1796 | } 1797 | remoteRef.media.forEach(function(remoteline, index) { 1798 | if(index < mLines.length) { 1799 | // the line is already present in the plan-b, so will be handled correctly by the browser; 1800 | return; 1801 | } 1802 | if(remoteline.mid === undefined) { 1803 | console.warn("remote sdp has undefined mid attribute"); 1804 | return; 1805 | } 1806 | if(remoteline.port === 0) { 1807 | var disabledline = {}; 1808 | disabledline.port = 0; 1809 | disabledline.type = remoteline.type; 1810 | disabledline.protocol = remoteline.protocol; 1811 | disabledline.payloads = remoteline.payloads; 1812 | disabledline.mid = remoteline.mid; 1813 | if(!session.connection) { 1814 | if(mLines[0].connection) { 1815 | disabledline.connection = copyObj(mLines[0].connection); 1816 | } else { 1817 | throw Error("missing connection attribute from sdp"); 1818 | } 1819 | } else { 1820 | disabledline.connection = copyObj(session.connection); 1821 | } 1822 | disabledline.connection.ip = "0.0.0.0"; 1823 | 1824 | mLines.push(disabledline); 1825 | console.log("added disabled m line to the media"); 1826 | } 1827 | else { 1828 | for(var i = 0; i < mLines.length; i ++) { 1829 | var typeref = mLines[i]; 1830 | //check if we have any lines of the same type in the current answer to 1831 | // build this new line from. 1832 | if(typeref.type === remoteline.type) { 1833 | var linecopy = copyObj(typeref); 1834 | linecopy.mid = remoteline.mid; 1835 | linecopy.direction = "recvonly"; 1836 | mLines.push(linecopy); 1837 | break; 1838 | } 1839 | } 1840 | } 1841 | }); 1842 | } 1843 | 1844 | session.media = mLines; 1845 | 1846 | var mids = []; 1847 | session.media.forEach(function (mLine) { 1848 | mids.push(mLine.mid); 1849 | } 1850 | ); 1851 | 1852 | session.groups.some(function (group) { 1853 | if (group.type === 'BUNDLE') { 1854 | group.mids = mids.join(' '); 1855 | return true; 1856 | } 1857 | } 1858 | ); 1859 | 1860 | 1861 | // msid semantic 1862 | session.msidSemantic = { 1863 | semantic: 'WMS', 1864 | token: '*' 1865 | }; 1866 | 1867 | var resStr = transform.write(session); 1868 | return new window.RTCSessionDescription({ 1869 | type: desc.type, 1870 | sdp: resStr 1871 | } 1872 | ); 1873 | }; 1874 | },{"../array-equals":6,"../transform":11}],11:[function(require,module,exports){ 1875 | /* Copyright @ 2015 Atlassian Pty Ltd 1876 | * 1877 | * Licensed under the Apache License, Version 2.0 (the "License"); 1878 | * you may not use this file except in compliance with the License. 1879 | * You may obtain a copy of the License at 1880 | * 1881 | * http://www.apache.org/licenses/LICENSE-2.0 1882 | * 1883 | * Unless required by applicable law or agreed to in writing, software 1884 | * distributed under the License is distributed on an "AS IS" BASIS, 1885 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 1886 | * See the License for the specific language governing permissions and 1887 | * limitations under the License. 1888 | */ 1889 | 1890 | var transform = require('sdp-transform'); 1891 | 1892 | exports.write = function(session, opts) { 1893 | 1894 | if (typeof session !== 'undefined' && 1895 | typeof session.media !== 'undefined' && 1896 | Array.isArray(session.media)) { 1897 | 1898 | session.media.forEach(function (mLine) { 1899 | // expand sources to ssrcs 1900 | if (typeof mLine.sources !== 'undefined' && 1901 | Object.keys(mLine.sources).length !== 0) { 1902 | mLine.ssrcs = []; 1903 | Object.keys(mLine.sources).forEach(function (ssrc) { 1904 | var source = mLine.sources[ssrc]; 1905 | Object.keys(source).forEach(function (attribute) { 1906 | mLine.ssrcs.push({ 1907 | id: ssrc, 1908 | attribute: attribute, 1909 | value: source[attribute] 1910 | }); 1911 | }); 1912 | }); 1913 | delete mLine.sources; 1914 | } 1915 | 1916 | // join ssrcs in ssrc groups 1917 | if (typeof mLine.ssrcGroups !== 'undefined' && 1918 | Array.isArray(mLine.ssrcGroups)) { 1919 | mLine.ssrcGroups.forEach(function (ssrcGroup) { 1920 | if (typeof ssrcGroup.ssrcs !== 'undefined' && 1921 | Array.isArray(ssrcGroup.ssrcs)) { 1922 | ssrcGroup.ssrcs = ssrcGroup.ssrcs.join(' '); 1923 | } 1924 | }); 1925 | } 1926 | }); 1927 | } 1928 | 1929 | // join group mids 1930 | if (typeof session !== 'undefined' && 1931 | typeof session.groups !== 'undefined' && Array.isArray(session.groups)) { 1932 | 1933 | session.groups.forEach(function (g) { 1934 | if (typeof g.mids !== 'undefined' && Array.isArray(g.mids)) { 1935 | g.mids = g.mids.join(' '); 1936 | } 1937 | }); 1938 | } 1939 | 1940 | return transform.write(session, opts); 1941 | }; 1942 | 1943 | exports.parse = function(sdp) { 1944 | var session = transform.parse(sdp); 1945 | 1946 | if (typeof session !== 'undefined' && typeof session.media !== 'undefined' && 1947 | Array.isArray(session.media)) { 1948 | 1949 | session.media.forEach(function (mLine) { 1950 | // group sources attributes by ssrc 1951 | if (typeof mLine.ssrcs !== 'undefined' && Array.isArray(mLine.ssrcs)) { 1952 | mLine.sources = {}; 1953 | mLine.ssrcs.forEach(function (ssrc) { 1954 | if (!mLine.sources[ssrc.id]) 1955 | mLine.sources[ssrc.id] = {}; 1956 | mLine.sources[ssrc.id][ssrc.attribute] = ssrc.value; 1957 | }); 1958 | 1959 | delete mLine.ssrcs; 1960 | } 1961 | 1962 | // split ssrcs in ssrc groups 1963 | if (typeof mLine.ssrcGroups !== 'undefined' && 1964 | Array.isArray(mLine.ssrcGroups)) { 1965 | mLine.ssrcGroups.forEach(function (ssrcGroup) { 1966 | if (typeof ssrcGroup.ssrcs === 'string') { 1967 | ssrcGroup.ssrcs = ssrcGroup.ssrcs.split(' '); 1968 | } 1969 | }); 1970 | } 1971 | }); 1972 | } 1973 | // split group mids 1974 | if (typeof session !== 'undefined' && 1975 | typeof session.groups !== 'undefined' && Array.isArray(session.groups)) { 1976 | 1977 | session.groups.forEach(function (g) { 1978 | if (typeof g.mids === 'string') { 1979 | g.mids = g.mids.split(' '); 1980 | } 1981 | }); 1982 | } 1983 | 1984 | return session; 1985 | }; 1986 | 1987 | 1988 | },{"sdp-transform":2}]},{},[5])(5) 1989 | }); -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | /////////////////////////////////////////////////////////////////////////////// 2 | // Cyber Mega Phone 2K 3 | // Copyright (C) 2017 Digium, Inc. 4 | // 5 | // This program is free software, distributed under the terms of the 6 | // MIT License. See the LICENSE file at the top of the source tree. 7 | /////////////////////////////////////////////////////////////////////////////// 8 | 9 | function FullScreen(obj) { 10 | this._obj = obj; 11 | } 12 | 13 | FullScreen.prototype.can = function () { 14 | return !!(document.fullscreenEnabled || document.mozFullScreenEnabled || 15 | document.msFullscreenEnabled || document.webkitSupportsFullscreen || 16 | document.webkitFullscreenEnabled); 17 | }; 18 | 19 | FullScreen.prototype.is = function() { 20 | return !!(document.fullScreen || document.webkitIsFullScreen || 21 | document.mozFullScreen || document.msFullscreenElement || 22 | document.fullscreenElement); 23 | }; 24 | 25 | FullScreen.prototype.setData = function(state) { 26 | this._obj.setAttribute('data-fullscreen', !!state); 27 | }; 28 | 29 | FullScreen.prototype.exit = function() { 30 | if (!this.is()) { 31 | return; 32 | } 33 | 34 | if (document.exitFullscreen) { 35 | document.exitFullscreen(); 36 | } else if (document.mozCancelFullScreen) { 37 | document.mozCancelFullScreen(); 38 | } else if (document.webkitCancelFullScreen) { 39 | document.webkitCancelFullScreen(); 40 | } else if (document.msExitFullscreen) { 41 | document.msExitFullscreen(); 42 | } 43 | 44 | this.setData(false); 45 | }; 46 | 47 | --------------------------------------------------------------------------------