windhamdavid 8 years ago
parent
commit
d71b10c739

+ 93 - 4
app/css/style.min.css

@@ -6766,7 +6766,7 @@ body {
   background-position: center center;
   background-position: center center;
   background-repeat: no-repeat;
   background-repeat: no-repeat;
   background: -webkit-gradient(radial, center center, 0, center center, 460, from(#cecece), to(#222));
   background: -webkit-gradient(radial, center center, 0, center center, 460, from(#cecece), to(#222));
-  background: -webkit-radial-gradient(circle, #cecece, #222);
+  background: -webkit-radial-gradient(circle, #ffffff, #000000);
   background: -moz-radial-gradient(circle, #cecece, #222);
   background: -moz-radial-gradient(circle, #cecece, #222);
   background: -ms-radial-gradient(circle, #cecece, #222);
   background: -ms-radial-gradient(circle, #cecece, #222);
 }
 }
@@ -6813,6 +6813,95 @@ div#player{
 	margin: 0 auto;
 	margin: 0 auto;
   line-height:14px;
   line-height:14px;
 }
 }
+input[type=range] {
+  -webkit-appearance: none;
+  margin: 18px 0;
+  width: 100%;
+}
+input[type=range]:focus {
+  outline: none;
+}
+input[type=range]::-webkit-slider-runnable-track {
+  width: 100%;
+  height: 8.4px;
+  cursor: pointer;
+  animate: 0.2s;
+  box-shadow: 0px 3px 8px #aaa, inset 0px 2px 3px #fff;
+  background: #337ab7;
+  border-radius: 1.3px;
+  border: 0.2px solid #010101;
+}
+input[type=range]::-webkit-slider-thumb {
+
+  box-shadow: 0px 3px 8px #aaa, inset 0px 2px 3px #fff;
+  border: 1px solid #333;
+  height: 36px;
+  width: 16px;
+  border-radius: 3px;
+  background: #f7f7f7;
+  cursor: pointer;
+  -webkit-appearance: none;
+  margin-top: -14px;
+}
+input[type=range]:focus::-webkit-slider-runnable-track {
+  background: #367ebd;
+}
+input[type=range]::-moz-range-track {
+  width: 100%;
+  height: 8.4px;
+  cursor: pointer;
+  animate: 0.2s;
+  box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
+  background: #337ab7;
+  border-radius: 1.3px;
+  border: 0.2px solid #010101;
+}
+input[type=range]::-moz-range-thumb {
+  box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
+  border: 1px solid #000000;
+  height: 36px;
+  width: 16px;
+  border-radius: 3px;
+  background: #ffffff;
+  cursor: pointer;
+}
+input[type=range]::-ms-track {
+  width: 100%;
+  height: 8.4px;
+  cursor: pointer;
+  animate: 0.2s;
+  background: transparent;
+  border-color: transparent;
+  border-width: 16px 0;
+  color: transparent;
+}
+input[type=range]::-ms-fill-lower {
+  background: #2a6495;
+  border: 0.2px solid #010101;
+  border-radius: 2.6px;
+  box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
+}
+input[type=range]::-ms-fill-upper {
+  background: #337ab7;
+  border: 0.2px solid #010101;
+  border-radius: 2.6px;
+  box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
+}
+input[type=range]::-ms-thumb {
+  box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
+  border: 1px solid #000000;
+  height: 36px;
+  width: 16px;
+  border-radius: 3px;
+  background: #ffffff;
+  cursor: pointer;
+}
+input[type=range]:focus::-ms-fill-lower {
+  background: #3071a9;
+}
+input[type=range]:focus::-ms-fill-upper {
+  background: #367ebd;
+}
 output.volume {
 output.volume {
   position: absolute;
   position: absolute;
   background-image: -moz-linear-gradient(top, #444444, #999999);
   background-image: -moz-linear-gradient(top, #444444, #999999);
@@ -6954,10 +7043,10 @@ li#Lobby_tab {
 
 
 video {
 video {
   background:#000;
   background:#000;
-  height: 225px;
-  margin: 0 0 20px 0;
+  width: 100%;
+  margin: 0 0 5px 0;
   vertical-align: top;
   vertical-align: top;
-  width: calc(50% - 12px);
+  height: calc(50% - 12px);
 }
 }
 
 
 
 

+ 0 - 25
app/img/daveo-header.svg

@@ -4,31 +4,6 @@
 <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
 <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
 	 viewBox="0 0 1280 150" enable-background="new 0 0 1280 150" xml:space="preserve">
 	 viewBox="0 0 1280 150" enable-background="new 0 0 1280 150" xml:space="preserve">
 <g id="draw">
 <g id="draw">
-	<path fill="none" stroke="#000000" stroke-width="3" stroke-miterlimit="10" d="M201,83c12-1,23.8,3.3,34.7,8.5
-		c26.7,12.8,50.4,31.6,69.1,54.6c-24.8-22.8-56.8-35.8-88-48.4"/>
-	<path fill="none" stroke="#000000" stroke-width="3" stroke-miterlimit="10" d="M981.9,70.8c-15.2,13.2-25.9,31.6-29.8,51.4
-		c36.5-32.6,82.6-54.2,131-61.3"/>
-	<path fill="none" stroke="#000000" stroke-width="3" stroke-miterlimit="10" d="M98.9,74.1c8,14.8,16,29.7,24,44.5
-		c3.1-41.6,9.2-82.9,18-123.6"/>
-	<path fill="none" stroke="#000000" stroke-width="3" stroke-miterlimit="10" d="M297,34c0,11.2,0,22.3,0,33.5
-		c0,1.8,0.1,3.7,1.2,5.1c2.6,3.2,7.8,0.6,10.9-2.2c26-22.8,52.1-45.5,78.1-68.3"/>
-	<path fill="none" stroke="#000000" stroke-width="3" stroke-miterlimit="10" d="M1118.1,88.1c-0.5,8.8-0.9,17.9,2.3,26.1
-		c2.7,7,7.9,12.9,13.5,17.8c7.7,6.7,17,12.1,27.1,12.9c14.6,1.1,28.4-7.9,37-19.7c8.6-11.8,13-26,17.1-40"/>
-	<path fill="none" stroke="#000000" stroke-width="3" stroke-miterlimit="10" d="M1168.4,38.2c-27.1,20-56.4,37.1-87.2,51
-		c5.7-10.9,11.4-21.7,17.1-32.6c1.7-3.3,3.5-7.2,2.1-10.6c-1-2.4-3.3-3.9-5.5-5.1c-9.3-5-20.1-6.4-30.6-7.4
-		c-24.2-2.3-48.5-2.7-72.8-1.1c-9.4,0.6-19,1.6-27.3,6s-14.9,13.1-14.2,22.4c1,13.7,16.1,21.5,29.4,25.1c29.2,7.7,60.4,7.6,89.4-0.5
-		c-26.2,21.9-57.4,37.8-90.6,46"/>
-	<path fill="none" stroke="#000000" stroke-width="3" stroke-miterlimit="10" d="M78,163.9c-20.4,0.2-40.9-1-61.2-3.4
-		c-0.2-4.8,3.6-8.7,7.4-11.6C38.7,137.4,57,130,67.5,114.8c-9.4,1.6-18.8,3.3-28.2,4.9c14.1-8.8,28.2-17.5,42.3-26.3
-		c5-3.1,10.8-8,9.2-13.7c-21.1,0.6-38.4-3.6-58.9-5.1c-12.3-0.9-24.7-1.3-37.1-1.2c-5.8,0-28-2.5-32.1,1.4
-		c12.9-12,29.6-18.9,46-25.3c29.8-11.6,59.9-22.2,90.3-31.9c-30.5-0.5-61,1.4-91.2,5.9c18.5-14.7,37.8-28.3,58-40.5
-		C40.4-21.4,15.1-10.4-8.5,0.4c5.4-4,10.8-8,16.1-12c0.8,13.9-2,28-7.9,40.5"/>
-	<path fill="none" stroke="#000000" stroke-width="3" stroke-miterlimit="10" d="M1229,26.9c-44.5,0.5-89.1,0.3-133.6-0.5
-		c27-14.3,56.3-24.3,86.5-29.5c-2.1,19.8,0.1,40,6.4,58.8c30.2-13.9,48.1-46.1,76.7-63.1c6.8-4,14.8-7.3,22.5-5.1
-		c1.7,12.2,2.7,24.5,2.9,36.8c0.2,10.1-0.2,20.5-3.3,30.1c-3.2,9.6-9.5,18.6-18.7,22.9c-4.2-24.3-0.7-49.9,9.9-72.3
-		c-17.6,33.6-29.1,70.5-33.7,108.2c-0.2,1.6-0.4,3.3-1.4,4.5c-1.1,1.5-3,2.1-4.7,2.8c-13.9,5.9-20.1,25-12.4,37.9
-		c10-2.5,15.8-12.5,20.6-21.6c8.1-15.6,16.3-31.2,24.4-46.9c-8.4,6.9-15.6,15.2-21.2,24.5c12.7,1.7,25.8,0.1,37.8-4.6
-		c-0.1-6.5-5.4-11.6-10.4-15.7c-30.9-25.3-66.9-44.6-105.1-56.3"/>
 </g>
 </g>
 <g id="title">
 <g id="title">
 	<text transform="matrix(1 0 0 1 417.0188 95.1418)" font-family="'Phosphate-Inline'" font-size="72px">Daveo Radio</text>
 	<text transform="matrix(1 0 0 1 417.0188 95.1418)" font-family="'Phosphate-Inline'" font-size="72px">Daveo Radio</text>

+ 42 - 36
app/index.html

@@ -80,7 +80,7 @@
                         <li class="list-group-item list-group-item-success"><b>Dec 14th 8-10am</b>: Daveo Radio</li>
                         <li class="list-group-item list-group-item-success"><b>Dec 14th 8-10am</b>: Daveo Radio</li>
                         <li class="list-group-item list-group-item-success"><b>2-4pm</b>: Daveo Radio</li>
                         <li class="list-group-item list-group-item-success"><b>2-4pm</b>: Daveo Radio</li>
                         <li class="list-group-item"><b>Dec 22th 2-4pm</b>: Daveo Radio</li>
                         <li class="list-group-item"><b>Dec 22th 2-4pm</b>: Daveo Radio</li>
-                        <li class="list-group-item list-group-item-warning"><b>Dec 23th 2-4pm</b>: Somthing Else</li>
+                        <li class="list-group-item list-group-item-warning"><b>Dec 23th 2-4pm</b>: Something Else</li>
                         <li class="list-group-item list-group-item-danger"><b>Dec 24-28th</b>: Example</li>
                         <li class="list-group-item list-group-item-danger"><b>Dec 24-28th</b>: Example</li>
                         <li class="list-group-item"><b>Dec 31st 8-12pm</b>: Guest Person</li>
                         <li class="list-group-item"><b>Dec 31st 8-12pm</b>: Guest Person</li>
                         <li class="list-group-item list-group-item-info"><b>Jan 4th 10am</b>: Example Show</li>
                         <li class="list-group-item list-group-item-info"><b>Jan 4th 10am</b>: Example Show</li>
@@ -89,10 +89,10 @@
                   </div>
                   </div>
                </div>
                </div>
                   <ul class="nav nav-tabs">
                   <ul class="nav nav-tabs">
-                     <li class="active"><a href="#one" data-toggle="tab"><span class="glyphicon glyphicon-headphones" aria-hidden="true"></span>&nbsp;Recent</a></li>
-                     <li><a href="#two" data-toggle="tab"><span class="glyphicon glyphicon-fire" aria-hidden="true"></span>&nbsp;Tracks</a></li>
-                     <li><a href="#three" data-toggle="tab"><span class="glyphicon glyphicon-user" aria-hidden="true"></span>&nbsp;Artist</a></li>
-                     <li><a href="#four" data-toggle="tab"><span class="glyphicon glyphicon-time" aria-hidden="true"></span>&nbsp;Schedule</a></li>
+                     <li class="active"><a href="#one" data-toggle="tab" data-original-title="Recent" data-placement="top"><span class="glyphicon glyphicon-headphones" aria-hidden="true"></span></a></li>
+                     <li><a href="#two" data-toggle="tab" data-original-title="Tracks" data-placement="top"><span class="glyphicon glyphicon-fire" aria-hidden="true"></span></a></li>
+                     <li><a href="#three" data-toggle="tab" data-original-title="Artist" data-placement="top"><span class="glyphicon glyphicon-user" aria-hidden="true"></span></a></li>
+                     <li><a href="#four" data-toggle="tab" data-original-title="Calendar" data-placement="top"><span class="glyphicon glyphicon-time" aria-hidden="true"></span></a></li>
                   </ul>
                   </ul>
             </div>
             </div>
             <!-- END Playlist -->
             <!-- END Playlist -->
@@ -100,17 +100,16 @@
          </section>
          </section>
    	</div>
    	</div>
       
       
-      <!-- START Chat -->
+      <!-- START Connect -->
       <div class="col-sm-6">
       <div class="col-sm-6">
          <section id="request-line" class="bg-light-gray">
          <section id="request-line" class="bg-light-gray">
-   		  
-               
-            <!-- START Room Tabs -->
             <div class="panel panel-default">
             <div class="panel panel-default">
+               
+               <!-- START Room Tabs -->
                <div class="row-fluid">
                <div class="row-fluid">
                     <ul id="rooms_tabs" class="nav nav-tabs">
                     <ul id="rooms_tabs" class="nav nav-tabs">
                         <li class="dropdown">
                         <li class="dropdown">
-                            <a href="#" class="dropdown-toggle" data-toggle="dropdown"><span class="glyphicon glyphicon-th" aria-hidden="true"></span>&nbsp;<b class="caret"></b></a>
+                            <a href="#" class="dropdown-toggle" data-toggle="dropdown" data-original-title="Options" data-placement="bottom"><span class="glyphicon glyphicon-th" aria-hidden="true"></span>&nbsp;<b class="caret"></b></a>
                             <ul class="dropdown-menu">
                             <ul class="dropdown-menu">
                                 <li><a href="#modal_joinroom" data-toggle="modal"><span class="glyphicon glyphicon-plus-sign" aria-hidden="true"></span>&nbsp;Join room</a></li>
                                 <li><a href="#modal_joinroom" data-toggle="modal"><span class="glyphicon glyphicon-plus-sign" aria-hidden="true"></span>&nbsp;Join room</a></li>
                                 <li><a id="b_leave_room" href="#"><span class="glyphicon glyphicon-minus-sign" aria-hidden="true"></span>&nbsp;Leave room</a></li>
                                 <li><a id="b_leave_room" href="#"><span class="glyphicon glyphicon-minus-sign" aria-hidden="true"></span>&nbsp;Leave room</a></li>
@@ -118,9 +117,9 @@
                                 <li><a href="#modal_setnick" data-toggle="modal"><span class="glyphicon glyphicon-user" aria-hidden="true"></span>&nbsp;Set nickname</a></li>
                                 <li><a href="#modal_setnick" data-toggle="modal"><span class="glyphicon glyphicon-user" aria-hidden="true"></span>&nbsp;Set nickname</a></li>
                             </ul>
                             </ul>
                         </li>
                         </li>
-                        <li id="Lobby_tab" class="active"><a href="#Lobby" data-toggle="tab"><span class="glyphicon glyphicon-comment" aria-hidden="true"></span><span class="hidden-tab">Lobby</span></a></li>
-                        <li><a href="#call" data-toggle="tab"><span class="glyphicon glyphicon-phone-alt" aria-hidden="true"></span></a></li>
-                        <li><a href="#video" data-toggle="tab"><span class="glyphicon glyphicon-facetime-video" aria-hidden="true"></span></a></li>
+                        <li id="Lobby_tab" class="active"><a href="#Lobby" data-toggle="tab" data-original-title="Chat" data-placement="bottom"><span class="glyphicon glyphicon-comment" aria-hidden="true"></span><span class="hidden-tab">Lobby</span></a></li>
+                        <li><a href="#call" data-toggle="tab" data-original-title="Phone" data-placement="bottom"><span class="glyphicon glyphicon-phone-alt" aria-hidden="true"></span></a></li>
+                        <li><a href="#video" data-toggle="tab" data-original-title="Video" data-placement="bottom"><span class="glyphicon glyphicon-facetime-video" aria-hidden="true"></span></a></li>
                     </ul>
                     </ul>
                </div>
                </div>
                <!-- END Room Tabs -->
                <!-- END Room Tabs -->
@@ -141,24 +140,25 @@
                            </div>
                            </div>
                        </div>              
                        </div>              
                    
                    
-                      <!-- START message -->
-                      <div class="panel-footer">
-                         <div class="row-fluid">
-                             <form class="form">
-                                <div class="input-group">
-                                   <input id="message_text" class="form-control" type="text" placeholder="What say you?" >
-                                    <span class="input-group-btn">
-                                       <button id="b_send_message" class="btn btn-primary"><span class="glyphicon glyphicon-bullhorn" aria-hidden="true"></span> Post</button>
-                                    </span>
-                                 </div>
-                              </form>
-                              <!--<button id="set_nick" class="btn btn-success" data-toggle="modal" data-target="#modal_setnick"><span class="glyphicon glyphicon-user" aria-hidden="true"></span> Set Nick</button>-->
-                         </div>
+                   <!-- START message -->
+                   <div class="panel-footer">
+                      <div class="row-fluid">
+                          <form class="form">
+                             <div class="input-group">
+                                <input id="message_text" class="form-control" type="text" placeholder="What say you?" >
+                                 <span class="input-group-btn">
+                                    <button id="b_send_message" class="btn btn-primary"><span class="glyphicon glyphicon-bullhorn" aria-hidden="true"></span> Post</button>
+                                 </span>
+                              </div>
+                           </form>
+                           <!--<button id="set_nick" class="btn btn-success" data-toggle="modal" data-target="#modal_setnick"><span class="glyphicon glyphicon-user" aria-hidden="true"></span> Set Nick</button>-->
                       </div>
                       </div>
-                      <!-- END message -->
+                   </div>
+                   <!-- END message -->
                       
                       
                    </div>
                    </div>
                    
                    
+                   <!-- START Call -->
                    <div class="tab-pane" id="call">
                    <div class="tab-pane" id="call">
                       <div id="audio">
                       <div id="audio">
                          <div class="well">
                          <div class="well">
@@ -184,24 +184,27 @@
                           </div>
                           </div>
                        </div>
                        </div>
                    </div>
                    </div>
+                   <!-- END Call -->
+                   
+                   <!-- START Video -->
                    <div class="tab-pane" id="video">
                    <div class="tab-pane" id="video">
                       <div id="video">
                       <div id="video">
                          <div class="well">
                          <div class="well">
                             <video id="localVideo" autoplay muted></video>
                             <video id="localVideo" autoplay muted></video>
                             <video id="remoteVideo" autoplay></video>
                             <video id="remoteVideo" autoplay></video>
                          </div>
                          </div>
-                            <div class="panel-footer">
-                              <button id="startButton" class="btn btn-success">Start</button>
-                              <button id="callButton" class="btn btn-primary">Call</button>
-                              <button id="hangupButton" class="btn btn-danger">Hang Up</button>
-                            </div>
+                         <div class="panel-footer">
+                           <button id="startButton" class="btn btn-success">Start</button>
+                           <button id="callButton" class="btn btn-primary">Call</button>
+                           <button id="hangupButton" class="btn btn-danger">Hang Up</button>
                          </div>
                          </div>
                       <div>
                       <div>
                    </div>
                    </div>
+                   <!-- END Video -->
+                   
                </div>
                </div>
                <!-- END Rooms -->
                <!-- END Rooms -->
                
                
-
             </div>
             </div>
 
 
              
              
@@ -248,7 +251,7 @@
             
             
          </section>
          </section>
       </div>
       </div>
-      <!-- End Chat -->
+      <!-- End Connect -->
       
       
          
          
    </div>
    </div>
@@ -326,10 +329,11 @@
     <div class="modal-content">
     <div class="modal-content">
       <div class="modal-header">
       <div class="modal-header">
         <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
         <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
-        <h4 class="modal-title" id="about">About</h4>
+        <h4 class="modal-title" id="about">Help</h4>
       </div>
       </div>
       <div class="modal-body">
       <div class="modal-body">
-        <p><b>Daveo Radio</b> is just a little place I can share and talk about some music with friends. I started toying around with different streaming servers and I am using an Icecast streaming media server Iโ€™ve set up. This page is powered by Linode, Node.js, Express, Socket.io, and Icecast-kh.</p>   <p>I enjoy listening to music and I spent quite a bit of time doing so. Iโ€™ve gone entirely digital in recent years. I sold my collection of albums years ago and I donated my CD collection to a music student at Vanderbilt. I now have a collection of about 100,000 files that I keep on a networked hard drive. I do pay for ad-free streaming services, but I agree with artists who want a better revenue model from streaming. The analog/digital debate is null in my opinion. I use Polk in my living room and Klipsch in my office. In the car, I most often use playlists Iโ€™ve created and I rarely tune into a radio or satellite stations even though I have service. On occasion, at home I like to tune into terrestrial stations that stream to help find new music, but Iโ€™ve found almost all of the robot mixes from the big streaming providers unsatisfying.</p>
+        <p><b>Audio Streaming</b>: There are a number of ways you can listen to the audio stream. This page uses the HTML5 audio element to stream it to your computer or phone. You can also point your favorite media player to the stream at <a href="http://stream.davidawindham.com/stream">http://stream.davidawindham.com/</a>.</p>
+        <p><b>Video Stream</b>: Older web browers will not support all of the features here. In order to use the video and call capabilities of this page, you'll need to use a modern web browser </p>
       </div>
       </div>
       <div class="modal-footer">
       <div class="modal-footer">
         <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
         <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
@@ -365,6 +369,8 @@
 <script src="js/vendor.min.js"></script>
 <script src="js/vendor.min.js"></script>
 <script src="/socket.io/socket.io.js"></script>
 <script src="/socket.io/socket.io.js"></script>
 <script src="js/radio.min.js"></script>
 <script src="js/radio.min.js"></script>
+<script src="js/webrtc-adapter.js"></script>
+<script src="js/webrtc-video.js"></script>
 <script>
 <script>
   var _paq = _paq || [];
   var _paq = _paq || [];
   _paq.push(['trackPageView']);
   _paq.push(['trackPageView']);

File diff suppressed because it is too large
+ 0 - 0
app/js/radio.min.js


+ 1575 - 0
app/js/webrtc-adapter.js

@@ -0,0 +1,1575 @@
+/*
+ *  Copyright (c) 2015 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+
+/* More information about these options at jshint.com/docs/options */
+/* jshint browser: true, camelcase: true, curly: true, devel: true,
+   eqeqeq: true, forin: false, globalstrict: true, node: true,
+   quotmark: single, undef: true, unused: strict */
+/* global mozRTCIceCandidate, mozRTCPeerConnection, Promise,
+mozRTCSessionDescription, webkitRTCPeerConnection, MediaStreamTrack */
+/* exported trace,requestUserMedia */
+
+'use strict';
+
+var getUserMedia = null;
+var attachMediaStream = null;
+var reattachMediaStream = null;
+var webrtcDetectedBrowser = null;
+var webrtcDetectedVersion = null;
+var webrtcMinimumVersion = null;
+var webrtcUtils = {
+  log: function() {
+    // suppress console.log output when being included as a module.
+    if (typeof module !== 'undefined' ||
+        typeof require === 'function' && typeof define === 'function') {
+      return;
+    }
+    console.log.apply(console, arguments);
+  },
+  extractVersion: function(uastring, expr, pos) {
+    var match = uastring.match(expr);
+    return match && match.length >= pos && parseInt(match[pos]);
+  }
+};
+
+function trace(text) {
+  // This function is used for logging.
+  if (text[text.length - 1] === '\n') {
+    text = text.substring(0, text.length - 1);
+  }
+  if (window.performance) {
+    var now = (window.performance.now() / 1000).toFixed(3);
+    webrtcUtils.log(now + ': ' + text);
+  } else {
+    webrtcUtils.log(text);
+  }
+}
+
+if (typeof window === 'object') {
+  if (window.HTMLMediaElement &&
+    !('srcObject' in window.HTMLMediaElement.prototype)) {
+    // Shim the srcObject property, once, when HTMLMediaElement is found.
+    Object.defineProperty(window.HTMLMediaElement.prototype, 'srcObject', {
+      get: function() {
+        // If prefixed srcObject property exists, return it.
+        // Otherwise use the shimmed property, _srcObject
+        return 'mozSrcObject' in this ? this.mozSrcObject : this._srcObject;
+      },
+      set: function(stream) {
+        if ('mozSrcObject' in this) {
+          this.mozSrcObject = stream;
+        } else {
+          // Use _srcObject as a private property for this shim
+          this._srcObject = stream;
+          // TODO: revokeObjectUrl(this.src) when !stream to release resources?
+          this.src = URL.createObjectURL(stream);
+        }
+      }
+    });
+  }
+  // Proxy existing globals
+  getUserMedia = window.navigator && window.navigator.getUserMedia;
+}
+
+// Attach a media stream to an element.
+attachMediaStream = function(element, stream) {
+  element.srcObject = stream;
+};
+
+reattachMediaStream = function(to, from) {
+  to.srcObject = from.srcObject;
+};
+
+if (typeof window === 'undefined' || !window.navigator) {
+  webrtcUtils.log('This does not appear to be a browser');
+  webrtcDetectedBrowser = 'not a browser';
+} else if (navigator.mozGetUserMedia && window.mozRTCPeerConnection) {
+  webrtcUtils.log('This appears to be Firefox');
+
+  webrtcDetectedBrowser = 'firefox';
+
+  // the detected firefox version.
+  webrtcDetectedVersion = webrtcUtils.extractVersion(navigator.userAgent,
+      /Firefox\/([0-9]+)\./, 1);
+
+  // the minimum firefox version still supported by adapter.
+  webrtcMinimumVersion = 31;
+
+  // The RTCPeerConnection object.
+  window.RTCPeerConnection = function(pcConfig, pcConstraints) {
+    if (webrtcDetectedVersion < 38) {
+      // .urls is not supported in FF < 38.
+      // create RTCIceServers with a single url.
+      if (pcConfig && pcConfig.iceServers) {
+        var newIceServers = [];
+        for (var i = 0; i < pcConfig.iceServers.length; i++) {
+          var server = pcConfig.iceServers[i];
+          if (server.hasOwnProperty('urls')) {
+            for (var j = 0; j < server.urls.length; j++) {
+              var newServer = {
+                url: server.urls[j]
+              };
+              if (server.urls[j].indexOf('turn') === 0) {
+                newServer.username = server.username;
+                newServer.credential = server.credential;
+              }
+              newIceServers.push(newServer);
+            }
+          } else {
+            newIceServers.push(pcConfig.iceServers[i]);
+          }
+        }
+        pcConfig.iceServers = newIceServers;
+      }
+    }
+    return new mozRTCPeerConnection(pcConfig, pcConstraints); // jscs:ignore requireCapitalizedConstructors
+  };
+
+  // The RTCSessionDescription object.
+  if (!window.RTCSessionDescription) {
+    window.RTCSessionDescription = mozRTCSessionDescription;
+  }
+
+  // The RTCIceCandidate object.
+  if (!window.RTCIceCandidate) {
+    window.RTCIceCandidate = mozRTCIceCandidate;
+  }
+
+  // getUserMedia constraints shim.
+  getUserMedia = function(constraints, onSuccess, onError) {
+    var constraintsToFF37 = function(c) {
+      if (typeof c !== 'object' || c.require) {
+        return c;
+      }
+      var require = [];
+      Object.keys(c).forEach(function(key) {
+        if (key === 'require' || key === 'advanced' || key === 'mediaSource') {
+          return;
+        }
+        var r = c[key] = (typeof c[key] === 'object') ?
+            c[key] : {ideal: c[key]};
+        if (r.min !== undefined ||
+            r.max !== undefined || r.exact !== undefined) {
+          require.push(key);
+        }
+        if (r.exact !== undefined) {
+          if (typeof r.exact === 'number') {
+            r.min = r.max = r.exact;
+          } else {
+            c[key] = r.exact;
+          }
+          delete r.exact;
+        }
+        if (r.ideal !== undefined) {
+          c.advanced = c.advanced || [];
+          var oc = {};
+          if (typeof r.ideal === 'number') {
+            oc[key] = {min: r.ideal, max: r.ideal};
+          } else {
+            oc[key] = r.ideal;
+          }
+          c.advanced.push(oc);
+          delete r.ideal;
+          if (!Object.keys(r).length) {
+            delete c[key];
+          }
+        }
+      });
+      if (require.length) {
+        c.require = require;
+      }
+      return c;
+    };
+    if (webrtcDetectedVersion < 38) {
+      webrtcUtils.log('spec: ' + JSON.stringify(constraints));
+      if (constraints.audio) {
+        constraints.audio = constraintsToFF37(constraints.audio);
+      }
+      if (constraints.video) {
+        constraints.video = constraintsToFF37(constraints.video);
+      }
+      webrtcUtils.log('ff37: ' + JSON.stringify(constraints));
+    }
+    return navigator.mozGetUserMedia(constraints, onSuccess, onError);
+  };
+
+  navigator.getUserMedia = getUserMedia;
+
+  // Shim for mediaDevices on older versions.
+  if (!navigator.mediaDevices) {
+    navigator.mediaDevices = {getUserMedia: requestUserMedia,
+      addEventListener: function() { },
+      removeEventListener: function() { }
+    };
+  }
+  navigator.mediaDevices.enumerateDevices =
+      navigator.mediaDevices.enumerateDevices || function() {
+    return new Promise(function(resolve) {
+      var infos = [
+        {kind: 'audioinput', deviceId: 'default', label: '', groupId: ''},
+        {kind: 'videoinput', deviceId: 'default', label: '', groupId: ''}
+      ];
+      resolve(infos);
+    });
+  };
+
+  if (webrtcDetectedVersion < 41) {
+    // Work around http://bugzil.la/1169665
+    var orgEnumerateDevices =
+        navigator.mediaDevices.enumerateDevices.bind(navigator.mediaDevices);
+    navigator.mediaDevices.enumerateDevices = function() {
+      return orgEnumerateDevices().then(undefined, function(e) {
+        if (e.name === 'NotFoundError') {
+          return [];
+        }
+        throw e;
+      });
+    };
+  }
+} else if (navigator.webkitGetUserMedia && window.webkitRTCPeerConnection) {
+  webrtcUtils.log('This appears to be Chrome');
+
+  webrtcDetectedBrowser = 'chrome';
+
+  // the detected chrome version.
+  webrtcDetectedVersion = webrtcUtils.extractVersion(navigator.userAgent,
+      /Chrom(e|ium)\/([0-9]+)\./, 2);
+
+  // the minimum chrome version still supported by adapter.
+  webrtcMinimumVersion = 38;
+
+  // The RTCPeerConnection object.
+  window.RTCPeerConnection = function(pcConfig, pcConstraints) {
+    // Translate iceTransportPolicy to iceTransports,
+    // see https://code.google.com/p/webrtc/issues/detail?id=4869
+    if (pcConfig && pcConfig.iceTransportPolicy) {
+      pcConfig.iceTransports = pcConfig.iceTransportPolicy;
+    }
+
+    var pc = new webkitRTCPeerConnection(pcConfig, pcConstraints); // jscs:ignore requireCapitalizedConstructors
+    var origGetStats = pc.getStats.bind(pc);
+    pc.getStats = function(selector, successCallback, errorCallback) { // jshint ignore: line
+      var self = this;
+      var args = arguments;
+
+      // If selector is a function then we are in the old style stats so just
+      // pass back the original getStats format to avoid breaking old users.
+      if (arguments.length > 0 && typeof selector === 'function') {
+        return origGetStats(selector, successCallback);
+      }
+
+      var fixChromeStats = function(response) {
+        var standardReport = {};
+        var reports = response.result();
+        reports.forEach(function(report) {
+          var standardStats = {
+            id: report.id,
+            timestamp: report.timestamp,
+            type: report.type
+          };
+          report.names().forEach(function(name) {
+            standardStats[name] = report.stat(name);
+          });
+          standardReport[standardStats.id] = standardStats;
+        });
+
+        return standardReport;
+      };
+
+      if (arguments.length >= 2) {
+        var successCallbackWrapper = function(response) {
+          args[1](fixChromeStats(response));
+        };
+
+        return origGetStats.apply(this, [successCallbackWrapper, arguments[0]]);
+      }
+
+      // promise-support
+      return new Promise(function(resolve, reject) {
+        if (args.length === 1 && selector === null) {
+          origGetStats.apply(self, [
+              function(response) {
+                resolve.apply(null, [fixChromeStats(response)]);
+              }, reject]);
+        } else {
+          origGetStats.apply(self, [resolve, reject]);
+        }
+      });
+    };
+
+    return pc;
+  };
+
+  // add promise support
+  ['createOffer', 'createAnswer'].forEach(function(method) {
+    var nativeMethod = webkitRTCPeerConnection.prototype[method];
+    webkitRTCPeerConnection.prototype[method] = function() {
+      var self = this;
+      if (arguments.length < 1 || (arguments.length === 1 &&
+          typeof(arguments[0]) === 'object')) {
+        var opts = arguments.length === 1 ? arguments[0] : undefined;
+        return new Promise(function(resolve, reject) {
+          nativeMethod.apply(self, [resolve, reject, opts]);
+        });
+      } else {
+        return nativeMethod.apply(this, arguments);
+      }
+    };
+  });
+
+  ['setLocalDescription', 'setRemoteDescription',
+      'addIceCandidate'].forEach(function(method) {
+    var nativeMethod = webkitRTCPeerConnection.prototype[method];
+    webkitRTCPeerConnection.prototype[method] = function() {
+      var args = arguments;
+      var self = this;
+      return new Promise(function(resolve, reject) {
+        nativeMethod.apply(self, [args[0],
+            function() {
+              resolve();
+              if (args.length >= 2) {
+                args[1].apply(null, []);
+              }
+            },
+            function(err) {
+              reject(err);
+              if (args.length >= 3) {
+                args[2].apply(null, [err]);
+              }
+            }]
+          );
+      });
+    };
+  });
+
+  // getUserMedia constraints shim.
+  var constraintsToChrome = function(c) {
+    if (typeof c !== 'object' || c.mandatory || c.optional) {
+      return c;
+    }
+    var cc = {};
+    Object.keys(c).forEach(function(key) {
+      if (key === 'require' || key === 'advanced' || key === 'mediaSource') {
+        return;
+      }
+      var r = (typeof c[key] === 'object') ? c[key] : {ideal: c[key]};
+      if (r.exact !== undefined && typeof r.exact === 'number') {
+        r.min = r.max = r.exact;
+      }
+      var oldname = function(prefix, name) {
+        if (prefix) {
+          return prefix + name.charAt(0).toUpperCase() + name.slice(1);
+        }
+        return (name === 'deviceId') ? 'sourceId' : name;
+      };
+      if (r.ideal !== undefined) {
+        cc.optional = cc.optional || [];
+        var oc = {};
+        if (typeof r.ideal === 'number') {
+          oc[oldname('min', key)] = r.ideal;
+          cc.optional.push(oc);
+          oc = {};
+          oc[oldname('max', key)] = r.ideal;
+          cc.optional.push(oc);
+        } else {
+          oc[oldname('', key)] = r.ideal;
+          cc.optional.push(oc);
+        }
+      }
+      if (r.exact !== undefined && typeof r.exact !== 'number') {
+        cc.mandatory = cc.mandatory || {};
+        cc.mandatory[oldname('', key)] = r.exact;
+      } else {
+        ['min', 'max'].forEach(function(mix) {
+          if (r[mix] !== undefined) {
+            cc.mandatory = cc.mandatory || {};
+            cc.mandatory[oldname(mix, key)] = r[mix];
+          }
+        });
+      }
+    });
+    if (c.advanced) {
+      cc.optional = (cc.optional || []).concat(c.advanced);
+    }
+    return cc;
+  };
+
+  getUserMedia = function(constraints, onSuccess, onError) {
+    if (constraints.audio) {
+      constraints.audio = constraintsToChrome(constraints.audio);
+    }
+    if (constraints.video) {
+      constraints.video = constraintsToChrome(constraints.video);
+    }
+    webrtcUtils.log('chrome: ' + JSON.stringify(constraints));
+    return navigator.webkitGetUserMedia(constraints, onSuccess, onError);
+  };
+  navigator.getUserMedia = getUserMedia;
+
+  if (!navigator.mediaDevices) {
+    navigator.mediaDevices = {getUserMedia: requestUserMedia,
+                              enumerateDevices: function() {
+      return new Promise(function(resolve) {
+        var kinds = {audio: 'audioinput', video: 'videoinput'};
+        return MediaStreamTrack.getSources(function(devices) {
+          resolve(devices.map(function(device) {
+            return {label: device.label,
+                    kind: kinds[device.kind],
+                    deviceId: device.id,
+                    groupId: ''};
+          }));
+        });
+      });
+    }};
+  }
+
+  // A shim for getUserMedia method on the mediaDevices object.
+  // TODO(KaptenJansson) remove once implemented in Chrome stable.
+  if (!navigator.mediaDevices.getUserMedia) {
+    navigator.mediaDevices.getUserMedia = function(constraints) {
+      return requestUserMedia(constraints);
+    };
+  } else {
+    // Even though Chrome 45 has navigator.mediaDevices and a getUserMedia
+    // function which returns a Promise, it does not accept spec-style
+    // constraints.
+    var origGetUserMedia = navigator.mediaDevices.getUserMedia.
+        bind(navigator.mediaDevices);
+    navigator.mediaDevices.getUserMedia = function(c) {
+      webrtcUtils.log('spec:   ' + JSON.stringify(c)); // whitespace for alignment
+      c.audio = constraintsToChrome(c.audio);
+      c.video = constraintsToChrome(c.video);
+      webrtcUtils.log('chrome: ' + JSON.stringify(c));
+      return origGetUserMedia(c);
+    };
+  }
+
+  // Dummy devicechange event methods.
+  // TODO(KaptenJansson) remove once implemented in Chrome stable.
+  if (typeof navigator.mediaDevices.addEventListener === 'undefined') {
+    navigator.mediaDevices.addEventListener = function() {
+      webrtcUtils.log('Dummy mediaDevices.addEventListener called.');
+    };
+  }
+  if (typeof navigator.mediaDevices.removeEventListener === 'undefined') {
+    navigator.mediaDevices.removeEventListener = function() {
+      webrtcUtils.log('Dummy mediaDevices.removeEventListener called.');
+    };
+  }
+
+  // Attach a media stream to an element.
+  attachMediaStream = function(element, stream) {
+    if (webrtcDetectedVersion >= 43) {
+      element.srcObject = stream;
+    } else if (typeof element.src !== 'undefined') {
+      element.src = URL.createObjectURL(stream);
+    } else {
+      webrtcUtils.log('Error attaching stream to element.');
+    }
+  };
+  reattachMediaStream = function(to, from) {
+    if (webrtcDetectedVersion >= 43) {
+      to.srcObject = from.srcObject;
+    } else {
+      to.src = from.src;
+    }
+  };
+
+} else if (navigator.mediaDevices && navigator.userAgent.match(
+    /Edge\/(\d+).(\d+)$/)) {
+  webrtcUtils.log('This appears to be Edge');
+  webrtcDetectedBrowser = 'edge';
+
+  webrtcDetectedVersion = webrtcUtils.extractVersion(navigator.userAgent,
+      /Edge\/(\d+).(\d+)$/, 2);
+
+  // the minimum version still supported by adapter.
+  webrtcMinimumVersion = 12;
+
+  if (RTCIceGatherer) {
+    window.RTCIceCandidate = function(args) {
+      return args;
+    };
+    window.RTCSessionDescription = function(args) {
+      return args;
+    };
+
+    window.RTCPeerConnection = function(config) {
+      var self = this;
+
+      this.onicecandidate = null;
+      this.onaddstream = null;
+      this.onremovestream = null;
+      this.onsignalingstatechange = null;
+      this.oniceconnectionstatechange = null;
+      this.onnegotiationneeded = null;
+      this.ondatachannel = null;
+
+      this.localStreams = [];
+      this.remoteStreams = [];
+      this.getLocalStreams = function() { return self.localStreams; };
+      this.getRemoteStreams = function() { return self.remoteStreams; };
+
+      this.localDescription = new RTCSessionDescription({
+        type: '',
+        sdp: ''
+      });
+      this.remoteDescription = new RTCSessionDescription({
+        type: '',
+        sdp: ''
+      });
+      this.signalingState = 'stable';
+      this.iceConnectionState = 'new';
+
+      this.iceOptions = {
+        gatherPolicy: 'all',
+        iceServers: []
+      };
+      if (config && config.iceTransportPolicy) {
+        switch (config.iceTransportPolicy) {
+        case 'all':
+        case 'relay':
+          this.iceOptions.gatherPolicy = config.iceTransportPolicy;
+          break;
+        case 'none':
+          // FIXME: remove once implementation and spec have added this.
+          throw new TypeError('iceTransportPolicy "none" not supported');
+        }
+      }
+      if (config && config.iceServers) {
+        this.iceOptions.iceServers = config.iceServers;
+      }
+
+      // per-track iceGathers etc
+      this.mLines = [];
+
+      this._iceCandidates = [];
+
+      this._peerConnectionId = 'PC_' + Math.floor(Math.random() * 65536);
+
+      // FIXME: Should be generated according to spec (guid?)
+      // and be the same for all PCs from the same JS
+      this._cname = Math.random().toString(36).substr(2, 10);
+    };
+
+    window.RTCPeerConnection.prototype.addStream = function(stream) {
+      // clone just in case we're working in a local demo
+      // FIXME: seems to be fixed
+      this.localStreams.push(stream.clone());
+
+      // FIXME: maybe trigger negotiationneeded?
+    };
+
+    window.RTCPeerConnection.prototype.removeStream = function(stream) {
+      var idx = this.localStreams.indexOf(stream);
+      if (idx > -1) {
+        this.localStreams.splice(idx, 1);
+      }
+      // FIXME: maybe trigger negotiationneeded?
+    };
+
+    // SDP helper from sdp-jingle-json with modifications.
+    window.RTCPeerConnection.prototype._toCandidateJSON = function(line) {
+      var parts;
+      if (line.indexOf('a=candidate:') === 0) {
+        parts = line.substring(12).split(' ');
+      } else { // no a=candidate
+        parts = line.substring(10).split(' ');
+      }
+
+      var candidate = {
+        foundation: parts[0],
+        component: parts[1],
+        protocol: parts[2].toLowerCase(),
+        priority: parseInt(parts[3], 10),
+        ip: parts[4],
+        port: parseInt(parts[5], 10),
+        // skip parts[6] == 'typ'
+        type: parts[7]
+        //generation: '0'
+      };
+
+      for (var i = 8; i < parts.length; i += 2) {
+        if (parts[i] === 'raddr') {
+          candidate.relatedAddress = parts[i + 1]; // was: relAddr
+        } else if (parts[i] === 'rport') {
+          candidate.relatedPort = parseInt(parts[i + 1], 10); // was: relPort
+        } else if (parts[i] === 'generation') {
+          candidate.generation = parts[i + 1];
+        } else if (parts[i] === 'tcptype') {
+          candidate.tcpType = parts[i + 1];
+        }
+      }
+      return candidate;
+    };
+
+    // SDP helper from sdp-jingle-json with modifications.
+    window.RTCPeerConnection.prototype._toCandidateSDP = function(candidate) {
+      var sdp = [];
+      sdp.push(candidate.foundation);
+      sdp.push(candidate.component);
+      sdp.push(candidate.protocol.toUpperCase());
+      sdp.push(candidate.priority);
+      sdp.push(candidate.ip);
+      sdp.push(candidate.port);
+
+      var type = candidate.type;
+      sdp.push('typ');
+      sdp.push(type);
+      if (type === 'srflx' || type === 'prflx' || type === 'relay') {
+        if (candidate.relatedAddress && candidate.relatedPort) {
+          sdp.push('raddr');
+          sdp.push(candidate.relatedAddress); // was: relAddr
+          sdp.push('rport');
+          sdp.push(candidate.relatedPort); // was: relPort
+        }
+      }
+      if (candidate.tcpType && candidate.protocol.toUpperCase() === 'TCP') {
+        sdp.push('tcptype');
+        sdp.push(candidate.tcpType);
+      }
+      return 'a=candidate:' + sdp.join(' ');
+    };
+
+    // SDP helper from sdp-jingle-json with modifications.
+    window.RTCPeerConnection.prototype._parseRtpMap = function(line) {
+      var parts = line.substr(9).split(' ');
+      var parsed = {
+        payloadType: parseInt(parts.shift(), 10) // was: id
+      };
+
+      parts = parts[0].split('/');
+
+      parsed.name = parts[0];
+      parsed.clockRate = parseInt(parts[1], 10); // was: clockrate
+      parsed.numChannels = parts.length === 3 ? parseInt(parts[2], 10) : 1; // was: channels
+      return parsed;
+    };
+
+    // Parses SDP to determine capabilities.
+    window.RTCPeerConnection.prototype._getRemoteCapabilities =
+        function(section) {
+      var remoteCapabilities = {
+        codecs: [],
+        headerExtensions: [],
+        fecMechanisms: []
+      };
+      var i;
+      var lines = section.split('\r\n');
+      var mline = lines[0].substr(2).split(' ');
+      var rtpmapFilter = function(line) {
+        return line.indexOf('a=rtpmap:' + mline[i]) === 0;
+      };
+      var fmtpFilter = function(line) {
+        return line.indexOf('a=fmtp:' + mline[i]) === 0;
+      };
+      var parseFmtp = function(line) {
+        var parsed = {};
+        var kv;
+        var parts = line.substr(('a=fmtp:' + mline[i]).length + 1).split(';');
+        for (var j = 0; j < parts.length; j++) {
+          kv = parts[j].split('=');
+          parsed[kv[0].trim()] = kv[1];
+        }
+        console.log('fmtp', mline[i], parsed);
+        return parsed;
+      };
+      var rtcpFbFilter = function(line) {
+        return line.indexOf('a=rtcp-fb:' + mline[i]) === 0;
+      };
+      var parseRtcpFb = function(line) {
+        var parts = line.substr(('a=rtcp-fb:' + mline[i]).length + 1)
+            .split(' ');
+        return {
+          type: parts.shift(),
+          parameter: parts.join(' ')
+        };
+      };
+      for (i = 3; i < mline.length; i++) { // find all codecs from mline[3..]
+        var line = lines.filter(rtpmapFilter)[0];
+        if (line) {
+          var codec = this._parseRtpMap(line);
+
+          var fmtp = lines.filter(fmtpFilter);
+          codec.parameters = fmtp.length ? parseFmtp(fmtp[0]) : {};
+          codec.rtcpFeedback = lines.filter(rtcpFbFilter).map(parseRtcpFb);
+
+          remoteCapabilities.codecs.push(codec);
+        }
+      }
+      return remoteCapabilities;
+    };
+
+    // Serializes capabilities to SDP.
+    window.RTCPeerConnection.prototype._capabilitiesToSDP = function(caps) {
+      var sdp = '';
+      caps.codecs.forEach(function(codec) {
+        var pt = codec.payloadType;
+        if (codec.preferredPayloadType !== undefined) {
+          pt = codec.preferredPayloadType;
+        }
+        sdp += 'a=rtpmap:' + pt +
+            ' ' + codec.name +
+            '/' + codec.clockRate +
+            (codec.numChannels !== 1 ? '/' + codec.numChannels : '') +
+            '\r\n';
+        if (codec.parameters && codec.parameters.length) {
+          sdp += 'a=ftmp:' + pt + ' ';
+          Object.keys(codec.parameters).forEach(function(param) {
+            sdp += param + '=' + codec.parameters[param];
+          });
+          sdp += '\r\n';
+        }
+        if (codec.rtcpFeedback) {
+          // FIXME: special handling for trr-int?
+          codec.rtcpFeedback.forEach(function(fb) {
+            sdp += 'a=rtcp-fb:' + pt + ' ' + fb.type + ' ' +
+                fb.parameter + '\r\n';
+          });
+        }
+      });
+      return sdp;
+    };
+
+    // Calculates the intersection of local and remote capabilities.
+    window.RTCPeerConnection.prototype._getCommonCapabilities =
+        function(localCapabilities, remoteCapabilities) {
+      var commonCapabilities = {
+        codecs: [],
+        headerExtensions: [],
+        fecMechanisms: []
+      };
+      localCapabilities.codecs.forEach(function(lCodec) {
+        for (var i = 0; i < remoteCapabilities.codecs.length; i++) {
+          var rCodec = remoteCapabilities.codecs[i];
+          if (lCodec.name === rCodec.name &&
+              lCodec.clockRate === rCodec.clockRate &&
+              lCodec.numChannels === rCodec.numChannels) {
+            // push rCodec so we reply with offerer payload type
+            commonCapabilities.codecs.push(rCodec);
+
+            // FIXME: also need to calculate intersection between
+            // .rtcpFeedback and .parameters
+            break;
+          }
+        }
+      });
+
+      localCapabilities.headerExtensions.forEach(function(lHeaderExtension) {
+        for (var i = 0; i < remoteCapabilities.headerExtensions.length; i++) {
+          var rHeaderExtension = remoteCapabilities.headerExtensions[i];
+          if (lHeaderExtension.uri === rHeaderExtension.uri) {
+            commonCapabilities.headerExtensions.push(rHeaderExtension);
+            break;
+          }
+        }
+      });
+
+      // FIXME: fecMechanisms
+      return commonCapabilities;
+    };
+
+    // Parses DTLS parameters from SDP section or sessionpart.
+    window.RTCPeerConnection.prototype._getDtlsParameters =
+        function(section, session) {
+      var lines = section.split('\r\n');
+      lines = lines.concat(session.split('\r\n')); // Search in session part, too.
+      var fpLine = lines.filter(function(line) {
+        return line.indexOf('a=fingerprint:') === 0;
+      });
+      fpLine = fpLine[0].substr(14);
+      var dtlsParameters = {
+        role: 'auto',
+        fingerprints: [{
+          algorithm: fpLine.split(' ')[0],
+          value: fpLine.split(' ')[1]
+        }]
+      };
+      return dtlsParameters;
+    };
+
+    // Serializes DTLS parameters to SDP.
+    window.RTCPeerConnection.prototype._dtlsParametersToSDP =
+        function(params, setupType) {
+      var sdp = 'a=setup:' + setupType + '\r\n';
+      params.fingerprints.forEach(function(fp) {
+        sdp += 'a=fingerprint:' + fp.algorithm + ' ' + fp.value + '\r\n';
+      });
+      return sdp;
+    };
+
+    // Parses ICE information from SDP section or sessionpart.
+    window.RTCPeerConnection.prototype._getIceParameters =
+        function(section, session) {
+      var lines = section.split('\r\n');
+      lines = lines.concat(session.split('\r\n')); // Search in session part, too.
+      var iceParameters = {
+        usernameFragment: lines.filter(function(line) {
+          return line.indexOf('a=ice-ufrag:') === 0;
+        })[0].substr(12),
+        password: lines.filter(function(line) {
+          return line.indexOf('a=ice-pwd:') === 0;
+        })[0].substr(10),
+      };
+      return iceParameters;
+    };
+
+    // Serializes ICE parameters to SDP.
+    window.RTCPeerConnection.prototype._iceParametersToSDP = function(params) {
+      return 'a=ice-ufrag:' + params.usernameFragment + '\r\n' +
+          'a=ice-pwd:' + params.password + '\r\n';
+    };
+
+    window.RTCPeerConnection.prototype._getEncodingParameters = function(ssrc) {
+      return {
+        ssrc: ssrc,
+        codecPayloadType: 0,
+        fec: 0,
+        rtx: 0,
+        priority: 1.0,
+        maxBitrate: 2000000.0,
+        minQuality: 0,
+        framerateBias: 0.5,
+        resolutionScale: 1.0,
+        framerateScale: 1.0,
+        active: true,
+        dependencyEncodingId: undefined,
+        encodingId: undefined
+      };
+    };
+
+    // Create ICE gatherer, ICE transport and DTLS transport.
+    window.RTCPeerConnection.prototype._createIceAndDtlsTransports =
+        function(mid, sdpMLineIndex) {
+      var self = this;
+      var iceGatherer = new RTCIceGatherer(self.iceOptions);
+      var iceTransport = new RTCIceTransport(iceGatherer);
+      iceGatherer.onlocalcandidate = function(evt) {
+        var event = {};
+        event.candidate = {sdpMid: mid, sdpMLineIndex: sdpMLineIndex};
+
+        var cand = evt.candidate;
+        var isEndOfCandidates = !(cand && Object.keys(cand).length > 0);
+        if (isEndOfCandidates) {
+          event.candidate.candidate =
+              'candidate:1 1 udp 1 0.0.0.0 9 typ endOfCandidates';
+        } else {
+          // RTCIceCandidate doesn't have a component, needs to be added
+          cand.component = iceTransport.component === 'RTCP' ? 2 : 1;
+          event.candidate.candidate = self._toCandidateSDP(cand);
+        }
+        if (self.onicecandidate !== null) {
+          if (self.localDescription && self.localDescription.type === '') {
+            self._iceCandidates.push(event);
+          } else {
+            self.onicecandidate(event);
+          }
+        }
+      };
+      iceTransport.onicestatechange = function() {
+        /*
+        console.log(self._peerConnectionId,
+            'ICE state change', iceTransport.state);
+        */
+        self._updateIceConnectionState(iceTransport.state);
+      };
+
+      var dtlsTransport = new RTCDtlsTransport(iceTransport);
+      dtlsTransport.ondtlsstatechange = function() {
+        /*
+        console.log(self._peerConnectionId, sdpMLineIndex,
+            'dtls state change', dtlsTransport.state);
+        */
+      };
+      dtlsTransport.onerror = function(error) {
+        console.error('dtls error', error);
+      };
+      return {
+        iceGatherer: iceGatherer,
+        iceTransport: iceTransport,
+        dtlsTransport: dtlsTransport
+      };
+    };
+
+    window.RTCPeerConnection.prototype.setLocalDescription =
+        function(description) {
+      var self = this;
+      if (description.type === 'offer') {
+        if (!description.ortc) {
+          // FIXME: throw?
+        } else {
+          this.mLines = description.ortc;
+        }
+      } else if (description.type === 'answer') {
+        var sections = self.remoteDescription.sdp.split('\r\nm=');
+        var sessionpart = sections.shift();
+        sections.forEach(function(section, sdpMLineIndex) {
+          section = 'm=' + section;
+
+          var iceGatherer = self.mLines[sdpMLineIndex].iceGatherer;
+          var iceTransport = self.mLines[sdpMLineIndex].iceTransport;
+          var dtlsTransport = self.mLines[sdpMLineIndex].dtlsTransport;
+          var rtpSender = self.mLines[sdpMLineIndex].rtpSender;
+          var localCapabilities =
+              self.mLines[sdpMLineIndex].localCapabilities;
+          var remoteCapabilities =
+              self.mLines[sdpMLineIndex].remoteCapabilities;
+          var sendSSRC = self.mLines[sdpMLineIndex].sendSSRC;
+          var recvSSRC = self.mLines[sdpMLineIndex].recvSSRC;
+
+          var remoteIceParameters = self._getIceParameters(section,
+              sessionpart);
+          iceTransport.start(iceGatherer, remoteIceParameters, 'controlled');
+
+          var remoteDtlsParameters = self._getDtlsParameters(section,
+              sessionpart);
+          dtlsTransport.start(remoteDtlsParameters);
+
+          if (rtpSender) {
+            // calculate intersection of capabilities
+            var params = self._getCommonCapabilities(localCapabilities,
+                remoteCapabilities);
+            params.muxId = sendSSRC;
+            params.encodings = [self._getEncodingParameters(sendSSRC)];
+            params.rtcp = {
+              cname: self._cname,
+              reducedSize: false,
+              ssrc: recvSSRC,
+              mux: true
+            };
+            rtpSender.send(params);
+          }
+        });
+      }
+
+      this.localDescription = description;
+      switch (description.type) {
+      case 'offer':
+        this._updateSignalingState('have-local-offer');
+        break;
+      case 'answer':
+        this._updateSignalingState('stable');
+        break;
+      }
+
+      // FIXME: need to _reliably_ execute after args[1] or promise
+      window.setTimeout(function() {
+        // FIXME: need to apply ice candidates in a way which is async but in-order
+        self._iceCandidates.forEach(function(event) {
+          if (self.onicecandidate !== null) {
+            self.onicecandidate(event);
+          }
+        });
+        self._iceCandidates = [];
+      }, 50);
+      if (arguments.length > 1 && typeof arguments[1] === 'function') {
+        window.setTimeout(arguments[1], 0);
+      }
+      return new Promise(function(resolve) {
+        resolve();
+      });
+    };
+
+    window.RTCPeerConnection.prototype.setRemoteDescription =
+        function(description) {
+      // FIXME: for type=offer this creates state. which should not
+      //  happen before SLD with type=answer but... we need the stream
+      //  here for onaddstream.
+      var self = this;
+      var sections = description.sdp.split('\r\nm=');
+      var sessionpart = sections.shift();
+      var stream = new MediaStream();
+      sections.forEach(function(section, sdpMLineIndex) {
+        section = 'm=' + section;
+        var lines = section.split('\r\n');
+        var mline = lines[0].substr(2).split(' ');
+        var kind = mline[0];
+        var line;
+
+        var iceGatherer;
+        var iceTransport;
+        var dtlsTransport;
+        var rtpSender;
+        var rtpReceiver;
+        var sendSSRC;
+        var recvSSRC;
+
+        var mid = lines.filter(function(line) {
+          return line.indexOf('a=mid:') === 0;
+        })[0].substr(6);
+
+        var cname;
+
+        var remoteCapabilities;
+        var params;
+
+        if (description.type === 'offer') {
+          var transports = self._createIceAndDtlsTransports(mid, sdpMLineIndex);
+
+          var localCapabilities = RTCRtpReceiver.getCapabilities(kind);
+          // determine remote caps from SDP
+          remoteCapabilities = self._getRemoteCapabilities(section);
+
+          line = lines.filter(function(line) {
+            return line.indexOf('a=ssrc:') === 0 &&
+                line.split(' ')[1].indexOf('cname:') === 0;
+          });
+          sendSSRC = (2 * sdpMLineIndex + 2) * 1001;
+          if (line) { // FIXME: alot of assumptions here
+            recvSSRC = line[0].split(' ')[0].split(':')[1];
+            cname = line[0].split(' ')[1].split(':')[1];
+          }
+          rtpReceiver = new RTCRtpReceiver(transports.dtlsTransport, kind);
+
+          // calculate intersection so no unknown caps get passed into the RTPReciver
+          params = self._getCommonCapabilities(localCapabilities,
+              remoteCapabilities);
+
+          params.muxId = recvSSRC;
+          params.encodings = [self._getEncodingParameters(recvSSRC)];
+          params.rtcp = {
+            cname: cname,
+            reducedSize: false,
+            ssrc: sendSSRC,
+            mux: true
+          };
+          rtpReceiver.receive(params);
+          // FIXME: not correct when there are multiple streams but that is
+          // not currently supported.
+          stream.addTrack(rtpReceiver.track);
+
+          // FIXME: honor a=sendrecv
+          if (self.localStreams.length > 0 &&
+              self.localStreams[0].getTracks().length >= sdpMLineIndex) {
+            // FIXME: actually more complicated, needs to match types etc
+            var localtrack = self.localStreams[0].getTracks()[sdpMLineIndex];
+            rtpSender = new RTCRtpSender(localtrack, transports.dtlsTransport);
+          }
+
+          self.mLines[sdpMLineIndex] = {
+            iceGatherer: transports.iceGatherer,
+            iceTransport: transports.iceTransport,
+            dtlsTransport: transports.dtlsTransport,
+            localCapabilities: localCapabilities,
+            remoteCapabilities: remoteCapabilities,
+            rtpSender: rtpSender,
+            rtpReceiver: rtpReceiver,
+            kind: kind,
+            mid: mid,
+            sendSSRC: sendSSRC,
+            recvSSRC: recvSSRC
+          };
+        } else {
+          iceGatherer = self.mLines[sdpMLineIndex].iceGatherer;
+          iceTransport = self.mLines[sdpMLineIndex].iceTransport;
+          dtlsTransport = self.mLines[sdpMLineIndex].dtlsTransport;
+          rtpSender = self.mLines[sdpMLineIndex].rtpSender;
+          rtpReceiver = self.mLines[sdpMLineIndex].rtpReceiver;
+          sendSSRC = self.mLines[sdpMLineIndex].sendSSRC;
+          recvSSRC = self.mLines[sdpMLineIndex].recvSSRC;
+        }
+
+        var remoteIceParameters = self._getIceParameters(section, sessionpart);
+        var remoteDtlsParameters = self._getDtlsParameters(section,
+            sessionpart);
+
+        // for answers we start ice and dtls here, otherwise this is done in SLD
+        if (description.type === 'answer') {
+          iceTransport.start(iceGatherer, remoteIceParameters, 'controlling');
+          dtlsTransport.start(remoteDtlsParameters);
+
+          // determine remote caps from SDP
+          remoteCapabilities = self._getRemoteCapabilities(section);
+          // FIXME: store remote caps?
+
+          if (rtpSender) {
+            params = remoteCapabilities;
+            params.muxId = sendSSRC;
+            params.encodings = [self._getEncodingParameters(sendSSRC)];
+            params.rtcp = {
+              cname: self._cname,
+              reducedSize: false,
+              ssrc: recvSSRC,
+              mux: true
+            };
+            rtpSender.send(params);
+          }
+
+          // FIXME: only if a=sendrecv
+          var bidi = lines.filter(function(line) {
+            return line.indexOf('a=ssrc:') === 0;
+          }).length > 0;
+          if (rtpReceiver && bidi) {
+            line = lines.filter(function(line) {
+              return line.indexOf('a=ssrc:') === 0 &&
+                  line.split(' ')[1].indexOf('cname:') === 0;
+            });
+            if (line) { // FIXME: alot of assumptions here
+              recvSSRC = line[0].split(' ')[0].split(':')[1];
+              cname = line[0].split(' ')[1].split(':')[1];
+            }
+            params = remoteCapabilities;
+            params.muxId = recvSSRC;
+            params.encodings = [self._getEncodingParameters(recvSSRC)];
+            params.rtcp = {
+              cname: cname,
+              reducedSize: false,
+              ssrc: sendSSRC,
+              mux: true
+            };
+            rtpReceiver.receive(params, kind);
+            stream.addTrack(rtpReceiver.track);
+            self.mLines[sdpMLineIndex].recvSSRC = recvSSRC;
+          }
+        }
+      });
+
+      this.remoteDescription = description;
+      switch (description.type) {
+      case 'offer':
+        this._updateSignalingState('have-remote-offer');
+        break;
+      case 'answer':
+        this._updateSignalingState('stable');
+        break;
+      }
+      window.setTimeout(function() {
+        if (self.onaddstream !== null && stream.getTracks().length) {
+          self.remoteStreams.push(stream);
+          window.setTimeout(function() {
+            self.onaddstream({stream: stream});
+          }, 0);
+        }
+      }, 0);
+      if (arguments.length > 1 && typeof arguments[1] === 'function') {
+        window.setTimeout(arguments[1], 0);
+      }
+      return new Promise(function(resolve) {
+        resolve();
+      });
+    };
+
+    window.RTCPeerConnection.prototype.close = function() {
+      this.mLines.forEach(function(mLine) {
+        /* not yet
+        if (mLine.iceGatherer) {
+          mLine.iceGatherer.close();
+        }
+        */
+        if (mLine.iceTransport) {
+          mLine.iceTransport.stop();
+        }
+        if (mLine.dtlsTransport) {
+          mLine.dtlsTransport.stop();
+        }
+        if (mLine.rtpSender) {
+          mLine.rtpSender.stop();
+        }
+        if (mLine.rtpReceiver) {
+          mLine.rtpReceiver.stop();
+        }
+      });
+      // FIXME: clean up tracks, local streams, remote streams, etc
+      this._updateSignalingState('closed');
+      this._updateIceConnectionState('closed');
+    };
+
+    // Update the signaling state.
+    window.RTCPeerConnection.prototype._updateSignalingState =
+        function(newState) {
+      this.signalingState = newState;
+      if (this.onsignalingstatechange !== null) {
+        this.onsignalingstatechange();
+      }
+    };
+
+    // Update the ICE connection state.
+    // FIXME: should be called 'updateConnectionState', also be called for
+    //  DTLS changes and implement
+    //  https://lists.w3.org/Archives/Public/public-webrtc/2015Sep/0033.html
+    window.RTCPeerConnection.prototype._updateIceConnectionState =
+        function(newState) {
+      var self = this;
+      if (this.iceConnectionState !== newState) {
+        var agreement = self.mLines.every(function(mLine) {
+          return mLine.iceTransport.state === newState;
+        });
+        if (agreement) {
+          self.iceConnectionState = newState;
+          if (this.oniceconnectionstatechange !== null) {
+            this.oniceconnectionstatechange();
+          }
+        }
+      }
+    };
+
+    window.RTCPeerConnection.prototype.createOffer = function() {
+      var self = this;
+      var offerOptions;
+      if (arguments.length === 1 && typeof arguments[0] !== 'function') {
+        offerOptions = arguments[0];
+      } else if (arguments.length === 3) {
+        offerOptions = arguments[2];
+      }
+
+      var tracks = [];
+      var numAudioTracks = 0;
+      var numVideoTracks = 0;
+      // Default to sendrecv.
+      if (this.localStreams.length) {
+        numAudioTracks = this.localStreams[0].getAudioTracks().length;
+        numVideoTracks = this.localStreams[0].getAudioTracks().length;
+      }
+      // Determine number of audio and video tracks we need to send/recv.
+      if (offerOptions) {
+        // Deal with Chrome legacy constraints...
+        if (offerOptions.mandatory) {
+          if (offerOptions.mandatory.OfferToReceiveAudio) {
+            numAudioTracks = 1;
+          } else if (offerOptions.mandatory.OfferToReceiveAudio === false) {
+            numAudioTracks = 0;
+          }
+          if (offerOptions.mandatory.OfferToReceiveVideo) {
+            numVideoTracks = 1;
+          } else if (offerOptions.mandatory.OfferToReceiveVideo === false) {
+            numVideoTracks = 0;
+          }
+        } else {
+          if (offerOptions.offerToReceiveAudio !== undefined) {
+            numAudioTracks = offerOptions.offerToReceiveAudio;
+          }
+          if (offerOptions.offerToReceiveVideo !== undefined) {
+            numVideoTracks = offerOptions.offerToReceiveVideo;
+          }
+        }
+      }
+      if (this.localStreams.length) {
+        // Push local streams.
+        this.localStreams[0].getTracks().forEach(function(track) {
+          tracks.push({
+            kind: track.kind,
+            track: track,
+            wantReceive: track.kind === 'audio' ?
+                numAudioTracks > 0 : numVideoTracks > 0
+          });
+          if (track.kind === 'audio') {
+            numAudioTracks--;
+          } else if (track.kind === 'video') {
+            numVideoTracks--;
+          }
+        });
+      }
+      // Create M-lines for recvonly streams.
+      while (numAudioTracks > 0 || numVideoTracks > 0) {
+        if (numAudioTracks > 0) {
+          tracks.push({
+            kind: 'audio',
+            wantReceive: true
+          });
+          numAudioTracks--;
+        }
+        if (numVideoTracks > 0) {
+          tracks.push({
+            kind: 'video',
+            wantReceive: true
+          });
+          numVideoTracks--;
+        }
+      }
+
+      var sdp = 'v=0\r\n' +
+          'o=thisisadapterortc 8169639915646943137 2 IN IP4 127.0.0.1\r\n' +
+          's=-\r\n' +
+          't=0 0\r\n';
+      var mLines = [];
+      tracks.forEach(function(mline, sdpMLineIndex) {
+        // For each track, create an ice gatherer, ice transport, dtls transport,
+        // potentially rtpsender and rtpreceiver.
+        var track = mline.track;
+        var kind = mline.kind;
+        var mid = Math.random().toString(36).substr(2, 10);
+
+        var transports = self._createIceAndDtlsTransports(mid, sdpMLineIndex);
+
+        var localCapabilities = RTCRtpSender.getCapabilities(kind);
+        var rtpSender;
+        // generate an ssrc now, to be used later in rtpSender.send
+        var sendSSRC = (2 * sdpMLineIndex + 1) * 1001; //Math.floor(Math.random()*4294967295);
+        var recvSSRC; // don't know yet
+        if (track) {
+          rtpSender = new RTCRtpSender(track, transports.dtlsTransport);
+        }
+
+        var rtpReceiver;
+        if (mline.wantReceive) {
+          rtpReceiver = new RTCRtpReceiver(transports.dtlsTransport, kind);
+        }
+
+        mLines[sdpMLineIndex] = {
+          iceGatherer: transports.iceGatherer,
+          iceTransport: transports.iceTransport,
+          dtlsTransport: transports.dtlsTransport,
+          localCapabilities: localCapabilities,
+          remoteCapabilities: null,
+          rtpSender: rtpSender,
+          rtpReceiver: rtpReceiver,
+          kind: kind,
+          mid: mid,
+          sendSSRC: sendSSRC,
+          recvSSRC: recvSSRC
+        };
+
+        // Map things to SDP.
+        // Build the mline.
+        sdp += 'm=' + kind + ' 9 UDP/TLS/RTP/SAVPF ';
+        sdp += localCapabilities.codecs.map(function(codec) {
+          return codec.preferredPayloadType;
+        }).join(' ') + '\r\n';
+
+        sdp += 'c=IN IP4 0.0.0.0\r\n';
+        sdp += 'a=rtcp:9 IN IP4 0.0.0.0\r\n';
+
+        // Map ICE parameters (ufrag, pwd) to SDP.
+        sdp += self._iceParametersToSDP(
+            transports.iceGatherer.getLocalParameters());
+
+        // Map DTLS parameters to SDP.
+        sdp += self._dtlsParametersToSDP(
+            transports.dtlsTransport.getLocalParameters(), 'actpass');
+
+        sdp += 'a=mid:' + mid + '\r\n';
+
+        if (rtpSender && rtpReceiver) {
+          sdp += 'a=sendrecv\r\n';
+        } else if (rtpSender) {
+          sdp += 'a=sendonly\r\n';
+        } else if (rtpReceiver) {
+          sdp += 'a=recvonly\r\n';
+        } else {
+          sdp += 'a=inactive\r\n';
+        }
+        sdp += 'a=rtcp-mux\r\n';
+
+        // Add a=rtpmap lines for each codec. Also fmtp and rtcp-fb.
+        sdp += self._capabilitiesToSDP(localCapabilities);
+
+        if (track) {
+          sdp += 'a=msid:' + self.localStreams[0].id + ' ' + track.id + '\r\n';
+          sdp += 'a=ssrc:' + sendSSRC + ' ' + 'msid:' +
+              self.localStreams[0].id + ' ' + track.id + '\r\n';
+        }
+        sdp += 'a=ssrc:' + sendSSRC + ' cname:' + self._cname + '\r\n';
+      });
+
+      var desc = new RTCSessionDescription({
+        type: 'offer',
+        sdp: sdp,
+        ortc: mLines
+      });
+      if (arguments.length && typeof arguments[0] === 'function') {
+        window.setTimeout(arguments[0], 0, desc);
+      }
+      return new Promise(function(resolve) {
+        resolve(desc);
+      });
+    };
+
+    window.RTCPeerConnection.prototype.createAnswer = function() {
+      var self = this;
+      var answerOptions;
+      if (arguments.length === 1 && typeof arguments[0] !== 'function') {
+        answerOptions = arguments[0];
+      } else if (arguments.length === 3) {
+        answerOptions = arguments[2];
+      }
+
+      var sdp = 'v=0\r\n' +
+          'o=thisisadapterortc 8169639915646943137 2 IN IP4 127.0.0.1\r\n' +
+          's=-\r\n' +
+          't=0 0\r\n';
+      this.mLines.forEach(function(mLine/*, sdpMLineIndex*/) {
+        var iceGatherer = mLine.iceGatherer;
+        //var iceTransport = mLine.iceTransport;
+        var dtlsTransport = mLine.dtlsTransport;
+        var localCapabilities = mLine.localCapabilities;
+        var remoteCapabilities = mLine.remoteCapabilities;
+        var rtpSender = mLine.rtpSender;
+        var rtpReceiver = mLine.rtpReceiver;
+        var kind = mLine.kind;
+        var sendSSRC = mLine.sendSSRC;
+        //var recvSSRC = mLine.recvSSRC;
+
+        // Calculate intersection of capabilities.
+        var commonCapabilities = self._getCommonCapabilities(localCapabilities,
+            remoteCapabilities);
+
+        // Map things to SDP.
+        // Build the mline.
+        sdp += 'm=' + kind + ' 9 UDP/TLS/RTP/SAVPF ';
+        sdp += commonCapabilities.codecs.map(function(codec) {
+          return codec.payloadType;
+        }).join(' ') + '\r\n';
+
+        sdp += 'c=IN IP4 0.0.0.0\r\n';
+        sdp += 'a=rtcp:9 IN IP4 0.0.0.0\r\n';
+
+        // Map ICE parameters (ufrag, pwd) to SDP.
+        sdp += self._iceParametersToSDP(iceGatherer.getLocalParameters());
+
+        // Map DTLS parameters to SDP.
+        sdp += self._dtlsParametersToSDP(dtlsTransport.getLocalParameters(),
+            'active');
+
+        sdp += 'a=mid:' + mLine.mid + '\r\n';
+
+        if (rtpSender && rtpReceiver) {
+          sdp += 'a=sendrecv\r\n';
+        } else if (rtpReceiver) {
+          sdp += 'a=sendonly\r\n';
+        } else if (rtpSender) {
+          sdp += 'a=sendonly\r\n';
+        } else {
+          sdp += 'a=inactive\r\n';
+        }
+        sdp += 'a=rtcp-mux\r\n';
+
+        // Add a=rtpmap lines for each codec. Also fmtp and rtcp-fb.
+        sdp += self._capabilitiesToSDP(commonCapabilities);
+
+        if (rtpSender) {
+          // add a=ssrc lines from RTPSender
+          sdp += 'a=msid:' + self.localStreams[0].id + ' ' +
+              rtpSender.track.id + '\r\n';
+          sdp += 'a=ssrc:' + sendSSRC + ' ' + 'msid:' +
+              self.localStreams[0].id + ' ' + rtpSender.track.id + '\r\n';
+        }
+        sdp += 'a=ssrc:' + sendSSRC + ' cname:' + self._cname + '\r\n';
+      });
+
+      var desc = new RTCSessionDescription({
+        type: 'answer',
+        sdp: sdp
+        // ortc: tracks -- state is created in SRD already
+      });
+      if (arguments.length && typeof arguments[0] === 'function') {
+        window.setTimeout(arguments[0], 0, desc);
+      }
+      return new Promise(function(resolve) {
+        resolve(desc);
+      });
+    };
+
+    window.RTCPeerConnection.prototype.addIceCandidate = function(candidate) {
+      // TODO: lookup by mid
+      var mLine = this.mLines[candidate.sdpMLineIndex];
+      if (mLine) {
+        var cand = Object.keys(candidate.candidate).length > 0 ?
+            this._toCandidateJSON(candidate.candidate) : {};
+        // dirty hack to make simplewebrtc work.
+        // FIXME: need another dirty hack to avoid adding candidates after this
+        if (cand.type === 'endOfCandidates') {
+          cand = {};
+        }
+        // dirty hack to make chrome work.
+        if (cand.protocol === 'tcp' && cand.port === 0) {
+          cand = {};
+        }
+        mLine.iceTransport.addRemoteCandidate(cand);
+      }
+      if (arguments.length > 1 && typeof arguments[1] === 'function') {
+        window.setTimeout(arguments[1], 0);
+      }
+      return new Promise(function(resolve) {
+        resolve();
+      });
+    };
+
+    window.RTCPeerConnection.prototype.getStats = function() {
+      var promises = [];
+      this.mLines.forEach(function(mLine) {
+        ['rtpSender', 'rtpReceiver', 'iceGatherer', 'iceTransport',
+            'dtlsTransport'].forEach(function(thing) {
+          if (mLine[thing]) {
+            promises.push(mLine[thing].getStats());
+          }
+        });
+      });
+      var cb = arguments.length > 1 && typeof arguments[1] === 'function' &&
+          arguments[1];
+      return new Promise(function(resolve) {
+        var results = {};
+        Promise.all(promises).then(function(res) {
+          res.forEach(function(result) {
+            Object.keys(result).forEach(function(id) {
+              results[id] = result[id];
+            });
+          });
+          if (cb) {
+            window.setTimeout(cb, 0, results);
+          }
+          resolve(results);
+        });
+      });
+    };
+  }
+} else {
+  webrtcUtils.log('Browser does not appear to be WebRTC-capable');
+}
+
+// Returns the result of getUserMedia as a Promise.
+function requestUserMedia(constraints) {
+  return new Promise(function(resolve, reject) {
+    getUserMedia(constraints, resolve, reject);
+  });
+}
+
+var webrtcTesting = {};
+try {
+  Object.defineProperty(webrtcTesting, 'version', {
+    set: function(version) {
+      webrtcDetectedVersion = version;
+    }
+  });
+} catch (e) {}
+
+if (typeof module !== 'undefined') {
+  var RTCPeerConnection;
+  if (typeof window !== 'undefined') {
+    RTCPeerConnection = window.RTCPeerConnection;
+  }
+  module.exports = {
+    RTCPeerConnection: RTCPeerConnection,
+    getUserMedia: getUserMedia,
+    attachMediaStream: attachMediaStream,
+    reattachMediaStream: reattachMediaStream,
+    webrtcDetectedBrowser: webrtcDetectedBrowser,
+    webrtcDetectedVersion: webrtcDetectedVersion,
+    webrtcMinimumVersion: webrtcMinimumVersion,
+    webrtcTesting: webrtcTesting,
+    webrtcUtils: webrtcUtils
+    //requestUserMedia: not exposed on purpose.
+    //trace: not exposed on purpose.
+  };
+} else if ((typeof require === 'function') && (typeof define === 'function')) {
+  // Expose objects and functions when RequireJS is doing the loading.
+  define([], function() {
+    return {
+      RTCPeerConnection: window.RTCPeerConnection,
+      getUserMedia: getUserMedia,
+      attachMediaStream: attachMediaStream,
+      reattachMediaStream: reattachMediaStream,
+      webrtcDetectedBrowser: webrtcDetectedBrowser,
+      webrtcDetectedVersion: webrtcDetectedVersion,
+      webrtcMinimumVersion: webrtcMinimumVersion,
+      webrtcTesting: webrtcTesting,
+      webrtcUtils: webrtcUtils
+      //requestUserMedia: not exposed on purpose.
+      //trace: not exposed on purpose.
+    };
+  });
+}

+ 280 - 0
app/js/webrtc-audio.js

@@ -0,0 +1,280 @@
+/*
+ *  Copyright (c) 2015 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+/* global TimelineDataSeries, TimelineGraphView */
+
+'use strict';
+
+var audio2 = document.querySelector('audio#audio2');
+var callButton = document.querySelector('button#callButton');
+var hangupButton = document.querySelector('button#hangupButton');
+var codecSelector = document.querySelector('select#codec');
+hangupButton.disabled = true;
+callButton.onclick = call;
+hangupButton.onclick = hangup;
+
+var pc1;
+var pc2;
+var localStream;
+
+var bitrateGraph;
+var bitrateSeries;
+
+var packetGraph;
+var packetSeries;
+
+var lastResult;
+
+var offerOptions = {
+  offerToReceiveAudio: 1,
+  offerToReceiveVideo: 0,
+  voiceActivityDetection: false
+};
+
+function gotStream(stream) {
+  trace('Received local stream');
+  localStream = stream;
+  var audioTracks = localStream.getAudioTracks();
+  if (audioTracks.length > 0) {
+    trace('Using Audio device: ' + audioTracks[0].label);
+  }
+  pc1.addStream(localStream);
+  trace('Adding Local Stream to peer connection');
+
+  pc1.createOffer(gotDescription1, onCreateSessionDescriptionError,
+      offerOptions);
+
+  bitrateSeries = new TimelineDataSeries();
+  bitrateGraph = new TimelineGraphView('bitrateGraph', 'bitrateCanvas');
+  bitrateGraph.updateEndDate();
+
+  packetSeries = new TimelineDataSeries();
+  packetGraph = new TimelineGraphView('packetGraph', 'packetCanvas');
+  packetGraph.updateEndDate();
+}
+
+function onCreateSessionDescriptionError(error) {
+  trace('Failed to create session description: ' + error.toString());
+}
+
+function call() {
+  callButton.disabled = true;
+  hangupButton.disabled = false;
+  codecSelector.disabled = true;
+  trace('Starting call');
+  var servers = null;
+  var pcConstraints = {
+    'optional': []
+  };
+  pc1 = new RTCPeerConnection(servers, pcConstraints);
+  trace('Created local peer connection object pc1');
+  pc1.onicecandidate = iceCallback1;
+  pc2 = new RTCPeerConnection(servers, pcConstraints);
+  trace('Created remote peer connection object pc2');
+  pc2.onicecandidate = iceCallback2;
+  pc2.onaddstream = gotRemoteStream;
+  trace('Requesting local stream');
+  navigator.mediaDevices.getUserMedia({
+    audio: true,
+    video: false
+  })
+  .then(gotStream)
+  .catch(function(e) {
+    alert('getUserMedia() error: ' + e.name);
+  });
+}
+
+function gotDescription1(desc) {
+  desc.sdp = forceChosenAudioCodec(desc.sdp);
+  trace('Offer from pc1 \n' + desc.sdp);
+  pc1.setLocalDescription(desc, function() {
+    pc2.setRemoteDescription(desc, function() {
+      // Since the 'remote' side has no media stream we need
+      // to pass in the right constraints in order for it to
+      // accept the incoming offer of audio.
+      pc2.createAnswer(gotDescription2, onCreateSessionDescriptionError);
+    }, onSetSessionDescriptionError);
+  }, onSetSessionDescriptionError);
+}
+
+function gotDescription2(desc) {
+  desc.sdp = forceChosenAudioCodec(desc.sdp);
+  pc2.setLocalDescription(desc, function() {
+    trace('Answer from pc2 \n' + desc.sdp);
+    pc1.setRemoteDescription(desc, function() {
+    }, onSetSessionDescriptionError);
+  }, onSetSessionDescriptionError);
+}
+
+function hangup() {
+  trace('Ending call');
+  localStream.getTracks().forEach(function(track) {
+    track.stop();
+  });
+  pc1.close();
+  pc2.close();
+  pc1 = null;
+  pc2 = null;
+  hangupButton.disabled = true;
+  callButton.disabled = false;
+  codecSelector.disabled = false;
+}
+
+function gotRemoteStream(e) {
+  audio2.srcObject = e.stream;
+  trace('Received remote stream');
+}
+
+function iceCallback1(event) {
+  if (event.candidate) {
+    pc2.addIceCandidate(new RTCIceCandidate(event.candidate),
+        onAddIceCandidateSuccess, onAddIceCandidateError);
+    trace('Local ICE candidate: \n' + event.candidate.candidate);
+  }
+}
+
+function iceCallback2(event) {
+  if (event.candidate) {
+    pc1.addIceCandidate(new RTCIceCandidate(event.candidate),
+        onAddIceCandidateSuccess, onAddIceCandidateError);
+    trace('Remote ICE candidate: \n ' + event.candidate.candidate);
+  }
+}
+
+function onAddIceCandidateSuccess() {
+  trace('AddIceCandidate success.');
+}
+
+function onAddIceCandidateError(error) {
+  trace('Failed to add ICE Candidate: ' + error.toString());
+}
+
+function onSetSessionDescriptionError(error) {
+  trace('Failed to set session description: ' + error.toString());
+}
+
+function forceChosenAudioCodec(sdp) {
+  return maybePreferCodec(sdp, 'audio', 'send', codecSelector.value);
+}
+
+// Copied from AppRTC's sdputils.js:
+
+// Sets |codec| as the default |type| codec if it's present.
+// The format of |codec| is 'NAME/RATE', e.g. 'opus/48000'.
+function maybePreferCodec(sdp, type, dir, codec) {
+  var str = type + ' ' + dir + ' codec';
+  if (codec === '') {
+    trace('No preference on ' + str + '.');
+    return sdp;
+  }
+
+  trace('Prefer ' + str + ': ' + codec);
+
+  var sdpLines = sdp.split('\r\n');
+
+  // Search for m line.
+  var mLineIndex = findLine(sdpLines, 'm=', type);
+  if (mLineIndex === null) {
+    return sdp;
+  }
+
+  // If the codec is available, set it as the default in m line.
+  var codecIndex = findLine(sdpLines, 'a=rtpmap', codec);
+  console.log('codecIndex', codecIndex);
+  if (codecIndex) {
+    var payload = getCodecPayloadType(sdpLines[codecIndex]);
+    if (payload) {
+      sdpLines[mLineIndex] = setDefaultCodec(sdpLines[mLineIndex], payload);
+    }
+  }
+
+  sdp = sdpLines.join('\r\n');
+  return sdp;
+}
+
+// Find the line in sdpLines that starts with |prefix|, and, if specified,
+// contains |substr| (case-insensitive search).
+function findLine(sdpLines, prefix, substr) {
+  return findLineInRange(sdpLines, 0, -1, prefix, substr);
+}
+
+// Find the line in sdpLines[startLine...endLine - 1] that starts with |prefix|
+// and, if specified, contains |substr| (case-insensitive search).
+function findLineInRange(sdpLines, startLine, endLine, prefix, substr) {
+  var realEndLine = endLine !== -1 ? endLine : sdpLines.length;
+  for (var i = startLine; i < realEndLine; ++i) {
+    if (sdpLines[i].indexOf(prefix) === 0) {
+      if (!substr ||
+          sdpLines[i].toLowerCase().indexOf(substr.toLowerCase()) !== -1) {
+        return i;
+      }
+    }
+  }
+  return null;
+}
+
+// Gets the codec payload type from an a=rtpmap:X line.
+function getCodecPayloadType(sdpLine) {
+  var pattern = new RegExp('a=rtpmap:(\\d+) \\w+\\/\\d+');
+  var result = sdpLine.match(pattern);
+  return (result && result.length === 2) ? result[1] : null;
+}
+
+// Returns a new m= line with the specified codec as the first one.
+function setDefaultCodec(mLine, payload) {
+  var elements = mLine.split(' ');
+
+  // Just copy the first three parameters; codec order starts on fourth.
+  var newLine = elements.slice(0, 3);
+
+  // Put target payload first and copy in the rest.
+  newLine.push(payload);
+  for (var i = 3; i < elements.length; i++) {
+    if (elements[i] !== payload) {
+      newLine.push(elements[i]);
+    }
+  }
+  return newLine.join(' ');
+}
+
+// query getStats every second
+window.setInterval(function() {
+  if (!window.pc1) {
+    return;
+  }
+  window.pc1.getStats(null).then(function(res) {
+    Object.keys(res).forEach(function(key) {
+      var report = res[key];
+      var bytes;
+      var packets;
+      var now = report.timestamp;
+      if ((report.type === 'outboundrtp') ||
+          (report.type === 'outbound-rtp') ||
+          (report.type === 'ssrc' && report.bytesSent)) {
+        bytes = report.bytesSent;
+        packets = report.packetsSent;
+        if (lastResult && lastResult[report.id]) {
+          // calculate bitrate
+          var bitrate = 8 * (bytes - lastResult[report.id].bytesSent) /
+              (now - lastResult[report.id].timestamp);
+
+          // append to chart
+          bitrateSeries.addPoint(now, bitrate);
+          bitrateGraph.setDataSeries([bitrateSeries]);
+          bitrateGraph.updateEndDate();
+
+          // calculate number of packets and append to chart
+          packetSeries.addPoint(now, packets -
+              lastResult[report.id].packetsSent);
+          packetGraph.setDataSeries([packetSeries]);
+          packetGraph.updateEndDate();
+        }
+      }
+    });
+    lastResult = res;
+  });
+}, 1000);

+ 209 - 0
app/js/webrtc-video.js

@@ -0,0 +1,209 @@
+/*
+ *  Copyright (c) 2015 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+
+'use strict';
+
+var startButton = document.getElementById('startButton');
+var callButton = document.getElementById('callButton');
+var hangupButton = document.getElementById('hangupButton');
+callButton.disabled = true;
+hangupButton.disabled = true;
+startButton.onclick = start;
+callButton.onclick = call;
+hangupButton.onclick = hangup;
+
+var startTime;
+var localVideo = document.getElementById('localVideo');
+var remoteVideo = document.getElementById('remoteVideo');
+
+localVideo.addEventListener('loadedmetadata', function() {
+  trace('Local video videoWidth: ' + this.videoWidth +
+    'px,  videoHeight: ' + this.videoHeight + 'px');
+});
+
+remoteVideo.addEventListener('loadedmetadata', function() {
+  trace('Remote video videoWidth: ' + this.videoWidth +
+    'px,  videoHeight: ' + this.videoHeight + 'px');
+});
+
+remoteVideo.onresize = function() {
+  trace('Remote video size changed to ' +
+    remoteVideo.videoWidth + 'x' + remoteVideo.videoHeight);
+  // We'll use the first onsize callback as an indication that video has started
+  // playing out.
+  if (startTime) {
+    var elapsedTime = window.performance.now() - startTime;
+    trace('Setup time: ' + elapsedTime.toFixed(3) + 'ms');
+    startTime = null;
+  }
+};
+
+var localStream;
+var pc1;
+var pc2;
+var offerOptions = {
+  offerToReceiveAudio: 1,
+  offerToReceiveVideo: 1
+};
+
+function getName(pc) {
+  return (pc === pc1) ? 'pc1' : 'pc2';
+}
+
+function getOtherPc(pc) {
+  return (pc === pc1) ? pc2 : pc1;
+}
+
+function gotStream(stream) {
+  trace('Received local stream');
+  localVideo.srcObject = stream;
+  localStream = stream;
+  callButton.disabled = false;
+}
+
+function start() {
+  trace('Requesting local stream');
+  startButton.disabled = true;
+  navigator.mediaDevices.getUserMedia({
+    audio: true,
+    video: true
+  })
+  .then(gotStream)
+  .catch(function(e) {
+    alert('getUserMedia() error: ' + e.name);
+  });
+}
+
+function call() {
+  callButton.disabled = true;
+  hangupButton.disabled = false;
+  trace('Starting call');
+  startTime = window.performance.now();
+  var videoTracks = localStream.getVideoTracks();
+  var audioTracks = localStream.getAudioTracks();
+  if (videoTracks.length > 0) {
+    trace('Using video device: ' + videoTracks[0].label);
+  }
+  if (audioTracks.length > 0) {
+    trace('Using audio device: ' + audioTracks[0].label);
+  }
+  var servers = null;
+  pc1 = new RTCPeerConnection(servers);
+  trace('Created local peer connection object pc1');
+  pc1.onicecandidate = function(e) {
+    onIceCandidate(pc1, e);
+  };
+  pc2 = new RTCPeerConnection(servers);
+  trace('Created remote peer connection object pc2');
+  pc2.onicecandidate = function(e) {
+    onIceCandidate(pc2, e);
+  };
+  pc1.oniceconnectionstatechange = function(e) {
+    onIceStateChange(pc1, e);
+  };
+  pc2.oniceconnectionstatechange = function(e) {
+    onIceStateChange(pc2, e);
+  };
+  pc2.onaddstream = gotRemoteStream;
+
+  pc1.addStream(localStream);
+  trace('Added local stream to pc1');
+
+  trace('pc1 createOffer start');
+  pc1.createOffer(onCreateOfferSuccess, onCreateSessionDescriptionError,
+      offerOptions);
+}
+
+function onCreateSessionDescriptionError(error) {
+  trace('Failed to create session description: ' + error.toString());
+}
+
+function onCreateOfferSuccess(desc) {
+  trace('Offer from pc1\n' + desc.sdp);
+  trace('pc1 setLocalDescription start');
+  pc1.setLocalDescription(desc, function() {
+    onSetLocalSuccess(pc1);
+  }, onSetSessionDescriptionError);
+  trace('pc2 setRemoteDescription start');
+  pc2.setRemoteDescription(desc, function() {
+    onSetRemoteSuccess(pc2);
+  }, onSetSessionDescriptionError);
+  trace('pc2 createAnswer start');
+  // Since the 'remote' side has no media stream we need
+  // to pass in the right constraints in order for it to
+  // accept the incoming offer of audio and video.
+  pc2.createAnswer(onCreateAnswerSuccess, onCreateSessionDescriptionError);
+}
+
+function onSetLocalSuccess(pc) {
+  trace(getName(pc) + ' setLocalDescription complete');
+}
+
+function onSetRemoteSuccess(pc) {
+  trace(getName(pc) + ' setRemoteDescription complete');
+}
+
+function onSetSessionDescriptionError(error) {
+  trace('Failed to set session description: ' + error.toString());
+}
+
+function gotRemoteStream(e) {
+  remoteVideo.srcObject = e.stream;
+  trace('pc2 received remote stream');
+}
+
+function onCreateAnswerSuccess(desc) {
+  trace('Answer from pc2:\n' + desc.sdp);
+  trace('pc2 setLocalDescription start');
+  pc2.setLocalDescription(desc, function() {
+    onSetLocalSuccess(pc2);
+  }, onSetSessionDescriptionError);
+  trace('pc1 setRemoteDescription start');
+  pc1.setRemoteDescription(desc, function() {
+    onSetRemoteSuccess(pc1);
+  }, onSetSessionDescriptionError);
+}
+
+function onIceCandidate(pc, event) {
+  if (event.candidate) {
+    getOtherPc(pc).addIceCandidate(new RTCIceCandidate(event.candidate),
+        function() {
+          onAddIceCandidateSuccess(pc);
+        },
+        function(err) {
+          onAddIceCandidateError(pc, err);
+        }
+    );
+    trace(getName(pc) + ' ICE candidate: \n' + event.candidate.candidate);
+  }
+}
+
+function onAddIceCandidateSuccess(pc) {
+  trace(getName(pc) + ' addIceCandidate success');
+}
+
+function onAddIceCandidateError(pc, error) {
+  trace(getName(pc) + ' failed to add ICE Candidate: ' + error.toString());
+}
+
+function onIceStateChange(pc, event) {
+  if (pc) {
+    trace(getName(pc) + ' ICE state: ' + pc.iceConnectionState);
+    console.log('ICE state change event: ', event);
+  }
+}
+
+function hangup() {
+  trace('Ending call');
+  pc1.close();
+  pc2.close();
+  pc1 = null;
+  pc2 = null;
+  hangupButton.disabled = true;
+  callButton.disabled = false;
+}

BIN
dump.rdb


+ 93 - 4
src/css/main.css

@@ -5,7 +5,7 @@ body {
   background-position: center center;
   background-position: center center;
   background-repeat: no-repeat;
   background-repeat: no-repeat;
   background: -webkit-gradient(radial, center center, 0, center center, 460, from(#cecece), to(#222));
   background: -webkit-gradient(radial, center center, 0, center center, 460, from(#cecece), to(#222));
-  background: -webkit-radial-gradient(circle, #cecece, #222);
+  background: -webkit-radial-gradient(circle, #ffffff, #000000);
   background: -moz-radial-gradient(circle, #cecece, #222);
   background: -moz-radial-gradient(circle, #cecece, #222);
   background: -ms-radial-gradient(circle, #cecece, #222);
   background: -ms-radial-gradient(circle, #cecece, #222);
 }
 }
@@ -52,6 +52,95 @@ div#player{
 	margin: 0 auto;
 	margin: 0 auto;
   line-height:14px;
   line-height:14px;
 }
 }
+input[type=range] {
+  -webkit-appearance: none;
+  margin: 18px 0;
+  width: 100%;
+}
+input[type=range]:focus {
+  outline: none;
+}
+input[type=range]::-webkit-slider-runnable-track {
+  width: 100%;
+  height: 8.4px;
+  cursor: pointer;
+  animate: 0.2s;
+  box-shadow: 0px 3px 8px #aaa, inset 0px 2px 3px #fff;
+  background: #337ab7;
+  border-radius: 1.3px;
+  border: 0.2px solid #010101;
+}
+input[type=range]::-webkit-slider-thumb {
+
+  box-shadow: 0px 3px 8px #aaa, inset 0px 2px 3px #fff;
+  border: 1px solid #333;
+  height: 36px;
+  width: 16px;
+  border-radius: 3px;
+  background: #f7f7f7;
+  cursor: pointer;
+  -webkit-appearance: none;
+  margin-top: -14px;
+}
+input[type=range]:focus::-webkit-slider-runnable-track {
+  background: #367ebd;
+}
+input[type=range]::-moz-range-track {
+  width: 100%;
+  height: 8.4px;
+  cursor: pointer;
+  animate: 0.2s;
+  box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
+  background: #337ab7;
+  border-radius: 1.3px;
+  border: 0.2px solid #010101;
+}
+input[type=range]::-moz-range-thumb {
+  box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
+  border: 1px solid #000000;
+  height: 36px;
+  width: 16px;
+  border-radius: 3px;
+  background: #ffffff;
+  cursor: pointer;
+}
+input[type=range]::-ms-track {
+  width: 100%;
+  height: 8.4px;
+  cursor: pointer;
+  animate: 0.2s;
+  background: transparent;
+  border-color: transparent;
+  border-width: 16px 0;
+  color: transparent;
+}
+input[type=range]::-ms-fill-lower {
+  background: #2a6495;
+  border: 0.2px solid #010101;
+  border-radius: 2.6px;
+  box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
+}
+input[type=range]::-ms-fill-upper {
+  background: #337ab7;
+  border: 0.2px solid #010101;
+  border-radius: 2.6px;
+  box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
+}
+input[type=range]::-ms-thumb {
+  box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
+  border: 1px solid #000000;
+  height: 36px;
+  width: 16px;
+  border-radius: 3px;
+  background: #ffffff;
+  cursor: pointer;
+}
+input[type=range]:focus::-ms-fill-lower {
+  background: #3071a9;
+}
+input[type=range]:focus::-ms-fill-upper {
+  background: #367ebd;
+}
 output.volume {
 output.volume {
   position: absolute;
   position: absolute;
   background-image: -moz-linear-gradient(top, #444444, #999999);
   background-image: -moz-linear-gradient(top, #444444, #999999);
@@ -193,10 +282,10 @@ li#Lobby_tab {
 
 
 video {
 video {
   background:#000;
   background:#000;
-  height: 225px;
-  margin: 0 0 20px 0;
+  width: 100%;
+  margin: 0 0 5px 0;
   vertical-align: top;
   vertical-align: top;
-  width: calc(50% - 12px);
+  height: calc(50% - 12px);
 }
 }
 
 
 
 

+ 42 - 36
src/index.html

@@ -80,7 +80,7 @@
                         <li class="list-group-item list-group-item-success"><b>Dec 14th 8-10am</b>: Daveo Radio</li>
                         <li class="list-group-item list-group-item-success"><b>Dec 14th 8-10am</b>: Daveo Radio</li>
                         <li class="list-group-item list-group-item-success"><b>2-4pm</b>: Daveo Radio</li>
                         <li class="list-group-item list-group-item-success"><b>2-4pm</b>: Daveo Radio</li>
                         <li class="list-group-item"><b>Dec 22th 2-4pm</b>: Daveo Radio</li>
                         <li class="list-group-item"><b>Dec 22th 2-4pm</b>: Daveo Radio</li>
-                        <li class="list-group-item list-group-item-warning"><b>Dec 23th 2-4pm</b>: Somthing Else</li>
+                        <li class="list-group-item list-group-item-warning"><b>Dec 23th 2-4pm</b>: Something Else</li>
                         <li class="list-group-item list-group-item-danger"><b>Dec 24-28th</b>: Example</li>
                         <li class="list-group-item list-group-item-danger"><b>Dec 24-28th</b>: Example</li>
                         <li class="list-group-item"><b>Dec 31st 8-12pm</b>: Guest Person</li>
                         <li class="list-group-item"><b>Dec 31st 8-12pm</b>: Guest Person</li>
                         <li class="list-group-item list-group-item-info"><b>Jan 4th 10am</b>: Example Show</li>
                         <li class="list-group-item list-group-item-info"><b>Jan 4th 10am</b>: Example Show</li>
@@ -89,10 +89,10 @@
                   </div>
                   </div>
                </div>
                </div>
                   <ul class="nav nav-tabs">
                   <ul class="nav nav-tabs">
-                     <li class="active"><a href="#one" data-toggle="tab"><span class="glyphicon glyphicon-headphones" aria-hidden="true"></span>&nbsp;Recent</a></li>
-                     <li><a href="#two" data-toggle="tab"><span class="glyphicon glyphicon-fire" aria-hidden="true"></span>&nbsp;Tracks</a></li>
-                     <li><a href="#three" data-toggle="tab"><span class="glyphicon glyphicon-user" aria-hidden="true"></span>&nbsp;Artist</a></li>
-                     <li><a href="#four" data-toggle="tab"><span class="glyphicon glyphicon-time" aria-hidden="true"></span>&nbsp;Schedule</a></li>
+                     <li class="active"><a href="#one" data-toggle="tab" data-original-title="Recent" data-placement="top"><span class="glyphicon glyphicon-headphones" aria-hidden="true"></span></a></li>
+                     <li><a href="#two" data-toggle="tab" data-original-title="Tracks" data-placement="top"><span class="glyphicon glyphicon-fire" aria-hidden="true"></span></a></li>
+                     <li><a href="#three" data-toggle="tab" data-original-title="Artist" data-placement="top"><span class="glyphicon glyphicon-user" aria-hidden="true"></span></a></li>
+                     <li><a href="#four" data-toggle="tab" data-original-title="Calendar" data-placement="top"><span class="glyphicon glyphicon-time" aria-hidden="true"></span></a></li>
                   </ul>
                   </ul>
             </div>
             </div>
             <!-- END Playlist -->
             <!-- END Playlist -->
@@ -100,17 +100,16 @@
          </section>
          </section>
    	</div>
    	</div>
       
       
-      <!-- START Chat -->
+      <!-- START Connect -->
       <div class="col-sm-6">
       <div class="col-sm-6">
          <section id="request-line" class="bg-light-gray">
          <section id="request-line" class="bg-light-gray">
-   		  
-               
-            <!-- START Room Tabs -->
             <div class="panel panel-default">
             <div class="panel panel-default">
+               
+               <!-- START Room Tabs -->
                <div class="row-fluid">
                <div class="row-fluid">
                     <ul id="rooms_tabs" class="nav nav-tabs">
                     <ul id="rooms_tabs" class="nav nav-tabs">
                         <li class="dropdown">
                         <li class="dropdown">
-                            <a href="#" class="dropdown-toggle" data-toggle="dropdown"><span class="glyphicon glyphicon-th" aria-hidden="true"></span>&nbsp;<b class="caret"></b></a>
+                            <a href="#" class="dropdown-toggle" data-toggle="dropdown" data-original-title="Options" data-placement="bottom"><span class="glyphicon glyphicon-th" aria-hidden="true"></span>&nbsp;<b class="caret"></b></a>
                             <ul class="dropdown-menu">
                             <ul class="dropdown-menu">
                                 <li><a href="#modal_joinroom" data-toggle="modal"><span class="glyphicon glyphicon-plus-sign" aria-hidden="true"></span>&nbsp;Join room</a></li>
                                 <li><a href="#modal_joinroom" data-toggle="modal"><span class="glyphicon glyphicon-plus-sign" aria-hidden="true"></span>&nbsp;Join room</a></li>
                                 <li><a id="b_leave_room" href="#"><span class="glyphicon glyphicon-minus-sign" aria-hidden="true"></span>&nbsp;Leave room</a></li>
                                 <li><a id="b_leave_room" href="#"><span class="glyphicon glyphicon-minus-sign" aria-hidden="true"></span>&nbsp;Leave room</a></li>
@@ -118,9 +117,9 @@
                                 <li><a href="#modal_setnick" data-toggle="modal"><span class="glyphicon glyphicon-user" aria-hidden="true"></span>&nbsp;Set nickname</a></li>
                                 <li><a href="#modal_setnick" data-toggle="modal"><span class="glyphicon glyphicon-user" aria-hidden="true"></span>&nbsp;Set nickname</a></li>
                             </ul>
                             </ul>
                         </li>
                         </li>
-                        <li id="Lobby_tab" class="active"><a href="#Lobby" data-toggle="tab"><span class="glyphicon glyphicon-comment" aria-hidden="true"></span><span class="hidden-tab">Lobby</span></a></li>
-                        <li><a href="#call" data-toggle="tab"><span class="glyphicon glyphicon-phone-alt" aria-hidden="true"></span></a></li>
-                        <li><a href="#video" data-toggle="tab"><span class="glyphicon glyphicon-facetime-video" aria-hidden="true"></span></a></li>
+                        <li id="Lobby_tab" class="active"><a href="#Lobby" data-toggle="tab" data-original-title="Chat" data-placement="bottom"><span class="glyphicon glyphicon-comment" aria-hidden="true"></span><span class="hidden-tab">Lobby</span></a></li>
+                        <li><a href="#call" data-toggle="tab" data-original-title="Phone" data-placement="bottom"><span class="glyphicon glyphicon-phone-alt" aria-hidden="true"></span></a></li>
+                        <li><a href="#video" data-toggle="tab" data-original-title="Video" data-placement="bottom"><span class="glyphicon glyphicon-facetime-video" aria-hidden="true"></span></a></li>
                     </ul>
                     </ul>
                </div>
                </div>
                <!-- END Room Tabs -->
                <!-- END Room Tabs -->
@@ -141,24 +140,25 @@
                            </div>
                            </div>
                        </div>              
                        </div>              
                    
                    
-                      <!-- START message -->
-                      <div class="panel-footer">
-                         <div class="row-fluid">
-                             <form class="form">
-                                <div class="input-group">
-                                   <input id="message_text" class="form-control" type="text" placeholder="What say you?" >
-                                    <span class="input-group-btn">
-                                       <button id="b_send_message" class="btn btn-primary"><span class="glyphicon glyphicon-bullhorn" aria-hidden="true"></span> Post</button>
-                                    </span>
-                                 </div>
-                              </form>
-                              <!--<button id="set_nick" class="btn btn-success" data-toggle="modal" data-target="#modal_setnick"><span class="glyphicon glyphicon-user" aria-hidden="true"></span> Set Nick</button>-->
-                         </div>
+                   <!-- START message -->
+                   <div class="panel-footer">
+                      <div class="row-fluid">
+                          <form class="form">
+                             <div class="input-group">
+                                <input id="message_text" class="form-control" type="text" placeholder="What say you?" >
+                                 <span class="input-group-btn">
+                                    <button id="b_send_message" class="btn btn-primary"><span class="glyphicon glyphicon-bullhorn" aria-hidden="true"></span> Post</button>
+                                 </span>
+                              </div>
+                           </form>
+                           <!--<button id="set_nick" class="btn btn-success" data-toggle="modal" data-target="#modal_setnick"><span class="glyphicon glyphicon-user" aria-hidden="true"></span> Set Nick</button>-->
                       </div>
                       </div>
-                      <!-- END message -->
+                   </div>
+                   <!-- END message -->
                       
                       
                    </div>
                    </div>
                    
                    
+                   <!-- START Call -->
                    <div class="tab-pane" id="call">
                    <div class="tab-pane" id="call">
                       <div id="audio">
                       <div id="audio">
                          <div class="well">
                          <div class="well">
@@ -184,24 +184,27 @@
                           </div>
                           </div>
                        </div>
                        </div>
                    </div>
                    </div>
+                   <!-- END Call -->
+                   
+                   <!-- START Video -->
                    <div class="tab-pane" id="video">
                    <div class="tab-pane" id="video">
                       <div id="video">
                       <div id="video">
                          <div class="well">
                          <div class="well">
                             <video id="localVideo" autoplay muted></video>
                             <video id="localVideo" autoplay muted></video>
                             <video id="remoteVideo" autoplay></video>
                             <video id="remoteVideo" autoplay></video>
                          </div>
                          </div>
-                            <div class="panel-footer">
-                              <button id="startButton" class="btn btn-success">Start</button>
-                              <button id="callButton" class="btn btn-primary">Call</button>
-                              <button id="hangupButton" class="btn btn-danger">Hang Up</button>
-                            </div>
+                         <div class="panel-footer">
+                           <button id="startButton" class="btn btn-success">Start</button>
+                           <button id="callButton" class="btn btn-primary">Call</button>
+                           <button id="hangupButton" class="btn btn-danger">Hang Up</button>
                          </div>
                          </div>
                       <div>
                       <div>
                    </div>
                    </div>
+                   <!-- END Video -->
+                   
                </div>
                </div>
                <!-- END Rooms -->
                <!-- END Rooms -->
                
                
-
             </div>
             </div>
 
 
              
              
@@ -248,7 +251,7 @@
             
             
          </section>
          </section>
       </div>
       </div>
-      <!-- End Chat -->
+      <!-- End Connect -->
       
       
          
          
    </div>
    </div>
@@ -326,10 +329,11 @@
     <div class="modal-content">
     <div class="modal-content">
       <div class="modal-header">
       <div class="modal-header">
         <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
         <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
-        <h4 class="modal-title" id="about">About</h4>
+        <h4 class="modal-title" id="about">Help</h4>
       </div>
       </div>
       <div class="modal-body">
       <div class="modal-body">
-        <p><b>Daveo Radio</b> is just a little place I can share and talk about some music with friends. I started toying around with different streaming servers and I am using an Icecast streaming media server Iโ€™ve set up. This page is powered by Linode, Node.js, Express, Socket.io, and Icecast-kh.</p>   <p>I enjoy listening to music and I spent quite a bit of time doing so. Iโ€™ve gone entirely digital in recent years. I sold my collection of albums years ago and I donated my CD collection to a music student at Vanderbilt. I now have a collection of about 100,000 files that I keep on a networked hard drive. I do pay for ad-free streaming services, but I agree with artists who want a better revenue model from streaming. The analog/digital debate is null in my opinion. I use Polk in my living room and Klipsch in my office. In the car, I most often use playlists Iโ€™ve created and I rarely tune into a radio or satellite stations even though I have service. On occasion, at home I like to tune into terrestrial stations that stream to help find new music, but Iโ€™ve found almost all of the robot mixes from the big streaming providers unsatisfying.</p>
+        <p><b>Audio Streaming</b>: There are a number of ways you can listen to the audio stream. This page uses the HTML5 audio element to stream it to your computer or phone. You can also point your favorite media player to the stream at <a href="http://stream.davidawindham.com/stream">http://stream.davidawindham.com/</a>.</p>
+        <p><b>Video Stream</b>: Older web browers will not support all of the features here. In order to use the video and call capabilities of this page, you'll need to use a modern web browser </p>
       </div>
       </div>
       <div class="modal-footer">
       <div class="modal-footer">
         <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
         <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
@@ -365,6 +369,8 @@
 <script src="js/vendor.min.js"></script>
 <script src="js/vendor.min.js"></script>
 <script src="/socket.io/socket.io.js"></script>
 <script src="/socket.io/socket.io.js"></script>
 <script src="js/radio.min.js"></script>
 <script src="js/radio.min.js"></script>
+<script src="js/webrtc-adapter.js"></script>
+<script src="js/webrtc-video.js"></script>
 <script>
 <script>
   var _paq = _paq || [];
   var _paq = _paq || [];
   _paq.push(['trackPageView']);
   _paq.push(['trackPageView']);

File diff suppressed because it is too large
+ 0 - 0
src/js/radio.js


+ 1620 - 0
src/js/underscore-1.8.3.js

@@ -0,0 +1,1620 @@
+//     Underscore.js 1.8.3
+//     http://underscorejs.org
+//     (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
+//     Underscore may be freely distributed under the MIT license.
+
+(function() {
+
+  // Baseline setup
+  // --------------
+
+  // Establish the root object, `window` (`self`) in the browser, `global`
+  // on the server, or `this` in some virtual machines. We use `self`
+  // instead of `window` for `WebWorker` support.
+  var root = typeof self == 'object' && self.self === self && self ||
+            typeof global == 'object' && global.global === global && global ||
+            this;
+
+  // Save the previous value of the `_` variable.
+  var previousUnderscore = root._;
+
+  // Save bytes in the minified (but not gzipped) version:
+  var ArrayProto = Array.prototype, ObjProto = Object.prototype;
+
+  // Create quick reference variables for speed access to core prototypes.
+  var
+    push = ArrayProto.push,
+    slice = ArrayProto.slice,
+    toString = ObjProto.toString,
+    hasOwnProperty = ObjProto.hasOwnProperty;
+
+  // All **ECMAScript 5** native function implementations that we hope to use
+  // are declared here.
+  var
+    nativeIsArray = Array.isArray,
+    nativeKeys = Object.keys,
+    nativeCreate = Object.create;
+
+  // Naked function reference for surrogate-prototype-swapping.
+  var Ctor = function(){};
+
+  // Create a safe reference to the Underscore object for use below.
+  var _ = function(obj) {
+    if (obj instanceof _) return obj;
+    if (!(this instanceof _)) return new _(obj);
+    this._wrapped = obj;
+  };
+
+  // Export the Underscore object for **Node.js**, with
+  // backwards-compatibility for their old module API. If we're in
+  // the browser, add `_` as a global object.
+  // (`nodeType` is checked to ensure that `module`
+  // and `exports` are not HTML elements.)
+  if (typeof exports != 'undefined' && !exports.nodeType) {
+    if (typeof module != 'undefined' && !module.nodeType && module.exports) {
+      exports = module.exports = _;
+    }
+    exports._ = _;
+  } else {
+    root._ = _;
+  }
+
+  // Current version.
+  _.VERSION = '1.8.3';
+
+  // Internal function that returns an efficient (for current engines) version
+  // of the passed-in callback, to be repeatedly applied in other Underscore
+  // functions.
+  var optimizeCb = function(func, context, argCount) {
+    if (context === void 0) return func;
+    switch (argCount == null ? 3 : argCount) {
+      case 1: return function(value) {
+        return func.call(context, value);
+      };
+      // The 2-parameter case has been omitted only because no current consumers
+      // made use of it.
+      case 3: return function(value, index, collection) {
+        return func.call(context, value, index, collection);
+      };
+      case 4: return function(accumulator, value, index, collection) {
+        return func.call(context, accumulator, value, index, collection);
+      };
+    }
+    return function() {
+      return func.apply(context, arguments);
+    };
+  };
+
+  // A mostly-internal function to generate callbacks that can be applied
+  // to each element in a collection, returning the desired result โ€” either
+  // `identity`, an arbitrary callback, a property matcher, or a property accessor.
+  var cb = function(value, context, argCount) {
+    if (value == null) return _.identity;
+    if (_.isFunction(value)) return optimizeCb(value, context, argCount);
+    if (_.isObject(value)) return _.matcher(value);
+    return _.property(value);
+  };
+
+  _.iteratee = function(value, context) {
+    return cb(value, context, Infinity);
+  };
+
+  // Similar to ES6's rest param (http://ariya.ofilabs.com/2013/03/es6-and-rest-parameter.html)
+  // This accumulates the arguments passed into an array, after a given index.
+  var restArgs = function(func, startIndex) {
+    startIndex = startIndex == null ? func.length - 1 : +startIndex;
+    return function() {
+      var length = Math.max(arguments.length - startIndex, 0);
+      var rest = Array(length);
+      for (var index = 0; index < length; index++) {
+        rest[index] = arguments[index + startIndex];
+      }
+      switch (startIndex) {
+        case 0: return func.call(this, rest);
+        case 1: return func.call(this, arguments[0], rest);
+        case 2: return func.call(this, arguments[0], arguments[1], rest);
+      }
+      var args = Array(startIndex + 1);
+      for (index = 0; index < startIndex; index++) {
+        args[index] = arguments[index];
+      }
+      args[startIndex] = rest;
+      return func.apply(this, args);
+    };
+  };
+
+  // An internal function for creating a new object that inherits from another.
+  var baseCreate = function(prototype) {
+    if (!_.isObject(prototype)) return {};
+    if (nativeCreate) return nativeCreate(prototype);
+    Ctor.prototype = prototype;
+    var result = new Ctor;
+    Ctor.prototype = null;
+    return result;
+  };
+
+  var property = function(key) {
+    return function(obj) {
+      return obj == null ? void 0 : obj[key];
+    };
+  };
+
+  // Helper for collection methods to determine whether a collection
+  // should be iterated as an array or as an object.
+  // Related: http://people.mozilla.org/~jorendorff/es6-draft.html#sec-tolength
+  // Avoids a very nasty iOS 8 JIT bug on ARM-64. #2094
+  var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;
+  var getLength = property('length');
+  var isArrayLike = function(collection) {
+    var length = getLength(collection);
+    return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;
+  };
+
+  // Collection Functions
+  // --------------------
+
+  // The cornerstone, an `each` implementation, aka `forEach`.
+  // Handles raw objects in addition to array-likes. Treats all
+  // sparse array-likes as if they were dense.
+  _.each = _.forEach = function(obj, iteratee, context) {
+    iteratee = optimizeCb(iteratee, context);
+    var i, length;
+    if (isArrayLike(obj)) {
+      for (i = 0, length = obj.length; i < length; i++) {
+        iteratee(obj[i], i, obj);
+      }
+    } else {
+      var keys = _.keys(obj);
+      for (i = 0, length = keys.length; i < length; i++) {
+        iteratee(obj[keys[i]], keys[i], obj);
+      }
+    }
+    return obj;
+  };
+
+  // Return the results of applying the iteratee to each element.
+  _.map = _.collect = function(obj, iteratee, context) {
+    iteratee = cb(iteratee, context);
+    var keys = !isArrayLike(obj) && _.keys(obj),
+        length = (keys || obj).length,
+        results = Array(length);
+    for (var index = 0; index < length; index++) {
+      var currentKey = keys ? keys[index] : index;
+      results[index] = iteratee(obj[currentKey], currentKey, obj);
+    }
+    return results;
+  };
+
+  // Create a reducing function iterating left or right.
+  var createReduce = function(dir) {
+    // Optimized iterator function as using arguments.length
+    // in the main function will deoptimize the, see #1991.
+    var reducer = function(obj, iteratee, memo, initial) {
+      var keys = !isArrayLike(obj) && _.keys(obj),
+          length = (keys || obj).length,
+          index = dir > 0 ? 0 : length - 1;
+      if (!initial) {
+        memo = obj[keys ? keys[index] : index];
+        index += dir;
+      }
+      for (; index >= 0 && index < length; index += dir) {
+        var currentKey = keys ? keys[index] : index;
+        memo = iteratee(memo, obj[currentKey], currentKey, obj);
+      }
+      return memo;
+    };
+
+    return function(obj, iteratee, memo, context) {
+      var initial = arguments.length >= 3;
+      return reducer(obj, optimizeCb(iteratee, context, 4), memo, initial);
+    };
+  };
+
+  // **Reduce** builds up a single result from a list of values, aka `inject`,
+  // or `foldl`.
+  _.reduce = _.foldl = _.inject = createReduce(1);
+
+  // The right-associative version of reduce, also known as `foldr`.
+  _.reduceRight = _.foldr = createReduce(-1);
+
+  // Return the first value which passes a truth test. Aliased as `detect`.
+  _.find = _.detect = function(obj, predicate, context) {
+    var key;
+    if (isArrayLike(obj)) {
+      key = _.findIndex(obj, predicate, context);
+    } else {
+      key = _.findKey(obj, predicate, context);
+    }
+    if (key !== void 0 && key !== -1) return obj[key];
+  };
+
+  // Return all the elements that pass a truth test.
+  // Aliased as `select`.
+  _.filter = _.select = function(obj, predicate, context) {
+    var results = [];
+    predicate = cb(predicate, context);
+    _.each(obj, function(value, index, list) {
+      if (predicate(value, index, list)) results.push(value);
+    });
+    return results;
+  };
+
+  // Return all the elements for which a truth test fails.
+  _.reject = function(obj, predicate, context) {
+    return _.filter(obj, _.negate(cb(predicate)), context);
+  };
+
+  // Determine whether all of the elements match a truth test.
+  // Aliased as `all`.
+  _.every = _.all = function(obj, predicate, context) {
+    predicate = cb(predicate, context);
+    var keys = !isArrayLike(obj) && _.keys(obj),
+        length = (keys || obj).length;
+    for (var index = 0; index < length; index++) {
+      var currentKey = keys ? keys[index] : index;
+      if (!predicate(obj[currentKey], currentKey, obj)) return false;
+    }
+    return true;
+  };
+
+  // Determine if at least one element in the object matches a truth test.
+  // Aliased as `any`.
+  _.some = _.any = function(obj, predicate, context) {
+    predicate = cb(predicate, context);
+    var keys = !isArrayLike(obj) && _.keys(obj),
+        length = (keys || obj).length;
+    for (var index = 0; index < length; index++) {
+      var currentKey = keys ? keys[index] : index;
+      if (predicate(obj[currentKey], currentKey, obj)) return true;
+    }
+    return false;
+  };
+
+  // Determine if the array or object contains a given item (using `===`).
+  // Aliased as `includes` and `include`.
+  _.contains = _.includes = _.include = function(obj, item, fromIndex, guard) {
+    if (!isArrayLike(obj)) obj = _.values(obj);
+    if (typeof fromIndex != 'number' || guard) fromIndex = 0;
+    return _.indexOf(obj, item, fromIndex) >= 0;
+  };
+
+  // Invoke a method (with arguments) on every item in a collection.
+  _.invoke = restArgs(function(obj, method, args) {
+    var isFunc = _.isFunction(method);
+    return _.map(obj, function(value) {
+      var func = isFunc ? method : value[method];
+      return func == null ? func : func.apply(value, args);
+    });
+  });
+
+  // Convenience version of a common use case of `map`: fetching a property.
+  _.pluck = function(obj, key) {
+    return _.map(obj, _.property(key));
+  };
+
+  // Convenience version of a common use case of `filter`: selecting only objects
+  // containing specific `key:value` pairs.
+  _.where = function(obj, attrs) {
+    return _.filter(obj, _.matcher(attrs));
+  };
+
+  // Convenience version of a common use case of `find`: getting the first object
+  // containing specific `key:value` pairs.
+  _.findWhere = function(obj, attrs) {
+    return _.find(obj, _.matcher(attrs));
+  };
+
+  // Return the maximum element (or element-based computation).
+  _.max = function(obj, iteratee, context) {
+    var result = -Infinity, lastComputed = -Infinity,
+        value, computed;
+    if (iteratee == null || (typeof iteratee == 'number' && typeof obj[0] != 'object') && obj != null) {
+      obj = isArrayLike(obj) ? obj : _.values(obj);
+      for (var i = 0, length = obj.length; i < length; i++) {
+        value = obj[i];
+        if (value != null && value > result) {
+          result = value;
+        }
+      }
+    } else {
+      iteratee = cb(iteratee, context);
+      _.each(obj, function(v, index, list) {
+        computed = iteratee(v, index, list);
+        if (computed > lastComputed || computed === -Infinity && result === -Infinity) {
+          result = v;
+          lastComputed = computed;
+        }
+      });
+    }
+    return result;
+  };
+
+  // Return the minimum element (or element-based computation).
+  _.min = function(obj, iteratee, context) {
+    var result = Infinity, lastComputed = Infinity,
+        value, computed;
+    if (iteratee == null || (typeof iteratee == 'number' && typeof obj[0] != 'object') && obj != null) {
+      obj = isArrayLike(obj) ? obj : _.values(obj);
+      for (var i = 0, length = obj.length; i < length; i++) {
+        value = obj[i];
+        if (value != null && value < result) {
+          result = value;
+        }
+      }
+    } else {
+      iteratee = cb(iteratee, context);
+      _.each(obj, function(v, index, list) {
+        computed = iteratee(v, index, list);
+        if (computed < lastComputed || computed === Infinity && result === Infinity) {
+          result = v;
+          lastComputed = computed;
+        }
+      });
+    }
+    return result;
+  };
+
+  // Shuffle a collection.
+  _.shuffle = function(obj) {
+    return _.sample(obj, Infinity);
+  };
+
+  // Sample **n** random values from a collection using the modern version of the
+  // [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisherโ€“Yates_shuffle).
+  // If **n** is not specified, returns a single random element.
+  // The internal `guard` argument allows it to work with `map`.
+  _.sample = function(obj, n, guard) {
+    if (n == null || guard) {
+      if (!isArrayLike(obj)) obj = _.values(obj);
+      return obj[_.random(obj.length - 1)];
+    }
+    var sample = isArrayLike(obj) ? _.clone(obj) : _.values(obj);
+    var length = getLength(sample);
+    n = Math.max(Math.min(n, length), 0);
+    var last = length - 1;
+    for (var index = 0; index < n; index++) {
+      var rand = _.random(index, last);
+      var temp = sample[index];
+      sample[index] = sample[rand];
+      sample[rand] = temp;
+    }
+    return sample.slice(0, n);
+  };
+
+  // Sort the object's values by a criterion produced by an iteratee.
+  _.sortBy = function(obj, iteratee, context) {
+    var index = 0;
+    iteratee = cb(iteratee, context);
+    return _.pluck(_.map(obj, function(value, key, list) {
+      return {
+        value: value,
+        index: index++,
+        criteria: iteratee(value, key, list)
+      };
+    }).sort(function(left, right) {
+      var a = left.criteria;
+      var b = right.criteria;
+      if (a !== b) {
+        if (a > b || a === void 0) return 1;
+        if (a < b || b === void 0) return -1;
+      }
+      return left.index - right.index;
+    }), 'value');
+  };
+
+  // An internal function used for aggregate "group by" operations.
+  var group = function(behavior, partition) {
+    return function(obj, iteratee, context) {
+      var result = partition ? [[], []] : {};
+      iteratee = cb(iteratee, context);
+      _.each(obj, function(value, index) {
+        var key = iteratee(value, index, obj);
+        behavior(result, value, key);
+      });
+      return result;
+    };
+  };
+
+  // Groups the object's values by a criterion. Pass either a string attribute
+  // to group by, or a function that returns the criterion.
+  _.groupBy = group(function(result, value, key) {
+    if (_.has(result, key)) result[key].push(value); else result[key] = [value];
+  });
+
+  // Indexes the object's values by a criterion, similar to `groupBy`, but for
+  // when you know that your index values will be unique.
+  _.indexBy = group(function(result, value, key) {
+    result[key] = value;
+  });
+
+  // Counts instances of an object that group by a certain criterion. Pass
+  // either a string attribute to count by, or a function that returns the
+  // criterion.
+  _.countBy = group(function(result, value, key) {
+    if (_.has(result, key)) result[key]++; else result[key] = 1;
+  });
+
+  var reStrSymbol = /[^\ud800-\udfff]|[\ud800-\udbff][\udc00-\udfff]|[\ud800-\udfff]/g;
+  // Safely create a real, live array from anything iterable.
+  _.toArray = function(obj) {
+    if (!obj) return [];
+    if (_.isArray(obj)) return slice.call(obj);
+    if (_.isString(obj)) {
+      // Keep surrogate pair characters together
+      return obj.match(reStrSymbol);
+    }
+    if (isArrayLike(obj)) return _.map(obj, _.identity);
+    return _.values(obj);
+  };
+
+  // Return the number of elements in an object.
+  _.size = function(obj) {
+    if (obj == null) return 0;
+    return isArrayLike(obj) ? obj.length : _.keys(obj).length;
+  };
+
+  // Split a collection into two arrays: one whose elements all satisfy the given
+  // predicate, and one whose elements all do not satisfy the predicate.
+  _.partition = group(function(result, value, pass) {
+    result[pass ? 0 : 1].push(value);
+  }, true);
+
+  // Array Functions
+  // ---------------
+
+  // Get the first element of an array. Passing **n** will return the first N
+  // values in the array. Aliased as `head` and `take`. The **guard** check
+  // allows it to work with `_.map`.
+  _.first = _.head = _.take = function(array, n, guard) {
+    if (array == null) return void 0;
+    if (n == null || guard) return array[0];
+    return _.initial(array, array.length - n);
+  };
+
+  // Returns everything but the last entry of the array. Especially useful on
+  // the arguments object. Passing **n** will return all the values in
+  // the array, excluding the last N.
+  _.initial = function(array, n, guard) {
+    return slice.call(array, 0, Math.max(0, array.length - (n == null || guard ? 1 : n)));
+  };
+
+  // Get the last element of an array. Passing **n** will return the last N
+  // values in the array.
+  _.last = function(array, n, guard) {
+    if (array == null) return void 0;
+    if (n == null || guard) return array[array.length - 1];
+    return _.rest(array, Math.max(0, array.length - n));
+  };
+
+  // Returns everything but the first entry of the array. Aliased as `tail` and `drop`.
+  // Especially useful on the arguments object. Passing an **n** will return
+  // the rest N values in the array.
+  _.rest = _.tail = _.drop = function(array, n, guard) {
+    return slice.call(array, n == null || guard ? 1 : n);
+  };
+
+  // Trim out all falsy values from an array.
+  _.compact = function(array) {
+    return _.filter(array, _.identity);
+  };
+
+  // Internal implementation of a recursive `flatten` function.
+  var flatten = function(input, shallow, strict, output) {
+    output = output || [];
+    var idx = output.length;
+    for (var i = 0, length = getLength(input); i < length; i++) {
+      var value = input[i];
+      if (isArrayLike(value) && (_.isArray(value) || _.isArguments(value))) {
+        // Flatten current level of array or arguments object
+        if (shallow) {
+          var j = 0, len = value.length;
+          while (j < len) output[idx++] = value[j++];
+        } else {
+          flatten(value, shallow, strict, output);
+          idx = output.length;
+        }
+      } else if (!strict) {
+        output[idx++] = value;
+      }
+    }
+    return output;
+  };
+
+  // Flatten out an array, either recursively (by default), or just one level.
+  _.flatten = function(array, shallow) {
+    return flatten(array, shallow, false);
+  };
+
+  // Return a version of the array that does not contain the specified value(s).
+  _.without = restArgs(function(array, otherArrays) {
+    return _.difference(array, otherArrays);
+  });
+
+  // Produce a duplicate-free version of the array. If the array has already
+  // been sorted, you have the option of using a faster algorithm.
+  // Aliased as `unique`.
+  _.uniq = _.unique = function(array, isSorted, iteratee, context) {
+    if (!_.isBoolean(isSorted)) {
+      context = iteratee;
+      iteratee = isSorted;
+      isSorted = false;
+    }
+    if (iteratee != null) iteratee = cb(iteratee, context);
+    var result = [];
+    var seen = [];
+    for (var i = 0, length = getLength(array); i < length; i++) {
+      var value = array[i],
+          computed = iteratee ? iteratee(value, i, array) : value;
+      if (isSorted) {
+        if (!i || seen !== computed) result.push(value);
+        seen = computed;
+      } else if (iteratee) {
+        if (!_.contains(seen, computed)) {
+          seen.push(computed);
+          result.push(value);
+        }
+      } else if (!_.contains(result, value)) {
+        result.push(value);
+      }
+    }
+    return result;
+  };
+
+  // Produce an array that contains the union: each distinct element from all of
+  // the passed-in arrays.
+  _.union = restArgs(function(arrays) {
+    return _.uniq(flatten(arrays, true, true));
+  });
+
+  // Produce an array that contains every item shared between all the
+  // passed-in arrays.
+  _.intersection = function(array) {
+    var result = [];
+    var argsLength = arguments.length;
+    for (var i = 0, length = getLength(array); i < length; i++) {
+      var item = array[i];
+      if (_.contains(result, item)) continue;
+      var j;
+      for (j = 1; j < argsLength; j++) {
+        if (!_.contains(arguments[j], item)) break;
+      }
+      if (j === argsLength) result.push(item);
+    }
+    return result;
+  };
+
+  // Take the difference between one array and a number of other arrays.
+  // Only the elements present in just the first array will remain.
+  _.difference = restArgs(function(array, rest) {
+    rest = flatten(rest, true, true);
+    return _.filter(array, function(value){
+      return !_.contains(rest, value);
+    });
+  });
+
+  // Complement of _.zip. Unzip accepts an array of arrays and groups
+  // each array's elements on shared indices
+  _.unzip = function(array) {
+    var length = array && _.max(array, getLength).length || 0;
+    var result = Array(length);
+
+    for (var index = 0; index < length; index++) {
+      result[index] = _.pluck(array, index);
+    }
+    return result;
+  };
+
+  // Zip together multiple lists into a single array -- elements that share
+  // an index go together.
+  _.zip = restArgs(_.unzip);
+
+  // Converts lists into objects. Pass either a single array of `[key, value]`
+  // pairs, or two parallel arrays of the same length -- one of keys, and one of
+  // the corresponding values.
+  _.object = function(list, values) {
+    var result = {};
+    for (var i = 0, length = getLength(list); i < length; i++) {
+      if (values) {
+        result[list[i]] = values[i];
+      } else {
+        result[list[i][0]] = list[i][1];
+      }
+    }
+    return result;
+  };
+
+  // Generator function to create the findIndex and findLastIndex functions
+  var createPredicateIndexFinder = function(dir) {
+    return function(array, predicate, context) {
+      predicate = cb(predicate, context);
+      var length = getLength(array);
+      var index = dir > 0 ? 0 : length - 1;
+      for (; index >= 0 && index < length; index += dir) {
+        if (predicate(array[index], index, array)) return index;
+      }
+      return -1;
+    };
+  };
+
+  // Returns the first index on an array-like that passes a predicate test
+  _.findIndex = createPredicateIndexFinder(1);
+  _.findLastIndex = createPredicateIndexFinder(-1);
+
+  // Use a comparator function to figure out the smallest index at which
+  // an object should be inserted so as to maintain order. Uses binary search.
+  _.sortedIndex = function(array, obj, iteratee, context) {
+    iteratee = cb(iteratee, context, 1);
+    var value = iteratee(obj);
+    var low = 0, high = getLength(array);
+    while (low < high) {
+      var mid = Math.floor((low + high) / 2);
+      if (iteratee(array[mid]) < value) low = mid + 1; else high = mid;
+    }
+    return low;
+  };
+
+  // Generator function to create the indexOf and lastIndexOf functions
+  var createIndexFinder = function(dir, predicateFind, sortedIndex) {
+    return function(array, item, idx) {
+      var i = 0, length = getLength(array);
+      if (typeof idx == 'number') {
+        if (dir > 0) {
+          i = idx >= 0 ? idx : Math.max(idx + length, i);
+        } else {
+          length = idx >= 0 ? Math.min(idx + 1, length) : idx + length + 1;
+        }
+      } else if (sortedIndex && idx && length) {
+        idx = sortedIndex(array, item);
+        return array[idx] === item ? idx : -1;
+      }
+      if (item !== item) {
+        idx = predicateFind(slice.call(array, i, length), _.isNaN);
+        return idx >= 0 ? idx + i : -1;
+      }
+      for (idx = dir > 0 ? i : length - 1; idx >= 0 && idx < length; idx += dir) {
+        if (array[idx] === item) return idx;
+      }
+      return -1;
+    };
+  };
+
+  // Return the position of the first occurrence of an item in an array,
+  // or -1 if the item is not included in the array.
+  // If the array is large and already in sort order, pass `true`
+  // for **isSorted** to use binary search.
+  _.indexOf = createIndexFinder(1, _.findIndex, _.sortedIndex);
+  _.lastIndexOf = createIndexFinder(-1, _.findLastIndex);
+
+  // Generate an integer Array containing an arithmetic progression. A port of
+  // the native Python `range()` function. See
+  // [the Python documentation](http://docs.python.org/library/functions.html#range).
+  _.range = function(start, stop, step) {
+    if (stop == null) {
+      stop = start || 0;
+      start = 0;
+    }
+    step = step || 1;
+
+    var length = Math.max(Math.ceil((stop - start) / step), 0);
+    var range = Array(length);
+
+    for (var idx = 0; idx < length; idx++, start += step) {
+      range[idx] = start;
+    }
+
+    return range;
+  };
+
+  // Split an **array** into several arrays containing **count** or less elements
+  // of initial array
+  _.chunk = function(array, count) {
+    if (count == null || count < 1) return [];
+
+    var result = [];
+    var i = 0, length = array.length;
+    while (i < length) {
+      result.push(slice.call(array, i, i += count));
+    }
+    return result;
+  };
+
+  // Function (ahem) Functions
+  // ------------------
+
+  // Determines whether to execute a function as a constructor
+  // or a normal function with the provided arguments
+  var executeBound = function(sourceFunc, boundFunc, context, callingContext, args) {
+    if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args);
+    var self = baseCreate(sourceFunc.prototype);
+    var result = sourceFunc.apply(self, args);
+    if (_.isObject(result)) return result;
+    return self;
+  };
+
+  // Create a function bound to a given object (assigning `this`, and arguments,
+  // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if
+  // available.
+  _.bind = restArgs(function(func, context, args) {
+    if (!_.isFunction(func)) throw new TypeError('Bind must be called on a function');
+    var bound = restArgs(function(callArgs) {
+      return executeBound(func, bound, context, this, args.concat(callArgs));
+    });
+    return bound;
+  });
+
+  // Partially apply a function by creating a version that has had some of its
+  // arguments pre-filled, without changing its dynamic `this` context. _ acts
+  // as a placeholder by default, allowing any combination of arguments to be
+  // pre-filled. Set `_.partial.placeholder` for a custom placeholder argument.
+  _.partial = restArgs(function(func, boundArgs) {
+    var placeholder = _.partial.placeholder;
+    var bound = function() {
+      var position = 0, length = boundArgs.length;
+      var args = Array(length);
+      for (var i = 0; i < length; i++) {
+        args[i] = boundArgs[i] === placeholder ? arguments[position++] : boundArgs[i];
+      }
+      while (position < arguments.length) args.push(arguments[position++]);
+      return executeBound(func, bound, this, this, args);
+    };
+    return bound;
+  });
+
+  _.partial.placeholder = _;
+
+  // Bind a number of an object's methods to that object. Remaining arguments
+  // are the method names to be bound. Useful for ensuring that all callbacks
+  // defined on an object belong to it.
+  _.bindAll = restArgs(function(obj, keys) {
+    keys = flatten(keys, false, false);
+    var index = keys.length;
+    if (index < 1) throw new Error('bindAll must be passed function names');
+    while (index--) {
+      var key = keys[index];
+      obj[key] = _.bind(obj[key], obj);
+    }
+  });
+
+  // Memoize an expensive function by storing its results.
+  _.memoize = function(func, hasher) {
+    var memoize = function(key) {
+      var cache = memoize.cache;
+      var address = '' + (hasher ? hasher.apply(this, arguments) : key);
+      if (!_.has(cache, address)) cache[address] = func.apply(this, arguments);
+      return cache[address];
+    };
+    memoize.cache = {};
+    return memoize;
+  };
+
+  // Delays a function for the given number of milliseconds, and then calls
+  // it with the arguments supplied.
+  _.delay = restArgs(function(func, wait, args) {
+    return setTimeout(function() {
+      return func.apply(null, args);
+    }, wait);
+  });
+
+  // Defers a function, scheduling it to run after the current call stack has
+  // cleared.
+  _.defer = _.partial(_.delay, _, 1);
+
+  // Returns a function, that, when invoked, will only be triggered at most once
+  // during a given window of time. Normally, the throttled function will run
+  // as much as it can, without ever going more than once per `wait` duration;
+  // but if you'd like to disable the execution on the leading edge, pass
+  // `{leading: false}`. To disable execution on the trailing edge, ditto.
+  _.throttle = function(func, wait, options) {
+    var timeout, context, args, result;
+    var previous = 0;
+    if (!options) options = {};
+
+    var later = function() {
+      previous = options.leading === false ? 0 : _.now();
+      timeout = null;
+      result = func.apply(context, args);
+      if (!timeout) context = args = null;
+    };
+
+    var throttled = function() {
+      var now = _.now();
+      if (!previous && options.leading === false) previous = now;
+      var remaining = wait - (now - previous);
+      context = this;
+      args = arguments;
+      if (remaining <= 0 || remaining > wait) {
+        if (timeout) {
+          clearTimeout(timeout);
+          timeout = null;
+        }
+        previous = now;
+        result = func.apply(context, args);
+        if (!timeout) context = args = null;
+      } else if (!timeout && options.trailing !== false) {
+        timeout = setTimeout(later, remaining);
+      }
+      return result;
+    };
+
+    throttled.clear = function() {
+      clearTimeout(timeout);
+      previous = 0;
+      timeout = context = args = null;
+    };
+
+    return throttled;
+  };
+
+  // Returns a function, that, as long as it continues to be invoked, will not
+  // be triggered. The function will be called after it stops being called for
+  // N milliseconds. If `immediate` is passed, trigger the function on the
+  // leading edge, instead of the trailing.
+  _.debounce = function(func, wait, immediate) {
+    var timeout, result;
+
+    var later = function(context, args) {
+      timeout = null;
+      if (args) result = func.apply(context, args);
+    };
+
+    var debounced = restArgs(function(args) {
+      var callNow = immediate && !timeout;
+      if (timeout) clearTimeout(timeout);
+      if (callNow) {
+        timeout = setTimeout(later, wait);
+        result = func.apply(this, args);
+      } else if (!immediate) {
+        timeout = _.delay(later, wait, this, args);
+      }
+
+      return result;
+    });
+
+    debounced.clear = function() {
+      clearTimeout(timeout);
+      timeout = null;
+    };
+
+    return debounced;
+  };
+
+  // Returns the first function passed as an argument to the second,
+  // allowing you to adjust arguments, run code before and after, and
+  // conditionally execute the original function.
+  _.wrap = function(func, wrapper) {
+    return _.partial(wrapper, func);
+  };
+
+  // Returns a negated version of the passed-in predicate.
+  _.negate = function(predicate) {
+    return function() {
+      return !predicate.apply(this, arguments);
+    };
+  };
+
+  // Returns a function that is the composition of a list of functions, each
+  // consuming the return value of the function that follows.
+  _.compose = function() {
+    var args = arguments;
+    var start = args.length - 1;
+    return function() {
+      var i = start;
+      var result = args[start].apply(this, arguments);
+      while (i--) result = args[i].call(this, result);
+      return result;
+    };
+  };
+
+  // Returns a function that will only be executed on and after the Nth call.
+  _.after = function(times, func) {
+    return function() {
+      if (--times < 1) {
+        return func.apply(this, arguments);
+      }
+    };
+  };
+
+  // Returns a function that will only be executed up to (but not including) the Nth call.
+  _.before = function(times, func) {
+    var memo;
+    return function() {
+      if (--times > 0) {
+        memo = func.apply(this, arguments);
+      }
+      if (times <= 1) func = null;
+      return memo;
+    };
+  };
+
+  // Returns a function that will be executed at most one time, no matter how
+  // often you call it. Useful for lazy initialization.
+  _.once = _.partial(_.before, 2);
+
+  _.restArgs = restArgs;
+
+  // Object Functions
+  // ----------------
+
+  // Keys in IE < 9 that won't be iterated by `for key in ...` and thus missed.
+  var hasEnumBug = !{toString: null}.propertyIsEnumerable('toString');
+  var nonEnumerableProps = ['valueOf', 'isPrototypeOf', 'toString',
+                      'propertyIsEnumerable', 'hasOwnProperty', 'toLocaleString'];
+
+  var collectNonEnumProps = function(obj, keys) {
+    var nonEnumIdx = nonEnumerableProps.length;
+    var constructor = obj.constructor;
+    var proto = _.isFunction(constructor) && constructor.prototype || ObjProto;
+
+    // Constructor is a special case.
+    var prop = 'constructor';
+    if (_.has(obj, prop) && !_.contains(keys, prop)) keys.push(prop);
+
+    while (nonEnumIdx--) {
+      prop = nonEnumerableProps[nonEnumIdx];
+      if (prop in obj && obj[prop] !== proto[prop] && !_.contains(keys, prop)) {
+        keys.push(prop);
+      }
+    }
+  };
+
+  // Retrieve the names of an object's own properties.
+  // Delegates to **ECMAScript 5**'s native `Object.keys`
+  _.keys = function(obj) {
+    if (!_.isObject(obj)) return [];
+    if (nativeKeys) return nativeKeys(obj);
+    var keys = [];
+    for (var key in obj) if (_.has(obj, key)) keys.push(key);
+    // Ahem, IE < 9.
+    if (hasEnumBug) collectNonEnumProps(obj, keys);
+    return keys;
+  };
+
+  // Retrieve all the property names of an object.
+  _.allKeys = function(obj) {
+    if (!_.isObject(obj)) return [];
+    var keys = [];
+    for (var key in obj) keys.push(key);
+    // Ahem, IE < 9.
+    if (hasEnumBug) collectNonEnumProps(obj, keys);
+    return keys;
+  };
+
+  // Retrieve the values of an object's properties.
+  _.values = function(obj) {
+    var keys = _.keys(obj);
+    var length = keys.length;
+    var values = Array(length);
+    for (var i = 0; i < length; i++) {
+      values[i] = obj[keys[i]];
+    }
+    return values;
+  };
+
+  // Returns the results of applying the iteratee to each element of the object
+  // In contrast to _.map it returns an object
+  _.mapObject = function(obj, iteratee, context) {
+    iteratee = cb(iteratee, context);
+    var keys = _.keys(obj),
+      length = keys.length,
+      results = {};
+    for (var index = 0; index < length; index++) {
+      var currentKey = keys[index];
+      results[currentKey] = iteratee(obj[currentKey], currentKey, obj);
+    }
+    return results;
+  };
+
+  // Convert an object into a list of `[key, value]` pairs.
+  _.pairs = function(obj) {
+    var keys = _.keys(obj);
+    var length = keys.length;
+    var pairs = Array(length);
+    for (var i = 0; i < length; i++) {
+      pairs[i] = [keys[i], obj[keys[i]]];
+    }
+    return pairs;
+  };
+
+  // Invert the keys and values of an object. The values must be serializable.
+  _.invert = function(obj) {
+    var result = {};
+    var keys = _.keys(obj);
+    for (var i = 0, length = keys.length; i < length; i++) {
+      result[obj[keys[i]]] = keys[i];
+    }
+    return result;
+  };
+
+  // Return a sorted list of the function names available on the object.
+  // Aliased as `methods`
+  _.functions = _.methods = function(obj) {
+    var names = [];
+    for (var key in obj) {
+      if (_.isFunction(obj[key])) names.push(key);
+    }
+    return names.sort();
+  };
+
+  // An internal function for creating assigner functions.
+  var createAssigner = function(keysFunc, defaults) {
+    return function(obj) {
+      var length = arguments.length;
+      if (defaults) obj = Object(obj);
+      if (length < 2 || obj == null) return obj;
+      for (var index = 1; index < length; index++) {
+        var source = arguments[index],
+            keys = keysFunc(source),
+            l = keys.length;
+        for (var i = 0; i < l; i++) {
+          var key = keys[i];
+          if (!defaults || obj[key] === void 0) obj[key] = source[key];
+        }
+      }
+      return obj;
+    };
+  };
+
+  // Extend a given object with all the properties in passed-in object(s).
+  _.extend = createAssigner(_.allKeys);
+
+  // Assigns a given object with all the own properties in the passed-in object(s)
+  // (https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/assign)
+  _.extendOwn = _.assign = createAssigner(_.keys);
+
+  // Returns the first key on an object that passes a predicate test
+  _.findKey = function(obj, predicate, context) {
+    predicate = cb(predicate, context);
+    var keys = _.keys(obj), key;
+    for (var i = 0, length = keys.length; i < length; i++) {
+      key = keys[i];
+      if (predicate(obj[key], key, obj)) return key;
+    }
+  };
+
+  // Internal pick helper function to determine if `obj` has key `key`.
+  var keyInObj = function(value, key, obj) {
+    return key in obj;
+  };
+
+  // Return a copy of the object only containing the whitelisted properties.
+  _.pick = restArgs(function(obj, keys) {
+    var result = {}, iteratee = keys[0];
+    if (obj == null) return result;
+    if (_.isFunction(iteratee)) {
+      if (keys.length > 1) iteratee = optimizeCb(iteratee, keys[1]);
+      keys = _.allKeys(obj);
+    } else {
+      iteratee = keyInObj;
+      keys = flatten(keys, false, false);
+      obj = Object(obj);
+    }
+    for (var i = 0, length = keys.length; i < length; i++) {
+      var key = keys[i];
+      var value = obj[key];
+      if (iteratee(value, key, obj)) result[key] = value;
+    }
+    return result;
+  });
+
+   // Return a copy of the object without the blacklisted properties.
+  _.omit = restArgs(function(obj, keys) {
+    var iteratee = keys[0], context;
+    if (_.isFunction(iteratee)) {
+      iteratee = _.negate(iteratee);
+      if (keys.length > 1) context = keys[1];
+    } else {
+      keys = _.map(flatten(keys, false, false), String);
+      iteratee = function(value, key) {
+        return !_.contains(keys, key);
+      };
+    }
+    return _.pick(obj, iteratee, context);
+  });
+
+  // Fill in a given object with default properties.
+  _.defaults = createAssigner(_.allKeys, true);
+
+  // Creates an object that inherits from the given prototype object.
+  // If additional properties are provided then they will be added to the
+  // created object.
+  _.create = function(prototype, props) {
+    var result = baseCreate(prototype);
+    if (props) _.extendOwn(result, props);
+    return result;
+  };
+
+  // Create a (shallow-cloned) duplicate of an object.
+  _.clone = function(obj) {
+    if (!_.isObject(obj)) return obj;
+    return _.isArray(obj) ? obj.slice() : _.extend({}, obj);
+  };
+
+  // Invokes interceptor with the obj, and then returns obj.
+  // The primary purpose of this method is to "tap into" a method chain, in
+  // order to perform operations on intermediate results within the chain.
+  _.tap = function(obj, interceptor) {
+    interceptor(obj);
+    return obj;
+  };
+
+  // Returns whether an object has a given set of `key:value` pairs.
+  _.isMatch = function(object, attrs) {
+    var keys = _.keys(attrs), length = keys.length;
+    if (object == null) return !length;
+    var obj = Object(object);
+    for (var i = 0; i < length; i++) {
+      var key = keys[i];
+      if (attrs[key] !== obj[key] || !(key in obj)) return false;
+    }
+    return true;
+  };
+
+
+  // Internal recursive comparison function for `isEqual`.
+  var eq, deepEq;
+  eq = function(a, b, aStack, bStack) {
+    // Identical objects are equal. `0 === -0`, but they aren't identical.
+    // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal).
+    if (a === b) return a !== 0 || 1 / a === 1 / b;
+    // A strict comparison is necessary because `null == undefined`.
+    if (a == null || b == null) return a === b;
+    // `NaN`s are equivalent, but non-reflexive.
+    if (a !== a) return b !== b;
+    // Exhaust primitive checks
+    var type = typeof a;
+    if (type !== 'function' && type !== 'object' && typeof b != 'object') return false;
+    return deepEq(a, b, aStack, bStack);
+  };
+
+  // Internal recursive comparison function for `isEqual`.
+  deepEq = function(a, b, aStack, bStack) {
+    // Unwrap any wrapped objects.
+    if (a instanceof _) a = a._wrapped;
+    if (b instanceof _) b = b._wrapped;
+    // Compare `[[Class]]` names.
+    var className = toString.call(a);
+    if (className !== toString.call(b)) return false;
+    switch (className) {
+      // Strings, numbers, regular expressions, dates, and booleans are compared by value.
+      case '[object RegExp]':
+      // RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i')
+      case '[object String]':
+        // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is
+        // equivalent to `new String("5")`.
+        return '' + a === '' + b;
+      case '[object Number]':
+        // `NaN`s are equivalent, but non-reflexive.
+        // Object(NaN) is equivalent to NaN
+        if (+a !== +a) return +b !== +b;
+        // An `egal` comparison is performed for other numeric values.
+        return +a === 0 ? 1 / +a === 1 / b : +a === +b;
+      case '[object Date]':
+      case '[object Boolean]':
+        // Coerce dates and booleans to numeric primitive values. Dates are compared by their
+        // millisecond representations. Note that invalid dates with millisecond representations
+        // of `NaN` are not equivalent.
+        return +a === +b;
+    }
+
+    var areArrays = className === '[object Array]';
+    if (!areArrays) {
+      if (typeof a != 'object' || typeof b != 'object') return false;
+
+      // Objects with different constructors are not equivalent, but `Object`s or `Array`s
+      // from different frames are.
+      var aCtor = a.constructor, bCtor = b.constructor;
+      if (aCtor !== bCtor && !(_.isFunction(aCtor) && aCtor instanceof aCtor &&
+                               _.isFunction(bCtor) && bCtor instanceof bCtor)
+                          && ('constructor' in a && 'constructor' in b)) {
+        return false;
+      }
+    }
+    // Assume equality for cyclic structures. The algorithm for detecting cyclic
+    // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.
+
+    // Initializing stack of traversed objects.
+    // It's done here since we only need them for objects and arrays comparison.
+    aStack = aStack || [];
+    bStack = bStack || [];
+    var length = aStack.length;
+    while (length--) {
+      // Linear search. Performance is inversely proportional to the number of
+      // unique nested structures.
+      if (aStack[length] === a) return bStack[length] === b;
+    }
+
+    // Add the first object to the stack of traversed objects.
+    aStack.push(a);
+    bStack.push(b);
+
+    // Recursively compare objects and arrays.
+    if (areArrays) {
+      // Compare array lengths to determine if a deep comparison is necessary.
+      length = a.length;
+      if (length !== b.length) return false;
+      // Deep compare the contents, ignoring non-numeric properties.
+      while (length--) {
+        if (!eq(a[length], b[length], aStack, bStack)) return false;
+      }
+    } else {
+      // Deep compare objects.
+      var keys = _.keys(a), key;
+      length = keys.length;
+      // Ensure that both objects contain the same number of properties before comparing deep equality.
+      if (_.keys(b).length !== length) return false;
+      while (length--) {
+        // Deep compare each member
+        key = keys[length];
+        if (!(_.has(b, key) && eq(a[key], b[key], aStack, bStack))) return false;
+      }
+    }
+    // Remove the first object from the stack of traversed objects.
+    aStack.pop();
+    bStack.pop();
+    return true;
+  };
+
+  // Perform a deep comparison to check if two objects are equal.
+  _.isEqual = function(a, b) {
+    return eq(a, b);
+  };
+
+  // Is a given array, string, or object empty?
+  // An "empty" object has no enumerable own-properties.
+  _.isEmpty = function(obj) {
+    if (obj == null) return true;
+    if (isArrayLike(obj) && (_.isArray(obj) || _.isString(obj) || _.isArguments(obj))) return obj.length === 0;
+    return _.keys(obj).length === 0;
+  };
+
+  // Is a given value a DOM element?
+  _.isElement = function(obj) {
+    return !!(obj && obj.nodeType === 1);
+  };
+
+  // Is a given value an array?
+  // Delegates to ECMA5's native Array.isArray
+  _.isArray = nativeIsArray || function(obj) {
+    return toString.call(obj) === '[object Array]';
+  };
+
+  // Is a given variable an object?
+  _.isObject = function(obj) {
+    var type = typeof obj;
+    return type === 'function' || type === 'object' && !!obj;
+  };
+
+  // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp, isError.
+  _.each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp', 'Error'], function(name) {
+    _['is' + name] = function(obj) {
+      return toString.call(obj) === '[object ' + name + ']';
+    };
+  });
+
+  // Define a fallback version of the method in browsers (ahem, IE < 9), where
+  // there isn't any inspectable "Arguments" type.
+  if (!_.isArguments(arguments)) {
+    _.isArguments = function(obj) {
+      return _.has(obj, 'callee');
+    };
+  }
+
+  // Optimize `isFunction` if appropriate. Work around some typeof bugs in old v8,
+  // IE 11 (#1621), Safari 8 (#1929), and PhantomJS (#2236).
+  var nodelist = root.document && root.document.childNodes;
+  if (typeof /./ != 'function' && typeof Int8Array != 'object' && typeof nodelist != 'function') {
+    _.isFunction = function(obj) {
+      return typeof obj == 'function' || false;
+    };
+  }
+
+  // Is a given object a finite number?
+  _.isFinite = function(obj) {
+    return isFinite(obj) && !isNaN(parseFloat(obj));
+  };
+
+  // Is the given value `NaN`?
+  _.isNaN = function(obj) {
+    return _.isNumber(obj) && isNaN(obj);
+  };
+
+  // Is a given value a boolean?
+  _.isBoolean = function(obj) {
+    return obj === true || obj === false || toString.call(obj) === '[object Boolean]';
+  };
+
+  // Is a given value equal to null?
+  _.isNull = function(obj) {
+    return obj === null;
+  };
+
+  // Is a given variable undefined?
+  _.isUndefined = function(obj) {
+    return obj === void 0;
+  };
+
+  // Shortcut function for checking if an object has a given property directly
+  // on itself (in other words, not on a prototype).
+  _.has = function(obj, key) {
+    return obj != null && hasOwnProperty.call(obj, key);
+  };
+
+  // Utility Functions
+  // -----------------
+
+  // Run Underscore.js in *noConflict* mode, returning the `_` variable to its
+  // previous owner. Returns a reference to the Underscore object.
+  _.noConflict = function() {
+    root._ = previousUnderscore;
+    return this;
+  };
+
+  // Keep the identity function around for default iteratees.
+  _.identity = function(value) {
+    return value;
+  };
+
+  // Predicate-generating functions. Often useful outside of Underscore.
+  _.constant = function(value) {
+    return function() {
+      return value;
+    };
+  };
+
+  _.noop = function(){};
+
+  _.property = property;
+
+  // Generates a function for a given object that returns a given property.
+  _.propertyOf = function(obj) {
+    return obj == null ? function(){} : function(key) {
+      return obj[key];
+    };
+  };
+
+  // Returns a predicate for checking whether an object has a given set of
+  // `key:value` pairs.
+  _.matcher = _.matches = function(attrs) {
+    attrs = _.extendOwn({}, attrs);
+    return function(obj) {
+      return _.isMatch(obj, attrs);
+    };
+  };
+
+  // Run a function **n** times.
+  _.times = function(n, iteratee, context) {
+    var accum = Array(Math.max(0, n));
+    iteratee = optimizeCb(iteratee, context, 1);
+    for (var i = 0; i < n; i++) accum[i] = iteratee(i);
+    return accum;
+  };
+
+  // Return a random integer between min and max (inclusive).
+  _.random = function(min, max) {
+    if (max == null) {
+      max = min;
+      min = 0;
+    }
+    return min + Math.floor(Math.random() * (max - min + 1));
+  };
+
+  // A (possibly faster) way to get the current timestamp as an integer.
+  _.now = Date.now || function() {
+    return new Date().getTime();
+  };
+
+   // List of HTML entities for escaping.
+  var escapeMap = {
+    '&': '&amp;',
+    '<': '&lt;',
+    '>': '&gt;',
+    '"': '&quot;',
+    "'": '&#x27;',
+    '`': '&#x60;'
+  };
+  var unescapeMap = _.invert(escapeMap);
+
+  // Functions for escaping and unescaping strings to/from HTML interpolation.
+  var createEscaper = function(map) {
+    var escaper = function(match) {
+      return map[match];
+    };
+    // Regexes for identifying a key that needs to be escaped
+    var source = '(?:' + _.keys(map).join('|') + ')';
+    var testRegexp = RegExp(source);
+    var replaceRegexp = RegExp(source, 'g');
+    return function(string) {
+      string = string == null ? '' : '' + string;
+      return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string;
+    };
+  };
+  _.escape = createEscaper(escapeMap);
+  _.unescape = createEscaper(unescapeMap);
+
+  // If the value of the named `property` is a function then invoke it with the
+  // `object` as context; otherwise, return it.
+  _.result = function(object, prop, fallback) {
+    var value = object == null ? void 0 : object[prop];
+    if (value === void 0) {
+      value = fallback;
+    }
+    return _.isFunction(value) ? value.call(object) : value;
+  };
+
+  // Generate a unique integer id (unique within the entire client session).
+  // Useful for temporary DOM ids.
+  var idCounter = 0;
+  _.uniqueId = function(prefix) {
+    var id = ++idCounter + '';
+    return prefix ? prefix + id : id;
+  };
+
+  // By default, Underscore uses ERB-style template delimiters, change the
+  // following template settings to use alternative delimiters.
+  _.templateSettings = {
+    evaluate: /<%([\s\S]+?)%>/g,
+    interpolate: /<%=([\s\S]+?)%>/g,
+    escape: /<%-([\s\S]+?)%>/g
+  };
+
+  // When customizing `templateSettings`, if you don't want to define an
+  // interpolation, evaluation or escaping regex, we need one that is
+  // guaranteed not to match.
+  var noMatch = /(.)^/;
+
+  // Certain characters need to be escaped so that they can be put into a
+  // string literal.
+  var escapes = {
+    "'": "'",
+    '\\': '\\',
+    '\r': 'r',
+    '\n': 'n',
+    '\u2028': 'u2028',
+    '\u2029': 'u2029'
+  };
+
+  var escapeRegExp = /\\|'|\r|\n|\u2028|\u2029/g;
+
+  var escapeChar = function(match) {
+    return '\\' + escapes[match];
+  };
+
+  // JavaScript micro-templating, similar to John Resig's implementation.
+  // Underscore templating handles arbitrary delimiters, preserves whitespace,
+  // and correctly escapes quotes within interpolated code.
+  // NB: `oldSettings` only exists for backwards compatibility.
+  _.template = function(text, settings, oldSettings) {
+    if (!settings && oldSettings) settings = oldSettings;
+    settings = _.defaults({}, settings, _.templateSettings);
+
+    // Combine delimiters into one regular expression via alternation.
+    var matcher = RegExp([
+      (settings.escape || noMatch).source,
+      (settings.interpolate || noMatch).source,
+      (settings.evaluate || noMatch).source
+    ].join('|') + '|$', 'g');
+
+    // Compile the template source, escaping string literals appropriately.
+    var index = 0;
+    var source = "__p+='";
+    text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
+      source += text.slice(index, offset).replace(escapeRegExp, escapeChar);
+      index = offset + match.length;
+
+      if (escape) {
+        source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
+      } else if (interpolate) {
+        source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
+      } else if (evaluate) {
+        source += "';\n" + evaluate + "\n__p+='";
+      }
+
+      // Adobe VMs need the match returned to produce the correct offset.
+      return match;
+    });
+    source += "';\n";
+
+    // If a variable is not specified, place data values in local scope.
+    if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';
+
+    source = "var __t,__p='',__j=Array.prototype.join," +
+      "print=function(){__p+=__j.call(arguments,'');};\n" +
+      source + 'return __p;\n';
+
+    var render;
+    try {
+      render = new Function(settings.variable || 'obj', '_', source);
+    } catch (e) {
+      e.source = source;
+      throw e;
+    }
+
+    var template = function(data) {
+      return render.call(this, data, _);
+    };
+
+    // Provide the compiled source as a convenience for precompilation.
+    var argument = settings.variable || 'obj';
+    template.source = 'function(' + argument + '){\n' + source + '}';
+
+    return template;
+  };
+
+  // Add a "chain" function. Start chaining a wrapped Underscore object.
+  _.chain = function(obj) {
+    var instance = _(obj);
+    instance._chain = true;
+    return instance;
+  };
+
+  // OOP
+  // ---------------
+  // If Underscore is called as a function, it returns a wrapped object that
+  // can be used OO-style. This wrapper holds altered versions of all the
+  // underscore functions. Wrapped objects may be chained.
+
+  // Helper function to continue chaining intermediate results.
+  var chainResult = function(instance, obj) {
+    return instance._chain ? _(obj).chain() : obj;
+  };
+
+  // Add your own custom functions to the Underscore object.
+  _.mixin = function(obj) {
+    _.each(_.functions(obj), function(name) {
+      var func = _[name] = obj[name];
+      _.prototype[name] = function() {
+        var args = [this._wrapped];
+        push.apply(args, arguments);
+        return chainResult(this, func.apply(_, args));
+      };
+    });
+  };
+
+  // Add all of the Underscore functions to the wrapper object.
+  _.mixin(_);
+
+  // Add all mutator Array functions to the wrapper.
+  _.each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {
+    var method = ArrayProto[name];
+    _.prototype[name] = function() {
+      var obj = this._wrapped;
+      method.apply(obj, arguments);
+      if ((name === 'shift' || name === 'splice') && obj.length === 0) delete obj[0];
+      return chainResult(this, obj);
+    };
+  });
+
+  // Add all accessor Array functions to the wrapper.
+  _.each(['concat', 'join', 'slice'], function(name) {
+    var method = ArrayProto[name];
+    _.prototype[name] = function() {
+      return chainResult(this, method.apply(this._wrapped, arguments));
+    };
+  });
+
+  // Extracts the result from a wrapped and chained object.
+  _.prototype.value = function() {
+    return this._wrapped;
+  };
+
+  // Provide unwrapping proxy for some methods used in engine operations
+  // such as arithmetic and JSON stringification.
+  _.prototype.valueOf = _.prototype.toJSON = _.prototype.value;
+
+  _.prototype.toString = function() {
+    return '' + this._wrapped;
+  };
+
+  // AMD registration happens at the end for compatibility with AMD loaders
+  // that may not enforce next-turn semantics on modules. Even though general
+  // practice for AMD registration is to be anonymous, underscore registers
+  // as a named module because, like jQuery, it is a base library that is
+  // popular enough to be bundled in a third party lib, but not be part of
+  // an AMD load request. Those cases could generate an error when an
+  // anonymous define() is called outside of a loader request.
+  if (typeof define == 'function' && define.amd) {
+    define('underscore', [], function() {
+      return _;
+    });
+  }
+}());

+ 1575 - 0
src/js/webrtc-adapter.js

@@ -0,0 +1,1575 @@
+/*
+ *  Copyright (c) 2015 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+
+/* More information about these options at jshint.com/docs/options */
+/* jshint browser: true, camelcase: true, curly: true, devel: true,
+   eqeqeq: true, forin: false, globalstrict: true, node: true,
+   quotmark: single, undef: true, unused: strict */
+/* global mozRTCIceCandidate, mozRTCPeerConnection, Promise,
+mozRTCSessionDescription, webkitRTCPeerConnection, MediaStreamTrack */
+/* exported trace,requestUserMedia */
+
+'use strict';
+
+var getUserMedia = null;
+var attachMediaStream = null;
+var reattachMediaStream = null;
+var webrtcDetectedBrowser = null;
+var webrtcDetectedVersion = null;
+var webrtcMinimumVersion = null;
+var webrtcUtils = {
+  log: function() {
+    // suppress console.log output when being included as a module.
+    if (typeof module !== 'undefined' ||
+        typeof require === 'function' && typeof define === 'function') {
+      return;
+    }
+    console.log.apply(console, arguments);
+  },
+  extractVersion: function(uastring, expr, pos) {
+    var match = uastring.match(expr);
+    return match && match.length >= pos && parseInt(match[pos]);
+  }
+};
+
+function trace(text) {
+  // This function is used for logging.
+  if (text[text.length - 1] === '\n') {
+    text = text.substring(0, text.length - 1);
+  }
+  if (window.performance) {
+    var now = (window.performance.now() / 1000).toFixed(3);
+    webrtcUtils.log(now + ': ' + text);
+  } else {
+    webrtcUtils.log(text);
+  }
+}
+
+if (typeof window === 'object') {
+  if (window.HTMLMediaElement &&
+    !('srcObject' in window.HTMLMediaElement.prototype)) {
+    // Shim the srcObject property, once, when HTMLMediaElement is found.
+    Object.defineProperty(window.HTMLMediaElement.prototype, 'srcObject', {
+      get: function() {
+        // If prefixed srcObject property exists, return it.
+        // Otherwise use the shimmed property, _srcObject
+        return 'mozSrcObject' in this ? this.mozSrcObject : this._srcObject;
+      },
+      set: function(stream) {
+        if ('mozSrcObject' in this) {
+          this.mozSrcObject = stream;
+        } else {
+          // Use _srcObject as a private property for this shim
+          this._srcObject = stream;
+          // TODO: revokeObjectUrl(this.src) when !stream to release resources?
+          this.src = URL.createObjectURL(stream);
+        }
+      }
+    });
+  }
+  // Proxy existing globals
+  getUserMedia = window.navigator && window.navigator.getUserMedia;
+}
+
+// Attach a media stream to an element.
+attachMediaStream = function(element, stream) {
+  element.srcObject = stream;
+};
+
+reattachMediaStream = function(to, from) {
+  to.srcObject = from.srcObject;
+};
+
+if (typeof window === 'undefined' || !window.navigator) {
+  webrtcUtils.log('This does not appear to be a browser');
+  webrtcDetectedBrowser = 'not a browser';
+} else if (navigator.mozGetUserMedia && window.mozRTCPeerConnection) {
+  webrtcUtils.log('This appears to be Firefox');
+
+  webrtcDetectedBrowser = 'firefox';
+
+  // the detected firefox version.
+  webrtcDetectedVersion = webrtcUtils.extractVersion(navigator.userAgent,
+      /Firefox\/([0-9]+)\./, 1);
+
+  // the minimum firefox version still supported by adapter.
+  webrtcMinimumVersion = 31;
+
+  // The RTCPeerConnection object.
+  window.RTCPeerConnection = function(pcConfig, pcConstraints) {
+    if (webrtcDetectedVersion < 38) {
+      // .urls is not supported in FF < 38.
+      // create RTCIceServers with a single url.
+      if (pcConfig && pcConfig.iceServers) {
+        var newIceServers = [];
+        for (var i = 0; i < pcConfig.iceServers.length; i++) {
+          var server = pcConfig.iceServers[i];
+          if (server.hasOwnProperty('urls')) {
+            for (var j = 0; j < server.urls.length; j++) {
+              var newServer = {
+                url: server.urls[j]
+              };
+              if (server.urls[j].indexOf('turn') === 0) {
+                newServer.username = server.username;
+                newServer.credential = server.credential;
+              }
+              newIceServers.push(newServer);
+            }
+          } else {
+            newIceServers.push(pcConfig.iceServers[i]);
+          }
+        }
+        pcConfig.iceServers = newIceServers;
+      }
+    }
+    return new mozRTCPeerConnection(pcConfig, pcConstraints); // jscs:ignore requireCapitalizedConstructors
+  };
+
+  // The RTCSessionDescription object.
+  if (!window.RTCSessionDescription) {
+    window.RTCSessionDescription = mozRTCSessionDescription;
+  }
+
+  // The RTCIceCandidate object.
+  if (!window.RTCIceCandidate) {
+    window.RTCIceCandidate = mozRTCIceCandidate;
+  }
+
+  // getUserMedia constraints shim.
+  getUserMedia = function(constraints, onSuccess, onError) {
+    var constraintsToFF37 = function(c) {
+      if (typeof c !== 'object' || c.require) {
+        return c;
+      }
+      var require = [];
+      Object.keys(c).forEach(function(key) {
+        if (key === 'require' || key === 'advanced' || key === 'mediaSource') {
+          return;
+        }
+        var r = c[key] = (typeof c[key] === 'object') ?
+            c[key] : {ideal: c[key]};
+        if (r.min !== undefined ||
+            r.max !== undefined || r.exact !== undefined) {
+          require.push(key);
+        }
+        if (r.exact !== undefined) {
+          if (typeof r.exact === 'number') {
+            r.min = r.max = r.exact;
+          } else {
+            c[key] = r.exact;
+          }
+          delete r.exact;
+        }
+        if (r.ideal !== undefined) {
+          c.advanced = c.advanced || [];
+          var oc = {};
+          if (typeof r.ideal === 'number') {
+            oc[key] = {min: r.ideal, max: r.ideal};
+          } else {
+            oc[key] = r.ideal;
+          }
+          c.advanced.push(oc);
+          delete r.ideal;
+          if (!Object.keys(r).length) {
+            delete c[key];
+          }
+        }
+      });
+      if (require.length) {
+        c.require = require;
+      }
+      return c;
+    };
+    if (webrtcDetectedVersion < 38) {
+      webrtcUtils.log('spec: ' + JSON.stringify(constraints));
+      if (constraints.audio) {
+        constraints.audio = constraintsToFF37(constraints.audio);
+      }
+      if (constraints.video) {
+        constraints.video = constraintsToFF37(constraints.video);
+      }
+      webrtcUtils.log('ff37: ' + JSON.stringify(constraints));
+    }
+    return navigator.mozGetUserMedia(constraints, onSuccess, onError);
+  };
+
+  navigator.getUserMedia = getUserMedia;
+
+  // Shim for mediaDevices on older versions.
+  if (!navigator.mediaDevices) {
+    navigator.mediaDevices = {getUserMedia: requestUserMedia,
+      addEventListener: function() { },
+      removeEventListener: function() { }
+    };
+  }
+  navigator.mediaDevices.enumerateDevices =
+      navigator.mediaDevices.enumerateDevices || function() {
+    return new Promise(function(resolve) {
+      var infos = [
+        {kind: 'audioinput', deviceId: 'default', label: '', groupId: ''},
+        {kind: 'videoinput', deviceId: 'default', label: '', groupId: ''}
+      ];
+      resolve(infos);
+    });
+  };
+
+  if (webrtcDetectedVersion < 41) {
+    // Work around http://bugzil.la/1169665
+    var orgEnumerateDevices =
+        navigator.mediaDevices.enumerateDevices.bind(navigator.mediaDevices);
+    navigator.mediaDevices.enumerateDevices = function() {
+      return orgEnumerateDevices().then(undefined, function(e) {
+        if (e.name === 'NotFoundError') {
+          return [];
+        }
+        throw e;
+      });
+    };
+  }
+} else if (navigator.webkitGetUserMedia && window.webkitRTCPeerConnection) {
+  webrtcUtils.log('This appears to be Chrome');
+
+  webrtcDetectedBrowser = 'chrome';
+
+  // the detected chrome version.
+  webrtcDetectedVersion = webrtcUtils.extractVersion(navigator.userAgent,
+      /Chrom(e|ium)\/([0-9]+)\./, 2);
+
+  // the minimum chrome version still supported by adapter.
+  webrtcMinimumVersion = 38;
+
+  // The RTCPeerConnection object.
+  window.RTCPeerConnection = function(pcConfig, pcConstraints) {
+    // Translate iceTransportPolicy to iceTransports,
+    // see https://code.google.com/p/webrtc/issues/detail?id=4869
+    if (pcConfig && pcConfig.iceTransportPolicy) {
+      pcConfig.iceTransports = pcConfig.iceTransportPolicy;
+    }
+
+    var pc = new webkitRTCPeerConnection(pcConfig, pcConstraints); // jscs:ignore requireCapitalizedConstructors
+    var origGetStats = pc.getStats.bind(pc);
+    pc.getStats = function(selector, successCallback, errorCallback) { // jshint ignore: line
+      var self = this;
+      var args = arguments;
+
+      // If selector is a function then we are in the old style stats so just
+      // pass back the original getStats format to avoid breaking old users.
+      if (arguments.length > 0 && typeof selector === 'function') {
+        return origGetStats(selector, successCallback);
+      }
+
+      var fixChromeStats = function(response) {
+        var standardReport = {};
+        var reports = response.result();
+        reports.forEach(function(report) {
+          var standardStats = {
+            id: report.id,
+            timestamp: report.timestamp,
+            type: report.type
+          };
+          report.names().forEach(function(name) {
+            standardStats[name] = report.stat(name);
+          });
+          standardReport[standardStats.id] = standardStats;
+        });
+
+        return standardReport;
+      };
+
+      if (arguments.length >= 2) {
+        var successCallbackWrapper = function(response) {
+          args[1](fixChromeStats(response));
+        };
+
+        return origGetStats.apply(this, [successCallbackWrapper, arguments[0]]);
+      }
+
+      // promise-support
+      return new Promise(function(resolve, reject) {
+        if (args.length === 1 && selector === null) {
+          origGetStats.apply(self, [
+              function(response) {
+                resolve.apply(null, [fixChromeStats(response)]);
+              }, reject]);
+        } else {
+          origGetStats.apply(self, [resolve, reject]);
+        }
+      });
+    };
+
+    return pc;
+  };
+
+  // add promise support
+  ['createOffer', 'createAnswer'].forEach(function(method) {
+    var nativeMethod = webkitRTCPeerConnection.prototype[method];
+    webkitRTCPeerConnection.prototype[method] = function() {
+      var self = this;
+      if (arguments.length < 1 || (arguments.length === 1 &&
+          typeof(arguments[0]) === 'object')) {
+        var opts = arguments.length === 1 ? arguments[0] : undefined;
+        return new Promise(function(resolve, reject) {
+          nativeMethod.apply(self, [resolve, reject, opts]);
+        });
+      } else {
+        return nativeMethod.apply(this, arguments);
+      }
+    };
+  });
+
+  ['setLocalDescription', 'setRemoteDescription',
+      'addIceCandidate'].forEach(function(method) {
+    var nativeMethod = webkitRTCPeerConnection.prototype[method];
+    webkitRTCPeerConnection.prototype[method] = function() {
+      var args = arguments;
+      var self = this;
+      return new Promise(function(resolve, reject) {
+        nativeMethod.apply(self, [args[0],
+            function() {
+              resolve();
+              if (args.length >= 2) {
+                args[1].apply(null, []);
+              }
+            },
+            function(err) {
+              reject(err);
+              if (args.length >= 3) {
+                args[2].apply(null, [err]);
+              }
+            }]
+          );
+      });
+    };
+  });
+
+  // getUserMedia constraints shim.
+  var constraintsToChrome = function(c) {
+    if (typeof c !== 'object' || c.mandatory || c.optional) {
+      return c;
+    }
+    var cc = {};
+    Object.keys(c).forEach(function(key) {
+      if (key === 'require' || key === 'advanced' || key === 'mediaSource') {
+        return;
+      }
+      var r = (typeof c[key] === 'object') ? c[key] : {ideal: c[key]};
+      if (r.exact !== undefined && typeof r.exact === 'number') {
+        r.min = r.max = r.exact;
+      }
+      var oldname = function(prefix, name) {
+        if (prefix) {
+          return prefix + name.charAt(0).toUpperCase() + name.slice(1);
+        }
+        return (name === 'deviceId') ? 'sourceId' : name;
+      };
+      if (r.ideal !== undefined) {
+        cc.optional = cc.optional || [];
+        var oc = {};
+        if (typeof r.ideal === 'number') {
+          oc[oldname('min', key)] = r.ideal;
+          cc.optional.push(oc);
+          oc = {};
+          oc[oldname('max', key)] = r.ideal;
+          cc.optional.push(oc);
+        } else {
+          oc[oldname('', key)] = r.ideal;
+          cc.optional.push(oc);
+        }
+      }
+      if (r.exact !== undefined && typeof r.exact !== 'number') {
+        cc.mandatory = cc.mandatory || {};
+        cc.mandatory[oldname('', key)] = r.exact;
+      } else {
+        ['min', 'max'].forEach(function(mix) {
+          if (r[mix] !== undefined) {
+            cc.mandatory = cc.mandatory || {};
+            cc.mandatory[oldname(mix, key)] = r[mix];
+          }
+        });
+      }
+    });
+    if (c.advanced) {
+      cc.optional = (cc.optional || []).concat(c.advanced);
+    }
+    return cc;
+  };
+
+  getUserMedia = function(constraints, onSuccess, onError) {
+    if (constraints.audio) {
+      constraints.audio = constraintsToChrome(constraints.audio);
+    }
+    if (constraints.video) {
+      constraints.video = constraintsToChrome(constraints.video);
+    }
+    webrtcUtils.log('chrome: ' + JSON.stringify(constraints));
+    return navigator.webkitGetUserMedia(constraints, onSuccess, onError);
+  };
+  navigator.getUserMedia = getUserMedia;
+
+  if (!navigator.mediaDevices) {
+    navigator.mediaDevices = {getUserMedia: requestUserMedia,
+                              enumerateDevices: function() {
+      return new Promise(function(resolve) {
+        var kinds = {audio: 'audioinput', video: 'videoinput'};
+        return MediaStreamTrack.getSources(function(devices) {
+          resolve(devices.map(function(device) {
+            return {label: device.label,
+                    kind: kinds[device.kind],
+                    deviceId: device.id,
+                    groupId: ''};
+          }));
+        });
+      });
+    }};
+  }
+
+  // A shim for getUserMedia method on the mediaDevices object.
+  // TODO(KaptenJansson) remove once implemented in Chrome stable.
+  if (!navigator.mediaDevices.getUserMedia) {
+    navigator.mediaDevices.getUserMedia = function(constraints) {
+      return requestUserMedia(constraints);
+    };
+  } else {
+    // Even though Chrome 45 has navigator.mediaDevices and a getUserMedia
+    // function which returns a Promise, it does not accept spec-style
+    // constraints.
+    var origGetUserMedia = navigator.mediaDevices.getUserMedia.
+        bind(navigator.mediaDevices);
+    navigator.mediaDevices.getUserMedia = function(c) {
+      webrtcUtils.log('spec:   ' + JSON.stringify(c)); // whitespace for alignment
+      c.audio = constraintsToChrome(c.audio);
+      c.video = constraintsToChrome(c.video);
+      webrtcUtils.log('chrome: ' + JSON.stringify(c));
+      return origGetUserMedia(c);
+    };
+  }
+
+  // Dummy devicechange event methods.
+  // TODO(KaptenJansson) remove once implemented in Chrome stable.
+  if (typeof navigator.mediaDevices.addEventListener === 'undefined') {
+    navigator.mediaDevices.addEventListener = function() {
+      webrtcUtils.log('Dummy mediaDevices.addEventListener called.');
+    };
+  }
+  if (typeof navigator.mediaDevices.removeEventListener === 'undefined') {
+    navigator.mediaDevices.removeEventListener = function() {
+      webrtcUtils.log('Dummy mediaDevices.removeEventListener called.');
+    };
+  }
+
+  // Attach a media stream to an element.
+  attachMediaStream = function(element, stream) {
+    if (webrtcDetectedVersion >= 43) {
+      element.srcObject = stream;
+    } else if (typeof element.src !== 'undefined') {
+      element.src = URL.createObjectURL(stream);
+    } else {
+      webrtcUtils.log('Error attaching stream to element.');
+    }
+  };
+  reattachMediaStream = function(to, from) {
+    if (webrtcDetectedVersion >= 43) {
+      to.srcObject = from.srcObject;
+    } else {
+      to.src = from.src;
+    }
+  };
+
+} else if (navigator.mediaDevices && navigator.userAgent.match(
+    /Edge\/(\d+).(\d+)$/)) {
+  webrtcUtils.log('This appears to be Edge');
+  webrtcDetectedBrowser = 'edge';
+
+  webrtcDetectedVersion = webrtcUtils.extractVersion(navigator.userAgent,
+      /Edge\/(\d+).(\d+)$/, 2);
+
+  // the minimum version still supported by adapter.
+  webrtcMinimumVersion = 12;
+
+  if (RTCIceGatherer) {
+    window.RTCIceCandidate = function(args) {
+      return args;
+    };
+    window.RTCSessionDescription = function(args) {
+      return args;
+    };
+
+    window.RTCPeerConnection = function(config) {
+      var self = this;
+
+      this.onicecandidate = null;
+      this.onaddstream = null;
+      this.onremovestream = null;
+      this.onsignalingstatechange = null;
+      this.oniceconnectionstatechange = null;
+      this.onnegotiationneeded = null;
+      this.ondatachannel = null;
+
+      this.localStreams = [];
+      this.remoteStreams = [];
+      this.getLocalStreams = function() { return self.localStreams; };
+      this.getRemoteStreams = function() { return self.remoteStreams; };
+
+      this.localDescription = new RTCSessionDescription({
+        type: '',
+        sdp: ''
+      });
+      this.remoteDescription = new RTCSessionDescription({
+        type: '',
+        sdp: ''
+      });
+      this.signalingState = 'stable';
+      this.iceConnectionState = 'new';
+
+      this.iceOptions = {
+        gatherPolicy: 'all',
+        iceServers: []
+      };
+      if (config && config.iceTransportPolicy) {
+        switch (config.iceTransportPolicy) {
+        case 'all':
+        case 'relay':
+          this.iceOptions.gatherPolicy = config.iceTransportPolicy;
+          break;
+        case 'none':
+          // FIXME: remove once implementation and spec have added this.
+          throw new TypeError('iceTransportPolicy "none" not supported');
+        }
+      }
+      if (config && config.iceServers) {
+        this.iceOptions.iceServers = config.iceServers;
+      }
+
+      // per-track iceGathers etc
+      this.mLines = [];
+
+      this._iceCandidates = [];
+
+      this._peerConnectionId = 'PC_' + Math.floor(Math.random() * 65536);
+
+      // FIXME: Should be generated according to spec (guid?)
+      // and be the same for all PCs from the same JS
+      this._cname = Math.random().toString(36).substr(2, 10);
+    };
+
+    window.RTCPeerConnection.prototype.addStream = function(stream) {
+      // clone just in case we're working in a local demo
+      // FIXME: seems to be fixed
+      this.localStreams.push(stream.clone());
+
+      // FIXME: maybe trigger negotiationneeded?
+    };
+
+    window.RTCPeerConnection.prototype.removeStream = function(stream) {
+      var idx = this.localStreams.indexOf(stream);
+      if (idx > -1) {
+        this.localStreams.splice(idx, 1);
+      }
+      // FIXME: maybe trigger negotiationneeded?
+    };
+
+    // SDP helper from sdp-jingle-json with modifications.
+    window.RTCPeerConnection.prototype._toCandidateJSON = function(line) {
+      var parts;
+      if (line.indexOf('a=candidate:') === 0) {
+        parts = line.substring(12).split(' ');
+      } else { // no a=candidate
+        parts = line.substring(10).split(' ');
+      }
+
+      var candidate = {
+        foundation: parts[0],
+        component: parts[1],
+        protocol: parts[2].toLowerCase(),
+        priority: parseInt(parts[3], 10),
+        ip: parts[4],
+        port: parseInt(parts[5], 10),
+        // skip parts[6] == 'typ'
+        type: parts[7]
+        //generation: '0'
+      };
+
+      for (var i = 8; i < parts.length; i += 2) {
+        if (parts[i] === 'raddr') {
+          candidate.relatedAddress = parts[i + 1]; // was: relAddr
+        } else if (parts[i] === 'rport') {
+          candidate.relatedPort = parseInt(parts[i + 1], 10); // was: relPort
+        } else if (parts[i] === 'generation') {
+          candidate.generation = parts[i + 1];
+        } else if (parts[i] === 'tcptype') {
+          candidate.tcpType = parts[i + 1];
+        }
+      }
+      return candidate;
+    };
+
+    // SDP helper from sdp-jingle-json with modifications.
+    window.RTCPeerConnection.prototype._toCandidateSDP = function(candidate) {
+      var sdp = [];
+      sdp.push(candidate.foundation);
+      sdp.push(candidate.component);
+      sdp.push(candidate.protocol.toUpperCase());
+      sdp.push(candidate.priority);
+      sdp.push(candidate.ip);
+      sdp.push(candidate.port);
+
+      var type = candidate.type;
+      sdp.push('typ');
+      sdp.push(type);
+      if (type === 'srflx' || type === 'prflx' || type === 'relay') {
+        if (candidate.relatedAddress && candidate.relatedPort) {
+          sdp.push('raddr');
+          sdp.push(candidate.relatedAddress); // was: relAddr
+          sdp.push('rport');
+          sdp.push(candidate.relatedPort); // was: relPort
+        }
+      }
+      if (candidate.tcpType && candidate.protocol.toUpperCase() === 'TCP') {
+        sdp.push('tcptype');
+        sdp.push(candidate.tcpType);
+      }
+      return 'a=candidate:' + sdp.join(' ');
+    };
+
+    // SDP helper from sdp-jingle-json with modifications.
+    window.RTCPeerConnection.prototype._parseRtpMap = function(line) {
+      var parts = line.substr(9).split(' ');
+      var parsed = {
+        payloadType: parseInt(parts.shift(), 10) // was: id
+      };
+
+      parts = parts[0].split('/');
+
+      parsed.name = parts[0];
+      parsed.clockRate = parseInt(parts[1], 10); // was: clockrate
+      parsed.numChannels = parts.length === 3 ? parseInt(parts[2], 10) : 1; // was: channels
+      return parsed;
+    };
+
+    // Parses SDP to determine capabilities.
+    window.RTCPeerConnection.prototype._getRemoteCapabilities =
+        function(section) {
+      var remoteCapabilities = {
+        codecs: [],
+        headerExtensions: [],
+        fecMechanisms: []
+      };
+      var i;
+      var lines = section.split('\r\n');
+      var mline = lines[0].substr(2).split(' ');
+      var rtpmapFilter = function(line) {
+        return line.indexOf('a=rtpmap:' + mline[i]) === 0;
+      };
+      var fmtpFilter = function(line) {
+        return line.indexOf('a=fmtp:' + mline[i]) === 0;
+      };
+      var parseFmtp = function(line) {
+        var parsed = {};
+        var kv;
+        var parts = line.substr(('a=fmtp:' + mline[i]).length + 1).split(';');
+        for (var j = 0; j < parts.length; j++) {
+          kv = parts[j].split('=');
+          parsed[kv[0].trim()] = kv[1];
+        }
+        console.log('fmtp', mline[i], parsed);
+        return parsed;
+      };
+      var rtcpFbFilter = function(line) {
+        return line.indexOf('a=rtcp-fb:' + mline[i]) === 0;
+      };
+      var parseRtcpFb = function(line) {
+        var parts = line.substr(('a=rtcp-fb:' + mline[i]).length + 1)
+            .split(' ');
+        return {
+          type: parts.shift(),
+          parameter: parts.join(' ')
+        };
+      };
+      for (i = 3; i < mline.length; i++) { // find all codecs from mline[3..]
+        var line = lines.filter(rtpmapFilter)[0];
+        if (line) {
+          var codec = this._parseRtpMap(line);
+
+          var fmtp = lines.filter(fmtpFilter);
+          codec.parameters = fmtp.length ? parseFmtp(fmtp[0]) : {};
+          codec.rtcpFeedback = lines.filter(rtcpFbFilter).map(parseRtcpFb);
+
+          remoteCapabilities.codecs.push(codec);
+        }
+      }
+      return remoteCapabilities;
+    };
+
+    // Serializes capabilities to SDP.
+    window.RTCPeerConnection.prototype._capabilitiesToSDP = function(caps) {
+      var sdp = '';
+      caps.codecs.forEach(function(codec) {
+        var pt = codec.payloadType;
+        if (codec.preferredPayloadType !== undefined) {
+          pt = codec.preferredPayloadType;
+        }
+        sdp += 'a=rtpmap:' + pt +
+            ' ' + codec.name +
+            '/' + codec.clockRate +
+            (codec.numChannels !== 1 ? '/' + codec.numChannels : '') +
+            '\r\n';
+        if (codec.parameters && codec.parameters.length) {
+          sdp += 'a=ftmp:' + pt + ' ';
+          Object.keys(codec.parameters).forEach(function(param) {
+            sdp += param + '=' + codec.parameters[param];
+          });
+          sdp += '\r\n';
+        }
+        if (codec.rtcpFeedback) {
+          // FIXME: special handling for trr-int?
+          codec.rtcpFeedback.forEach(function(fb) {
+            sdp += 'a=rtcp-fb:' + pt + ' ' + fb.type + ' ' +
+                fb.parameter + '\r\n';
+          });
+        }
+      });
+      return sdp;
+    };
+
+    // Calculates the intersection of local and remote capabilities.
+    window.RTCPeerConnection.prototype._getCommonCapabilities =
+        function(localCapabilities, remoteCapabilities) {
+      var commonCapabilities = {
+        codecs: [],
+        headerExtensions: [],
+        fecMechanisms: []
+      };
+      localCapabilities.codecs.forEach(function(lCodec) {
+        for (var i = 0; i < remoteCapabilities.codecs.length; i++) {
+          var rCodec = remoteCapabilities.codecs[i];
+          if (lCodec.name === rCodec.name &&
+              lCodec.clockRate === rCodec.clockRate &&
+              lCodec.numChannels === rCodec.numChannels) {
+            // push rCodec so we reply with offerer payload type
+            commonCapabilities.codecs.push(rCodec);
+
+            // FIXME: also need to calculate intersection between
+            // .rtcpFeedback and .parameters
+            break;
+          }
+        }
+      });
+
+      localCapabilities.headerExtensions.forEach(function(lHeaderExtension) {
+        for (var i = 0; i < remoteCapabilities.headerExtensions.length; i++) {
+          var rHeaderExtension = remoteCapabilities.headerExtensions[i];
+          if (lHeaderExtension.uri === rHeaderExtension.uri) {
+            commonCapabilities.headerExtensions.push(rHeaderExtension);
+            break;
+          }
+        }
+      });
+
+      // FIXME: fecMechanisms
+      return commonCapabilities;
+    };
+
+    // Parses DTLS parameters from SDP section or sessionpart.
+    window.RTCPeerConnection.prototype._getDtlsParameters =
+        function(section, session) {
+      var lines = section.split('\r\n');
+      lines = lines.concat(session.split('\r\n')); // Search in session part, too.
+      var fpLine = lines.filter(function(line) {
+        return line.indexOf('a=fingerprint:') === 0;
+      });
+      fpLine = fpLine[0].substr(14);
+      var dtlsParameters = {
+        role: 'auto',
+        fingerprints: [{
+          algorithm: fpLine.split(' ')[0],
+          value: fpLine.split(' ')[1]
+        }]
+      };
+      return dtlsParameters;
+    };
+
+    // Serializes DTLS parameters to SDP.
+    window.RTCPeerConnection.prototype._dtlsParametersToSDP =
+        function(params, setupType) {
+      var sdp = 'a=setup:' + setupType + '\r\n';
+      params.fingerprints.forEach(function(fp) {
+        sdp += 'a=fingerprint:' + fp.algorithm + ' ' + fp.value + '\r\n';
+      });
+      return sdp;
+    };
+
+    // Parses ICE information from SDP section or sessionpart.
+    window.RTCPeerConnection.prototype._getIceParameters =
+        function(section, session) {
+      var lines = section.split('\r\n');
+      lines = lines.concat(session.split('\r\n')); // Search in session part, too.
+      var iceParameters = {
+        usernameFragment: lines.filter(function(line) {
+          return line.indexOf('a=ice-ufrag:') === 0;
+        })[0].substr(12),
+        password: lines.filter(function(line) {
+          return line.indexOf('a=ice-pwd:') === 0;
+        })[0].substr(10),
+      };
+      return iceParameters;
+    };
+
+    // Serializes ICE parameters to SDP.
+    window.RTCPeerConnection.prototype._iceParametersToSDP = function(params) {
+      return 'a=ice-ufrag:' + params.usernameFragment + '\r\n' +
+          'a=ice-pwd:' + params.password + '\r\n';
+    };
+
+    window.RTCPeerConnection.prototype._getEncodingParameters = function(ssrc) {
+      return {
+        ssrc: ssrc,
+        codecPayloadType: 0,
+        fec: 0,
+        rtx: 0,
+        priority: 1.0,
+        maxBitrate: 2000000.0,
+        minQuality: 0,
+        framerateBias: 0.5,
+        resolutionScale: 1.0,
+        framerateScale: 1.0,
+        active: true,
+        dependencyEncodingId: undefined,
+        encodingId: undefined
+      };
+    };
+
+    // Create ICE gatherer, ICE transport and DTLS transport.
+    window.RTCPeerConnection.prototype._createIceAndDtlsTransports =
+        function(mid, sdpMLineIndex) {
+      var self = this;
+      var iceGatherer = new RTCIceGatherer(self.iceOptions);
+      var iceTransport = new RTCIceTransport(iceGatherer);
+      iceGatherer.onlocalcandidate = function(evt) {
+        var event = {};
+        event.candidate = {sdpMid: mid, sdpMLineIndex: sdpMLineIndex};
+
+        var cand = evt.candidate;
+        var isEndOfCandidates = !(cand && Object.keys(cand).length > 0);
+        if (isEndOfCandidates) {
+          event.candidate.candidate =
+              'candidate:1 1 udp 1 0.0.0.0 9 typ endOfCandidates';
+        } else {
+          // RTCIceCandidate doesn't have a component, needs to be added
+          cand.component = iceTransport.component === 'RTCP' ? 2 : 1;
+          event.candidate.candidate = self._toCandidateSDP(cand);
+        }
+        if (self.onicecandidate !== null) {
+          if (self.localDescription && self.localDescription.type === '') {
+            self._iceCandidates.push(event);
+          } else {
+            self.onicecandidate(event);
+          }
+        }
+      };
+      iceTransport.onicestatechange = function() {
+        /*
+        console.log(self._peerConnectionId,
+            'ICE state change', iceTransport.state);
+        */
+        self._updateIceConnectionState(iceTransport.state);
+      };
+
+      var dtlsTransport = new RTCDtlsTransport(iceTransport);
+      dtlsTransport.ondtlsstatechange = function() {
+        /*
+        console.log(self._peerConnectionId, sdpMLineIndex,
+            'dtls state change', dtlsTransport.state);
+        */
+      };
+      dtlsTransport.onerror = function(error) {
+        console.error('dtls error', error);
+      };
+      return {
+        iceGatherer: iceGatherer,
+        iceTransport: iceTransport,
+        dtlsTransport: dtlsTransport
+      };
+    };
+
+    window.RTCPeerConnection.prototype.setLocalDescription =
+        function(description) {
+      var self = this;
+      if (description.type === 'offer') {
+        if (!description.ortc) {
+          // FIXME: throw?
+        } else {
+          this.mLines = description.ortc;
+        }
+      } else if (description.type === 'answer') {
+        var sections = self.remoteDescription.sdp.split('\r\nm=');
+        var sessionpart = sections.shift();
+        sections.forEach(function(section, sdpMLineIndex) {
+          section = 'm=' + section;
+
+          var iceGatherer = self.mLines[sdpMLineIndex].iceGatherer;
+          var iceTransport = self.mLines[sdpMLineIndex].iceTransport;
+          var dtlsTransport = self.mLines[sdpMLineIndex].dtlsTransport;
+          var rtpSender = self.mLines[sdpMLineIndex].rtpSender;
+          var localCapabilities =
+              self.mLines[sdpMLineIndex].localCapabilities;
+          var remoteCapabilities =
+              self.mLines[sdpMLineIndex].remoteCapabilities;
+          var sendSSRC = self.mLines[sdpMLineIndex].sendSSRC;
+          var recvSSRC = self.mLines[sdpMLineIndex].recvSSRC;
+
+          var remoteIceParameters = self._getIceParameters(section,
+              sessionpart);
+          iceTransport.start(iceGatherer, remoteIceParameters, 'controlled');
+
+          var remoteDtlsParameters = self._getDtlsParameters(section,
+              sessionpart);
+          dtlsTransport.start(remoteDtlsParameters);
+
+          if (rtpSender) {
+            // calculate intersection of capabilities
+            var params = self._getCommonCapabilities(localCapabilities,
+                remoteCapabilities);
+            params.muxId = sendSSRC;
+            params.encodings = [self._getEncodingParameters(sendSSRC)];
+            params.rtcp = {
+              cname: self._cname,
+              reducedSize: false,
+              ssrc: recvSSRC,
+              mux: true
+            };
+            rtpSender.send(params);
+          }
+        });
+      }
+
+      this.localDescription = description;
+      switch (description.type) {
+      case 'offer':
+        this._updateSignalingState('have-local-offer');
+        break;
+      case 'answer':
+        this._updateSignalingState('stable');
+        break;
+      }
+
+      // FIXME: need to _reliably_ execute after args[1] or promise
+      window.setTimeout(function() {
+        // FIXME: need to apply ice candidates in a way which is async but in-order
+        self._iceCandidates.forEach(function(event) {
+          if (self.onicecandidate !== null) {
+            self.onicecandidate(event);
+          }
+        });
+        self._iceCandidates = [];
+      }, 50);
+      if (arguments.length > 1 && typeof arguments[1] === 'function') {
+        window.setTimeout(arguments[1], 0);
+      }
+      return new Promise(function(resolve) {
+        resolve();
+      });
+    };
+
+    window.RTCPeerConnection.prototype.setRemoteDescription =
+        function(description) {
+      // FIXME: for type=offer this creates state. which should not
+      //  happen before SLD with type=answer but... we need the stream
+      //  here for onaddstream.
+      var self = this;
+      var sections = description.sdp.split('\r\nm=');
+      var sessionpart = sections.shift();
+      var stream = new MediaStream();
+      sections.forEach(function(section, sdpMLineIndex) {
+        section = 'm=' + section;
+        var lines = section.split('\r\n');
+        var mline = lines[0].substr(2).split(' ');
+        var kind = mline[0];
+        var line;
+
+        var iceGatherer;
+        var iceTransport;
+        var dtlsTransport;
+        var rtpSender;
+        var rtpReceiver;
+        var sendSSRC;
+        var recvSSRC;
+
+        var mid = lines.filter(function(line) {
+          return line.indexOf('a=mid:') === 0;
+        })[0].substr(6);
+
+        var cname;
+
+        var remoteCapabilities;
+        var params;
+
+        if (description.type === 'offer') {
+          var transports = self._createIceAndDtlsTransports(mid, sdpMLineIndex);
+
+          var localCapabilities = RTCRtpReceiver.getCapabilities(kind);
+          // determine remote caps from SDP
+          remoteCapabilities = self._getRemoteCapabilities(section);
+
+          line = lines.filter(function(line) {
+            return line.indexOf('a=ssrc:') === 0 &&
+                line.split(' ')[1].indexOf('cname:') === 0;
+          });
+          sendSSRC = (2 * sdpMLineIndex + 2) * 1001;
+          if (line) { // FIXME: alot of assumptions here
+            recvSSRC = line[0].split(' ')[0].split(':')[1];
+            cname = line[0].split(' ')[1].split(':')[1];
+          }
+          rtpReceiver = new RTCRtpReceiver(transports.dtlsTransport, kind);
+
+          // calculate intersection so no unknown caps get passed into the RTPReciver
+          params = self._getCommonCapabilities(localCapabilities,
+              remoteCapabilities);
+
+          params.muxId = recvSSRC;
+          params.encodings = [self._getEncodingParameters(recvSSRC)];
+          params.rtcp = {
+            cname: cname,
+            reducedSize: false,
+            ssrc: sendSSRC,
+            mux: true
+          };
+          rtpReceiver.receive(params);
+          // FIXME: not correct when there are multiple streams but that is
+          // not currently supported.
+          stream.addTrack(rtpReceiver.track);
+
+          // FIXME: honor a=sendrecv
+          if (self.localStreams.length > 0 &&
+              self.localStreams[0].getTracks().length >= sdpMLineIndex) {
+            // FIXME: actually more complicated, needs to match types etc
+            var localtrack = self.localStreams[0].getTracks()[sdpMLineIndex];
+            rtpSender = new RTCRtpSender(localtrack, transports.dtlsTransport);
+          }
+
+          self.mLines[sdpMLineIndex] = {
+            iceGatherer: transports.iceGatherer,
+            iceTransport: transports.iceTransport,
+            dtlsTransport: transports.dtlsTransport,
+            localCapabilities: localCapabilities,
+            remoteCapabilities: remoteCapabilities,
+            rtpSender: rtpSender,
+            rtpReceiver: rtpReceiver,
+            kind: kind,
+            mid: mid,
+            sendSSRC: sendSSRC,
+            recvSSRC: recvSSRC
+          };
+        } else {
+          iceGatherer = self.mLines[sdpMLineIndex].iceGatherer;
+          iceTransport = self.mLines[sdpMLineIndex].iceTransport;
+          dtlsTransport = self.mLines[sdpMLineIndex].dtlsTransport;
+          rtpSender = self.mLines[sdpMLineIndex].rtpSender;
+          rtpReceiver = self.mLines[sdpMLineIndex].rtpReceiver;
+          sendSSRC = self.mLines[sdpMLineIndex].sendSSRC;
+          recvSSRC = self.mLines[sdpMLineIndex].recvSSRC;
+        }
+
+        var remoteIceParameters = self._getIceParameters(section, sessionpart);
+        var remoteDtlsParameters = self._getDtlsParameters(section,
+            sessionpart);
+
+        // for answers we start ice and dtls here, otherwise this is done in SLD
+        if (description.type === 'answer') {
+          iceTransport.start(iceGatherer, remoteIceParameters, 'controlling');
+          dtlsTransport.start(remoteDtlsParameters);
+
+          // determine remote caps from SDP
+          remoteCapabilities = self._getRemoteCapabilities(section);
+          // FIXME: store remote caps?
+
+          if (rtpSender) {
+            params = remoteCapabilities;
+            params.muxId = sendSSRC;
+            params.encodings = [self._getEncodingParameters(sendSSRC)];
+            params.rtcp = {
+              cname: self._cname,
+              reducedSize: false,
+              ssrc: recvSSRC,
+              mux: true
+            };
+            rtpSender.send(params);
+          }
+
+          // FIXME: only if a=sendrecv
+          var bidi = lines.filter(function(line) {
+            return line.indexOf('a=ssrc:') === 0;
+          }).length > 0;
+          if (rtpReceiver && bidi) {
+            line = lines.filter(function(line) {
+              return line.indexOf('a=ssrc:') === 0 &&
+                  line.split(' ')[1].indexOf('cname:') === 0;
+            });
+            if (line) { // FIXME: alot of assumptions here
+              recvSSRC = line[0].split(' ')[0].split(':')[1];
+              cname = line[0].split(' ')[1].split(':')[1];
+            }
+            params = remoteCapabilities;
+            params.muxId = recvSSRC;
+            params.encodings = [self._getEncodingParameters(recvSSRC)];
+            params.rtcp = {
+              cname: cname,
+              reducedSize: false,
+              ssrc: sendSSRC,
+              mux: true
+            };
+            rtpReceiver.receive(params, kind);
+            stream.addTrack(rtpReceiver.track);
+            self.mLines[sdpMLineIndex].recvSSRC = recvSSRC;
+          }
+        }
+      });
+
+      this.remoteDescription = description;
+      switch (description.type) {
+      case 'offer':
+        this._updateSignalingState('have-remote-offer');
+        break;
+      case 'answer':
+        this._updateSignalingState('stable');
+        break;
+      }
+      window.setTimeout(function() {
+        if (self.onaddstream !== null && stream.getTracks().length) {
+          self.remoteStreams.push(stream);
+          window.setTimeout(function() {
+            self.onaddstream({stream: stream});
+          }, 0);
+        }
+      }, 0);
+      if (arguments.length > 1 && typeof arguments[1] === 'function') {
+        window.setTimeout(arguments[1], 0);
+      }
+      return new Promise(function(resolve) {
+        resolve();
+      });
+    };
+
+    window.RTCPeerConnection.prototype.close = function() {
+      this.mLines.forEach(function(mLine) {
+        /* not yet
+        if (mLine.iceGatherer) {
+          mLine.iceGatherer.close();
+        }
+        */
+        if (mLine.iceTransport) {
+          mLine.iceTransport.stop();
+        }
+        if (mLine.dtlsTransport) {
+          mLine.dtlsTransport.stop();
+        }
+        if (mLine.rtpSender) {
+          mLine.rtpSender.stop();
+        }
+        if (mLine.rtpReceiver) {
+          mLine.rtpReceiver.stop();
+        }
+      });
+      // FIXME: clean up tracks, local streams, remote streams, etc
+      this._updateSignalingState('closed');
+      this._updateIceConnectionState('closed');
+    };
+
+    // Update the signaling state.
+    window.RTCPeerConnection.prototype._updateSignalingState =
+        function(newState) {
+      this.signalingState = newState;
+      if (this.onsignalingstatechange !== null) {
+        this.onsignalingstatechange();
+      }
+    };
+
+    // Update the ICE connection state.
+    // FIXME: should be called 'updateConnectionState', also be called for
+    //  DTLS changes and implement
+    //  https://lists.w3.org/Archives/Public/public-webrtc/2015Sep/0033.html
+    window.RTCPeerConnection.prototype._updateIceConnectionState =
+        function(newState) {
+      var self = this;
+      if (this.iceConnectionState !== newState) {
+        var agreement = self.mLines.every(function(mLine) {
+          return mLine.iceTransport.state === newState;
+        });
+        if (agreement) {
+          self.iceConnectionState = newState;
+          if (this.oniceconnectionstatechange !== null) {
+            this.oniceconnectionstatechange();
+          }
+        }
+      }
+    };
+
+    window.RTCPeerConnection.prototype.createOffer = function() {
+      var self = this;
+      var offerOptions;
+      if (arguments.length === 1 && typeof arguments[0] !== 'function') {
+        offerOptions = arguments[0];
+      } else if (arguments.length === 3) {
+        offerOptions = arguments[2];
+      }
+
+      var tracks = [];
+      var numAudioTracks = 0;
+      var numVideoTracks = 0;
+      // Default to sendrecv.
+      if (this.localStreams.length) {
+        numAudioTracks = this.localStreams[0].getAudioTracks().length;
+        numVideoTracks = this.localStreams[0].getAudioTracks().length;
+      }
+      // Determine number of audio and video tracks we need to send/recv.
+      if (offerOptions) {
+        // Deal with Chrome legacy constraints...
+        if (offerOptions.mandatory) {
+          if (offerOptions.mandatory.OfferToReceiveAudio) {
+            numAudioTracks = 1;
+          } else if (offerOptions.mandatory.OfferToReceiveAudio === false) {
+            numAudioTracks = 0;
+          }
+          if (offerOptions.mandatory.OfferToReceiveVideo) {
+            numVideoTracks = 1;
+          } else if (offerOptions.mandatory.OfferToReceiveVideo === false) {
+            numVideoTracks = 0;
+          }
+        } else {
+          if (offerOptions.offerToReceiveAudio !== undefined) {
+            numAudioTracks = offerOptions.offerToReceiveAudio;
+          }
+          if (offerOptions.offerToReceiveVideo !== undefined) {
+            numVideoTracks = offerOptions.offerToReceiveVideo;
+          }
+        }
+      }
+      if (this.localStreams.length) {
+        // Push local streams.
+        this.localStreams[0].getTracks().forEach(function(track) {
+          tracks.push({
+            kind: track.kind,
+            track: track,
+            wantReceive: track.kind === 'audio' ?
+                numAudioTracks > 0 : numVideoTracks > 0
+          });
+          if (track.kind === 'audio') {
+            numAudioTracks--;
+          } else if (track.kind === 'video') {
+            numVideoTracks--;
+          }
+        });
+      }
+      // Create M-lines for recvonly streams.
+      while (numAudioTracks > 0 || numVideoTracks > 0) {
+        if (numAudioTracks > 0) {
+          tracks.push({
+            kind: 'audio',
+            wantReceive: true
+          });
+          numAudioTracks--;
+        }
+        if (numVideoTracks > 0) {
+          tracks.push({
+            kind: 'video',
+            wantReceive: true
+          });
+          numVideoTracks--;
+        }
+      }
+
+      var sdp = 'v=0\r\n' +
+          'o=thisisadapterortc 8169639915646943137 2 IN IP4 127.0.0.1\r\n' +
+          's=-\r\n' +
+          't=0 0\r\n';
+      var mLines = [];
+      tracks.forEach(function(mline, sdpMLineIndex) {
+        // For each track, create an ice gatherer, ice transport, dtls transport,
+        // potentially rtpsender and rtpreceiver.
+        var track = mline.track;
+        var kind = mline.kind;
+        var mid = Math.random().toString(36).substr(2, 10);
+
+        var transports = self._createIceAndDtlsTransports(mid, sdpMLineIndex);
+
+        var localCapabilities = RTCRtpSender.getCapabilities(kind);
+        var rtpSender;
+        // generate an ssrc now, to be used later in rtpSender.send
+        var sendSSRC = (2 * sdpMLineIndex + 1) * 1001; //Math.floor(Math.random()*4294967295);
+        var recvSSRC; // don't know yet
+        if (track) {
+          rtpSender = new RTCRtpSender(track, transports.dtlsTransport);
+        }
+
+        var rtpReceiver;
+        if (mline.wantReceive) {
+          rtpReceiver = new RTCRtpReceiver(transports.dtlsTransport, kind);
+        }
+
+        mLines[sdpMLineIndex] = {
+          iceGatherer: transports.iceGatherer,
+          iceTransport: transports.iceTransport,
+          dtlsTransport: transports.dtlsTransport,
+          localCapabilities: localCapabilities,
+          remoteCapabilities: null,
+          rtpSender: rtpSender,
+          rtpReceiver: rtpReceiver,
+          kind: kind,
+          mid: mid,
+          sendSSRC: sendSSRC,
+          recvSSRC: recvSSRC
+        };
+
+        // Map things to SDP.
+        // Build the mline.
+        sdp += 'm=' + kind + ' 9 UDP/TLS/RTP/SAVPF ';
+        sdp += localCapabilities.codecs.map(function(codec) {
+          return codec.preferredPayloadType;
+        }).join(' ') + '\r\n';
+
+        sdp += 'c=IN IP4 0.0.0.0\r\n';
+        sdp += 'a=rtcp:9 IN IP4 0.0.0.0\r\n';
+
+        // Map ICE parameters (ufrag, pwd) to SDP.
+        sdp += self._iceParametersToSDP(
+            transports.iceGatherer.getLocalParameters());
+
+        // Map DTLS parameters to SDP.
+        sdp += self._dtlsParametersToSDP(
+            transports.dtlsTransport.getLocalParameters(), 'actpass');
+
+        sdp += 'a=mid:' + mid + '\r\n';
+
+        if (rtpSender && rtpReceiver) {
+          sdp += 'a=sendrecv\r\n';
+        } else if (rtpSender) {
+          sdp += 'a=sendonly\r\n';
+        } else if (rtpReceiver) {
+          sdp += 'a=recvonly\r\n';
+        } else {
+          sdp += 'a=inactive\r\n';
+        }
+        sdp += 'a=rtcp-mux\r\n';
+
+        // Add a=rtpmap lines for each codec. Also fmtp and rtcp-fb.
+        sdp += self._capabilitiesToSDP(localCapabilities);
+
+        if (track) {
+          sdp += 'a=msid:' + self.localStreams[0].id + ' ' + track.id + '\r\n';
+          sdp += 'a=ssrc:' + sendSSRC + ' ' + 'msid:' +
+              self.localStreams[0].id + ' ' + track.id + '\r\n';
+        }
+        sdp += 'a=ssrc:' + sendSSRC + ' cname:' + self._cname + '\r\n';
+      });
+
+      var desc = new RTCSessionDescription({
+        type: 'offer',
+        sdp: sdp,
+        ortc: mLines
+      });
+      if (arguments.length && typeof arguments[0] === 'function') {
+        window.setTimeout(arguments[0], 0, desc);
+      }
+      return new Promise(function(resolve) {
+        resolve(desc);
+      });
+    };
+
+    window.RTCPeerConnection.prototype.createAnswer = function() {
+      var self = this;
+      var answerOptions;
+      if (arguments.length === 1 && typeof arguments[0] !== 'function') {
+        answerOptions = arguments[0];
+      } else if (arguments.length === 3) {
+        answerOptions = arguments[2];
+      }
+
+      var sdp = 'v=0\r\n' +
+          'o=thisisadapterortc 8169639915646943137 2 IN IP4 127.0.0.1\r\n' +
+          's=-\r\n' +
+          't=0 0\r\n';
+      this.mLines.forEach(function(mLine/*, sdpMLineIndex*/) {
+        var iceGatherer = mLine.iceGatherer;
+        //var iceTransport = mLine.iceTransport;
+        var dtlsTransport = mLine.dtlsTransport;
+        var localCapabilities = mLine.localCapabilities;
+        var remoteCapabilities = mLine.remoteCapabilities;
+        var rtpSender = mLine.rtpSender;
+        var rtpReceiver = mLine.rtpReceiver;
+        var kind = mLine.kind;
+        var sendSSRC = mLine.sendSSRC;
+        //var recvSSRC = mLine.recvSSRC;
+
+        // Calculate intersection of capabilities.
+        var commonCapabilities = self._getCommonCapabilities(localCapabilities,
+            remoteCapabilities);
+
+        // Map things to SDP.
+        // Build the mline.
+        sdp += 'm=' + kind + ' 9 UDP/TLS/RTP/SAVPF ';
+        sdp += commonCapabilities.codecs.map(function(codec) {
+          return codec.payloadType;
+        }).join(' ') + '\r\n';
+
+        sdp += 'c=IN IP4 0.0.0.0\r\n';
+        sdp += 'a=rtcp:9 IN IP4 0.0.0.0\r\n';
+
+        // Map ICE parameters (ufrag, pwd) to SDP.
+        sdp += self._iceParametersToSDP(iceGatherer.getLocalParameters());
+
+        // Map DTLS parameters to SDP.
+        sdp += self._dtlsParametersToSDP(dtlsTransport.getLocalParameters(),
+            'active');
+
+        sdp += 'a=mid:' + mLine.mid + '\r\n';
+
+        if (rtpSender && rtpReceiver) {
+          sdp += 'a=sendrecv\r\n';
+        } else if (rtpReceiver) {
+          sdp += 'a=sendonly\r\n';
+        } else if (rtpSender) {
+          sdp += 'a=sendonly\r\n';
+        } else {
+          sdp += 'a=inactive\r\n';
+        }
+        sdp += 'a=rtcp-mux\r\n';
+
+        // Add a=rtpmap lines for each codec. Also fmtp and rtcp-fb.
+        sdp += self._capabilitiesToSDP(commonCapabilities);
+
+        if (rtpSender) {
+          // add a=ssrc lines from RTPSender
+          sdp += 'a=msid:' + self.localStreams[0].id + ' ' +
+              rtpSender.track.id + '\r\n';
+          sdp += 'a=ssrc:' + sendSSRC + ' ' + 'msid:' +
+              self.localStreams[0].id + ' ' + rtpSender.track.id + '\r\n';
+        }
+        sdp += 'a=ssrc:' + sendSSRC + ' cname:' + self._cname + '\r\n';
+      });
+
+      var desc = new RTCSessionDescription({
+        type: 'answer',
+        sdp: sdp
+        // ortc: tracks -- state is created in SRD already
+      });
+      if (arguments.length && typeof arguments[0] === 'function') {
+        window.setTimeout(arguments[0], 0, desc);
+      }
+      return new Promise(function(resolve) {
+        resolve(desc);
+      });
+    };
+
+    window.RTCPeerConnection.prototype.addIceCandidate = function(candidate) {
+      // TODO: lookup by mid
+      var mLine = this.mLines[candidate.sdpMLineIndex];
+      if (mLine) {
+        var cand = Object.keys(candidate.candidate).length > 0 ?
+            this._toCandidateJSON(candidate.candidate) : {};
+        // dirty hack to make simplewebrtc work.
+        // FIXME: need another dirty hack to avoid adding candidates after this
+        if (cand.type === 'endOfCandidates') {
+          cand = {};
+        }
+        // dirty hack to make chrome work.
+        if (cand.protocol === 'tcp' && cand.port === 0) {
+          cand = {};
+        }
+        mLine.iceTransport.addRemoteCandidate(cand);
+      }
+      if (arguments.length > 1 && typeof arguments[1] === 'function') {
+        window.setTimeout(arguments[1], 0);
+      }
+      return new Promise(function(resolve) {
+        resolve();
+      });
+    };
+
+    window.RTCPeerConnection.prototype.getStats = function() {
+      var promises = [];
+      this.mLines.forEach(function(mLine) {
+        ['rtpSender', 'rtpReceiver', 'iceGatherer', 'iceTransport',
+            'dtlsTransport'].forEach(function(thing) {
+          if (mLine[thing]) {
+            promises.push(mLine[thing].getStats());
+          }
+        });
+      });
+      var cb = arguments.length > 1 && typeof arguments[1] === 'function' &&
+          arguments[1];
+      return new Promise(function(resolve) {
+        var results = {};
+        Promise.all(promises).then(function(res) {
+          res.forEach(function(result) {
+            Object.keys(result).forEach(function(id) {
+              results[id] = result[id];
+            });
+          });
+          if (cb) {
+            window.setTimeout(cb, 0, results);
+          }
+          resolve(results);
+        });
+      });
+    };
+  }
+} else {
+  webrtcUtils.log('Browser does not appear to be WebRTC-capable');
+}
+
+// Returns the result of getUserMedia as a Promise.
+function requestUserMedia(constraints) {
+  return new Promise(function(resolve, reject) {
+    getUserMedia(constraints, resolve, reject);
+  });
+}
+
+var webrtcTesting = {};
+try {
+  Object.defineProperty(webrtcTesting, 'version', {
+    set: function(version) {
+      webrtcDetectedVersion = version;
+    }
+  });
+} catch (e) {}
+
+if (typeof module !== 'undefined') {
+  var RTCPeerConnection;
+  if (typeof window !== 'undefined') {
+    RTCPeerConnection = window.RTCPeerConnection;
+  }
+  module.exports = {
+    RTCPeerConnection: RTCPeerConnection,
+    getUserMedia: getUserMedia,
+    attachMediaStream: attachMediaStream,
+    reattachMediaStream: reattachMediaStream,
+    webrtcDetectedBrowser: webrtcDetectedBrowser,
+    webrtcDetectedVersion: webrtcDetectedVersion,
+    webrtcMinimumVersion: webrtcMinimumVersion,
+    webrtcTesting: webrtcTesting,
+    webrtcUtils: webrtcUtils
+    //requestUserMedia: not exposed on purpose.
+    //trace: not exposed on purpose.
+  };
+} else if ((typeof require === 'function') && (typeof define === 'function')) {
+  // Expose objects and functions when RequireJS is doing the loading.
+  define([], function() {
+    return {
+      RTCPeerConnection: window.RTCPeerConnection,
+      getUserMedia: getUserMedia,
+      attachMediaStream: attachMediaStream,
+      reattachMediaStream: reattachMediaStream,
+      webrtcDetectedBrowser: webrtcDetectedBrowser,
+      webrtcDetectedVersion: webrtcDetectedVersion,
+      webrtcMinimumVersion: webrtcMinimumVersion,
+      webrtcTesting: webrtcTesting,
+      webrtcUtils: webrtcUtils
+      //requestUserMedia: not exposed on purpose.
+      //trace: not exposed on purpose.
+    };
+  });
+}

+ 280 - 0
src/js/webrtc-audio.js

@@ -0,0 +1,280 @@
+/*
+ *  Copyright (c) 2015 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+/* global TimelineDataSeries, TimelineGraphView */
+
+'use strict';
+
+var audio2 = document.querySelector('audio#audio2');
+var callButton = document.querySelector('button#callButton');
+var hangupButton = document.querySelector('button#hangupButton');
+var codecSelector = document.querySelector('select#codec');
+hangupButton.disabled = true;
+callButton.onclick = call;
+hangupButton.onclick = hangup;
+
+var pc1;
+var pc2;
+var localStream;
+
+var bitrateGraph;
+var bitrateSeries;
+
+var packetGraph;
+var packetSeries;
+
+var lastResult;
+
+var offerOptions = {
+  offerToReceiveAudio: 1,
+  offerToReceiveVideo: 0,
+  voiceActivityDetection: false
+};
+
+function gotStream(stream) {
+  trace('Received local stream');
+  localStream = stream;
+  var audioTracks = localStream.getAudioTracks();
+  if (audioTracks.length > 0) {
+    trace('Using Audio device: ' + audioTracks[0].label);
+  }
+  pc1.addStream(localStream);
+  trace('Adding Local Stream to peer connection');
+
+  pc1.createOffer(gotDescription1, onCreateSessionDescriptionError,
+      offerOptions);
+
+  bitrateSeries = new TimelineDataSeries();
+  bitrateGraph = new TimelineGraphView('bitrateGraph', 'bitrateCanvas');
+  bitrateGraph.updateEndDate();
+
+  packetSeries = new TimelineDataSeries();
+  packetGraph = new TimelineGraphView('packetGraph', 'packetCanvas');
+  packetGraph.updateEndDate();
+}
+
+function onCreateSessionDescriptionError(error) {
+  trace('Failed to create session description: ' + error.toString());
+}
+
+function call() {
+  callButton.disabled = true;
+  hangupButton.disabled = false;
+  codecSelector.disabled = true;
+  trace('Starting call');
+  var servers = null;
+  var pcConstraints = {
+    'optional': []
+  };
+  pc1 = new RTCPeerConnection(servers, pcConstraints);
+  trace('Created local peer connection object pc1');
+  pc1.onicecandidate = iceCallback1;
+  pc2 = new RTCPeerConnection(servers, pcConstraints);
+  trace('Created remote peer connection object pc2');
+  pc2.onicecandidate = iceCallback2;
+  pc2.onaddstream = gotRemoteStream;
+  trace('Requesting local stream');
+  navigator.mediaDevices.getUserMedia({
+    audio: true,
+    video: false
+  })
+  .then(gotStream)
+  .catch(function(e) {
+    alert('getUserMedia() error: ' + e.name);
+  });
+}
+
+function gotDescription1(desc) {
+  desc.sdp = forceChosenAudioCodec(desc.sdp);
+  trace('Offer from pc1 \n' + desc.sdp);
+  pc1.setLocalDescription(desc, function() {
+    pc2.setRemoteDescription(desc, function() {
+      // Since the 'remote' side has no media stream we need
+      // to pass in the right constraints in order for it to
+      // accept the incoming offer of audio.
+      pc2.createAnswer(gotDescription2, onCreateSessionDescriptionError);
+    }, onSetSessionDescriptionError);
+  }, onSetSessionDescriptionError);
+}
+
+function gotDescription2(desc) {
+  desc.sdp = forceChosenAudioCodec(desc.sdp);
+  pc2.setLocalDescription(desc, function() {
+    trace('Answer from pc2 \n' + desc.sdp);
+    pc1.setRemoteDescription(desc, function() {
+    }, onSetSessionDescriptionError);
+  }, onSetSessionDescriptionError);
+}
+
+function hangup() {
+  trace('Ending call');
+  localStream.getTracks().forEach(function(track) {
+    track.stop();
+  });
+  pc1.close();
+  pc2.close();
+  pc1 = null;
+  pc2 = null;
+  hangupButton.disabled = true;
+  callButton.disabled = false;
+  codecSelector.disabled = false;
+}
+
+function gotRemoteStream(e) {
+  audio2.srcObject = e.stream;
+  trace('Received remote stream');
+}
+
+function iceCallback1(event) {
+  if (event.candidate) {
+    pc2.addIceCandidate(new RTCIceCandidate(event.candidate),
+        onAddIceCandidateSuccess, onAddIceCandidateError);
+    trace('Local ICE candidate: \n' + event.candidate.candidate);
+  }
+}
+
+function iceCallback2(event) {
+  if (event.candidate) {
+    pc1.addIceCandidate(new RTCIceCandidate(event.candidate),
+        onAddIceCandidateSuccess, onAddIceCandidateError);
+    trace('Remote ICE candidate: \n ' + event.candidate.candidate);
+  }
+}
+
+function onAddIceCandidateSuccess() {
+  trace('AddIceCandidate success.');
+}
+
+function onAddIceCandidateError(error) {
+  trace('Failed to add ICE Candidate: ' + error.toString());
+}
+
+function onSetSessionDescriptionError(error) {
+  trace('Failed to set session description: ' + error.toString());
+}
+
+function forceChosenAudioCodec(sdp) {
+  return maybePreferCodec(sdp, 'audio', 'send', codecSelector.value);
+}
+
+// Copied from AppRTC's sdputils.js:
+
+// Sets |codec| as the default |type| codec if it's present.
+// The format of |codec| is 'NAME/RATE', e.g. 'opus/48000'.
+function maybePreferCodec(sdp, type, dir, codec) {
+  var str = type + ' ' + dir + ' codec';
+  if (codec === '') {
+    trace('No preference on ' + str + '.');
+    return sdp;
+  }
+
+  trace('Prefer ' + str + ': ' + codec);
+
+  var sdpLines = sdp.split('\r\n');
+
+  // Search for m line.
+  var mLineIndex = findLine(sdpLines, 'm=', type);
+  if (mLineIndex === null) {
+    return sdp;
+  }
+
+  // If the codec is available, set it as the default in m line.
+  var codecIndex = findLine(sdpLines, 'a=rtpmap', codec);
+  console.log('codecIndex', codecIndex);
+  if (codecIndex) {
+    var payload = getCodecPayloadType(sdpLines[codecIndex]);
+    if (payload) {
+      sdpLines[mLineIndex] = setDefaultCodec(sdpLines[mLineIndex], payload);
+    }
+  }
+
+  sdp = sdpLines.join('\r\n');
+  return sdp;
+}
+
+// Find the line in sdpLines that starts with |prefix|, and, if specified,
+// contains |substr| (case-insensitive search).
+function findLine(sdpLines, prefix, substr) {
+  return findLineInRange(sdpLines, 0, -1, prefix, substr);
+}
+
+// Find the line in sdpLines[startLine...endLine - 1] that starts with |prefix|
+// and, if specified, contains |substr| (case-insensitive search).
+function findLineInRange(sdpLines, startLine, endLine, prefix, substr) {
+  var realEndLine = endLine !== -1 ? endLine : sdpLines.length;
+  for (var i = startLine; i < realEndLine; ++i) {
+    if (sdpLines[i].indexOf(prefix) === 0) {
+      if (!substr ||
+          sdpLines[i].toLowerCase().indexOf(substr.toLowerCase()) !== -1) {
+        return i;
+      }
+    }
+  }
+  return null;
+}
+
+// Gets the codec payload type from an a=rtpmap:X line.
+function getCodecPayloadType(sdpLine) {
+  var pattern = new RegExp('a=rtpmap:(\\d+) \\w+\\/\\d+');
+  var result = sdpLine.match(pattern);
+  return (result && result.length === 2) ? result[1] : null;
+}
+
+// Returns a new m= line with the specified codec as the first one.
+function setDefaultCodec(mLine, payload) {
+  var elements = mLine.split(' ');
+
+  // Just copy the first three parameters; codec order starts on fourth.
+  var newLine = elements.slice(0, 3);
+
+  // Put target payload first and copy in the rest.
+  newLine.push(payload);
+  for (var i = 3; i < elements.length; i++) {
+    if (elements[i] !== payload) {
+      newLine.push(elements[i]);
+    }
+  }
+  return newLine.join(' ');
+}
+
+// query getStats every second
+window.setInterval(function() {
+  if (!window.pc1) {
+    return;
+  }
+  window.pc1.getStats(null).then(function(res) {
+    Object.keys(res).forEach(function(key) {
+      var report = res[key];
+      var bytes;
+      var packets;
+      var now = report.timestamp;
+      if ((report.type === 'outboundrtp') ||
+          (report.type === 'outbound-rtp') ||
+          (report.type === 'ssrc' && report.bytesSent)) {
+        bytes = report.bytesSent;
+        packets = report.packetsSent;
+        if (lastResult && lastResult[report.id]) {
+          // calculate bitrate
+          var bitrate = 8 * (bytes - lastResult[report.id].bytesSent) /
+              (now - lastResult[report.id].timestamp);
+
+          // append to chart
+          bitrateSeries.addPoint(now, bitrate);
+          bitrateGraph.setDataSeries([bitrateSeries]);
+          bitrateGraph.updateEndDate();
+
+          // calculate number of packets and append to chart
+          packetSeries.addPoint(now, packets -
+              lastResult[report.id].packetsSent);
+          packetGraph.setDataSeries([packetSeries]);
+          packetGraph.updateEndDate();
+        }
+      }
+    });
+    lastResult = res;
+  });
+}, 1000);

+ 209 - 0
src/js/webrtc-video.js

@@ -0,0 +1,209 @@
+/*
+ *  Copyright (c) 2015 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+
+'use strict';
+
+var startButton = document.getElementById('startButton');
+var callButton = document.getElementById('callButton');
+var hangupButton = document.getElementById('hangupButton');
+callButton.disabled = true;
+hangupButton.disabled = true;
+startButton.onclick = start;
+callButton.onclick = call;
+hangupButton.onclick = hangup;
+
+var startTime;
+var localVideo = document.getElementById('localVideo');
+var remoteVideo = document.getElementById('remoteVideo');
+
+localVideo.addEventListener('loadedmetadata', function() {
+  trace('Local video videoWidth: ' + this.videoWidth +
+    'px,  videoHeight: ' + this.videoHeight + 'px');
+});
+
+remoteVideo.addEventListener('loadedmetadata', function() {
+  trace('Remote video videoWidth: ' + this.videoWidth +
+    'px,  videoHeight: ' + this.videoHeight + 'px');
+});
+
+remoteVideo.onresize = function() {
+  trace('Remote video size changed to ' +
+    remoteVideo.videoWidth + 'x' + remoteVideo.videoHeight);
+  // We'll use the first onsize callback as an indication that video has started
+  // playing out.
+  if (startTime) {
+    var elapsedTime = window.performance.now() - startTime;
+    trace('Setup time: ' + elapsedTime.toFixed(3) + 'ms');
+    startTime = null;
+  }
+};
+
+var localStream;
+var pc1;
+var pc2;
+var offerOptions = {
+  offerToReceiveAudio: 1,
+  offerToReceiveVideo: 1
+};
+
+function getName(pc) {
+  return (pc === pc1) ? 'pc1' : 'pc2';
+}
+
+function getOtherPc(pc) {
+  return (pc === pc1) ? pc2 : pc1;
+}
+
+function gotStream(stream) {
+  trace('Received local stream');
+  localVideo.srcObject = stream;
+  localStream = stream;
+  callButton.disabled = false;
+}
+
+function start() {
+  trace('Requesting local stream');
+  startButton.disabled = true;
+  navigator.mediaDevices.getUserMedia({
+    audio: true,
+    video: true
+  })
+  .then(gotStream)
+  .catch(function(e) {
+    alert('getUserMedia() error: ' + e.name);
+  });
+}
+
+function call() {
+  callButton.disabled = true;
+  hangupButton.disabled = false;
+  trace('Starting call');
+  startTime = window.performance.now();
+  var videoTracks = localStream.getVideoTracks();
+  var audioTracks = localStream.getAudioTracks();
+  if (videoTracks.length > 0) {
+    trace('Using video device: ' + videoTracks[0].label);
+  }
+  if (audioTracks.length > 0) {
+    trace('Using audio device: ' + audioTracks[0].label);
+  }
+  var servers = null;
+  pc1 = new RTCPeerConnection(servers);
+  trace('Created local peer connection object pc1');
+  pc1.onicecandidate = function(e) {
+    onIceCandidate(pc1, e);
+  };
+  pc2 = new RTCPeerConnection(servers);
+  trace('Created remote peer connection object pc2');
+  pc2.onicecandidate = function(e) {
+    onIceCandidate(pc2, e);
+  };
+  pc1.oniceconnectionstatechange = function(e) {
+    onIceStateChange(pc1, e);
+  };
+  pc2.oniceconnectionstatechange = function(e) {
+    onIceStateChange(pc2, e);
+  };
+  pc2.onaddstream = gotRemoteStream;
+
+  pc1.addStream(localStream);
+  trace('Added local stream to pc1');
+
+  trace('pc1 createOffer start');
+  pc1.createOffer(onCreateOfferSuccess, onCreateSessionDescriptionError,
+      offerOptions);
+}
+
+function onCreateSessionDescriptionError(error) {
+  trace('Failed to create session description: ' + error.toString());
+}
+
+function onCreateOfferSuccess(desc) {
+  trace('Offer from pc1\n' + desc.sdp);
+  trace('pc1 setLocalDescription start');
+  pc1.setLocalDescription(desc, function() {
+    onSetLocalSuccess(pc1);
+  }, onSetSessionDescriptionError);
+  trace('pc2 setRemoteDescription start');
+  pc2.setRemoteDescription(desc, function() {
+    onSetRemoteSuccess(pc2);
+  }, onSetSessionDescriptionError);
+  trace('pc2 createAnswer start');
+  // Since the 'remote' side has no media stream we need
+  // to pass in the right constraints in order for it to
+  // accept the incoming offer of audio and video.
+  pc2.createAnswer(onCreateAnswerSuccess, onCreateSessionDescriptionError);
+}
+
+function onSetLocalSuccess(pc) {
+  trace(getName(pc) + ' setLocalDescription complete');
+}
+
+function onSetRemoteSuccess(pc) {
+  trace(getName(pc) + ' setRemoteDescription complete');
+}
+
+function onSetSessionDescriptionError(error) {
+  trace('Failed to set session description: ' + error.toString());
+}
+
+function gotRemoteStream(e) {
+  remoteVideo.srcObject = e.stream;
+  trace('pc2 received remote stream');
+}
+
+function onCreateAnswerSuccess(desc) {
+  trace('Answer from pc2:\n' + desc.sdp);
+  trace('pc2 setLocalDescription start');
+  pc2.setLocalDescription(desc, function() {
+    onSetLocalSuccess(pc2);
+  }, onSetSessionDescriptionError);
+  trace('pc1 setRemoteDescription start');
+  pc1.setRemoteDescription(desc, function() {
+    onSetRemoteSuccess(pc1);
+  }, onSetSessionDescriptionError);
+}
+
+function onIceCandidate(pc, event) {
+  if (event.candidate) {
+    getOtherPc(pc).addIceCandidate(new RTCIceCandidate(event.candidate),
+        function() {
+          onAddIceCandidateSuccess(pc);
+        },
+        function(err) {
+          onAddIceCandidateError(pc, err);
+        }
+    );
+    trace(getName(pc) + ' ICE candidate: \n' + event.candidate.candidate);
+  }
+}
+
+function onAddIceCandidateSuccess(pc) {
+  trace(getName(pc) + ' addIceCandidate success');
+}
+
+function onAddIceCandidateError(pc, error) {
+  trace(getName(pc) + ' failed to add ICE Candidate: ' + error.toString());
+}
+
+function onIceStateChange(pc, event) {
+  if (pc) {
+    trace(getName(pc) + ' ICE state: ' + pc.iceConnectionState);
+    console.log('ICE state change event: ', event);
+  }
+}
+
+function hangup() {
+  trace('Ending call');
+  pc1.close();
+  pc2.close();
+  pc1 = null;
+  pc2 = null;
+  hangupButton.disabled = true;
+  callButton.disabled = false;
+}

Some files were not shown because too many files changed in this diff