├── .gitignore ├── LICENSE ├── README.md ├── index.html └── js └── resolutionScan.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .gitignore -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 webrtcHacks 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | WebRTC-Camera-Resolution 2 | ======================== 3 | 4 | Finds WebRTC Camera resolutions. 5 | 6 | Simple demo to show how one can automatically identify camera resolutions for use with WebRTC. 7 | Quick scan checks only common video resolutions. 8 | Full scan checks all 1:1, 4:3 and 16:9 resolutions between an entered range. 9 | 10 | ### Updated February 2016 11 | 12 | What's new: 13 | * support of the latest WebRTC getUserMedia and device enumeration specs 14 | * link to [adapter-latest.js](https://webrtc.github.io/adapter/adapter-latest.js) 15 | * added 1:1 aspect ratio scan to the full scanner 16 | * added bootstrap 17 | * added some links to more easily jump around the table 18 | * made sure it works with Chrome, Firefox, and Edge 19 | 20 | Try it at https://webrtchacks.github.io/WebRTC-Camera-Resolution/ 21 | 22 | Read more at https://webrtchacks.com/getusermedia-resolutions-3/ 23 | 24 | Brought to you by [webrtcHacks.com](http://webrtchacks.com) 25 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | WebRTC Camera Resolution Finder 7 | 8 | 9 | 10 | 29 | 30 | 31 | 32 | 33 |
34 |
35 |

WebRTC getUserMedia camera resolution finder

36 | 40 |
41 |
42 |

Click one of the buttons below to find camera resolutions:

43 |
44 | 45 |

46 |
47 |
48 | 49 |

vertical resolution range:
50 | max: 51 | 52 | to min: 53 | 54 |

55 |
56 |
57 |
58 | 59 |
60 |
61 | 62 | 63 | 64 | 65 | 66 | 67 |
68 |
69 |
70 |

Visit webrtcHacks.com for more details.

