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