]> vault307.fbx.one Git - esp32Cam.git/blob - index.html
esp32Camera webserver
[esp32Cam.git] / index.html
1 <!doctype html>
2 <html>
3 <head>
4 <meta charset="utf-8">
5 <meta name="viewport" content="width=device-width,initial-scale=1">
6 <title>ESP32 OV2640</title>
7 <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
8 <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
9 <link rel="stylesheet" type="text/css" href="/style.css">
10 <style>
11 @media (min-width: 800px) and (orientation:landscape) {
12 #content {
13 display:flex;
14 flex-wrap: nowrap;
15 align-items: stretch
16 }
17 }
18 </style>
19 </head>
20
21 <body>
22 <section class="main">
23 <div id="logo">
24 <label for="nav-toggle-cb" id="nav-toggle" style="float:left;">&#9776;&nbsp;&nbsp;Settings&nbsp;&nbsp;&nbsp;&nbsp;</label>
25 <button id="swap-viewer" style="float:left;" title="Swap to simple viewer">Simple</button>
26 <button id="get-still" style="float:left;">Get Still</button>
27 <button id="toggle-stream" style="float:left;" class="hidden">Start Stream</button>
28 <div id="wait-settings" style="float:left;" class="loader" title="Waiting for camera settings to load"></div>
29 </div>
30 <div id="content">
31 <div class="hidden" id="sidebar">
32 <input type="checkbox" id="nav-toggle-cb" checked="checked">
33 <nav id="menu">
34 <div class="input-group hidden" id="lamp-group" title="Flashlight LED.&#013;&#013;Warning:&#013;Built-In lamps can be Very Bright! Avoid looking directly at LED&#013;Can draw a lot of power and may cause visual artifacts, affect WiFi or even brownout the camera on high settings">
35 <label for="lamp">Light</label>
36 <div class="range-min">Off</div>
37 <input type="range" id="lamp" min="0" max="100" value="0" class="default-action">
38 <div class="range-max"><span style="font-size: 125%;">&#9888;</span>Full</div>
39 </div>
40 <div class="input-group hidden" id="autolamp-group" title="When enabled the lamp will only turn on while the camera is active">
41 <label for="autolamp">Auto Lamp</label>
42 <div class="switch">
43 <input id="autolamp" type="checkbox" class="default-action">
44 <label class="slider" for="autolamp"></label>
45 </div>
46 </div>
47
48 <div class="input-group" id="framesize-group" title="Camera resolution&#013;Higher resolutions will result in lower framerates">
49 <label for="framesize">Resolution</label>
50 <select id="framesize" class="default-action">
51 <option value="13">UXGA (1600x1200)</option>
52 <option value="12">SXGA (1280x1024)</option>
53 <option value="11">HD (1280x720)</option>
54 <option value="10">XGA (1024x768)</option>
55 <option value="9">SVGA (800x600)</option>
56 <option value="8">VGA (640x480)</option>
57 <option value="7">HVGA (480x320)</option>
58 <option value="6">CIF (400x296)</option>
59 <option value="5">QVGA (320x240)</option>
60 <option value="3">HQVGA (240x176)</option>
61 <option value="1">QQVGA (160x120)</option>
62 <option value="0">THUMB (96x96)</option>
63 </select>
64 </div>
65 <div class="input-group" id="quality-group" title="Camera Image and Stream quality factor&#013;Higher settings will result in lower framerates">
66 <label for="quality">Quality</label>
67 <div class="range-min">Low</div>
68 <!-- Note; the following element is 'flipped' in CSS so that it slides from High to Low
69 As a result the 'min' and 'max' values are reversed here too -->
70 <input type="range" id="quality" min="6" max="63" value="10" class="default-action">
71 <div class="range-max">High</div>
72 </div>
73 <div class="input-group" id="set-xclk-group" title="Camera Bus Clock Frequency&#013;Increasing this will raise the camera framerate and capture speed&#013;&#013;Raising too far will result in visual artifacts and/or incomplete frames&#013;This setting can vary a lot between boards, budget boards typically need lower values">
74 <label for="set-xclk">XCLK</label>
75 <div class="text">
76 <input id="xclk" type="number" min="2" max="32" size="3" step="1" class="default-action">
77 <div class="range-max">MHz</div>
78 </div>
79 </div>
80 <div class="input-group" id="brightness-group">
81 <label for="brightness">Brightness</label>
82 <div class="range-min">-2</div>
83 <input type="range" id="brightness" min="-2" max="2" value="0" class="default-action">
84 <div class="range-max">2</div>
85 </div>
86 <div class="input-group" id="contrast-group">
87 <label for="contrast">Contrast</label>
88 <div class="range-min">-2</div>
89 <input type="range" id="contrast" min="-2" max="2" value="0" class="default-action">
90 <div class="range-max">2</div>
91 </div>
92 <div class="input-group" id="saturation-group">
93 <label for="saturation">Saturation</label>
94 <div class="range-min">-2</div>
95 <input type="range" id="saturation" min="-2" max="2" value="0" class="default-action">
96 <div class="range-max">2</div>
97 </div>
98 <div class="input-group" id="special_effect-group">
99 <label for="special_effect">Special Effect</label>
100 <select id="special_effect" class="default-action">
101 <option value="0" selected="selected">No Effect</option>
102 <option value="1">Negative</option>
103 <option value="2">Grayscale</option>
104 <option value="3">Red Tint</option>
105 <option value="4">Green Tint</option>
106 <option value="5">Blue Tint</option>
107 <option value="6">Sepia</option>
108 </select>
109 </div>
110 <div class="input-group" id="awb-group">
111 <label for="awb">AWB Enable</label>
112 <div class="switch">
113 <input id="awb" type="checkbox" class="default-action" checked="checked">
114 <label class="slider" for="awb"></label>
115 </div>
116 </div>
117 <div class="input-group" id="awb_gain-group">
118 <label for="awb_gain">Manual AWB Gain</label>
119 <div class="switch">
120 <input id="awb_gain" type="checkbox" class="default-action" checked="checked">
121 <label class="slider" for="awb_gain"></label>
122 </div>
123 </div>
124 <div class="input-group" id="wb_mode-group">
125 <label for="wb_mode">WB Mode</label>
126 <select id="wb_mode" class="default-action">
127 <option value="0" selected="selected">Auto</option>
128 <option value="1">Sunny</option>
129 <option value="2">Cloudy</option>
130 <option value="3">Office</option>
131 <option value="4">Home</option>
132 </select>
133 </div>
134 <div class="input-group" id="aec-group">
135 <label for="aec">AEC Sensor Enable</label>
136 <div class="switch">
137 <input id="aec" type="checkbox" class="default-action" checked="checked">
138 <label class="slider" for="aec"></label>
139 </div>
140 </div>
141 <div class="input-group" id="aec2-group">
142 <label for="aec2">AEC DSP</label>
143 <div class="switch">
144 <input id="aec2" type="checkbox" class="default-action" checked="checked">
145 <label class="slider" for="aec2"></label>
146 </div>
147 </div>
148 <div class="input-group" id="ae_level-group">
149 <label for="ae_level">AE Level</label>
150 <div class="range-min">-2</div>
151 <input type="range" id="ae_level" min="-2" max="2" value="0" class="default-action">
152 <div class="range-max">2</div>
153 </div>
154 <div class="input-group" id="aec_value-group">
155 <label for="aec_value">Exposure</label>
156 <div class="range-min">0</div>
157 <input type="range" id="aec_value" min="0" max="1200" value="204" class="default-action">
158 <div class="range-max">1200</div>
159 </div>
160 <div class="input-group" id="agc-group">
161 <label for="agc">AGC</label>
162 <div class="switch">
163 <input id="agc" type="checkbox" class="default-action" checked="checked">
164 <label class="slider" for="agc"></label>
165 </div>
166 </div>
167 <div class="input-group hidden" id="agc_gain-group">
168 <label for="agc_gain">Gain</label>
169 <div class="range-min">1x</div>
170 <input type="range" id="agc_gain" min="0" max="30" value="5" class="default-action">
171 <div class="range-max">31x</div>
172 </div>
173 <div class="input-group" id="gainceiling-group">
174 <label for="gainceiling">Gain Ceiling</label>
175 <div class="range-min">2x</div>
176 <input type="range" id="gainceiling" min="0" max="6" value="0" class="default-action">
177 <div class="range-max">128x</div>
178 </div>
179 <div class="input-group" id="bpc-group">
180 <label for="bpc">BPC</label>
181 <div class="switch">
182 <input id="bpc" type="checkbox" class="default-action">
183 <label class="slider" for="bpc"></label>
184 </div>
185 </div>
186 <div class="input-group" id="wpc-group">
187 <label for="wpc">WPC</label>
188 <div class="switch">
189 <input id="wpc" type="checkbox" class="default-action" checked="checked">
190 <label class="slider" for="wpc"></label>
191 </div>
192 </div>
193 <div class="input-group" id="raw_gma-group">
194 <label for="raw_gma">Raw GMA Enable</label>
195 <div class="switch">
196 <input id="raw_gma" type="checkbox" class="default-action" checked="checked">
197 <label class="slider" for="raw_gma"></label>
198 </div>
199 </div>
200 <div class="input-group" id="lenc-group">
201 <label for="lenc">Lens Correction</label>
202 <div class="switch">
203 <input id="lenc" type="checkbox" class="default-action" checked="checked">
204 <label class="slider" for="lenc"></label>
205 </div>
206 </div>
207 <div class="input-group" id="hmirror-group">
208 <label for="hmirror">H-Mirror Stream</label>
209 <div class="switch">
210 <input id="hmirror" type="checkbox" class="default-action" checked="checked">
211 <label class="slider" for="hmirror"></label>
212 </div>
213 </div>
214 <div class="input-group" id="vflip-group">
215 <label for="vflip">V-Flip Stream</label>
216 <div class="switch">
217 <input id="vflip" type="checkbox" class="default-action" checked="checked">
218 <label class="slider" for="vflip"></label>
219 </div>
220 </div>
221 <div class="input-group" id="rotate-group">
222 <label for="rotate">Rotate in Browser</label>
223 <select id="rotate" class="default-action">
224 <option value="90">90&deg; (Right)</option>
225 <option value="0" selected="selected">0&deg; (None)</option>
226 <option value="-90">-90&deg; (Left)</option>
227 </select>
228 </div>
229 <div class="input-group" id="dcw-group">
230 <label for="dcw">DCW (Downsize EN)</label>
231 <div class="switch">
232 <input id="dcw" type="checkbox" class="default-action" checked="checked">
233 <label class="slider" for="dcw"></label>
234 </div>
235 </div>
236 <div class="input-group" id="colorbar-group">
237 <label for="colorbar">Test Pattern</label>
238 <div class="switch">
239 <input id="colorbar" type="checkbox" class="default-action">
240 <label class="slider" for="colorbar"></label>
241 </div>
242 </div>
243 <div class="input-group" id="min_frame_time-group" title="Minimum frame time&#013;Higher settings reduce the frame rate&#013;Use this for a smoother stream and to reduce load on the WiFi and browser">
244 <label for="min_frame_time">Frame Duration Limit</label>
245 <select id="min_frame_time" class="default-action">
246 <option value="3333">3.3s (0.3fps)</option>
247 <option value="2000">2s (0.5fps)</option>
248 <option value="1000">1s (1fps)</option>
249 <option value="500">500ms (2fps)</option>
250 <option value="333">333ms (3fps)</option>
251 <option value="200">200ms (5fps)</option>
252 <option value="100">100ms (10fps)</option>
253 <option value="50">50ms (20fps)</option>
254 <option value="0" selected="selected">Disabled</option>
255 </select>
256 </div>
257 <div class="input-group" id="preferences-group">
258 <label for="prefs" style="line-height: 2em;">Preferences</label>
259 <button id="reboot" title="Reboot the camera module">Reboot</button>
260 <button id="save_prefs" title="Save Preferences on camera module">Save</button>
261 <button id="clear_prefs" title="Erase saved Preferences on camera module">Erase</button>
262 </div>
263 <div class="input-group" id="cam_name-group">
264 <label for="cam_name">
265 <a href="/dump" title="System Info" target="_blank">Name</a></label>
266 <div id="cam_name" class="default-action"></div>
267 </div>
268 <div class="input-group" id="code_ver-group">
269 <label for="code_ver">
270 <a href="https://github.com/easytarget/esp32-cam-webserver"
271 title="ESP32 Cam Webserver on GitHub" target="_blank">Firmware</a></label>
272 <div id="code_ver" class="default-action"></div>
273 </div>
274 <div class="input-group hidden" id="stream-group">
275 <label for="stream_url" id="stream_link">Stream</label>
276 <div id="stream_url" class="default-action">Unknown</div>
277 </div>
278 </nav>
279 </div>
280 <figure>
281 <div id="stream-container" class="image-container hidden">
282 <div class="close close-rot-none" id="close-stream">×</div>
283 <img id="stream" src="">
284 </div>
285 </figure>
286 </div>
287 </section>
288 </body>
289
290 <script>
291 document.addEventListener('DOMContentLoaded', function (event) {
292 var baseHost = document.location.origin;
293 var streamURL = 'Undefined';
294 var viewerURL = 'Undefined';
295
296 const header = document.getElementById('logo')
297 const settings = document.getElementById('sidebar')
298 const waitSettings = document.getElementById('wait-settings')
299 const lampGroup = document.getElementById('lamp-group')
300 const autolampGroup = document.getElementById('autolamp-group')
301 const streamGroup = document.getElementById('stream-group')
302 const camName = document.getElementById('cam_name')
303 const codeVer = document.getElementById('code_ver')
304 const rotate = document.getElementById('rotate')
305 const view = document.getElementById('stream')
306 const viewContainer = document.getElementById('stream-container')
307 const stillButton = document.getElementById('get-still')
308 const streamButton = document.getElementById('toggle-stream')
309 const closeButton = document.getElementById('close-stream')
310 const streamLink = document.getElementById('stream_link')
311 const framesize = document.getElementById('framesize')
312 const xclk = document.getElementById('xclk')
313 const swapButton = document.getElementById('swap-viewer')
314 const savePrefsButton = document.getElementById('save_prefs')
315 const clearPrefsButton = document.getElementById('clear_prefs')
316 const rebootButton = document.getElementById('reboot')
317 const minFrameTime = document.getElementById('min_frame_time')
318
319 const hide = el => {
320 el.classList.add('hidden')
321 }
322 const show = el => {
323 el.classList.remove('hidden')
324 }
325
326 const disable = el => {
327 el.classList.add('disabled')
328 el.disabled = true
329 }
330
331 const enable = el => {
332 el.classList.remove('disabled')
333 el.disabled = false
334 }
335
336 const updateValue = (el, value, updateRemote) => {
337 updateRemote = updateRemote == null ? true : updateRemote
338 let initialValue
339 if (el.type === 'checkbox') {
340 initialValue = el.checked
341 value = !!value
342 el.checked = value
343 } else {
344 initialValue = el.value
345 el.value = value
346 }
347
348 if (updateRemote && initialValue !== value) {
349 updateConfig(el);
350 } else if(!updateRemote){
351 if(el.id === "aec"){
352 value ? hide(exposure) : show(exposure)
353 } else if(el.id === "agc"){
354 if (value) {
355 show(gainCeiling)
356 hide(agcGain)
357 } else {
358 hide(gainCeiling)
359 show(agcGain)
360 }
361 } else if(el.id === "awb_gain"){
362 value ? show(wb) : hide(wb)
363 } else if(el.id === "lamp"){
364 if (value == -1) {
365 hide(lampGroup)
366 hide(autolampGroup)
367 } else {
368 show(lampGroup)
369 show(autolampGroup)
370 }
371 } else if(el.id === "cam_name"){
372 camName.innerHTML = value;
373 window.document.title = value;
374 console.log('Name set to: ' + value);
375 } else if(el.id === "code_ver"){
376 codeVer.innerHTML = value;
377 console.log('Firmware Build: ' + value);
378 } else if(el.id === "rotate"){
379 rotate.value = value;
380 applyRotation();
381 } else if(el.id === "min_frame_time"){
382 min_frame_time.value = value;
383 } else if(el.id === "stream_url"){
384 streamURL = value;
385 viewerURL = value + 'view';
386 stream_url.innerHTML = value;
387 stream_link.setAttribute("title", `Open the standalone stream viewer :: ${viewerURL}`);
388 stream_link.style.textDecoration = "underline";
389 stream_link.style.cursor = "pointer";
390 streamButton.setAttribute("title", `Start the stream :: ${streamURL}`);
391 show(streamGroup)
392 console.log('Stream URL set to: ' + streamURL);
393 console.log('Stream Viewer URL set to: ' + viewerURL);
394 }
395 }
396 }
397
398 var rangeUpdateScheduled = false
399 var latestRangeConfig
400
401 function updateRangeConfig (el) {
402 latestRangeConfig = el
403 if (!rangeUpdateScheduled) {
404 rangeUpdateScheduled = true;
405 setTimeout(function(){
406 rangeUpdateScheduled = false
407 updateConfig(latestRangeConfig)
408 }, 150);
409 }
410 }
411
412 function updateConfig (el) {
413 let value
414 switch (el.type) {
415 case 'checkbox':
416 value = el.checked ? 1 : 0
417 break
418 case 'range':
419 case 'number':
420 case 'select-one':
421 value = el.value
422 break
423 case 'button':
424 case 'submit':
425 value = '1'
426 break
427 default:
428 return
429 }
430
431 const query = `${baseHost}/control?var=${el.id}&val=${value}`
432
433 fetch(query)
434 .then(response => {
435 console.log(`request to ${query} finished, status: ${response.status}`)
436 })
437 }
438
439 document
440 .querySelectorAll('.close')
441 .forEach(el => {
442 el.onclick = () => {
443 hide(el.parentNode)
444 }
445 })
446
447 // read initial values
448 fetch(`${baseHost}/status`)
449 .then(function (response) {
450 return response.json()
451 })
452 .then(function (state) {
453 document
454 .querySelectorAll('.default-action')
455 .forEach(el => {
456 updateValue(el, state[el.id], false)
457 })
458 hide(waitSettings);
459 show(settings);
460 show(streamButton);
461 //startStream();
462 })
463
464 // Put some helpful text on the 'Still' button
465 stillButton.setAttribute("title", `Capture a still image :: ${baseHost}/capture`);
466
467 const stopStream = () => {
468 window.stop();
469 streamButton.innerHTML = 'Start Stream';
470 streamButton.setAttribute("title", `Start the stream :: ${streamURL}`);
471 hide(viewContainer);
472 }
473
474 const startStream = () => {
475 view.src = streamURL;
476 view.scrollIntoView(false);
477 streamButton.innerHTML = 'Stop Stream';
478 streamButton.setAttribute("title", `Stop the stream`);
479 show(viewContainer);
480 }
481
482 const applyRotation = () => {
483 rot = rotate.value;
484 if (rot == -90) {
485 viewContainer.style.transform = `rotate(-90deg) translate(-100%)`;
486 closeButton.classList.remove('close-rot-none');
487 closeButton.classList.remove('close-rot-right');
488 closeButton.classList.add('close-rot-left');
489 } else if (rot == 90) {
490 viewContainer.style.transform = `rotate(90deg) translate(0, -100%)`;
491 closeButton.classList.remove('close-rot-left');
492 closeButton.classList.remove('close-rot-none');
493 closeButton.classList.add('close-rot-right');
494 } else {
495 viewContainer.style.transform = `rotate(0deg)`;
496 closeButton.classList.remove('close-rot-left');
497 closeButton.classList.remove('close-rot-right');
498 closeButton.classList.add('close-rot-none');
499 }
500 console.log('Rotation ' + rot + ' applied');
501 }
502
503 // Attach actions to controls
504
505 streamLink.onclick = () => {
506 stopStream();
507 window.open(viewerURL, "_blank");
508 }
509
510 stillButton.onclick = () => {
511 stopStream();
512 view.src = `${baseHost}/capture?_cb=${Date.now()}`;
513 view.scrollIntoView(false);
514 show(viewContainer);
515 }
516
517 closeButton.onclick = () => {
518 stopStream();
519 hide(viewContainer);
520 }
521
522 streamButton.onclick = () => {
523 const streamEnabled = streamButton.innerHTML === 'Stop Stream'
524 if (streamEnabled) {
525 stopStream();
526 } else {
527 startStream();
528 }
529 }
530
531 // Attach default on change action
532 document
533 .querySelectorAll('.default-action')
534 .forEach(el => {
535 el.onchange = () => updateConfig(el)
536 })
537
538 // Update range sliders as they are being moved
539 document
540 .querySelectorAll('input[type="range"]')
541 .forEach(el => {
542 el.oninput = () => updateRangeConfig(el)
543 })
544
545 // Custom actions
546 // Gain
547 const agc = document.getElementById('agc')
548 const agcGain = document.getElementById('agc_gain-group')
549 const gainCeiling = document.getElementById('gainceiling-group')
550 agc.onchange = () => {
551 updateConfig(agc)
552 if (agc.checked) {
553 show(gainCeiling)
554 hide(agcGain)
555 } else {
556 hide(gainCeiling)
557 show(agcGain)
558 }
559 }
560
561 // Exposure
562 const aec = document.getElementById('aec')
563 const exposure = document.getElementById('aec_value-group')
564 aec.onchange = () => {
565 updateConfig(aec)
566 aec.checked ? hide(exposure) : show(exposure)
567 }
568
569 // AWB
570 const awb = document.getElementById('awb_gain')
571 const wb = document.getElementById('wb_mode-group')
572 awb.onchange = () => {
573 updateConfig(awb)
574 awb.checked ? show(wb) : hide(wb)
575 }
576
577 // Detection and framesize
578 rotate.onchange = () => {
579 applyRotation();
580 updateConfig(rotate);
581 }
582
583 framesize.onchange = () => {
584 updateConfig(framesize)
585 }
586
587 minFrameTime.onchange = () => {
588 updateConfig(minFrameTime)
589 }
590
591 xclk.onchange = () => {
592 console.log("xclk:" , xclk);
593 updateConfig(xclk)
594 }
595
596 swapButton.onclick = () => {
597 window.open('/?view=simple','_self');
598 }
599
600 savePrefsButton.onclick = () => {
601 if (confirm("Save the current preferences?")) {
602 updateConfig(savePrefsButton);
603 }
604 }
605
606 clearPrefsButton.onclick = () => {
607 if (confirm("Remove the saved preferences?")) {
608 updateConfig(clearPrefsButton);
609 }
610 }
611
612 rebootButton.onclick = () => {
613 if (confirm("Reboot the Camera Module?")) {
614 updateConfig(rebootButton);
615 // Some sort of countdown here?
616 hide(settings);
617 hide(viewContainer);
618 header.innerHTML = '<h1>Rebooting!</h1><hr>Page will reload after 30 seconds.';
619 setTimeout(function() {
620 location.replace(document.URL);
621 }, 30000);
622 }
623 }
624
625 })
626 </script>
627 </html>