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 isVideoInputFound = false;
     13 var results = {};
     14 var testStatus = 'running';
     15 
     16 // Check if a video input device exists
     17 function checkVideoInput() {
     18   navigator.mediaDevices.enumerateDevices()
     19       .then(findVideoInput)
     20       .catch(gotEnumerateDevicesError);
     21 }
     22 
     23 function findVideoInput(devices) {
     24   isVideoInputFound = devices.some(dev => dev.kind == 'videoinput');
     25 }
     26 
     27 function gotEnumerateDevicesError(error) {
     28   console.log('navigator.mediaDevices.enumerateDevices error: ', error);
     29   results.cameraErrors.push('EnumerateDevices error: ' + error.toString());
     30 }
     31 
     32 // Starts the test.
     33 function testWebRtcLoopbackCall(videoCodec) {
     34   var test = new WebRtcLoopbackCallTest(videoCodec);
     35   test.run();
     36 }
     37 
     38 // Returns the results to caller.
     39 function getResults() {
     40   return results;
     41 }
     42 
     43 function setResults(stats) {
     44   results = stats;
     45 }
     46 
     47 function getStatus() {
     48   return testStatus;
     49 }
     50 
     51 // Calculates averages of array values.
     52 function 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.
     62 function 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 
     80 WebRtcLoopbackCallTest.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: Y709 = 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 
    273 window.onerror = function (message, filename, lineno, colno, error) {
    274   testStatus = 'exception-in-test-page: ' + error.stack;
    275 };
    276 
    277 // Used by munge_sdp.js.
    278 function failure(location, msg) {
    279   testStatus = 'failed-to-munge: ' + msg + ' in ' + location;
    280 }
    281 
    282 
    283 
    284