├── run.bat ├── stop.bat ├── ssh.bat ├── asterisk_config ├── extentions.conf ├── http.conf └── pjsip.conf ├── cyber_mega_phone_2k ├── .gitignore ├── LICENSE ├── utils.js ├── README.md ├── cyber_mega_phone.css ├── cyber_mega_phone.js ├── index.html └── lib │ └── sdp-interop-sl-1.4.0.js ├── nginx_config └── local ├── README.md ├── install.sh ├── Vagrantfile └── script └── ast_tls_cert /run.bat: -------------------------------------------------------------------------------- 1 | vagrant up 2 | -------------------------------------------------------------------------------- /stop.bat: -------------------------------------------------------------------------------- 1 | vagrant halt -------------------------------------------------------------------------------- /ssh.bat: -------------------------------------------------------------------------------- 1 | vagrant ssh 2 | pause 3 | -------------------------------------------------------------------------------- /asterisk_config/extentions.conf: -------------------------------------------------------------------------------- 1 | [default] 2 | exten => 200,1,Answer() 3 | same => n,Playback(demo-congrats) 4 | same => n,Hangup() 5 | -------------------------------------------------------------------------------- /asterisk_config/http.conf: -------------------------------------------------------------------------------- 1 | [general] 2 | enabled=yes 3 | bindaddr=0.0.0.0 4 | bindport=8088 5 | tlsenable=yes 6 | tlsbindaddr=0.0.0.0:8089 7 | tlscertfile=/etc/asterisk/keys/asterisk.pem 8 | tlsprivatekey=/etc/asterisk/keys/asterisk.key 9 | -------------------------------------------------------------------------------- /cyber_mega_phone_2k/.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 | -------------------------------------------------------------------------------- /nginx_config/local: -------------------------------------------------------------------------------- 1 | server_names_hash_bucket_size 64; 2 | 3 | server { 4 | listen 80; 5 | server_name $host; 6 | return 301 https://$host$request_uri; 7 | } 8 | server { 9 | listen 443 ssl; 10 | server_name $host; 11 | 12 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2; 13 | ssl_prefer_server_ciphers on; 14 | ssl_ciphers "EECDH+ECDSA+AESGCM:EECDH+aRSA+AESGCM:EECDH+ECDSA+SHA256:EECDH+aRSA+SHA256:EECDH+ECDSA+SHA384:EECDH+ECDSA+SHA256:EECDH+aRSA+SHA384:EDH+aRSA+AESGCM:EDH+aRSA+SHA256:EDH+aRSA:EECDH:!aNULL:!eNULL:!MEDIUM:!LOW:!3DES:!MD5:!EXP:!PSK:!SRP:!DSS:!RC4:!SEED"; 15 | 16 | add_header Strict-Transport-Security "max-age=31536000"; 17 | 18 | ssl_certificate /etc/asterisk/keys/asterisk.crt; 19 | ssl_certificate_key /etc/asterisk/keys/asterisk.key; 20 | 21 | 22 | root /usr/local/webrtc-sip-example; 23 | index index.html index.htm; 24 | } 25 | -------------------------------------------------------------------------------- /cyber_mega_phone_2k/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 | -------------------------------------------------------------------------------- /asterisk_config/pjsip.conf: -------------------------------------------------------------------------------- 1 | [transport-wss] 2 | type=transport 3 | protocol=wss 4 | bind=0.0.0.0 5 | 6 | [199] 7 | type=aor 8 | max_contacts=1 9 | remove_existing=yes 10 | 11 | [199] 12 | type=auth 13 | auth_type=userpass 14 | username=199 15 | password=199 ; This is a completely insecure password. Do NOT expose this 16 | ; system to the Internet without utilizing a better password. 17 | 18 | [199] 19 | type=endpoint 20 | aors=199 21 | auth=199 22 | use_avpf=yes 23 | media_encryption=dtls 24 | dtls_ca_file=/etc/asterisk/keys/ca.crt 25 | dtls_cert_file=/etc/asterisk/keys/asterisk.pem 26 | dtls_verify=fingerprint 27 | dtls_setup=actpass 28 | ice_support=yes 29 | media_use_received_transport=yes 30 | rtcp_mux=yes 31 | context=default 32 | disallow=all 33 | allow=opus 34 | allow=ulaw 35 | 36 | 37 | 38 | 39 | [outgoing] 40 | type = aor 41 | maximum_expiration = 60 42 | minimum_expiration = 60 43 | default_expiration = 180 44 | 45 | [outgoing] 46 | type = identify 47 | endpoint = outgoing 48 | 49 | [outgoing] 50 | type = endpoint 51 | context = default 52 | dtmf_mode = none 53 | disallow = all 54 | allow = all 55 | rtp_symmetric = yes 56 | force_rport = yes 57 | rewrite_contact = yes 58 | direct_media = no 59 | language = en 60 | aors = outgoing 61 | t38_udptl = yes 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Synopsis 2 | 3 | This project is made to provide a simple Audio an Video WebRTC to SIP gateway using the WebRTC possibility of new Astersik versions. 4 | It's based on https://github.com/asterisk/cyber_mega_phone_2k 5 | 6 | ## Motivation 7 | 8 | This gateway was made to easily connect a browser to any classic SIP endpoint like Softphone, PABX or MCU. 9 | 10 | ## Usefull link to build an Asterisk SIP-webrtc gateway 11 | https://github.com/asterisk/cyber_mega_phone_2k 12 | https://wiki.asterisk.org/wiki/display/AST/Installing+Asterisk+From+Source 13 | https://wiki.asterisk.org/wiki/display/AST/WebRTC+tutorial+using+SIPML5 14 | https://webrtc.ventures/2018/03/webrtc-sip-the-demo/ 15 | 16 | 17 | ## Installation on Ubuntu 18.04.03 LTS 18 | ```bash 19 | sh install.sh 20 | ``` 21 | 22 | 23 | 24 | 25 | ## Contributors 26 | Damien Fétis 27 | 28 | ## License 29 | 30 | Licensed under the Apache License, Version 2.0 (the "License"); 31 | you may not use this file except in compliance with the License. 32 | You may obtain a copy of the License at 33 | 34 | http://www.apache.org/licenses/LICENSE-2.0 35 | 36 | Unless required by applicable law or agreed to in writing, software 37 | distributed under the License is distributed on an "AS IS" BASIS, 38 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 39 | See the License for the specific language governing permissions and 40 | limitations under the License. 41 | -------------------------------------------------------------------------------- /cyber_mega_phone_2k/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 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Astersi webrtc installation script with self sign certificat 4 | 5 | #Asterisk installation 6 | 7 | INST_ROOT=/vagrant 8 | apt-get update 9 | 10 | cd /usr/local/src 11 | 12 | wget http://downloads.asterisk.org/pub/telephony/asterisk/asterisk-16-current.tar.gz 13 | 14 | 15 | tar -zxvf asterisk-16-current.tar.gz 16 | 17 | rm asterisk-16-current.tar.gz 18 | cd /usr/local/src/asterisk* 19 | 20 | #cd contrib/scripts 21 | 22 | #./install_prereq install 23 | 24 | #cd ../.. 25 | sudo DEBIAN_FRONTEND=noninteractive apt install -y wget gcc g++ ncurses-dev libxml2-dev libsqlite3-dev \ 26 | libsrtp-dev uuid-dev libssl-dev libjansson-dev build-essential libedit-dev 27 | 28 | ./configure 29 | make && make install 30 | 31 | #sleect res_crypto, res_http_websocket, and res_pjsip_transport_websocket and opus 32 | 33 | make samples && make config 34 | 35 | #Astersisk webrtc Installation 36 | mkdir /etc/asterisk/keys 37 | 38 | cd $INST_ROOT/scripts 39 | ./ast_tls_cert -C 192.168.33.10 -O "My Super Company" -d /etc/asterisk/keys 40 | 41 | 42 | #cat $INST_ROOT/asterisk_config/http.conf >> /etc/asterisk/http.conf 43 | 44 | #cat $INST_ROOT/asterisk_config/pjsip.conf >> /etc/asterisk/pjsip.conf 45 | 46 | 47 | #configure turn server 48 | 49 | #cat $INST_ROOT/asterisk_config/extentions.conf >> /etc/asterisk/extentions.conf 50 | 51 | ##asterisk -rx "restart"# 52 | 53 | 54 | #cyber_mega_phone_2k installation 55 | #cd /usr/local 56 | apt-get -y install git nginx 57 | #git clone https://github.com/asterisk/cyber_mega_phone_2k.git 58 | 59 | cd /usr/local 60 | git clone https://github.com/agilityfeat/webrtc-sip-example.git 61 | 62 | cd webrtc-sip-example 63 | 64 | sudo sh -c "echo 'noload => chan_sip.so' >> /etc/asterisk/modules.conf" 65 | 66 | cp -f asterisk-conf/* /etc/asterisk 67 | #push nginx configure 68 | cp $INST_ROOT/nginx_config/local /etc/nginx/sites-enabled 69 | service nginx restart 70 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | # All Vagrant configuration is done below. The "2" in Vagrant.configure 5 | # configures the configuration version (we support older styles for 6 | # backwards compatibility). Please don't change it unless you know what 7 | # you're doing. 8 | Vagrant.configure("2") do |config| 9 | # The most common configuration options are documented and commented below. 10 | # For a complete reference, please see the online documentation at 11 | # https://docs.vagrantup.com. 12 | 13 | # Every Vagrant development environment requires a box. You can search for 14 | # boxes at https://vagrantcloud.com/search. 15 | config.vm.box = "bento/ubuntu-18.04" 16 | 17 | # Disable automatic box update checking. If you disable this, then 18 | # boxes will only be checked for updates when the user runs 19 | # `vagrant box outdated`. This is not recommended. 20 | # config.vm.box_check_update = false 21 | 22 | # Create a forwarded port mapping which allows access to a specific port 23 | # within the machine from a port on the host machine. In the example below, 24 | # accessing "localhost:8080" will access port 80 on the guest machine. 25 | # NOTE: This will enable public access to the opened port 26 | # config.vm.network "forwarded_port", guest: 80, host: 8080 27 | 28 | # Create a forwarded port mapping which allows access to a specific port 29 | # within the machine from a port on the host machine and only allow access 30 | # via 127.0.0.1 to disable public access 31 | # config.vm.network "forwarded_port", guest: 80, host: 8080, host_ip: "127.0.0.1" 32 | 33 | # Create a private network, which allows host-only access to the machine 34 | # using a specific IP. 35 | config.vm.network "private_network", ip: "192.168.33.10" 36 | 37 | # Create a public network, which generally matched to bridged network. 38 | # Bridged networks make the machine appear as another physical device on 39 | # your network. 40 | config.vm.network "public_network" 41 | 42 | # Share an additional folder to the guest VM. The first argument is 43 | # the path on the host to the actual folder. The second argument is 44 | # the path on the guest to mount the folder. And the optional third 45 | # argument is a set of non-required options. 46 | # config.vm.synced_folder "../data", "/vagrant_data" 47 | 48 | # Provider-specific configuration so you can fine-tune various 49 | # backing providers for Vagrant. These expose provider-specific options. 50 | # Example for VirtualBox: 51 | # 52 | config.vm.provider "virtualbox" do |vb| 53 | # # Display the VirtualBox GUI when booting the machine 54 | # vb.gui = true 55 | # 56 | # # Customize the amount of memory on the VM: 57 | vb.memory = "2048" 58 | end 59 | # 60 | # View the documentation for the provider you are using for more 61 | # information on available options. 62 | 63 | # Enable provisioning with a shell script. Additional provisioners such as 64 | # Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the 65 | # documentation for more information about their specific syntax and use. 66 | # config.vm.provision "shell", inline: <<-SHELL 67 | # apt-get update 68 | # apt-get install -y apache2 69 | # SHELL 70 | 71 | # config.vm.provision :shell, path: "install.sh" 72 | end 73 | -------------------------------------------------------------------------------- /cyber_mega_phone_2k/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 | ### License 65 | 66 | Cyber Mega Phone 2K is released under the [MIT License](LICENSE) Copyright (C) 2017 Digium, Inc. 67 | -------------------------------------------------------------------------------- /cyber_mega_phone_2k/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 | -------------------------------------------------------------------------------- /script/ast_tls_cert: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | DEFAULT_ORG="Asterisk" 3 | DEFAULT_CA_CN="Asterisk Private CA" 4 | DEFAULT_CLIENT_CN="asterisk" 5 | DEFAULT_SERVER_CN=`hostname -f` 6 | 7 | # arguments 8 | # $1 "ca" if we are to generate a CA cert 9 | # $2 alternate config file name (for ca) 10 | # $3 alternate common name 11 | # $4 alternate org name 12 | create_config () { 13 | if [ "$1" = "ca" ] 14 | then 15 | castring=" 16 | [ext] 17 | basicConstraints=CA:TRUE" 18 | fi 19 | 20 | cat > ${2:-"${CONFIG_FILE}"} << EOF 21 | [req] 22 | distinguished_name = req_distinguished_name 23 | prompt = no 24 | 25 | [req_distinguished_name] 26 | CN=${3:-"${COMMON_NAME}"} 27 | O=${4:-"${ORG_NAME}"} 28 | ${castring} 29 | EOF 30 | } 31 | 32 | create_ca () { 33 | echo "Creating CA key ${CAKEY}" 34 | openssl genrsa -des3 -out ${CAKEY} 4096 > /dev/null 35 | if [ $? -ne 0 ]; 36 | then 37 | echo "Failed" 38 | exit 1 39 | fi 40 | echo "Creating CA certificate ${CACERT}" 41 | openssl req -nodes -new -config ${CACFG} -x509 -days 365 -key ${CAKEY} -out ${CACERT} > /dev/null 42 | if [ $? -ne 0 ]; 43 | then 44 | echo "Failed" 45 | exit 1 46 | fi 47 | } 48 | 49 | create_cert () { 50 | local base=${OUTPUT_DIR}/${OUTPUT_BASE} 51 | echo "Creating certificate ${base}.key" 52 | openssl genrsa -out ${base}.key 1024 > /dev/null 53 | if [ $? -ne 0 ]; 54 | then 55 | echo "Failed" 56 | exit 1 57 | fi 58 | echo "Creating signing request ${base}.csr" 59 | openssl req -batch -new -config ${CONFIG_FILE} -key ${base}.key -out ${base}.csr > /dev/null 60 | if [ $? -ne 0 ]; 61 | then 62 | echo "Failed" 63 | exit 1 64 | fi 65 | echo "Creating certificate ${base}.crt" 66 | openssl x509 -req -days 365 -in ${base}.csr -CA ${CACERT} -CAkey ${CAKEY} -set_serial 01 -out ${base}.crt > /dev/null 67 | if [ $? -ne 0 ]; 68 | then 69 | echo "Failed" 70 | exit 1 71 | fi 72 | echo "Combining key and crt into ${base}.pem" 73 | cat ${base}.key > ${base}.pem 74 | cat ${base}.crt >> ${base}.pem 75 | } 76 | 77 | usage () { 78 | cat << EOF 79 | This script is useful for quickly generating self-signed CA, server, and client 80 | certificates for use with Asterisk. It is still recommended to obtain 81 | certificates from a recognized Certificate Authority and to develop an 82 | understanding how SSL certificates work. Real security is hard work. 83 | 84 | OPTIONS: 85 | -h Show this message 86 | -m Type of cert "client" or "server". Defaults to server. 87 | -f Config filename (openssl config file format) 88 | -c CA cert filename (creates new CA cert/key as ca.crt/ca.key if not passed) 89 | -k CA key filename 90 | -C Common name (cert field) 91 | This should be the fully qualified domain name or IP address for 92 | the client or server. Make sure your certs have unique common 93 | names. 94 | -O Org name (cert field) 95 | An informational string (company name) 96 | -o Output filename base (defaults to asterisk) 97 | -d Output directory (defaults to the current directory) 98 | 99 | Example: 100 | 101 | To create a CA and a server (pbx.mycompany.com) cert with output in /tmp: 102 | ast_tls_cert -C pbx.mycompany.com -O "My Company" -d /tmp 103 | 104 | This will create a CA cert and key as well as asterisk.pem and the the two 105 | files that it is made from: asterisk.crt and asterisk.key. Copy asterisk.pem 106 | and ca.crt somewhere (like /etc/asterisk) and set tlscertfile=/etc/asterisk.pem 107 | and tlscafile=/etc/ca.crt. Since this is a self-signed key, many devices will 108 | require you to import the ca.crt file as a trusted cert. 109 | 110 | To create a client cert using the CA cert created by the example above: 111 | ast_tls_cert -m client -c /tmp/ca.crt -k /tmp/ca.key -C phone1.mycompany.com \\ 112 | -O "My Company" -d /tmp -o joe_user 113 | 114 | This will create client.crt/key/pem in /tmp. Use this if your device supports 115 | a client certificate. Make sure that you have the ca.crt file set up as 116 | a tlscafile in the necessary Asterisk configs. Make backups of all .key files 117 | in case you need them later. 118 | EOF 119 | } 120 | 121 | if ! type openssl >/dev/null 2>&1 122 | then 123 | echo "This script requires openssl to be in the path" 124 | exit 1 125 | fi 126 | 127 | OUTPUT_BASE=asterisk # Our default cert basename 128 | CERT_MODE=server 129 | ORG_NAME=${DEFAULT_ORG} 130 | 131 | while getopts "hf:c:k:o:d:m:C:O:" OPTION 132 | do 133 | case ${OPTION} in 134 | h) 135 | usage 136 | exit 1 137 | ;; 138 | f) 139 | CONFIG_FILE=${OPTARG} 140 | ;; 141 | c) 142 | CACERT=${OPTARG} 143 | ;; 144 | k) 145 | CAKEY=${OPTARG} 146 | ;; 147 | o) 148 | OUTPUT_BASE=${OPTARG} 149 | ;; 150 | d) 151 | OUTPUT_DIR=${OPTARG} 152 | ;; 153 | m) 154 | CERT_MODE=${OPTARG} 155 | ;; 156 | C) 157 | COMMON_NAME=${OPTARG} 158 | ;; 159 | O) 160 | ORG_NAME=${OPTARG} 161 | ;; 162 | ?) 163 | usage 164 | exit 165 | ;; 166 | esac 167 | done 168 | 169 | if [ -z "${OUTPUT_DIR}" ] 170 | then 171 | OUTPUT_DIR=. 172 | else 173 | mkdir -p "${OUTPUT_DIR}" 174 | fi 175 | 176 | umask 177 177 | 178 | case "${CERT_MODE}" in 179 | server) 180 | COMMON_NAME=${COMMON_NAME:-"${DEFAULT_SERVER_CN}"} 181 | ;; 182 | client) 183 | COMMON_NAME=${COMMON_NAME:-"${DEFAULT_CLIENT_CN}"} 184 | ;; 185 | *) 186 | echo 187 | echo "Unknown mode. Exiting." 188 | exit 1 189 | ;; 190 | esac 191 | 192 | if [ -z "${CONFIG_FILE}" ] 193 | then 194 | CONFIG_FILE="${OUTPUT_DIR}/tmp.cfg" 195 | echo 196 | echo "No config file specified, creating '${CONFIG_FILE}'" 197 | echo "You can use this config file to create additional certs without" 198 | echo "re-entering the information for the fields in the certificate" 199 | create_config 200 | fi 201 | 202 | if [ -z ${CACERT} ] 203 | then 204 | CAKEY=${OUTPUT_DIR}/ca.key 205 | CACERT=${OUTPUT_DIR}/ca.crt 206 | CACFG=${OUTPUT_DIR}/ca.cfg 207 | if [ ! -r "$CAKEY" ] && [ ! -r "$CACFG" ]; then 208 | create_config ca "${CACFG}" "${DEFAULT_CA_CN}" "${DEFAULT_CA_ORG}" 209 | fi 210 | if [ ! -r "$CACERT" ]; then 211 | create_ca 212 | fi 213 | else 214 | if [ -z ${CAKEY} ] 215 | then 216 | echo "-k must be specified if -c is" 217 | exit 1 218 | fi 219 | fi 220 | 221 | create_cert 222 | -------------------------------------------------------------------------------- /cyber_mega_phone_2k/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.webstore; 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 | this._locals = new Streams(); 28 | this._locals.bubble("streamAdded", this); 29 | this._locals.bubble("streamRemoved", this); 30 | 31 | this._remotes = new Streams(); 32 | this._remotes.bubble("streamAdded", this); 33 | this._remotes.bubble("streamRemoved", this); 34 | }; 35 | 36 | CyberMegaPhone.prototype = Object.create(EasyEvent.prototype); 37 | CyberMegaPhone.prototype.constructor = CyberMegaPhone; 38 | 39 | CyberMegaPhone.prototype.connect = function () { 40 | if (this._ua) { 41 | this._ua.start(); // Just reconnect 42 | return; 43 | } 44 | 45 | let that = this; 46 | 47 | let socket = new JsSIP.WebSocketInterface('wss://' + this.host + ':8089/ws'); 48 | let uri = 'sip:' + this.id + '@' + this.host; 49 | 50 | let config = { 51 | sockets: [ socket ], 52 | uri: uri, 53 | contact_uri: uri, 54 | username: this.name ? this.name : this.id, 55 | password: this.password, 56 | register: this.register, 57 | register_expires : 300 58 | }; 59 | 60 | this._ua = new JsSIP.UA(config); 61 | 62 | function bubble (obj, name) { 63 | obj.on(name, function (data) { 64 | that.raise(name, data); 65 | }); 66 | }; 67 | 68 | bubble(this._ua, 'connected'); 69 | bubble(this._ua, 'disconnected'); 70 | bubble(this._ua, 'registered'); 71 | bubble(this._ua, 'unregistered'); 72 | bubble(this._ua, 'registrationFailed'); 73 | 74 | this._ua.on('newRTCSession', function (data) { 75 | let rtc = data.session; 76 | rtc.interop = new SdpInterop.InteropChrome(); 77 | 78 | console.log('new session - ' + rtc.direction + ' - ' + rtc); 79 | 80 | rtc.on("confirmed", function () { 81 | // ACK was received 82 | let streams = rtc.connection.getLocalStreams(); 83 | for (let i = 0; i < streams.length; ++i) { 84 | console.log('confirmed: adding local stream ' + streams[i].id); 85 | streams[i].local = true; 86 | that._locals.add(streams[i]); 87 | } 88 | }); 89 | 90 | rtc.on("sdp", function (data) { 91 | if (isFirefox && data.originator === 'remote') { 92 | data.sdp = data.sdp.replace(/actpass/g, 'active'); 93 | } else if (isChrome) { 94 | let desc = new RTCSessionDescription({type:data.type, sdp:data.sdp}); 95 | if (data.originator === 'local') { 96 | converted = rtc.interop.toUnifiedPlan(desc); 97 | } else { 98 | converted = rtc.interop.toPlanB(desc); 99 | } 100 | 101 | data.sdp = converted.sdp; 102 | } 103 | }); 104 | 105 | bubble(rtc, 'muted'); 106 | bubble(rtc, 'unmuted'); 107 | bubble(rtc, 'failed'); 108 | bubble(rtc, 'ended'); 109 | 110 | rtc.connection.ontrack = function (event) { 111 | console.log('ontrack: ' + event.track.kind + ' - ' + event.track.id + 112 | ' stream ' + event.streams[0].id); 113 | if (event.track.kind == 'video') { 114 | event.track.enabled = false; 115 | } 116 | for (let i = 0; i < event.streams.length; ++i) { 117 | event.streams[i].local = false; 118 | that._remotes.add(event.streams[i]); 119 | } 120 | }; 121 | 122 | rtc.connection.onremovestream = function (event) { 123 | console.log('onremovestream: ' + event.stream.id); 124 | that._remotes.remove(event.stream); 125 | }; 126 | 127 | if (data.originator === "remote") { 128 | that.raise('incoming', data.request.ruri.toAor()); 129 | } 130 | }); 131 | 132 | this._ua.start(); 133 | }; 134 | 135 | CyberMegaPhone.prototype.disconnect = function () { 136 | this._locals.removeAll(); 137 | this._remotes.removeAll(); 138 | if (this._ua) { 139 | this._ua.stop(); 140 | } 141 | }; 142 | 143 | CyberMegaPhone.prototype.answer = function () { 144 | if (!this._ua) { 145 | return; 146 | } 147 | 148 | let options = { 149 | 'mediaConstraints': { 'audio': this.audio, 'video': this.video } 150 | }; 151 | 152 | this._rtc.answer(options); 153 | }; 154 | 155 | CyberMegaPhone.prototype.call = function (exten) { 156 | if (!this._ua || !exten) { 157 | return; 158 | } 159 | 160 | let options = { 161 | 'mediaConstraints': { 'audio': this.audio, 'video': this.video } 162 | }; 163 | 164 | if (exten.startsWith('sip:')) { 165 | this._rtc = this._ua.call(exten); 166 | } else { 167 | this._rtc = this._ua.call('sip:' + exten + '@' + this.host, options); 168 | } 169 | }; 170 | 171 | CyberMegaPhone.prototype.terminate = function () { 172 | this._locals.removeAll(); 173 | this._remotes.removeAll(); 174 | if (this._ua) { 175 | this._rtc.terminate(); 176 | } 177 | }; 178 | 179 | /////////////////////////////////////////////////////////////////////////////// 180 | 181 | function mute(stream, options) { 182 | 183 | function setTracks(tracks, val) { 184 | if (!tracks) { 185 | return; 186 | } 187 | 188 | for (let i = 0; i < tracks.length; ++i) { 189 | if (tracks[i].enabled == val) { 190 | tracks[i].enabled = !val; 191 | } 192 | } 193 | }; 194 | 195 | options = options || { audio: true, video: true }; 196 | 197 | if (typeof options.audio != 'undefined') { 198 | setTracks(stream.getAudioTracks(), options.audio); 199 | } 200 | 201 | if (typeof options.video != 'undefined') { 202 | setTracks(stream.getVideoTracks(), options.video); 203 | } 204 | } 205 | 206 | function unmute(stream, options) { 207 | let opts = options || { audio: false, video: false }; 208 | mute(stream, opts); 209 | } 210 | 211 | /////////////////////////////////////////////////////////////////////////////// 212 | 213 | function Streams () { 214 | EasyEvent.call(this); 215 | this._streams = []; 216 | }; 217 | 218 | Streams.prototype = Object.create(EasyEvent.prototype); 219 | Streams.prototype.constructor = Streams; 220 | 221 | Streams.prototype.add = function (stream) { 222 | if (this._streams.indexOf(stream) == -1) { 223 | this._streams.push(stream); 224 | console.log('Streams: added ' + stream.id); 225 | this.raise('streamAdded', stream); 226 | } 227 | }; 228 | 229 | Streams.prototype.remove = function (stream) { 230 | let index = typeof stream == 'number' ? stream : this._streams.indexOf(stream); 231 | 232 | if (index == -1) { 233 | return; 234 | } 235 | 236 | let removed = this._streams.splice(index, 1); 237 | for (let i = 0; i < removed.length; ++i) { 238 | console.log('Streams: removed ' + removed[i].id); 239 | this.raise('streamRemoved', removed[i]); 240 | } 241 | }; 242 | 243 | Streams.prototype.removeAll = function () { 244 | for (let i = this._streams.length - 1; i >= 0 ; --i) { 245 | this.remove(i); 246 | } 247 | }; 248 | 249 | /////////////////////////////////////////////////////////////////////////////// 250 | 251 | function EasyEvent () { 252 | this._events = {}; 253 | }; 254 | 255 | EasyEvent.prototype.handle = function (name, fun) { 256 | if (name in this._events) { 257 | this._events[name].push(fun); 258 | } else { 259 | this._events[name] = [fun]; 260 | } 261 | }; 262 | 263 | EasyEvent.prototype.raise = function (name) { 264 | if (name in this._events) { 265 | for (let i = 0; i < this._events[name].length; ++i) { 266 | this._events[name][i].apply(this, 267 | Array.prototype.slice.call(arguments, 1)); 268 | } 269 | } 270 | }; 271 | 272 | EasyEvent.prototype.bubble = function (name, obj) { 273 | this.handle(name, function (data) { 274 | obj.raise(name, data); 275 | }); 276 | }; 277 | 278 | EasyEvent.prototype.raiseForEach = function (name, array) { 279 | if (name in this._events) { 280 | for (let i = 0; i < array.length; ++i) { 281 | this.raise(name, array[i], i); 282 | } 283 | } 284 | }; 285 | -------------------------------------------------------------------------------- /cyber_mega_phone_2k/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 | -------------------------------------------------------------------------------- /cyber_mega_phone_2k/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 | }); --------------------------------------------------------------------------------