| <html> |
| <head> |
| <title>Constraints and Statistics</title> |
| <!-- Load the polyfill to switch-hit between Chrome and Firefox --> |
| <script src="../../base/adapter.js"></script> |
| |
| <style type="text/css"> |
| td { vertical-align: top; } |
| </style> |
| |
| <script> |
| var mystream; |
| var pc1; |
| var pc2; |
| var bytesPrev = 0; |
| var timestampPrev = 0; |
| |
| $ = function(id) { |
| return document.getElementById(id); |
| } |
| |
| function log(txt) { |
| console.log(txt); |
| } |
| |
| function openCamera() { |
| if (mystream) { |
| mystream.stop(); |
| } |
| navigator.getUserMedia(cameraConstraints(), gotStream, function() { |
| log("GetUserMedia failed"); |
| }); |
| } |
| |
| function gotStream(stream) { |
| log("GetUserMedia succeeded"); |
| mystream = stream; |
| attachMediaStream($("local-video"), stream); |
| } |
| |
| function cameraConstraints() { |
| var constraints = {}; |
| constraints.audio = true; |
| constraints.video = { mandatory: {}, optional: [] }; |
| if ($("minwidth").value != "0") { |
| constraints.video.mandatory.minWidth = $("minwidth").value; |
| } |
| if ($("maxwidth").value != "0") { |
| constraints.video.mandatory.maxWidth = $("maxwidth").value; |
| } |
| if ($("minheight").value != "0") { |
| constraints.video.mandatory.minHeight = $("minheight").value; |
| } |
| if ($("maxheight").value != "0") { |
| constraints.video.mandatory.maxHeight = $("maxheight").value; |
| } |
| if ($("frameRate").value != "0") { |
| constraints.video.mandatory.minFrameRate = $("frameRate").value; |
| } |
| log('Camera constraints are ' + JSON.stringify(constraints)); |
| $("cameraConstraints").innerHTML = JSON.stringify(constraints, null, ' '); |
| return constraints; |
| } |
| |
| function streamConstraints() { |
| var constraints = { mandatory: {}, optional: [] }; |
| if ($("bandwidth").value != "0") { |
| constraints.optional[0] = { 'bandwidth' : $('bandwidth').value }; |
| } |
| log('Constraints are ' + JSON.stringify(constraints)); |
| $("addStreamConstraints").innerHTML = JSON.stringify(constraints, null, ' '); |
| return constraints; |
| } |
| |
| function connect() { |
| pc1 = new RTCPeerConnection(null); |
| pc2 = new RTCPeerConnection(null); |
| pc1.addStream(mystream, streamConstraints()); |
| log('PC1 creating offer'); |
| pc1.onnegotiationeeded = function() { |
| log('Negotiation needed - PC1'); |
| } |
| pc2.onnegotiationeeded = function() { |
| log('Negotiation needed - PC2'); |
| } |
| pc1.onicecandidate = function(e) { |
| log('Candidate PC1'); |
| if (e.candidate) { |
| pc2.addIceCandidate(new RTCIceCandidate(e.candidate)); |
| } |
| } |
| pc2.onicecandidate = function(e) { |
| log('Candidate PC2'); |
| if (e.candidate) { |
| pc1.addIceCandidate(new RTCIceCandidate(e.candidate)); |
| } |
| } |
| pc2.onaddstream = function(e) { |
| log('PC2 got stream'); |
| attachMediaStream($('remote-video'), e.stream); |
| log('Remote video is ' + $('remote-video').src); |
| } |
| pc1.createOffer(function(desc) { |
| log('PC1 offering'); |
| pc1.setLocalDescription(desc); |
| pc2.setRemoteDescription(desc); |
| pc2.createAnswer(function(desc2) { |
| log('PC2 answering'); |
| pc2.setLocalDescription(desc2); |
| pc1.setRemoteDescription(desc2); |
| }); |
| }); |
| } |
| |
| // Augumentation of stats entries with utility functions. |
| // The augumented entry does what the stats entry does, but adds |
| // utility functions. |
| function AugumentedStatsResponse(response) { |
| this.response = response; |
| this.addressPairMap = []; |
| } |
| |
| AugumentedStatsResponse.prototype.collectAddressPairs = function(componentId) { |
| if (!this.addressPairMap[componentId]) { |
| this.addressPairMap[componentId] = []; |
| for (var i = 0; i < this.response.result().length; ++i) { |
| var res = this.response.result()[i]; |
| if (res.type == 'googCandidatePair' && |
| res.stat('googChannelId') == componentId) { |
| this.addressPairMap[componentId].push(res); |
| } |
| } |
| } |
| return this.addressPairMap[componentId]; |
| } |
| |
| AugumentedStatsResponse.prototype.result = function() { |
| return this.response.result(); |
| } |
| |
| // The indexed getter isn't easy to prototype. |
| AugumentedStatsResponse.prototype.get = function(key) { |
| return this.response[key]; |
| } |
| |
| |
| // Display statistics |
| var statCollector = setInterval(function() { |
| var display = function(str) { |
| $('bitrate').innerHTML = str; |
| } |
| |
| display("No stream"); |
| if (pc2 && pc2.getRemoteStreams()[0]) { |
| if (pc2.getStats) { |
| pc2.getStats(function(rawStats) { |
| stats = new AugumentedStatsResponse(rawStats); |
| var statsString = ''; |
| var results = stats.result(); |
| var videoFlowInfo = 'No bitrate stats'; |
| for (var i = 0; i < results.length; ++i) { |
| var res = results[i]; |
| statsString += '<h3>Report '; |
| statsString += i; |
| statsString += '</h3>'; |
| if (!res.local || res.local === res) { |
| statsString += dumpStats(res); |
| // The bandwidth info for video is in a type ssrc stats record |
| // with googFrameHeightReceived defined. |
| // Should check for mediatype = video, but this is not |
| // implemented yet. |
| if (res.type == 'ssrc' && res.stat('googFrameHeightReceived')) { |
| // This is the video flow. |
| videoFlowInfo = extractVideoFlowInfo(res, stats); |
| } |
| } else { |
| // Pre-227.0.1445 (188719) browser |
| if (res.local) { |
| statsString += "<p>Local "; |
| statsString += dumpStats(res.local); |
| } |
| if (res.remote) { |
| statsString += "<p>Remote "; |
| statsString += dumpStats(res.remote); |
| } |
| } |
| } |
| $('receiverstats').innerHTML = statsString; |
| display(videoFlowInfo); |
| }); |
| pc1.getStats(function(stats) { |
| var statsString = ''; |
| var results = stats.result(); |
| for (var i = 0; i < results.length; ++i) { |
| var res = results[i]; |
| statsString += '<h3>Report '; |
| statsString += i; |
| statsString += '</h3>'; |
| if (!res.local || res.local === res) { |
| statsString += dumpStats(res); |
| } |
| } |
| $('senderstats').innerHTML = statsString; |
| }); |
| } else { |
| display('No stats function. Use at least Chrome 24.0.1285'); |
| } |
| } else { |
| log('Not connected yet'); |
| } |
| // Collect some stats from the video tags. |
| local_video = $('local-video'); |
| if (local_video) { |
| $('local-video-stats').innerHTML = local_video.videoWidth + |
| 'x' + local_video.videoHeight; |
| } |
| remote_video = $('remote-video'); |
| if (remote_video) { |
| $('remote-video-stats').innerHTML = remote_video.videoWidth + |
| 'x' + remote_video.videoHeight; |
| } |
| }, 1000); |
| |
| function extractVideoFlowInfo(res, allStats) { |
| var description = ''; |
| var bytesNow = res.stat('bytesReceived'); |
| if (timestampPrev > 0) { |
| var bitRate = Math.round((bytesNow - bytesPrev) * 8 / |
| (res.timestamp - timestampPrev)); |
| description = bitRate + ' kbits/sec'; |
| } |
| timestampPrev = res.timestamp; |
| bytesPrev = bytesNow; |
| if (res.stat('transportId')) { |
| component = allStats.get(res.stat('transportId')); |
| if (component) { |
| addresses = allStats.collectAddressPairs(component.id); |
| if (addresses.length > 0) { |
| description += ' from IP '; |
| description += addresses[0].stat('googRemoteAddress'); |
| } else { |
| description += ' no address'; |
| } |
| } else { |
| description += ' No component stats'; |
| } |
| } else { |
| description += ' No component ID'; |
| } |
| return description; |
| } |
| |
| // Dumping a stats variable as a string. |
| // might be named toString? |
| function dumpStats(obj) { |
| var statsString = 'Timestamp:'; |
| statsString += obj.timestamp; |
| if (obj.id) { |
| statsString += "<br>id "; |
| statsString += obj.id; |
| } |
| if (obj.type) { |
| statsString += " type "; |
| statsString += obj.type; |
| } |
| if (obj.names) { |
| names = obj.names(); |
| for (var i = 0; i < names.length; ++i) { |
| statsString += '<br>'; |
| statsString += names[i]; |
| statsString += ':'; |
| statsString += obj.stat(names[i]); |
| } |
| } else { |
| if (obj.stat('audioOutputLevel')) { |
| statsString += "audioOutputLevel: "; |
| statsString += obj.stat('audioOutputLevel'); |
| statsString += "<br>"; |
| } |
| } |
| return statsString; |
| } |
| |
| |
| // Utility to show the value of a field in a span called name+Display |
| function showValue(name, value) { |
| $(name + 'Display').innerHTML = value; |
| } |
| </script> |
| </head> |
| <body> |
| <h1>Constraints and Statistics</h1> |
| This page is meant to give some hints on how one can use constraints and statistics in WebRTC applications. |
| <p> |
| The form to the left gives constraints you can set on the getUserMedia call. |
| When you hit "open", it will (re)open the camera with these constraints. |
| <p> |
| The left picture is the local preview. The right picture is the picture |
| after being passed through the PeerConnection (locally). |
| <p> |
| Underneath the picture you will see a running display of how many Kbits/sec |
| the video feed uses for transmission. |
| <hr> |
| <table> |
| <tr> |
| <td align="top"> |
| <h2>getUserMedia constraints</h2> |
| <table> |
| <tr><td><td>Min<td>Max |
| <tr><td>Horizontal |
| <td><input type="range" id="minwidth" min="0" max="1280" value="300" |
| onchange="showValue(this.id, this.value)"> |
| <td><input type="range" id="maxwidth" min="0" max="1280" value="640" |
| onchange="showValue(this.id, this.value)"> |
| <td><span id="minwidthDisplay">300</span>-<span id="maxwidthDisplay">640</span> |
| <tr><td>Vertical |
| <td><input type="range" id="minheight" min="0" max="1280" value="200" |
| onchange="showValue(this.id, this.value)"> |
| <td><input type="range" id="maxheight" min="0" max="1280" value="480" |
| onchange="showValue(this.id, this.value)"> |
| <td><span id="minheightDisplay">200</span>-<span id="maxheightDisplay">480</span> |
| <tr><td> |
| FrameRate |
| <td colspan=2><input type="range" id="frameRate" min="0" max="60" value="30" |
| onchange="showValue(this.id, this.value)"> |
| <td><span id="frameRateDisplay">30</span> |
| </table> |
| <input type="submit" name="capture" value="Capture!" onclick="openCamera()"> |
| </td> |
| <td align="top"> |
| <h2>addStream constraints</h2> |
| Maximum bitrate |
| <input type="range" id="bandwidth" min="0" max="2000" value="1000" |
| onchange="showValue(this.id, this.value)"> |
| <span id="bandwidthDisplay">1000</span> |
| <br> |
| <input type="submit" name="connect" value="Connect!" onclick="connect()"> |
| </td> |
| </tr> |
| <tr> |
| <td> |
| <video id="local-video" autoplay width=400 muted="true"></video> |
| </td> |
| <td> |
| <video id="remote-video" autoplay width=400></video> |
| </td> |
| <tr> |
| <td><span id="local-video-stats"></span> |
| <td><span id="remote-video-stats"></span> |
| <br> |
| <span id="bitrate">Bitrate unknown</span> |
| </td> |
| </tr> |
| <tr> |
| <td><pre><span id="cameraConstraints"></span></pre> |
| <td><pre><span id="addStreamConstraints"></span></pre> |
| </table> |
| <h2>Statistics report display</h2> |
| <table> |
| <tr> |
| <th>Sender side<th>Receiver side |
| <tr> |
| <td align="top"><div id="senderstats">Stats will appear here.</div> |
| <td align="top"><div id="receiverstats">Stats will appear here.</div> |
| </table> |
| </body> |
| </html> |