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">
11 @media (min-width:
800px) and (orientation:landscape) {
22 <section class=
"main">
24 <label for=
"nav-toggle-cb" id=
"nav-toggle" style=
"float:left;">☰ Settings
</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>
31 <div class=
"hidden" id=
"sidebar">
32 <input type=
"checkbox" id=
"nav-toggle-cb" checked=
"checked">
34 <div class=
"input-group hidden" id=
"lamp-group" title=
"Flashlight LED.

Warning:
Built-In lamps can be Very Bright! Avoid looking directly at LED
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%;">⚠</span>Full
</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>
43 <input id=
"autolamp" type=
"checkbox" class=
"default-action">
44 <label class=
"slider" for=
"autolamp"></label>
48 <div class=
"input-group" id=
"framesize-group" title=
"Camera resolution
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>
65 <div class=
"input-group" id=
"quality-group" title=
"Camera Image and Stream quality factor
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>
73 <div class=
"input-group" id=
"set-xclk-group" title=
"Camera Bus Clock Frequency
Increasing this will raise the camera framerate and capture speed

Raising too far will result in visual artifacts and/or incomplete frames
This setting can vary a lot between boards, budget boards typically need lower values">
74 <label for=
"set-xclk">XCLK
</label>
76 <input id=
"xclk" type=
"number" min=
"2" max=
"32" size=
"3" step=
"1" class=
"default-action">
77 <div class=
"range-max">MHz
</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>
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>
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>
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>
110 <div class=
"input-group" id=
"awb-group">
111 <label for=
"awb">AWB Enable
</label>
113 <input id=
"awb" type=
"checkbox" class=
"default-action" checked=
"checked">
114 <label class=
"slider" for=
"awb"></label>
117 <div class=
"input-group" id=
"awb_gain-group">
118 <label for=
"awb_gain">Manual AWB Gain
</label>
120 <input id=
"awb_gain" type=
"checkbox" class=
"default-action" checked=
"checked">
121 <label class=
"slider" for=
"awb_gain"></label>
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>
134 <div class=
"input-group" id=
"aec-group">
135 <label for=
"aec">AEC Sensor Enable
</label>
137 <input id=
"aec" type=
"checkbox" class=
"default-action" checked=
"checked">
138 <label class=
"slider" for=
"aec"></label>
141 <div class=
"input-group" id=
"aec2-group">
142 <label for=
"aec2">AEC DSP
</label>
144 <input id=
"aec2" type=
"checkbox" class=
"default-action" checked=
"checked">
145 <label class=
"slider" for=
"aec2"></label>
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>
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>
160 <div class=
"input-group" id=
"agc-group">
161 <label for=
"agc">AGC
</label>
163 <input id=
"agc" type=
"checkbox" class=
"default-action" checked=
"checked">
164 <label class=
"slider" for=
"agc"></label>
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>
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>
179 <div class=
"input-group" id=
"bpc-group">
180 <label for=
"bpc">BPC
</label>
182 <input id=
"bpc" type=
"checkbox" class=
"default-action">
183 <label class=
"slider" for=
"bpc"></label>
186 <div class=
"input-group" id=
"wpc-group">
187 <label for=
"wpc">WPC
</label>
189 <input id=
"wpc" type=
"checkbox" class=
"default-action" checked=
"checked">
190 <label class=
"slider" for=
"wpc"></label>
193 <div class=
"input-group" id=
"raw_gma-group">
194 <label for=
"raw_gma">Raw GMA Enable
</label>
196 <input id=
"raw_gma" type=
"checkbox" class=
"default-action" checked=
"checked">
197 <label class=
"slider" for=
"raw_gma"></label>
200 <div class=
"input-group" id=
"lenc-group">
201 <label for=
"lenc">Lens Correction
</label>
203 <input id=
"lenc" type=
"checkbox" class=
"default-action" checked=
"checked">
204 <label class=
"slider" for=
"lenc"></label>
207 <div class=
"input-group" id=
"hmirror-group">
208 <label for=
"hmirror">H-Mirror Stream
</label>
210 <input id=
"hmirror" type=
"checkbox" class=
"default-action" checked=
"checked">
211 <label class=
"slider" for=
"hmirror"></label>
214 <div class=
"input-group" id=
"vflip-group">
215 <label for=
"vflip">V-Flip Stream
</label>
217 <input id=
"vflip" type=
"checkbox" class=
"default-action" checked=
"checked">
218 <label class=
"slider" for=
"vflip"></label>
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° (Right)
</option>
225 <option value=
"0" selected=
"selected">0° (None)
</option>
226 <option value=
"-90">-
90° (Left)
</option>
229 <div class=
"input-group" id=
"dcw-group">
230 <label for=
"dcw">DCW (Downsize EN)
</label>
232 <input id=
"dcw" type=
"checkbox" class=
"default-action" checked=
"checked">
233 <label class=
"slider" for=
"dcw"></label>
236 <div class=
"input-group" id=
"colorbar-group">
237 <label for=
"colorbar">Test Pattern
</label>
239 <input id=
"colorbar" type=
"checkbox" class=
"default-action">
240 <label class=
"slider" for=
"colorbar"></label>
243 <div class=
"input-group" id=
"min_frame_time-group" title=
"Minimum frame time
Higher settings reduce the frame rate
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>
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>
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>
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>
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>
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=
"">
291 document.addEventListener('DOMContentLoaded', function (event) {
292 var baseHost = document.location.origin;
293 var streamURL = 'Undefined';
294 var viewerURL = 'Undefined';
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')
320 el.classList.add('hidden')
323 el.classList.remove('hidden')
326 const disable = el =
> {
327 el.classList.add('disabled')
331 const enable = el =
> {
332 el.classList.remove('disabled')
336 const updateValue = (el, value, updateRemote) =
> {
337 updateRemote = updateRemote == null ? true : updateRemote
339 if (el.type === 'checkbox') {
340 initialValue = el.checked
344 initialValue = el.value
348 if (updateRemote && initialValue !== value) {
350 } else if(!updateRemote){
352 value ? hide(exposure) : show(exposure)
353 } else if(el.id === "agc"){
361 } else if(el.id === "awb_gain"){
362 value ? show(wb) : hide(wb)
363 } else if(el.id === "lamp"){
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;
381 } else if(el.id === "min_frame_time"){
382 min_frame_time.value = value;
383 } else if(el.id === "stream_url"){
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}`);
392 console.log('Stream URL set to: ' + streamURL);
393 console.log('Stream Viewer URL set to: ' + viewerURL);
398 var rangeUpdateScheduled = false
399 var latestRangeConfig
401 function updateRangeConfig (el) {
402 latestRangeConfig = el
403 if (!rangeUpdateScheduled) {
404 rangeUpdateScheduled = true;
405 setTimeout(function(){
406 rangeUpdateScheduled = false
407 updateConfig(latestRangeConfig)
412 function updateConfig (el) {
416 value = el.checked ?
1 :
0
431 const query = `${baseHost}/control?var=${el.id}&val=${value}`
435 console.log(`request to ${query} finished, status: ${response.status}`)
440 .querySelectorAll('.close')
447 // read initial values
448 fetch(`${baseHost}/status`)
449 .then(function (response) {
450 return response.json()
452 .then(function (state) {
454 .querySelectorAll('.default-action')
456 updateValue(el, state[el.id], false)
464 // Put some helpful text on the 'Still' button
465 stillButton.setAttribute("title", `Capture a still image :: ${baseHost}/capture`);
467 const stopStream = () =
> {
469 streamButton.innerHTML = 'Start Stream';
470 streamButton.setAttribute("title", `Start the stream :: ${streamURL}`);
474 const startStream = () =
> {
475 view.src = streamURL;
476 view.scrollIntoView(false);
477 streamButton.innerHTML = 'Stop Stream';
478 streamButton.setAttribute("title", `Stop the stream`);
482 const applyRotation = () =
> {
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');
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');
500 console.log('Rotation ' + rot + ' applied');
503 // Attach actions to controls
505 streamLink.onclick = () =
> {
507 window.open(viewerURL, "_blank");
510 stillButton.onclick = () =
> {
512 view.src = `${baseHost}/capture?_cb=${Date.now()}`;
513 view.scrollIntoView(false);
517 closeButton.onclick = () =
> {
522 streamButton.onclick = () =
> {
523 const streamEnabled = streamButton.innerHTML === 'Stop Stream'
531 // Attach default on change action
533 .querySelectorAll('.default-action')
535 el.onchange = () =
> updateConfig(el)
538 // Update range sliders as they are being moved
540 .querySelectorAll('input[
type="range"]')
542 el.oninput = () =
> updateRangeConfig(el)
547 const agc = document.getElementById('agc')
548 const agcGain = document.getElementById('agc_gain-group')
549 const gainCeiling = document.getElementById('gainceiling-group')
550 agc.onchange = () =
> {
562 const aec = document.getElementById('aec')
563 const exposure = document.getElementById('aec_value-group')
564 aec.onchange = () =
> {
566 aec.checked ? hide(exposure) : show(exposure)
570 const awb = document.getElementById('awb_gain')
571 const wb = document.getElementById('wb_mode-group')
572 awb.onchange = () =
> {
574 awb.checked ? show(wb) : hide(wb)
577 // Detection and framesize
578 rotate.onchange = () =
> {
580 updateConfig(rotate);
583 framesize.onchange = () =
> {
584 updateConfig(framesize)
587 minFrameTime.onchange = () =
> {
588 updateConfig(minFrameTime)
591 xclk.onchange = () =
> {
592 console.log("xclk:" , xclk);
596 swapButton.onclick = () =
> {
597 window.open('/?view=simple','_self');
600 savePrefsButton.onclick = () =
> {
601 if (confirm("Save the current preferences?")) {
602 updateConfig(savePrefsButton);
606 clearPrefsButton.onclick = () =
> {
607 if (confirm("Remove the saved preferences?")) {
608 updateConfig(clearPrefsButton);
612 rebootButton.onclick = () =
> {
613 if (confirm("Reboot the Camera Module?")) {
614 updateConfig(rebootButton);
615 // Some sort of countdown here?
618 header.innerHTML = '
<h1>Rebooting!
</h1><hr>Page will reload after
30 seconds.';
619 setTimeout(function() {
620 location.replace(document.URL);