1<!DOCTYPE html>
2<!--
3  This page was created to help debug and study webrtc issues such as
4  bandwidth estimation problems. It allows one to easily launch a test
5  case that establishs a connection between 2 peer connections
6-->
7<html>
8<head>
9<title>Loopback test</title>
10
11<!-- In order to plot graphs, this tools uses google visualization API which is
12     loaded via goog.load provided by google api. -->
13<script src="//www.google.com/jsapi"></script>
14
15<!-- This file is included to allow loopback_test.js instantiate a
16     RTCPeerConnection on a browser and version agnostic way. -->
17<script src="adapter.js"></script>
18
19<!-- Provides class StatTracker used by loopback_test.js to keep track of
20     RTCPeerConnection stats -->
21<script src="stat_tracker.js"></script>
22
23<!-- Provides LoopbackTest class which has the core logic for the test itself.
24     Such as: create 2 peer connections, establish a call, filter turn
25     candidates, constraint video bitrate etc.
26  -->
27<script src="loopback_test.js"></script>
28
29<style>
30#chart {
31  height: 400px;
32}
33
34#control-range {
35  height: 100px;
36}
37</style>
38</head>
39<body>
40<div id="test-launcher">
41  <p>Duration (s): <input id="duration" type="text"></p>
42  <p>Max video bitrate (kbps): <input id="max-video-bitrate" type="text"></p>
43  <p>Peer connection constraints: <input id="pc-constraints" type="text"></p>
44  <p>Force TURN: <input id="force-turn" type="checkbox" checked></p>
45  <p><input id="launcher-button" type="button" value="Run test">
46  <div id="test-status" style="display:none"></div>
47  <div id="dashboard">
48    <div id="control-category"></div>
49    <div id="chart"></div>
50    <div id="control-range"></div>
51  </div>
52</div>
53<script>
54google.load('visualization', '1.0', {'packages':['controls']});
55
56var durationInput = document.getElementById('duration');
57var maxVideoBitrateInput = document.getElementById('max-video-bitrate');
58var forceTurnInput = document.getElementById('force-turn');
59var launcherButton = document.getElementById('launcher-button');
60var autoModeInput = document.createElement('input');
61var testStatus = document.getElementById('test-status');
62var pcConstraintsInput = document.getElementById('pc-constraints');
63
64launcherButton.onclick = start;
65
66// Load parameters from the url if present. This allows one to link to
67// a specific test configuration and is used to automatically pass parameters
68// for scripts such as record-test.sh
69function getURLParameter(name, default_value) {
70  var search =
71      RegExp('(^\\?|&)' + name + '=' + '(.+?)(&|$)').exec(location.search);
72  if (search)
73    return decodeURI(search[2]);
74  else
75    return default_value;
76}
77
78durationInput.value = getURLParameter('duration', 10);
79maxVideoBitrateInput.value = getURLParameter('max-video-bitrate', 2000);
80forceTurnInput.checked = (getURLParameter('force-turn', 'true') === 'true');
81autoModeInput.checked = (getURLParameter('auto-mode', 'false') === 'true');
82pcConstraintsInput.value = getURLParameter('pc-constraints', '');
83
84if (autoModeInput.checked) start();
85
86function start() {
87  var durationMs = parseInt(durationInput.value) * 1000;
88  var maxVideoBitrateKbps = parseInt(maxVideoBitrateInput.value);
89  var forceTurn = forceTurnInput.checked;
90  var autoClose = autoModeInput.checked;
91  var pcConstraints = pcConstraintsInput.value == "" ?
92                      null : JSON.parse(pcConstraintsInput.value);
93
94  var updateStatusInterval;
95  var testFinished = false;
96  function updateStatus() {
97    if (testFinished) {
98      testStatus.innerHTML = 'Test finished';
99      if (updateStatusInterval) {
100	clearInterval(updateStatusInterval);
101        updateStatusInterval = null;
102      }
103    } else {
104      if (!updateStatusInterval) {
105        updateStatusInterval = setInterval(updateStatus, 1000);
106        testStatus.innerHTML = 'Running';
107      }
108      testStatus.innerHTML += '.';
109    }
110  }
111
112  if (!(isFinite(maxVideoBitrateKbps) && maxVideoBitrateKbps > 0)) {
113    // TODO(andresp): Get a better way to show errors than alert.
114    alert("Invalid max video bitrate");
115    return;
116  }
117
118  if (!(isFinite(durationMs) && durationMs > 0)) {
119    alert("Invalid duration");
120    return;
121  }
122
123  durationInput.disabled = true;
124  forceTurnInput.disabled = true;
125  maxVideoBitrateInput.disabled = true;
126  launcherButton.style.display = 'none';
127  testStatus.style.display = 'block';
128
129  getUserMedia({audio:true, video:true},
130                gotStream, function() {});
131
132  function gotStream(stream) {
133    updateStatus();
134    var test = new LoopbackTest(stream, durationMs,
135                                forceTurn,
136                                pcConstraints,
137                                maxVideoBitrateKbps);
138    test.run(onTestFinished.bind(test));
139  }
140
141  function onTestFinished() {
142    testFinished = true;
143    updateStatus();
144    if (autoClose) {
145      window.close();
146    } else {
147      plotStats(this.getResults());
148    }
149  }
150}
151
152function plotStats(data) {
153  var dashboard = new google.visualization.Dashboard(
154      document.getElementById('dashboard'));
155
156  var chart = new google.visualization.ChartWrapper({
157      'containerId': 'chart',
158      'chartType': 'LineChart',
159      'options': { 'pointSize': 0, 'lineWidth': 1, 'interpolateNulls': true },
160    });
161
162  var rangeFilter = new google.visualization.ControlWrapper({
163     'controlType': 'ChartRangeFilter',
164     'containerId': 'control-range',
165     'options': {
166       'filterColumnIndex': 0,
167       'ui': {
168         'chartType': 'ScatterChart',
169         'chartOptions': {
170           'hAxis': {'baselineColor': 'none'}
171         },
172         'chartView': {
173           'columns': [0, 1]
174         },
175         'minRangeSize': 1000 // 1 second
176       }
177     },
178   });
179
180  // Create a table with the columns of the dataset.
181  var columnsTable = new google.visualization.DataTable();
182  columnsTable.addColumn('number', 'columnIndex');
183  columnsTable.addColumn('string', 'columnLabel');
184  var initState = {selectedValues: []};
185  for (var i = 1; i < data.getNumberOfColumns(); i++) {
186    columnsTable.addRow([i, data.getColumnLabel(i)]);
187    initState.selectedValues.push(data.getColumnLabel(i));
188  }
189
190  var columnFilter = new google.visualization.ControlWrapper({
191    controlType: 'CategoryFilter',
192    containerId: 'control-category',
193    dataTable: columnsTable,
194    options: {
195      filterColumnLabel: 'columnLabel',
196      ui: {
197        label: '',
198        allowNone: false,
199        selectedValuesLayout: 'aside'
200      }
201    },
202    state: initState
203  });
204  google.visualization.events.addListener(columnFilter, 'statechange',
205    function () {
206      var state = columnFilter.getState();
207      var row;
208      var columnIndices = [0];
209      for (var i = 0; i < state.selectedValues.length; i++) {
210        row = columnsTable.getFilteredRows([{
211            column: 1,
212            value: state.selectedValues[i]}])[0];
213        columnIndices.push(columnsTable.getValue(row, 0));
214      }
215      // Sort the indices into their original order
216      columnIndices.sort(function (a, b) { return (a - b); });
217      chart.setView({columns: columnIndices});
218      chart.draw();
219    });
220
221  columnFilter.draw();
222  dashboard.bind([rangeFilter], [chart]);
223  dashboard.draw(data);
224}
225</script>
226</body>
227</html>
228