📸 ESP32 OLED Studio: Display Photos & Videos on a 128×64 OLED Using ESP32 | DIY Electronics Project ⚡📺
ESP32 OLED Studio: Display Photos and Videos on a 128×64 OLED⚡📺
By LearnWithDTrend | ESP32 & Arduino Projects
Have you ever wanted to display your favourite photos on a tiny OLED screen using your mobile phone? What if you could also preview images, adjust brightness, and stream video frames to the same display without using a complicated application?
In this project, we build ESP32 OLED Studio, a browser-based photo and video display system using an ESP32 development board and a 128×64 SSD1306 OLED display.
The ESP32 creates its own Wi-Fi hotspot. After connecting your phone or laptop to the hotspot, you can open a simple web dashboard, select a photo or video, adjust the image settings, and send the converted monochrome image to the OLED.
The best part? The dashboard is designed to be simple, responsive, and accessible through a web browser on your local network.
Let's build it step by step!
🚀 Project Overview
ESP32 OLED Studio combines embedded programming, Wi-Fi communication, web development, and OLED graphics in one electronics project.
Instead of manually converting every photo into a bitmap before uploading it, the browser processes the selected media and prepares a black-and-white image suitable for the OLED display.
✨ Main Features
Photo upload: Select an image from your phone or computer.
Brightness adjustment: Increase or decrease brightness before displaying a photo.
Image framing: Choose between filling the screen by cropping the edges or fitting the complete image.
OLED preview: See the monochrome result on the web dashboard before sending it to the display.
Video selection: Select a compatible video and play it in your browser.
Video frame streaming: Convert video frames into monochrome images and transmit them to the OLED.
Contrast control: Adjust the threshold used to convert an image into black and white.
Responsive dashboard: Use the interface on a phone, tablet, or computer.
Local Wi-Fi connection: Operate through the ESP32 hotspot without requiring a cloud account.
Important: The OLED displays monochrome images, not full-colour video. It also does not reproduce video audio. Playback speed depends on the browser, Wi-Fi connection, and ESP32 performance.
🧰 Components Required
| Component | Quantity |
|---|---|
| ESP32 development board (classic ESP32) | 1 |
| SSD1306 I²C OLED display, 128×64 | 1 |
| USB cable for programming and power | 1 |
| Jumper wires | As required |
| Computer with Arduino IDE | 1 |
| Smartphone or laptop with a web browser | 1 |
You will also need the ESP32 OLED Studio Arduino sketch and the web dashboard code used in this project.
🔌 ESP32 OLED Wiring Diagram
The OLED communicates with the ESP32 using the I²C protocol. For the classic ESP32 development board, the connections used in this project are:
| OLED Pin | ESP32 Pin |
|---|---|
| VCC | 3.3V |
| GND | GND |
| SDA | GPIO 21 |
| SCL | GPIO 22 |
Wiring Instructions
Connect the OLED VCC pin to the ESP32 3.3V pin.
Connect OLED GND to ESP32 GND.
Connect SDA to GPIO 21.
Connect SCL to GPIO 22.
Check every connection before powering the circuit.
Note: These pins are intended for the classic ESP32 development board. Other ESP32 variants may use different pins. Check your board and OLED module specifications before connecting the circuit.
💻 Software and Libraries Required
Install the following software before uploading the project:
Arduino IDE
ESP32 board support package
Adafruit GFX Library
Adafruit SSD1306 Library
How to Install the OLED Libraries
Open Arduino IDE.
Select Sketch → Include Library → Manage Libraries.
Search for
Adafruit GFX Libraryand install it.Search for
Adafruit SSD1306and install it.If Arduino IDE requests additional dependencies, install them as well.
Select the correct ESP32 board and COM port.
The Arduino sketch must be compatible with the 128×64 SSD1306 display and the web dashboard provided with the project.
📥 ESP32 OLED Studio Code
The project has two main parts:
1. ESP32 firmware (.ino file)
The firmware controls the OLED, creates the Wi-Fi hotspot, hosts the web server, receives image data, and updates the display.
2. Web dashboard (HTML file)
The dashboard provides the photo and video selection interface, media preview, image controls, and buttons for sending images or video frames to the ESP32.
Download the Project Code
ESP32 Arduino Sketch: [Insert your actual .ino download link here before publishing.]
Web Dashboard Preview: Use the OLED Studio HTML dashboard supplied with the project.
Before uploading, ensure the firmware implements the image-upload and video-frame endpoints expected by the dashboard: /upload and /frame. The HTML dashboard alone is not the complete ESP32 firmware.
⚙️ How to Upload the Code
/* ESP32 OLED Studio - Photo + Video Target: Classic ESP32 Dev Module OLED: 0.96-inch 128x64 I2C SSD1306 (common configuration) I2C: SDA GPIO 21, SCL GPIO 22, address 0x3C
Libraries (Arduino Library Manager): - Adafruit GFX Library - Adafruit SSD1306
Phone/laptop connects to the ESP32 access point, then opens http://192.168.4.1 Photo: upload a monochrome 128x64 bitmap, displayed and saved in Preferences. Video: browser previews the local video and streams converted 128x64 frames to the OLED at a modest rate. Video frames are not saved to flash.*/
#include <WiFi.h>#include <WebServer.h>#include <Wire.h>#include <Preferences.h>#include <Adafruit_GFX.h>#include <Adafruit_SSD1306.h>#include <cstring>
#define OLED_WIDTH 128#define OLED_HEIGHT 64#define OLED_ADDRESS 0x3C#define OLED_SDA 21#define OLED_SCL 22#define OLED_RESET -1
const char *AP_SSID = "OLED_Studio";const char *AP_PASSWORD = "BlueOLED96";
WebServer server(80);Preferences preferences;Adafruit_SSD1306 display(OLED_WIDTH, OLED_HEIGHT, &Wire, OLED_RESET);
static constexpr size_t BITMAP_SIZE = (OLED_WIDTH * OLED_HEIGHT) / 8; // 1024 bytesuint8_t currentBitmap[BITMAP_SIZE] = {0};bool displayReady = false;bool hasSavedPhoto = false;
// The dashboard is served by the ESP32 itself. The browser converts images and// video frames to a 1-bit 128x64 bitmap, so the ESP32 does not decode JPEG/video.const char INDEX_HTML[] PROGMEM = R"HTML(<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>OLED Studio</title><style>:root{color-scheme:light;--blue:#2563eb;--blue2:#1d4ed8;--ink:#17243b;--muted:#68778d;--line:#dce5f1;--pale:#eff6ff;--bg:#f5f8fd;--white:#fff;--green:#15803d;--red:#b91c1c}*{box-sizing:border-box}body{margin:0;background:var(--bg);font-family:Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",Arial,sans-serif;color:var(--ink)}.topbar{background:var(--white);border-bottom:1px solid var(--line)}.topinner{max-width:1100px;margin:auto;padding:16px 20px;display:flex;align-items:center;gap:12px}.logo{width:38px;height:38px;border-radius:12px;background:var(--blue);color:white;display:grid;place-items:center;font-weight:800;letter-spacing:-1px}.brand{font-size:1.05rem;font-weight:800}.brand small{display:block;color:var(--muted);font-size:.76rem;font-weight:500;margin-top:2px}.local{margin-left:auto;display:flex;align-items:center;gap:7px;background:#ecfdf3;color:var(--green);border:1px solid #bbf7d0;border-radius:999px;padding:7px 10px;font-size:.78rem;font-weight:700}.dot{width:7px;height:7px;border-radius:50%;background:#22c55e}main{max-width:1100px;margin:auto;padding:28px 20px 44px}.intro{margin-bottom:22px}.eyebrow{font-size:.75rem;text-transform:uppercase;letter-spacing:.14em;color:var(--blue);font-weight:800}.intro h1{font-size:clamp(1.8rem,4vw,2.5rem);letter-spacing:-.04em;margin:8px 0}.intro p{margin:0;color:var(--muted);line-height:1.6;max-width:650px}.grid{display:grid;grid-template-columns:minmax(0,1.1fr) minmax(320px,.9fr);gap:18px;align-items:start}.card{background:var(--white);border:1px solid var(--line);border-radius:18px;padding:20px;box-shadow:0 8px 24px rgba(31,58,100,.045)}.cardhead{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:14px}.card h2{font-size:1rem;margin:0;letter-spacing:-.01em}.tag{background:var(--pale);color:var(--blue2);border-radius:7px;padding:5px 8px;font-size:.72rem;font-weight:800}.screenwrap{background:#eaf0f8;border:1px solid #d9e3f0;border-radius:14px;padding:18px;display:grid;place-items:center}.oled{display:block;width:min(100%,512px);height:auto;aspect-ratio:2/1;background:#000;border:7px solid #243247;border-radius:8px;image-rendering:pixelated;box-shadow:0 5px 13px #0b172a24}.screenfoot{display:flex;justify-content:space-between;gap:10px;margin-top:10px;color:var(--muted);font-size:.78rem}.media{margin-top:18px}.mediaframe{background:#0e1726;border-radius:12px;min-height:185px;display:grid;place-items:center;overflow:hidden;position:relative}.placeholder{color:#c6d4e8;text-align:center;padding:20px}.placeholder strong{display:block;color:white;margin:8px 0 4px}.previewMedia{display:none;max-width:100%;max-height:340px;width:auto;height:auto;object-fit:contain}.sectiontitle{font-weight:800;font-size:.92rem;margin:0 0 12px}.drop{display:block;border:1.5px dashed #b6c9e5;border-radius:13px;padding:18px;background:#f8fbff;cursor:pointer;text-align:center}.drop:hover{border-color:var(--blue);background:#f1f7ff}.dropicon{margin:auto auto 8px;width:38px;height:38px;border-radius:11px;background:#e4efff;color:var(--blue);display:grid;place-items:center;font-size:1.3rem}.drop strong{display:block;font-size:.92rem}.drop span{display:block;color:var(--muted);font-size:.78rem;line-height:1.45;margin-top:5px}.drop input{display:block;width:100%;margin-top:12px;font-size:.82rem;color:var(--muted)}.field{margin-top:16px}.fieldlabel{display:flex;justify-content:space-between;color:#34445c;font-size:.82rem;font-weight:700;margin-bottom:8px}.fieldlabel output{color:var(--blue)}select,input[type=range]{width:100%}select{border:1px solid var(--line);border-radius:10px;padding:10px;background:white;color:var(--ink)}input[type=range]{accent-color:var(--blue)}.actions{display:flex;gap:9px;flex-wrap:wrap;margin-top:18px}.btn{appearance:none;border:1px solid transparent;border-radius:10px;padding:11px 15px;background:var(--blue);color:white;font-weight:800;font-size:.86rem;cursor:pointer;transition:background .15s,transform .15s}.btn:hover{background:var(--blue2)}.btn:active{transform:translateY(1px)}.btn.secondary{background:white;color:#31435e;border-color:#cbd8e8}.btn.secondary:hover{background:#f3f7fc}.btn:disabled{opacity:.45;cursor:not-allowed}.status{padding:12px 13px;margin-top:14px;border-radius:10px;background:#f1f5f9;color:#526176;border:1px solid #e2e8f0;font-size:.82rem;line-height:1.45;min-height:43px}.status.ok{background:#f0fdf4;border-color:#bbf7d0;color:#166534}.status.err{background:#fef2f2;border-color:#fecaca;color:#991b1b}.hint{font-size:.78rem;color:var(--muted);line-height:1.55;margin:14px 0 0}.footer{text-align:center;color:#8090a5;font-size:.76rem;margin-top:22px}.hidden{display:none!important}@media(max-width:780px){main{padding:22px 14px 32px}.topinner{padding:12px 14px}.grid{grid-template-columns:1fr}.card{padding:17px}.local{font-size:.72rem}.mediaframe{min-height:150px}}</style></head><body><div class="topbar"><div class="topinner"><div class="logo">OS</div><div class="brand">OLED Studio<small>ESP32 photo & video display</small></div><div class="local"><span class="dot"></span> Local device</div></div></div><main><header class="intro"><div class="eyebrow">Personal display</div><h1>Make it visible.</h1><p>Choose a photo or video, preview it here, then send a monochrome version to your OLED display.</p></header><div class="grid"><section class="card"> <div class="cardhead"><h2>OLED output</h2><span class="tag">128 × 64 · MONO</span></div> <div class="screenwrap"><canvas id="oledCanvas" class="oled" width="128" height="64" aria-label="OLED preview"></canvas></div> <div class="screenfoot"><span>Black & white preview</span><span id="previewState">Waiting for media</span></div> <div class="media"> <div class="cardhead"><h2>Media preview</h2><span id="mediaType" class="tag">No file</span></div> <div class="mediaframe" id="mediaFrame"> <div class="placeholder" id="placeholder"><div style="font-size:1.6rem">▧</div><strong>Your media appears here</strong><span>Photo or video preview</span></div> <img id="imagePreview" class="previewMedia" alt="Selected photo preview"> <video id="videoPreview" class="previewMedia" controls playsinline></video> </div> </div></section><section class="card"> <div class="cardhead"><h2>Choose media</h2><span class="tag">JPG · PNG · MP4</span></div> <label class="drop"><div class="dropicon">↑</div><strong>Select a photo or video</strong><span>Files stay in your browser until you send frames to the ESP32.</span><input id="fileInput" type="file" accept="image/*,video/*"></label> <div class="field"><div class="fieldlabel"><label for="fit">Image fit</label></div><select id="fit"><option value="cover">Fill screen · crop edges</option><option value="contain">Fit entire image</option></select></div> <div class="field"><div class="fieldlabel"><label for="brightness">Brightness</label><output id="brightnessOut">+10</output></div><input id="brightness" type="range" min="-80" max="80" value="10"></div> <div class="field"><div class="fieldlabel"><label for="threshold">Contrast threshold</label><output id="thresholdOut">128</output></div><input id="threshold" type="range" min="70" max="190" value="128"></div> <div class="actions"><button id="showPhoto" class="btn" disabled>Show photo on OLED</button><button id="playVideo" class="btn" disabled>Play video on OLED</button><button id="stopVideo" class="btn secondary" disabled>Stop</button></div> <div id="status" class="status">Connect to the OLED Studio Wi-Fi, then open this page at 192.168.4.1.</div> <p class="hint">Video is converted to small black-and-white frames in your browser. The OLED has no colour or audio; playback speed depends on Wi-Fi and device performance.</p></section></div><div class="footer">OLED Studio · Local connection · No cloud account required</div></main><script>(()=>{const W=128,H=64,BYTES=1024;const canvas=document.getElementById('oledCanvas'),ctx=canvas.getContext('2d',{willReadFrequently:true});const work=document.createElement('canvas');work.width=W;work.height=H;const wctx=work.getContext('2d',{willReadFrequently:true});const fileInput=document.getElementById('fileInput'),img=document.getElementById('imagePreview'),video=document.getElementById('videoPreview');const placeholder=document.getElementById('placeholder'),statusBox=document.getElementById('status');const showPhoto=document.getElementById('showPhoto'),playVideo=document.getElementById('playVideo'),stopVideo=document.getElementById('stopVideo');let selectedType='',bitmap=null,objectUrl='',videoLoop=false,lastFrameAt=0;function setStatus(s,type=''){statusBox.textContent=s;statusBox.className='status '+type;}function sleep(ms){return new Promise(resolve=>setTimeout(resolve,ms));}function clamp(n){return Math.max(0,Math.min(255,n));}function drawBlack(){ctx.fillStyle='#000';ctx.fillRect(0,0,W,H);}drawBlack();function renderSource(source,sw,sh){ wctx.fillStyle='#000';wctx.fillRect(0,0,W,H); const mode=document.getElementById('fit').value; if(mode==='cover'){ const scale=Math.max(W/sw,H/sh),cw=W/scale,ch=H/scale; wctx.drawImage(source,(sw-cw)/2,(sh-ch)/2,cw,ch,0,0,W,H); }else{ const scale=Math.min(W/sw,H/sh),dw=sw*scale,dh=sh*scale; wctx.drawImage(source,(W-dw)/2,(H-dh)/2,dw,dh); } const data=wctx.getImageData(0,0,W,H).data,gray=new Float32Array(W*H),shift=Number(document.getElementById('brightness').value); for(let i=0,j=0;i<gray.length;i++,j+=4)gray[i]=clamp(.299*data[j]+.587*data[j+1]+.114*data[j+2]+shift); const cutoff=Number(document.getElementById('threshold').value),mono=new Uint8Array(W*H),packed=new Uint8Array(BYTES); // Floyd-Steinberg dithering keeps more visible detail in faces at 128x64. for(let y=0;y<H;y++)for(let x=0;x<W;x++){ const i=y*W+x,old=gray[i],val=old>=cutoff?255:0,err=old-val;mono[i]=val; if(x+1<W)gray[i+1]+=err*7/16; if(y+1<H){if(x>0)gray[i+W-1]+=err*3/16;gray[i+W]+=err*5/16;if(x+1<W)gray[i+W+1]+=err/16;} } const pixels=ctx.createImageData(W,H); for(let y=0;y<H;y++)for(let x=0;x<W;x++){ const i=y*W+x,j=i*4,v=mono[i];pixels.data[j]=v;pixels.data[j+1]=v;pixels.data[j+2]=v;pixels.data[j+3]=255; if(v)packed[y*(W/8)+(x>>3)]|=(0x80>>(x&7)); } ctx.putImageData(pixels,0,0);bitmap=packed;document.getElementById('previewState').textContent='Preview ready';}function renderSelected(){if(selectedType==='image'&&img.complete&&img.naturalWidth)renderSource(img,img.naturalWidth,img.naturalHeight);else if(selectedType==='video'&&video.videoWidth)renderSource(video,video.videoWidth,video.videoHeight);}async function sendBitmap(path,savePhoto=false){ if(!bitmap)throw new Error('Choose a photo or video first.'); let bin='';for(let i=0;i<bitmap.length;i++)bin+=String.fromCharCode(bitmap[i]); const response=await fetch(path,{method:'POST',headers:{'Content-Type':'text/plain'},body:btoa(bin)}); const text=await response.text();if(!response.ok)throw new Error(text||('HTTP '+response.status)); if(savePhoto)setStatus('Photo displayed on your OLED and saved in device memory.','ok');}fileInput.addEventListener('change',()=>{ const file=fileInput.files&&fileInput.files[0];if(!file)return; if(objectUrl)URL.revokeObjectURL(objectUrl);objectUrl=URL.createObjectURL(file); video.pause();videoLoop=false;selectedType=file.type.startsWith('video/')?'video':'image';bitmap=null; placeholder.classList.add('hidden');img.style.display='none';video.style.display='none'; document.getElementById('mediaType').textContent=selectedType==='video'?'Video':'Photo'; if(selectedType==='image'){ img.onload=()=>{renderSelected();showPhoto.disabled=false;playVideo.disabled=true;setStatus('Photo ready. Review the OLED preview, then select “Show photo on OLED”.','ok');}; img.onerror=()=>setStatus('Could not read that image. Try a JPG or PNG file.','err'); img.src=objectUrl;img.style.display='block'; }else{ video.onloadedmetadata=()=>{renderSelected();showPhoto.disabled=true;playVideo.disabled=false;setStatus('Video ready. Press “Play video on OLED” to stream frames.','ok');}; video.onerror=()=>setStatus('Could not open this video in your browser. Try MP4 with H.264 video.','err'); video.src=objectUrl;video.style.display='block';video.load(); }});[document.getElementById('fit'),document.getElementById('brightness'),document.getElementById('threshold')].forEach(el=>el.addEventListener('input',()=>{ document.getElementById('brightnessOut').textContent=(Number(document.getElementById('brightness').value)>0?'+':'')+document.getElementById('brightness').value; document.getElementById('thresholdOut').textContent=document.getElementById('threshold').value; if(selectedType==='image')renderSelected();}));showPhoto.addEventListener('click',async()=>{try{showPhoto.disabled=true;setStatus('Sending photo to the OLED…');await sendBitmap('/upload',true);}catch(e){setStatus('Upload failed: '+e.message,'err');}finally{showPhoto.disabled=selectedType!=='image';}});playVideo.addEventListener('click',async()=>{ if(selectedType!=='video')return; try{ videoLoop=true;playVideo.disabled=true;stopVideo.disabled=false; await video.play();setStatus('Streaming video frames to the OLED…','ok'); while(videoLoop&&!video.paused&&!video.ended){ const started=performance.now();renderSelected();await sendBitmap('/frame',false); const spent=performance.now()-started;await sleep(Math.max(20,250-spent)); } if(video.ended){videoLoop=false;playVideo.disabled=false;stopVideo.disabled=true;setStatus('Video finished.','ok');} }catch(e){videoLoop=false;playVideo.disabled=false;stopVideo.disabled=true;setStatus('Video stream stopped: '+e.message,'err');}});stopVideo.addEventListener('click',()=>{videoLoop=false;video.pause();playVideo.disabled=selectedType!=='video';stopVideo.disabled=true;setStatus('Video playback stopped.','');});video.addEventListener('pause',()=>{if(videoLoop){videoLoop=false;playVideo.disabled=false;stopVideo.disabled=true;}});video.addEventListener('seeked',renderSelected);})();</script></body></html>)HTML";
void drawBitmapToOLED(const uint8_t *bitmap) { if (!displayReady) return; display.clearDisplay(); display.drawBitmap(0, 0, bitmap, OLED_WIDTH, OLED_HEIGHT, SSD1306_WHITE); display.display();}
void showBootMessage(const char *line1, const char *line2 = nullptr) { if (!displayReady) return; display.clearDisplay(); display.setTextColor(SSD1306_WHITE); display.setTextSize(1); display.setCursor(0, 16); display.println(line1); if (line2) { display.setCursor(0, 31); display.println(line2); } display.display();}
int base64Value(char c) { if (c >= 'A' && c <= 'Z') return c - 'A'; if (c >= 'a' && c <= 'z') return c - 'a' + 26; if (c >= '0' && c <= '9') return c - '0' + 52; if (c == '+') return 62; if (c == '/') return 63; return -1;}
int decodeBase64(const String &input, uint8_t *output, size_t outputCapacity) { uint32_t accumulator = 0; int bits = -8; size_t outputLength = 0; for (size_t i = 0; i < input.length(); ++i) { char c = input[i]; if (c == '=') break; if (c == '\r' || c == '\n' || c == ' ' || c == '\t') continue; int value = base64Value(c); if (value < 0) return -1; accumulator = (accumulator << 6) | static_cast<uint32_t>(value); bits += 6; if (bits >= 0) { if (outputLength >= outputCapacity) return -1; output[outputLength++] = static_cast<uint8_t>((accumulator >> bits) & 0xFF); bits -= 8; } } return static_cast<int>(outputLength);}
bool receiveBitmap(uint8_t *destination) { if (!server.hasArg("plain")) { server.send(400, "text/plain", "Missing bitmap data. Refresh the page and try again."); return false; } const String &encoded = server.arg("plain"); if (encoded.length() < 1368 || encoded.length() > 1380) { server.send(400, "text/plain", "Invalid bitmap data size. Expected a 128x64 monochrome frame."); return false; } const int decoded = decodeBase64(encoded, destination, BITMAP_SIZE); if (decoded != static_cast<int>(BITMAP_SIZE)) { server.send(400, "text/plain", "Could not decode 128x64 bitmap."); return false; } return true;}
void handleRoot() { server.send_P(200, "text/html; charset=utf-8", INDEX_HTML);}
void handlePhotoUpload() { uint8_t newBitmap[BITMAP_SIZE]; if (!receiveBitmap(newBitmap)) return; memcpy(currentBitmap, newBitmap, BITMAP_SIZE); drawBitmapToOLED(currentBitmap); hasSavedPhoto = true; size_t saved = preferences.putBytes("photo", currentBitmap, BITMAP_SIZE); if (saved != BITMAP_SIZE) Serial.println("Warning: image displayed but flash save failed."); Serial.println("Photo received and displayed."); server.send(200, "text/plain", "Photo displayed on OLED and saved.");}
void handleVideoFrame() { uint8_t frame[BITMAP_SIZE]; if (!receiveBitmap(frame)) return; drawBitmapToOLED(frame); server.send(200, "text/plain", "Frame displayed.");}
void handleClear() { memset(currentBitmap, 0, BITMAP_SIZE); hasSavedPhoto = false; if (displayReady) { display.clearDisplay(); display.display(); } preferences.remove("photo"); Serial.println("OLED cleared."); server.send(200, "text/plain", "OLED cleared.");}
void setup() { Serial.begin(115200); delay(300); Serial.println(); Serial.println("Starting OLED Studio...");
Wire.begin(OLED_SDA, OLED_SCL); displayReady = display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS); if (displayReady) { showBootMessage("OLED Studio", "Starting Wi-Fi..."); Serial.println("SSD1306 OLED initialized."); } else { Serial.println("ERROR: SSD1306 OLED not detected. Check controller, I2C address, SDA/SCL and power."); Serial.println("Wi-Fi dashboard will still start, but the OLED will remain blank until the display configuration is fixed."); }
preferences.begin("oled-studio", false); if (preferences.getBytesLength("photo") == BITMAP_SIZE) { if (preferences.getBytes("photo", currentBitmap, BITMAP_SIZE) == BITMAP_SIZE) { hasSavedPhoto = true; drawBitmapToOLED(currentBitmap); Serial.println("Restored saved photo from flash."); } }
WiFi.mode(WIFI_AP); bool apStarted = WiFi.softAP(AP_SSID, AP_PASSWORD); delay(250); if (!apStarted) Serial.println("ERROR: Wi-Fi hotspot failed to start.");
server.on("/", HTTP_GET, handleRoot); server.on("/upload", HTTP_POST, handlePhotoUpload); server.on("/frame", HTTP_POST, handleVideoFrame); server.on("/clear", HTTP_POST, handleClear); server.onNotFound([]() { server.send(404, "text/plain", "Not found. Open http://192.168.4.1"); }); server.begin();
IPAddress ip = WiFi.softAPIP(); Serial.println(); Serial.println("OLED Studio is ready."); Serial.print("Wi-Fi name: "); Serial.println(AP_SSID); Serial.print("Wi-Fi password: "); Serial.println(AP_PASSWORD); Serial.print("Dashboard: http://"); Serial.println(ip); Serial.print("OLED status: "); Serial.println(displayReady ? "ready" : "not detected");
if (displayReady && !hasSavedPhoto) showBootMessage("OLED Studio ready", "Open 192.168.4.1");}
void loop() { server.handleClient(); delay(2);}
Follow these steps to get your OLED Studio project running.
Open the ESP32 OLED Studio
.inosketch in Arduino IDE.Confirm that the required libraries are installed.
Connect the ESP32 to your computer using a USB cable.
Under Tools → Board, select the appropriate ESP32 board, such as ESP32 Dev Module for a compatible classic ESP32 development board.
Select the correct COM port.
Click Verify to compile the sketch.
If the compilation succeeds, click Upload.
Open Tools → Serial Monitor and set the baud rate to
115200.
After startup, the Serial Monitor should report the Wi-Fi connection information configured in the sketch.
For this project, the expected settings are:
Wi-Fi name:
OLED_StudioWi-Fi password:
BlueOLED96Dashboard address:
http://192.168.4.1
These details must match the settings in your uploaded firmware.
📱 How to Use the OLED Studio Dashboard
Once the firmware is running, you can control the display through your phone or laptop.
Step 1: Connect to the ESP32 Wi-Fi
Open the Wi-Fi settings on your smartphone or laptop.
Connect to the following network:
SSID:
OLED_StudioPassword:
BlueOLED96
Your device may display a message saying that this Wi-Fi network has no internet access. That is normal for a standalone ESP32 hotspot. Stay connected to the ESP32 network.
Step 2: Open the Dashboard
Open a web browser such as Chrome and enter:
http://192.168.4.1
The OLED Studio dashboard should appear.
You will see a clean interface with the OLED output preview, media preview, file selection, image settings, and control buttons.
Step 3: Select a Photo
Click Select a photo or video and choose an image from your device.
JPG and PNG images are good formats to try first.
The dashboard converts the selected image into a black-and-white representation that matches the OLED's 128×64 resolution.
Step 4: Adjust Brightness and Image Framing
Use the brightness slider to make the image lighter or darker.
You can also change the image fit:
Fill screen: Uses the entire screen area and crops the edges if necessary.
Fit entire image: Keeps the complete image visible, leaving blank space where the aspect ratio requires it.
The contrast threshold controls which pixels are treated as white or black. Different settings can make facial features, outlines, and background details more visible.
Review the OLED preview before sending the image.
Step 5: Display the Photo
Click Show photo on OLED.
The browser sends the processed monochrome image to the ESP32 through the local Wi-Fi connection. The firmware then updates the OLED display.
Your photo is now visible on the small OLED screen!
Step 6: Play a Video on the OLED
Choose a compatible video file. An MP4 video using H.264 encoding is a good starting point.
Once the video is ready, click Play video on OLED.
The browser plays the video locally, converts its frames into monochrome images, and sends the frames to the ESP32. The OLED updates as new frames arrive.
Click Stop when you want to stop playback.
Remember that this is a small, low-resolution display experiment rather than a replacement for a normal video screen. The OLED has no audio, and the video frame rate may be limited.
🧠 How Does ESP32 OLED Studio Work?
The project works through a combination of browser-side image processing, Wi-Fi communication, and OLED graphics.
1. ESP32 Creates a Wi-Fi Hotspot
The ESP32 runs in access-point mode and creates a wireless network. Your phone or laptop connects directly to that network.
2. The ESP32 Hosts a Web Dashboard
A web server provides the dashboard at 192.168.4.1. You can use its controls in a browser instead of installing a separate mobile application.
3. The Browser Processes the Image
The dashboard scales and frames the image for a 128×64 display. It calculates grayscale values, applies the brightness and threshold settings, and uses dithering to preserve some visual detail in the monochrome result.
The processed pixels are packed into a bitmap for transmission.
4. The ESP32 Receives the Image Data
When you display a photo, the browser posts the processed bitmap to the /upload endpoint.
During video playback, the browser sends successive frames to the /frame endpoint.
The firmware must decode the received data using the same format expected by the browser.
5. The OLED Displays the Result
The ESP32 updates the OLED with the monochrome bitmap. For video, it repeats the process as frames arrive.
This design combines microcontroller programming, image processing, embedded displays, and browser-based control in a single project.
🌐 Why Use a Web Dashboard?
A browser-based interface makes this electronics project easier to demonstrate and operate.
You can select media from a phone or laptop, adjust settings without repeatedly modifying Arduino code, and preview the output before transmitting it.
The local connection also means that an internet connection or cloud account is not required for normal operation. The selected media is processed in your browser before the image or video frames are sent to the ESP32.
🛠️ Troubleshooting
Problem 1: The OLED screen is blank
Check the VCC, GND, SDA, and SCL connections. Verify that your firmware uses the correct I²C pins and that your display is compatible with the configured SSD1306 driver.
Problem 2: The ESP32 hotspot is not visible
Confirm that the code successfully starts Wi-Fi access-point mode. Restart the ESP32 and check the Serial Monitor for startup messages.
Problem 3: The dashboard does not open
Make sure your phone or laptop is connected to OLED_Studio. Then enter http://192.168.4.1 in the browser's address bar.
If the page still fails to open, verify the IP address printed by the firmware.
Problem 4: The photo looks too dark or unclear
Try increasing brightness or changing the contrast threshold. Choose a clear image with good lighting and visible contrast. Simple images and close-up subjects often work better on a 128×64 OLED.
Problem 5: The video does not play
Try an MP4 file with H.264 video encoding. Some browsers may not support particular video codecs or containers.
Also confirm that the ESP32 firmware implements the /frame endpoint expected by the dashboard.
Problem 6: Video playback is slow
Video playback depends on browser decoding speed, wireless communication, firmware processing, and OLED refresh speed. Short clips and simple scenes are useful for testing the project.
💡 Applications and Project Ideas
ESP32 OLED Studio can be used for:
Learning how ESP32 web servers work.
Exploring image processing and monochrome dithering.
Demonstrating I²C OLED displays.
Building small photo-display experiments.
Learning how browsers communicate with embedded devices.
Developing creative display features for robotics and electronics projects.
Exploring low-resolution animations on microcontroller displays.
You can extend the concept with additional display controls, selectable image effects, or other compatible display modules.
❓ Frequently Asked Questions
Can I display any photo on the OLED?
You can select common image files, including JPG and PNG. Results depend on the original image, its resolution, contrast, and the display's limited 128×64 pixel area.
Can I display a full-colour image?
No. The SSD1306 display used here produces a monochrome output. The dashboard converts images into black-and-white pixels before sending them to the OLED.
Can I play video with sound?
No. The video is converted into image frames for the OLED, so audio is not transmitted or reproduced by the display.
Do I need internet access?
No. The ESP32 creates a local Wi-Fi hotspot, and your phone or computer communicates directly with it. You may not have internet access while connected to this standalone network.
Can I use a different ESP32 board?
Possibly, but check the board's pin assignments, Wi-Fi capability, display wiring, and firmware compatibility. The supplied wiring instructions target a classic ESP32 development board.
Why is the OLED video not as smooth as a normal video?
The OLED has very low resolution, and the ESP32 must receive and display each frame over Wi-Fi. Playback speed depends on the browser, network communication, and firmware performance.
🎉 Final Result
With ESP32 OLED Studio, you can build a compact photo and video display system featuring:
ESP32-powered Wi-Fi access.
Browser-based photo selection and preview.
Brightness, framing, and contrast controls.
Monochrome output on a 128×64 SSD1306 OLED.
Browser-based video playback with frame streaming.
A simple responsive dashboard.
This project is a practical way to explore embedded systems, Arduino programming, OLED displays, web technologies, and image processing.
If you build this project, share your experience, questions, and improvements in the comments. Follow LearnWithDTrend for more Arduino projects, ESP32 experiments, robotics tutorials, and practical electronics ideas.

Comments
Post a Comment