71 |
72 |
73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /js/resolutionScan.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Main js file for WebRTC-Camera-Resolution finder 3 | * Created by chad on 7/19/2014. 4 | * Modified January 1, 2016 5 | */ 6 | 7 | 'use strict'; 8 | 9 | //Global variables 10 | let video = $('#video')[0], //where we will put & test our video output 11 | deviceList = $('#devices')[0], //device list dropdown 12 | devices = [], //getSources object to hold various camera options 13 | selectedCamera = [], //used to hold a camera's ID and other parameters 14 | tests, //holder for our test results 15 | r = 0, //used for iterating through the array 16 | camNum = 0, //used for iterating through number of camera 17 | scanning = false; //variable to show if we are in the middle of a scan 18 | 19 | function gotDevices(deviceInfos) { 20 | $('#selectArea').show(); 21 | let camcount = 1; //used for labeling if the device label is not enumerated 22 | for (let i = 0; i !== deviceInfos.length; ++i) { 23 | let deviceInfo = deviceInfos[i]; 24 | let option = document.createElement('option'); 25 | option.value = deviceInfo.deviceId; 26 | if (deviceInfo.kind === 'videoinput') { 27 | option.text = deviceInfo.label || 'camera ' + camcount; 28 | devices.push(option); 29 | deviceList.add(option); 30 | camcount++; 31 | } 32 | } 33 | } 34 | 35 | function errorCallback(error) { 36 | console.log('navigator.getUserMedia error: ', error); 37 | } 38 | 39 | 40 | //find & list camera devices on load 41 | $(document).ready(() => { 42 | 43 | console.log("adapter.js says this is " + adapter.browserDetails.browser + " " + adapter.browserDetails.version); 44 | 45 | if (!navigator.getUserMedia) { 46 | alert('You need a browser that supports WebRTC'); 47 | $("div").hide(); 48 | return; 49 | } 50 | 51 | //Call gUM early to force user gesture and allow device enumeration 52 | navigator.mediaDevices.getUserMedia({audio: false, video: true}) 53 | .then((mediaStream) => { 54 | 55 | window.stream = mediaStream; // make globally available 56 | video.srcObject = mediaStream; 57 | 58 | //Now enumerate devices 59 | navigator.mediaDevices.enumerateDevices() 60 | .then(gotDevices) 61 | .catch(errorCallback); 62 | 63 | }) 64 | .catch((error) => { 65 | console.error('getUserMedia error!', error); 66 | }); 67 | 68 | //Localhost unsecure http connections are allowed 69 | if (document.location.hostname !== "localhost") { 70 | //check if the user is using http vs. https & redirect to https if needed 71 | if (document.location.protocol !== "https:") { 72 | $(document).html("This doesn't work well on http. Redirecting to https"); 73 | console.log("redirecting to https"); 74 | document.location.href = "https:" + document.location.href.substring(document.location.protocol.length); 75 | } 76 | } 77 | //Show text of what res's are used on QuickScan 78 | let quickText = "Sizes:"; 79 | for (let q = 0; q < quickScan.length; q++) { 80 | quickText += " " + quickScan[q].label 81 | } 82 | $('#quickLabel').text(quickText); 83 | 84 | }); 85 | 86 | //Start scan by controlling the quick and full scan buttons 87 | $('button').click(function(){ 88 | 89 | //setup for a quick scan using the hand-built quickScan object 90 | if (this.innerHTML === "Quick Scan") { 91 | console.log("Quick scan"); 92 | tests = quickScan; 93 | } 94 | //setup for a full scan and build scan object based on inputs 95 | else if (this.innerHTML === "Full Scan") { 96 | let highRes = $('#hiRes').val(); 97 | let lowRes = $('#loRes').val(); 98 | console.log("Full scan from " + lowRes + " to " + highRes); 99 | tests = createAllResolutions(parseInt(lowRes), parseInt(highRes)); 100 | } 101 | else { 102 | return 103 | } 104 | 105 | scanning = true; 106 | $('button').prop("disabled", true); 107 | $('table').show(); 108 | $('#jump').show(); 109 | 110 | //if there is device enumeration 111 | if (devices) { 112 | 113 | //run through the deviceList to see what is selected 114 | for (let deviceCount = 0, d = 0; d < deviceList.length; d++) { 115 | if (deviceList[d].selected) { 116 | //if it is selected, check the label against the getSources array to select the proper ID 117 | for (let z = 0; z < devices.length; z++) { 118 | if (devices[z].value === deviceList[d].value) { 119 | 120 | //just pass along the id and label 121 | let camera = {}; 122 | camera.id = devices[z].value; 123 | camera.label = devices[z].text; 124 | selectedCamera[deviceCount] = camera; 125 | console.log(selectedCamera[deviceCount].label + "[" + selectedCamera[deviceCount].id + "] selected"); 126 | deviceCount++; 127 | } 128 | } 129 | } 130 | } 131 | 132 | //Make sure there is at least 1 camera selected before starting 133 | if (selectedCamera[0]) { 134 | gum(tests[r], selectedCamera[0]); 135 | } 136 | else { 137 | console.log("No camera selected. Defaulting to " + deviceList[0].text); 138 | //$('button').prop("disabled",false); 139 | 140 | selectedCamera[0] = {id: deviceList[0].value, label: deviceList[0].text}; 141 | gum(tests[r], selectedCamera[0]); 142 | 143 | } 144 | } 145 | //if no device enumeration don't pass a Camera ID 146 | else { 147 | selectedCamera[0] = {label: "Unknown"}; 148 | gum(tests[r]); 149 | } 150 | 151 | }); 152 | 153 | //calls getUserMedia for a given camera and constraints 154 | function gum(candidate, device) { 155 | console.log("trying " + candidate.label + " on " + device.label); 156 | 157 | //Kill any running streams; 158 | if (window.stream) { 159 | stream.getTracks().forEach((track) => { 160 | track.stop(); 161 | }); 162 | } 163 | 164 | //create constraints object 165 | let constraints = { 166 | audio: false, 167 | video: { 168 | deviceId: device.id ? {exact: device.id} : undefined, 169 | width: {exact: candidate.width}, //new syntax 170 | height: {exact: candidate.height} //new syntax 171 | } 172 | }; 173 | 174 | setTimeout(() => { 175 | navigator.mediaDevices.getUserMedia(constraints) 176 | .then(gotStream) 177 | .catch((error) => { 178 | console.log('getUserMedia error!', error); 179 | 180 | if (scanning) { 181 | captureResults("fail: " + error.name); 182 | } 183 | }); 184 | }, (window.stream ? 200 : 0)); //official examples had this at 200 185 | 186 | 187 | function gotStream(mediaStream) { 188 | 189 | //change the video dimensions 190 | console.log("Display size for " + candidate.label + ": " + candidate.width + "x" + candidate.height); 191 | video.width = candidate.width; 192 | video.height = candidate.height; 193 | 194 | window.stream = mediaStream; // make globally available 195 | video.srcObject = mediaStream; 196 | 197 | } 198 | } 199 | 200 | 201 | function displayVideoDimensions() { 202 | //This should only happen during setup 203 | if (tests === undefined) 204 | return; 205 | 206 | //Wait for dimensions if they don't show right away 207 | if (!video.videoWidth) { 208 | setTimeout(displayVideoDimensions, 500); //was 500 209 | } 210 | 211 | if (video.videoWidth * video.videoHeight > 0) { 212 | if (tests[r].width + "x" + tests[r].height !== video.videoWidth + "x" + video.videoHeight) { 213 | captureResults("fail: mismatch"); 214 | } 215 | else { 216 | captureResults("pass"); 217 | } 218 | } 219 | } 220 | 221 | 222 | video.onloadedmetadata = displayVideoDimensions; 223 | 224 | 225 | //Save results to the candidate so 226 | function captureResults(status) { 227 | console.log("Stream dimensions for " + tests[r].label + ": " + video.videoWidth + "x" + video.videoHeight); 228 | 229 | if (!scanning) //exit if scan is not active 230 | return; 231 | 232 | tests[r].status = status; 233 | tests[r].streamWidth = video.videoWidth; 234 | tests[r].streamHeight = video.videoHeight; 235 | 236 | let row = $('table#results')[0].insertRow(-1); 237 | let browserVer = row.insertCell(0); 238 | let deviceName = row.insertCell(1); 239 | let label = row.insertCell(2); 240 | let ratio = row.insertCell(3); 241 | let ask = row.insertCell(4); 242 | let actual = row.insertCell(5); 243 | let statusCell = row.insertCell(6); 244 | let deviceIndex = row.insertCell(7); 245 | let resIndex = row.insertCell(8); 246 | 247 | //don't show these 248 | deviceIndex.style.display = "none"; 249 | resIndex.style.display = "none"; 250 | 251 | deviceIndex.class = "hidden"; 252 | resIndex.class = "hidden"; 253 | 254 | browserVer.innerHTML = adapter.browserDetails.browser + " " + adapter.browserDetails.version; 255 | deviceName.innerHTML = selectedCamera[camNum].label; 256 | label.innerHTML = tests[r].label; 257 | ratio.innerHTML = tests[r].ratio; 258 | ask.innerHTML = tests[r].width + "x" + tests[r].height; 259 | actual.innerHTML = tests[r].streamWidth + "x" + tests[r].streamHeight; 260 | statusCell.innerHTML = tests[r].status; 261 | deviceIndex.innerHTML = camNum; //used for debugging 262 | resIndex.innerHTML = r; //used for debugging 263 | 264 | r++; 265 | 266 | //go to the next tests 267 | if (r < tests.length) { 268 | gum(tests[r], selectedCamera[camNum]); 269 | } 270 | else if (camNum < selectedCamera.length - 1) { //move on to the next camera 271 | camNum++; 272 | r = 0; 273 | gum(tests[r], selectedCamera[camNum]) 274 | } 275 | else { //finish up 276 | video.removeEventListener("onloadedmetadata", displayVideoDimensions); //turn off the event handler 277 | $('button').off("click"); //turn the generic button handler off 278 | 279 | scanning = false; 280 | 281 | $(".pfin").show(); 282 | $('#csvOut').click(function() { 283 | exportTableToCSV.apply(this, [$('#results'), 'gumResTestExport.csv']); 284 | }); 285 | 286 | //allow to click on a row to test (only works with device Enumeration 287 | if (devices) { 288 | clickRows(); 289 | } 290 | } 291 | } 292 | 293 | //allow clicking on a row to see the camera capture 294 | //To do: figure out why this doesn't work in Firefox 295 | function clickRows() { 296 | $('tr').click(function() { 297 | r = $(this).find("td").eq(8).html(); 298 | 299 | //lookup the device id based on the row label 300 | for (let z = 0; z < selectedCamera.length; z++) { 301 | if (selectedCamera[z].label === $(this).find("td").eq(1).html()) { 302 | var thisCam = selectedCamera[z]; //devices[z].value; 303 | console.log(this) 304 | } 305 | } 306 | 307 | console.log("table click! clicked on " + thisCam + ":" + tests[r].label); 308 | gum(tests[r], thisCam); 309 | }) 310 | } 311 | 312 | 313 | //Variables to use in the quick scan 314 | const quickScan = [ 315 | { 316 | "label": "4K(UHD)", 317 | "width": 3840, 318 | "height": 2160, 319 | "ratio": "16:9" 320 | }, 321 | { 322 | "label": "1080p(FHD)", 323 | "width": 1920, 324 | "height": 1080, 325 | "ratio": "16:9" 326 | }, 327 | { 328 | "label": "UXGA", 329 | "width": 1600, 330 | "height": 1200, 331 | "ratio": "4:3" 332 | }, 333 | { 334 | "label": "720p(HD)", 335 | "width": 1280, 336 | "height": 720, 337 | "ratio": "16:9" 338 | }, 339 | { 340 | "label": "SVGA", 341 | "width": 800, 342 | "height": 600, 343 | "ratio": "4:3" 344 | }, 345 | { 346 | "label": "VGA", 347 | "width": 640, 348 | "height": 480, 349 | "ratio": "4:3" 350 | }, 351 | { 352 | "label": "360p(nHD)", 353 | "width": 640, 354 | "height": 360, 355 | "ratio": "16:9" 356 | }, 357 | { 358 | "label": "CIF", 359 | "width": 352, 360 | "height": 288, 361 | "ratio": "4:3" 362 | }, 363 | { 364 | "label": "QVGA", 365 | "width": 320, 366 | "height": 240, 367 | "ratio": "4:3" 368 | }, 369 | { 370 | "label": "QCIF", 371 | "width": 176, 372 | "height": 144, 373 | "ratio": "4:3" 374 | }, 375 | { 376 | "label": "QQVGA", 377 | "width": 160, 378 | "height": 120, 379 | "ratio": "4:3" 380 | } 381 | 382 | ]; 383 | 384 | //creates an object with all HD & SD video ratios between two heights 385 | function createAllResolutions(minHeight, maxHeight) { 386 | const ratioHD = 16 / 9; 387 | const ratioSD = 4 / 3; 388 | 389 | let resolutions = [], 390 | res; 391 | 392 | for (let y = maxHeight; y >= minHeight; y--) { 393 | //HD 394 | res = { 395 | "label": (y * ratioHD).toFixed() + "x" + y, 396 | "width": parseInt((y * ratioHD).toFixed()), //this was returning a string 397 | "height": y, 398 | "ratio": "16:9" 399 | }; 400 | resolutions.push(res); 401 | 402 | //SD 403 | res = { 404 | "label": (y * ratioSD).toFixed() + "x" + y, 405 | "width": parseInt((y * ratioSD).toFixed()), 406 | "height": y, 407 | "ratio": "4:3" 408 | }; 409 | resolutions.push(res); 410 | 411 | //square 412 | //noinspection JSSuspiciousNameCombination 413 | res = { 414 | "label": y + "x" + y, 415 | "width": y, 416 | "height": y, 417 | "ratio": "1:1" 418 | }; 419 | resolutions.push(res); 420 | 421 | } 422 | console.log("resolutions length: " + resolutions.length); 423 | return resolutions; 424 | } 425 | 426 | 427 | /* 428 | Export results table to a CSV file in new window for download 429 | source: http://jsfiddle.net/terryyounghk/KPEGU/ 430 | */ 431 | function exportTableToCSV($table, filename) { 432 | 433 | let $rows = $table.find('tr:has(th), tr:has(td)'), 434 | 435 | // Temporary delimiter characters unlikely to be typed by keyboard 436 | // This is to avoid accidentally splitting the actual contents 437 | tmpColDelim = String.fromCharCode(11), // vertical tab character 438 | tmpRowDelim = String.fromCharCode(0), // null character 439 | 440 | // actual delimiter characters for CSV format 441 | colDelim = '","', 442 | rowDelim = '"\r\n"', 443 | 444 | // Grab text from table into CSV formatted string 445 | csv = '"' + $rows.map((i, row) => { 446 | let $row = $(row), 447 | $cols = $row.find('th, td'); 448 | 449 | return $cols.map((j, col) => { 450 | let $col = $(col), 451 | text = $col.text(); 452 | 453 | return text.replace(/"/g, '""'); // escape double quotes 454 | 455 | }).get().join(tmpColDelim); 456 | 457 | }).get().join(tmpRowDelim) 458 | .split(tmpRowDelim).join(rowDelim) 459 | .split(tmpColDelim).join(colDelim) + '"', 460 | 461 | // Data URI 462 | csvData = 'data:application/csv;charset=utf-8,' + encodeURIComponent(csv); 463 | 464 | $(this) 465 | .attr({ 466 | 'download': filename, 467 | 'href': csvData, 468 | 'target': '_blank' 469 | }); 470 | } 471 | --------------------------------------------------------------------------------