├── .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 |
37 |
Select camera(s) below:
38 |
39 |
40 |
41 |
42 |
Click one of the buttons below to find camera resolutions:
43 |
44 |
Quick Scan
45 |
46 |
47 |
56 |
57 |
58 |
59 |
60 |
61 |
Jump to bottom of table
62 |
63 | Browser Device Res Name Ratio Ask Actual Status deviceIndex resIndex
64 |
65 |
Refresh to run test again with same or different parameters (you'll lose the table above).
66 |
67 |
68 |
69 |
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 |
--------------------------------------------------------------------------------