• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1<!DOCTYPE html>
2<html>
3<head><title>Loopback test</title></head>
4<body>
5  <video id="localVideo" width="1280" height="720" autoplay muted></video>
6  <video id="remoteVideo" width="1280" height="720" autoplay muted></video>
7<script src="ssim.js"></script>
8<script src="blackframe.js"></script>
9<script>
10
11
12var results = {};
13var testProgress = 0;
14
15// Starts the test.
16function testCamera(resolution) {
17  var test = new CameraTest(resolution);
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
30// Calculates averages of array values.
31function average(array) {
32  var count = array.length;
33  var total = 0;
34  for (var i = 0; i < count; i++) {
35    total += parseInt(array[i]);
36  }
37  return Math.floor(total / count);
38}
39
40// Actual test object.
41function CameraTest(resolutionArray) {
42  this.resolution = resolutionArray;
43  this.localStream = null;
44  this.remoteStream = null;
45  this.results = {cameraType: '', cameraErrors: [], peerConnectionStats: [],
46      frameStats: {numBlackFrames: 0, numFrozenFrames:0, numFrames: 0}};
47
48  this.inFps = [];
49  this.outFps = [];
50  // Variables associated with nearly-frozen frames detection.
51  this.previousFrame = [];
52  this.identicalFrameSsimThreshold = 0.985;
53  this.frameComparator = new Ssim();
54
55  this.remoteVideo = document.getElementById("remoteVideo");
56  this.localVideo = document.getElementById("localVideo");
57  this.localVideo.width = this.resolution[0].toString();
58  this.localVideo.height = this.resolution[1].toString();
59  this.remoteVideo.width = this.resolution[0].toString();
60  this.remoteVideo.height = this.resolution[1].toString();
61}
62
63function resolutionMatchesIndependentOfRotation(aWidth, aHeight,
64                                                bWidth, bHeight) {
65  return (aWidth === bWidth && aHeight === bHeight) ||
66         (aWidth === bHeight && aHeight === bWidth);
67}
68
69CameraTest.prototype = {
70  collectAndAnalyzeStats: function() {
71    if (!resolutionMatchesIndependentOfRotation(this.localVideo.width,
72        this.localVideo.height, this.resolution[0], this.resolution[1])) {
73      this.reportError('resolution', 'Got resolution ' + this.resolution[0] +
74          + 'x' + this.resolution[1] + ', expected resolution' +
75          this.localVideo.width + 'x' + this.localVideo.height +
76          ' or rotated version thereof');
77    }
78    this.gatherStats(this.localPeerConnection, 100, 20000,
79        this.reportTestDone.bind(this));
80  },
81
82  setup: function() {
83    this.canvas = document.createElement('canvas');
84    this.canvas.width = localVideo.width;
85    this.canvas.height = localVideo.height;
86    this.context = this.canvas.getContext('2d');
87    this.remoteVideo.onloadedmetadata = this.collectAndAnalyzeStats.bind(this);
88    this.localVideo.addEventListener('play',
89        this.startCheckingVideoFrames.bind(this), false);
90  },
91
92  startCheckingVideoFrames: function() {
93    this.videoFrameChecker = setInterval(this.checkVideoFrame.bind(this), 20);
94  },
95
96  run: function() {
97    this.setup();
98    this.triggerGetUserMedia(this.resolution);
99  },
100
101  triggerGetUserMedia: function(resolution) {
102    var constraints = {
103      audio: false,
104      video: {
105        mandatory: {
106          minWidth:  resolution[0],
107          minHeight: resolution[1],
108          maxWidth:  resolution[0],
109          maxHeight: resolution[1]
110        }
111      }
112    };
113    try {
114      this.doGetUserMedia(constraints, this.gotLocalStream.bind(this),
115          this.onGetUserMediaError.bind(this));
116    } catch (exception) {
117      console.log('Unexpected exception: ', exception);
118      this.reportError('gUM', 'doGetUserMedia failed: ' + exception);
119    }
120  },
121
122  reportError: function(errorType, message) {
123    this.results.cameraErrors.push([errorType, message]);
124    console.log(message);
125  },
126
127  doGetUserMedia: function(constraints, onSuccess, onFail) {
128    navigator.getUserMedia = navigator.getUserMedia ||
129      navigator.webkitGetUserMedia;
130    navigator.getUserMedia(constraints, onSuccess, onFail);
131  },
132
133  gotLocalStream: function(stream) {
134    this.localStream = stream;
135    var servers = null;
136
137    this.localPeerConnection = new webkitRTCPeerConnection(servers);
138    this.localPeerConnection.onicecandidate = this.gotLocalIceCandidate.bind(
139        this);
140
141    this.remotePeerConnection = new webkitRTCPeerConnection(servers);
142    this.remotePeerConnection.onicecandidate = this.gotRemoteIceCandidate.bind(
143        this);
144    this.remotePeerConnection.onaddstream = this.gotRemoteStream.bind(this);
145
146    this.localPeerConnection.addStream(this.localStream);
147    this.localPeerConnection.createOffer(this.gotLocalDescription.bind(this));
148    this.localVideo.src = URL.createObjectURL(stream);
149
150    this.results.cameraType = stream.getVideoTracks()[0].label;
151  },
152
153  onGetUserMediaError: function(stream) {
154    this.reportError('gUM', 'gUM call failed');
155  },
156
157  gatherStats: function(peerConnection, interval, durationMs, callback) {
158    var startTime = new Date();
159    var pollFunction = setInterval(gatherOneReport.bind(this), interval);
160    function gatherOneReport() {
161      var elapsed = new Date() - startTime;
162      if (elapsed > durationMs) {
163        console.log('Done gathering stats.');
164        clearInterval(pollFunction);
165        callback();
166        return;
167      }
168      peerConnection.getStats(this.gotStats.bind(this));
169    }
170  },
171
172  getStatFromReport: function(data, name) {
173    if (data.type = 'ssrc' && data.stat(name)) {
174      return data.stat(name);
175    } else {
176      return null;
177    }
178  },
179
180  gotStats: function(response) {
181    var reports = response.result();
182    for (var i = 0; i < reports.length; ++i) {
183      var report = reports[i];
184      var incomingFps = this.getStatFromReport(report, 'googFrameRateInput');
185      if (incomingFps == null) {
186        // Skip on null.
187        continue;
188      }
189      var outgoingFps = this.getStatFromReport(report, 'googFrameRateSent');
190      // Save rates for later processing.
191      this.inFps.push(incomingFps)
192      this.outFps.push(outgoingFps);
193    }
194  },
195
196  reportTestDone: function() {
197    this.processStats();
198
199    clearInterval(this.videoFrameChecker);
200
201    setResults(this.results);
202
203    testProgress = 1;
204  },
205
206  processStats: function() {
207    if (this.inFps != [] && this.outFps != []) {
208      var minInFps = Math.min.apply(null, this.inFps);
209      var maxInFps = Math.max.apply(null, this.inFps);
210      var averageInFps = average(this.inFps);
211      var minOutFps = Math.min.apply(null, this.outFps);
212      var maxOutFps = Math.max.apply(null, this.outFps);
213      var averageOutFps = average(this.outFps);
214      this.results.peerConnectionStats = [minInFps, maxInFps, averageInFps,
215          minOutFps, maxOutFps, averageOutFps];
216    }
217  },
218
219  checkVideoFrame: function() {
220    this.context.drawImage(this.localVideo, 0, 0, this.canvas.width,
221      this.canvas.height);
222    var imageData = this.context.getImageData(0, 0, this.canvas.width,
223        this.canvas.height);
224
225      if (isBlackFrame(imageData.data, imageData.data.length)) {
226        this.results.frameStats.numBlackFrames++;
227      }
228
229      if (this.frameComparator.calculate(this.previousFrame, imageData.data) >
230        this.identicalFrameSsimThreshold) {
231        this.results.frameStats.numFrozenFrames++;
232      }
233
234      this.previousFrame = imageData.data;
235      this.results.frameStats.numFrames++;
236  },
237
238  isBlackFrame: function(data, length) {
239    var accumulatedLuma = 0;
240    for (var i = 4; i < length; i += 4) {
241      // Use Luma as in Rec. 709: Y709 = 0.21R + 0.72G + 0.07B;
242      accumulatedLuma += (0.21 * data[i] +  0.72 * data[i + 1]
243          + 0.07 * data[i + 2]);
244      // Early termination if the average Luma so far is bright enough.
245      if (accumulatedLuma > (this.nonBlackPixelLumaThreshold * i / 4)) {
246        return false;
247      }
248    }
249    return true;
250  },
251
252  gotRemoteStream: function(event) {
253    this.remoteVideo.src = URL.createObjectURL(event.stream);
254  },
255
256  gotLocalDescription: function(description) {
257    this.localPeerConnection.setLocalDescription(description);
258    this.remotePeerConnection.setRemoteDescription(description);
259    this.remotePeerConnection.createAnswer(this.gotRemoteDescription.bind(
260        this));
261  },
262
263  gotRemoteDescription: function(description) {
264    this.remotePeerConnection.setLocalDescription(description);
265    this.localPeerConnection.setRemoteDescription(description);
266  },
267
268  gotLocalIceCandidate: function(event) {
269    if (event.candidate)
270      this.remotePeerConnection.addIceCandidate(
271        new RTCIceCandidate(event.candidate));
272  },
273
274  gotRemoteIceCandidate: function(event) {
275    if (event.candidate)
276      this.localPeerConnection.addIceCandidate(
277        new RTCIceCandidate(event.candidate));
278  },
279}
280
281window.onerror = function (message, filename, lineno, colno, error) {
282  console.log("Something went wrong, here is the stack trace --> %s",
283    error.stack);
284};
285</script>
286</body>
287</html>
288