• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1<!DOCTYPE html>
2<html>
3<head><title>Loopback test</title></head>
4<body>
5  <video id="localVideo" autoplay muted></video>
6  <video id="remoteVideo" autoplay muted></video>
7<script src="blackframe.js"></script>
8<script src="munge_sdp.js"></script>
9<script src="ssim.js"></script>
10<script>
11
12var results = {};
13var testStatus = 'running';
14
15// Starts the test.
16function testWebRtcLoopbackCall(videoCodec) {
17  var test = new WebRtcLoopbackCallTest(videoCodec);
18  test.run();
19}
20
21// Returns the results to caller.
22function getResults() {
23  return results;
24}
25
26function setResults(stats) {
27  results = stats;
28}
29
30function getStatus() {
31  return testStatus;
32}
33
34// Calculates averages of array values.
35function average(array) {
36  var count = array.length;
37  var total = 0;
38  for (var i = 0; i < count; i++) {
39    total += parseInt(array[i]);
40  }
41  return Math.floor(total / count);
42}
43
44// Actual test object.
45function WebRtcLoopbackCallTest(videoCodec) {
46  this.videoCodec = videoCodec;
47  this.localStream = null;
48  this.remoteStream = null;
49  this.results = {cameraType: '', peerConnectionStats: [],
50      frameStats: {numBlackFrames: 0, numFrozenFrames:0, numFrames: 0}};
51
52  this.inFps = [];
53  this.outFps = [];
54  // Variables associated with nearly-frozen frames detection.
55  this.previousFrame = [];
56  this.identicalFrameSsimThreshold = 0.985;
57  this.frameComparator = new Ssim();
58
59  this.remoteVideo = document.getElementById("remoteVideo");
60  this.localVideo = document.getElementById("localVideo");
61}
62
63WebRtcLoopbackCallTest.prototype = {
64  collectAndAnalyzeStats: function() {
65    this.gatherStats(this.localPeerConnection, 100, 20000,
66        this.reportTestDone.bind(this));
67  },
68
69  setup: function() {
70    this.canvas = document.createElement('canvas');
71    this.context = this.canvas.getContext('2d');
72    this.remoteVideo.onloadedmetadata = this.collectAndAnalyzeStats.bind(this);
73    this.remoteVideo.addEventListener('play',
74        this.startCheckingVideoFrames.bind(this), false);
75  },
76
77  startCheckingVideoFrames: function() {
78    // TODO(phoglund): replace with MediaRecorder. setInterval isn't at all
79    // reliable, so the number of captured frames can probably vary wildly
80    // over the 20 second execution time.
81    this.videoFrameChecker = setInterval(this.checkVideoFrame.bind(this), 20);
82  },
83
84  run: function() {
85    this.setup();
86    this.triggerGetUserMedia();
87  },
88
89  triggerGetUserMedia: function() {
90    var constraints = {audio: false, video: true};
91    try {
92      navigator.getUserMedia = navigator.getUserMedia ||
93          navigator.webkitGetUserMedia;
94      navigator.getUserMedia(constraints, this.gotLocalStream.bind(this),
95          this.onGetUserMediaError.bind(this));
96    } catch (exception) {
97      this.reportError('getUserMedia exception: ' + exception.toString());
98    }
99  },
100
101  reportError: function(message) {
102    testStatus = message;
103  },
104
105  gotLocalStream: function(stream) {
106    this.localStream = stream;
107    var servers = null;
108
109    this.localPeerConnection = new webkitRTCPeerConnection(servers);
110    this.localPeerConnection.onicecandidate = this.gotLocalIceCandidate.bind(
111        this);
112
113    this.remotePeerConnection = new webkitRTCPeerConnection(servers);
114    this.remotePeerConnection.onicecandidate = this.gotRemoteIceCandidate.bind(
115        this);
116    this.remotePeerConnection.onaddstream = this.gotRemoteStream.bind(this);
117
118    this.localPeerConnection.addStream(this.localStream);
119    this.localPeerConnection.createOffer(this.gotOffer.bind(this),
120        function(error) {});
121    this.localVideo.src = URL.createObjectURL(stream);
122
123    this.results.cameraType = stream.getVideoTracks()[0].label;
124  },
125
126  onGetUserMediaError: function(error) {
127    this.reportError('getUserMedia failed: ' + error.toString());
128  },
129
130  gatherStats: function(peerConnection, interval, durationMs, callback) {
131    var startTime = new Date();
132    var pollFunction = setInterval(gatherOneReport.bind(this), interval);
133    function gatherOneReport() {
134      var elapsed = new Date() - startTime;
135      if (elapsed > durationMs) {
136        clearInterval(pollFunction);
137        callback();
138        return;
139      }
140      peerConnection.getStats(this.gotStats.bind(this));
141    }
142  },
143
144  getStatFromReport: function(data, name) {
145    if (data.type = 'ssrc' && data.stat(name)) {
146      return data.stat(name);
147    } else {
148      return null;
149    }
150  },
151
152  gotStats: function(response) {
153    var reports = response.result();
154    for (var i = 0; i < reports.length; ++i) {
155      var report = reports[i];
156      var incomingFps = this.getStatFromReport(report, 'googFrameRateInput');
157      if (incomingFps == null) {
158        // Skip on null.
159        continue;
160      }
161      var outgoingFps = this.getStatFromReport(report, 'googFrameRateSent');
162      // Save rates for later processing.
163      this.inFps.push(incomingFps)
164      this.outFps.push(outgoingFps);
165    }
166  },
167
168  reportTestDone: function() {
169    this.processStats();
170
171    clearInterval(this.videoFrameChecker);
172
173    setResults(this.results);
174
175    testStatus = 'ok-done';
176  },
177
178  processStats: function() {
179    if (this.inFps != [] && this.outFps != []) {
180      var minInFps = Math.min.apply(null, this.inFps);
181      var maxInFps = Math.max.apply(null, this.inFps);
182      var averageInFps = average(this.inFps);
183      var minOutFps = Math.min.apply(null, this.outFps);
184      var maxOutFps = Math.max.apply(null, this.outFps);
185      var averageOutFps = average(this.outFps);
186      this.results.peerConnectionStats = [minInFps, maxInFps, averageInFps,
187          minOutFps, maxOutFps, averageOutFps];
188    }
189  },
190
191  checkVideoFrame: function() {
192    this.context.drawImage(this.remoteVideo, 0, 0, this.canvas.width,
193      this.canvas.height);
194    var imageData = this.context.getImageData(0, 0, this.canvas.width,
195        this.canvas.height);
196
197      if (isBlackFrame(imageData.data, imageData.data.length)) {
198        this.results.frameStats.numBlackFrames++;
199      }
200
201      if (this.frameComparator.calculate(this.previousFrame, imageData.data) >
202        this.identicalFrameSsimThreshold) {
203        this.results.frameStats.numFrozenFrames++;
204      }
205
206      this.previousFrame = imageData.data;
207      this.results.frameStats.numFrames++;
208  },
209
210  isBlackFrame: function(data, length) {
211    var accumulatedLuma = 0;
212    for (var i = 4; i < length; i += 4) {
213      // Use Luma as in Rec. 709: Y′709 = 0.21R + 0.72G + 0.07B;
214      accumulatedLuma += (0.21 * data[i] +  0.72 * data[i + 1]
215          + 0.07 * data[i + 2]);
216      // Early termination if the average Luma so far is bright enough.
217      if (accumulatedLuma > (this.nonBlackPixelLumaThreshold * i / 4)) {
218        return false;
219      }
220    }
221    return true;
222  },
223
224  gotRemoteStream: function(event) {
225    this.remoteVideo.src = URL.createObjectURL(event.stream);
226  },
227
228  gotOffer: function(description) {
229    description.sdp =
230        setSdpDefaultVideoCodec(description.sdp, this.videoCodec);
231    this.localPeerConnection.setLocalDescription(description);
232    this.remotePeerConnection.setRemoteDescription(description);
233    this.remotePeerConnection.createAnswer(this.gotAnswer.bind(
234        this), function(error) {});
235  },
236
237  gotAnswer: function(description) {
238    var selectedCodec =
239        getSdpDefaultVideoCodec(description.sdp);
240    if (selectedCodec != this.videoCodec) {
241      this.reportError('Expected codec ' + this.videoCodec + ', but WebRTC ' +
242                       'selected ' + selectedCodec);
243    }
244    this.remotePeerConnection.setLocalDescription(description);
245    this.localPeerConnection.setRemoteDescription(description);
246  },
247
248  gotLocalIceCandidate: function(event) {
249    if (event.candidate)
250      this.remotePeerConnection.addIceCandidate(
251        new RTCIceCandidate(event.candidate));
252  },
253
254  gotRemoteIceCandidate: function(event) {
255    if (event.candidate)
256      this.localPeerConnection.addIceCandidate(
257        new RTCIceCandidate(event.candidate));
258  },
259}
260
261window.onerror = function (message, filename, lineno, colno, error) {
262  testStatus = 'exception-in-test-page: ' + error.stack;
263};
264
265// Used by munge_sdp.js.
266function failure(location, msg) {
267  testStatus = 'failed-to-munge: ' + msg + ' in ' + location;
268}
269</script>
270</body>
271</html>
272