Home | History | Annotate | Download | only in video_WebRtcPeerConnectionWithCamera
      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 
     12 var results = {};
     13 var testStatus = 'running';
     14 
     15 // Starts the test.
     16 function testWebRtcLoopbackCall(videoCodec) {
     17   var test = new WebRtcLoopbackCallTest(videoCodec);
     18   test.run();
     19 }
     20 
     21 // Returns the results to caller.
     22 function getResults() {
     23   return results;
     24 }
     25 
     26 function setResults(stats) {
     27   results = stats;
     28 }
     29 
     30 function getStatus() {
     31   return testStatus;
     32 }
     33 
     34 // Calculates averages of array values.
     35 function 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.
     45 function 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 
     63 WebRtcLoopbackCallTest.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: Y709 = 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 
    261 window.onerror = function (message, filename, lineno, colno, error) {
    262   testStatus = 'exception-in-test-page: ' + error.stack;
    263 };
    264 
    265 // Used by munge_sdp.js.
    266 function failure(location, msg) {
    267   testStatus = 'failed-to-munge: ' + msg + ' in ' + location;
    268 }
    269 
    270 
    271 
    272