ESP32-CAM Pan and Tilt Video Streaming Web Server (2 Axis)

In this project, we’ll attach the ESP32-CAM to a pan and tilt stand with two SG90 servo motors. With a pan and tilt camera stand, you can move the camera up, down, to the left, and the right— this is great for surveillance. The ESP32-CAM hosts a web server that shows video streaming and buttons to control the servo motors to move the camera.

Boards compatibility: for this project, you need an ESP32 camera development board with
access to two GPIOs to control two servo motors. You can use: ESP32-CAM AI-Thinker, T-Journal or TTGO T-Camera Plus.

Parts Required

For this project, we’ll use the following parts:

Pan and Tilt Stand and Motors

For this project, we’ll use a pan and tilt stand that already comes with two SG90 servo motors. The stand is shown in the following figure.

Pan and Tilt with SG90 Servo Motors ESP32-CAM

We got our stand from Banggood, but you can get yours from any other store.

Alternatively, you can get two SG90 servo motors and 3D print your own stand.

Servo motors have three wires with different colors:

WireColor
PowerRed
GNDBlack or brown
SignalYellow, orange or white

How to Control a Servo?

You can position the servo’s shaft at various angles from 0 to 180º. Servos are controlled using a pulse width modulation (PWM) signal. This means that the PWM signal sent to the motor determines the shaft’s position.

ESP32-CAM servo shaft angles from 0 to 180º

To control the servo motor, you can use the PWM capabilities of the ESP32 by sending a signal with the appropriate pulse width. Or you can use a library to make the code simpler. We’ll be using the ESP32Servo library.

Installing the ESP32Servo Library

To control servo motors, we’ll use the ESP32Servo library. Make sure you install that library before proceeding. In your Arduino IDE, go to Sketch > Include Library > Manage Libraries. Search for ESP32Servo and install the library as shown below.

Install ESP32Servo Library Arduino IDE

Code

Copy the following code to your Arduino IDE.

/*********
  Rui Santos
  Complete instructions at https://RandomNerdTutorials.com/esp32-cam-projects-ebook/
  
  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files.
  The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*********/

#include "esp_camera.h"
#include <WiFi.h>
#include "esp_timer.h"
#include "img_converters.h"
#include "Arduino.h"
#include "fb_gfx.h"
#include "soc/soc.h"             // disable brownout problems
#include "soc/rtc_cntl_reg.h"    // disable brownout problems
#include "esp_http_server.h"
#include <ESP32Servo.h>

// Replace with your network credentials
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";

#define PART_BOUNDARY "123456789000000000000987654321"

#define CAMERA_MODEL_AI_THINKER
//#define CAMERA_MODEL_M5STACK_PSRAM
//#define CAMERA_MODEL_M5STACK_WITHOUT_PSRAM
//#define CAMERA_MODEL_M5STACK_PSRAM_B
//#define CAMERA_MODEL_WROVER_KIT

#if defined(CAMERA_MODEL_WROVER_KIT)
  #define PWDN_GPIO_NUM    -1
  #define RESET_GPIO_NUM   -1
  #define XCLK_GPIO_NUM    21
  #define SIOD_GPIO_NUM    26
  #define SIOC_GPIO_NUM    27
  
  #define Y9_GPIO_NUM      35
  #define Y8_GPIO_NUM      34
  #define Y7_GPIO_NUM      39
  #define Y6_GPIO_NUM      36
  #define Y5_GPIO_NUM      19
  #define Y4_GPIO_NUM      18
  #define Y3_GPIO_NUM       5
  #define Y2_GPIO_NUM       4
  #define VSYNC_GPIO_NUM   25
  #define HREF_GPIO_NUM    23
  #define PCLK_GPIO_NUM    22

#elif defined(CAMERA_MODEL_M5STACK_PSRAM)
  #define PWDN_GPIO_NUM     -1
  #define RESET_GPIO_NUM    15
  #define XCLK_GPIO_NUM     27
  #define SIOD_GPIO_NUM     25
  #define SIOC_GPIO_NUM     23
  
  #define Y9_GPIO_NUM       19
  #define Y8_GPIO_NUM       36
  #define Y7_GPIO_NUM       18
  #define Y6_GPIO_NUM       39
  #define Y5_GPIO_NUM        5
  #define Y4_GPIO_NUM       34
  #define Y3_GPIO_NUM       35
  #define Y2_GPIO_NUM       32
  #define VSYNC_GPIO_NUM    22
  #define HREF_GPIO_NUM     26
  #define PCLK_GPIO_NUM     21

#elif defined(CAMERA_MODEL_M5STACK_WITHOUT_PSRAM)
  #define PWDN_GPIO_NUM     -1
  #define RESET_GPIO_NUM    15
  #define XCLK_GPIO_NUM     27
  #define SIOD_GPIO_NUM     25
  #define SIOC_GPIO_NUM     23
  
  #define Y9_GPIO_NUM       19
  #define Y8_GPIO_NUM       36
  #define Y7_GPIO_NUM       18
  #define Y6_GPIO_NUM       39
  #define Y5_GPIO_NUM        5
  #define Y4_GPIO_NUM       34
  #define Y3_GPIO_NUM       35
  #define Y2_GPIO_NUM       17
  #define VSYNC_GPIO_NUM    22
  #define HREF_GPIO_NUM     26
  #define PCLK_GPIO_NUM     21

#elif defined(CAMERA_MODEL_AI_THINKER)
  #define PWDN_GPIO_NUM     32
  #define RESET_GPIO_NUM    -1
  #define XCLK_GPIO_NUM      0
  #define SIOD_GPIO_NUM     26
  #define SIOC_GPIO_NUM     27
  
  #define Y9_GPIO_NUM       35
  #define Y8_GPIO_NUM       34
  #define Y7_GPIO_NUM       39
  #define Y6_GPIO_NUM       36
  #define Y5_GPIO_NUM       21
  #define Y4_GPIO_NUM       19
  #define Y3_GPIO_NUM       18
  #define Y2_GPIO_NUM        5
  #define VSYNC_GPIO_NUM    25
  #define HREF_GPIO_NUM     23
  #define PCLK_GPIO_NUM     22

#elif defined(CAMERA_MODEL_M5STACK_PSRAM_B)
  #define PWDN_GPIO_NUM     -1
  #define RESET_GPIO_NUM    15
  #define XCLK_GPIO_NUM     27
  #define SIOD_GPIO_NUM     22
  #define SIOC_GPIO_NUM     23
  
  #define Y9_GPIO_NUM       19
  #define Y8_GPIO_NUM       36
  #define Y7_GPIO_NUM       18
  #define Y6_GPIO_NUM       39
  #define Y5_GPIO_NUM        5
  #define Y4_GPIO_NUM       34
  #define Y3_GPIO_NUM       35
  #define Y2_GPIO_NUM       32
  #define VSYNC_GPIO_NUM    25
  #define HREF_GPIO_NUM     26
  #define PCLK_GPIO_NUM     21

#else
  #error "Camera model not selected"
#endif

#define SERVO_1      14
#define SERVO_2      15

#define SERVO_STEP   5

Servo servoN1;
Servo servoN2;
Servo servo1;
Servo servo2;

int servo1Pos = 0;
int servo2Pos = 0;

static const char* _STREAM_CONTENT_TYPE = "multipart/x-mixed-replace;boundary=" PART_BOUNDARY;
static const char* _STREAM_BOUNDARY = "\r\n--" PART_BOUNDARY "\r\n";
static const char* _STREAM_PART = "Content-Type: image/jpeg\r\nContent-Length: %u\r\n\r\n";

httpd_handle_t camera_httpd = NULL;
httpd_handle_t stream_httpd = NULL;

static const char PROGMEM INDEX_HTML[] = R"rawliteral(
<html>
  <head>
    <title>ESP32-CAM Robot</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <style>
      body { font-family: Arial; text-align: center; margin:0px auto; padding-top: 30px;}
      table { margin-left: auto; margin-right: auto; }
      td { padding: 8 px; }
      .button {
        background-color: #2f4468;
        border: none;
        color: white;
        padding: 10px 20px;
        text-align: center;
        text-decoration: none;
        display: inline-block;
        font-size: 18px;
        margin: 6px 3px;
        cursor: pointer;
        -webkit-touch-callout: none;
        -webkit-user-select: none;
        -khtml-user-select: none;
        -moz-user-select: none;
        -ms-user-select: none;
        user-select: none;
        -webkit-tap-highlight-color: rgba(0,0,0,0);
      }
      img {  width: auto ;
        max-width: 100% ;
        height: auto ; 
      }
    </style>
  </head>
  <body>
    <h1>ESP32-CAM Pan and Tilt</h1>
    <img src="" id="photo" >
    <table>
      <tr><td colspan="3" align="center"><button class="button" onmousedown="toggleCheckbox('up');" ontouchstart="toggleCheckbox('up');">Up</button></td></tr>
      <tr><td align="center"><button class="button" onmousedown="toggleCheckbox('left');" ontouchstart="toggleCheckbox('left');">Left</button></td><td align="center"></td><td align="center"><button class="button" onmousedown="toggleCheckbox('right');" ontouchstart="toggleCheckbox('right');">Right</button></td></tr>
      <tr><td colspan="3" align="center"><button class="button" onmousedown="toggleCheckbox('down');" ontouchstart="toggleCheckbox('down');">Down</button></td></tr>                   
    </table>
   <script>
   function toggleCheckbox(x) {
     var xhr = new XMLHttpRequest();
     xhr.open("GET", "/action?go=" + x, true);
     xhr.send();
   }
   window.onload = document.getElementById("photo").src = window.location.href.slice(0, -1) + ":81/stream";
  </script>
  </body>
</html>
)rawliteral";

static esp_err_t index_handler(httpd_req_t *req){
  httpd_resp_set_type(req, "text/html");
  return httpd_resp_send(req, (const char *)INDEX_HTML, strlen(INDEX_HTML));
}

static esp_err_t stream_handler(httpd_req_t *req){
  camera_fb_t * fb = NULL;
  esp_err_t res = ESP_OK;
  size_t _jpg_buf_len = 0;
  uint8_t * _jpg_buf = NULL;
  char * part_buf[64];

  res = httpd_resp_set_type(req, _STREAM_CONTENT_TYPE);
  if(res != ESP_OK){
    return res;
  }

  while(true){
    fb = esp_camera_fb_get();
    if (!fb) {
      Serial.println("Camera capture failed");
      res = ESP_FAIL;
    } else {
      if(fb->width > 400){
        if(fb->format != PIXFORMAT_JPEG){
          bool jpeg_converted = frame2jpg(fb, 80, &_jpg_buf, &_jpg_buf_len);
          esp_camera_fb_return(fb);
          fb = NULL;
          if(!jpeg_converted){
            Serial.println("JPEG compression failed");
            res = ESP_FAIL;
          }
        } else {
          _jpg_buf_len = fb->len;
          _jpg_buf = fb->buf;
        }
      }
    }
    if(res == ESP_OK){
      size_t hlen = snprintf((char *)part_buf, 64, _STREAM_PART, _jpg_buf_len);
      res = httpd_resp_send_chunk(req, (const char *)part_buf, hlen);
    }
    if(res == ESP_OK){
      res = httpd_resp_send_chunk(req, (const char *)_jpg_buf, _jpg_buf_len);
    }
    if(res == ESP_OK){
      res = httpd_resp_send_chunk(req, _STREAM_BOUNDARY, strlen(_STREAM_BOUNDARY));
    }
    if(fb){
      esp_camera_fb_return(fb);
      fb = NULL;
      _jpg_buf = NULL;
    } else if(_jpg_buf){
      free(_jpg_buf);
      _jpg_buf = NULL;
    }
    if(res != ESP_OK){
      break;
    }
    //Serial.printf("MJPG: %uB\n",(uint32_t)(_jpg_buf_len));
  }
  return res;
}

static esp_err_t cmd_handler(httpd_req_t *req){
  char*  buf;
  size_t buf_len;
  char variable[32] = {0,};
  
  buf_len = httpd_req_get_url_query_len(req) + 1;
  if (buf_len > 1) {
    buf = (char*)malloc(buf_len);
    if(!buf){
      httpd_resp_send_500(req);
      return ESP_FAIL;
    }
    if (httpd_req_get_url_query_str(req, buf, buf_len) == ESP_OK) {
      if (httpd_query_key_value(buf, "go", variable, sizeof(variable)) == ESP_OK) {
      } else {
        free(buf);
        httpd_resp_send_404(req);
        return ESP_FAIL;
      }
    } else {
      free(buf);
      httpd_resp_send_404(req);
      return ESP_FAIL;
    }
    free(buf);
  } else {
    httpd_resp_send_404(req);
    return ESP_FAIL;
  }

  sensor_t * s = esp_camera_sensor_get();
  //flip the camera vertically
  //s->set_vflip(s, 1);          // 0 = disable , 1 = enable
  // mirror effect
  //s->set_hmirror(s, 1);          // 0 = disable , 1 = enable

  int res = 0;
  
  if(!strcmp(variable, "up")) {
    if(servo1Pos <= 170) {
      servo1Pos += 10;
      servo1.write(servo1Pos);
    }
    Serial.println(servo1Pos);
    Serial.println("Up");
  }
  else if(!strcmp(variable, "left")) {
    if(servo2Pos <= 170) {
      servo2Pos += 10;
      servo2.write(servo2Pos);
    }
    Serial.println(servo2Pos);
    Serial.println("Left");
  }
  else if(!strcmp(variable, "right")) {
    if(servo2Pos >= 10) {
      servo2Pos -= 10;
      servo2.write(servo2Pos);
    }
    Serial.println(servo2Pos);
    Serial.println("Right");
  }
  else if(!strcmp(variable, "down")) {
    if(servo1Pos >= 10) {
      servo1Pos -= 10;
      servo1.write(servo1Pos);
    }
    Serial.println(servo1Pos);
    Serial.println("Down");
  }
  else {
    res = -1;
  }

  if(res){
    return httpd_resp_send_500(req);
  }

  httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
  return httpd_resp_send(req, NULL, 0);
}

void startCameraServer(){
  httpd_config_t config = HTTPD_DEFAULT_CONFIG();
  config.server_port = 80;
  httpd_uri_t index_uri = {
    .uri       = "/",
    .method    = HTTP_GET,
    .handler   = index_handler,
    .user_ctx  = NULL
  };

  httpd_uri_t cmd_uri = {
    .uri       = "/action",
    .method    = HTTP_GET,
    .handler   = cmd_handler,
    .user_ctx  = NULL
  };
  httpd_uri_t stream_uri = {
    .uri       = "/stream",
    .method    = HTTP_GET,
    .handler   = stream_handler,
    .user_ctx  = NULL
  };
  if (httpd_start(&camera_httpd, &config) == ESP_OK) {
    httpd_register_uri_handler(camera_httpd, &index_uri);
    httpd_register_uri_handler(camera_httpd, &cmd_uri);
  }
  config.server_port += 1;
  config.ctrl_port += 1;
  if (httpd_start(&stream_httpd, &config) == ESP_OK) {
    httpd_register_uri_handler(stream_httpd, &stream_uri);
  }
}

void setup() {
  WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); //disable brownout detector
  servo1.setPeriodHertz(50);    // standard 50 hz servo
  servo2.setPeriodHertz(50);    // standard 50 hz servo
  servoN1.attach(2, 1000, 2000);
  servoN2.attach(13, 1000, 2000);
  
  servo1.attach(SERVO_1, 1000, 2000);
  servo2.attach(SERVO_2, 1000, 2000);
  
  servo1.write(servo1Pos);
  servo2.write(servo2Pos);
  
  Serial.begin(115200);
  Serial.setDebugOutput(false);
  
  camera_config_t config;
  config.ledc_channel = LEDC_CHANNEL_0;
  config.ledc_timer = LEDC_TIMER_0;
  config.pin_d0 = Y2_GPIO_NUM;
  config.pin_d1 = Y3_GPIO_NUM;
  config.pin_d2 = Y4_GPIO_NUM;
  config.pin_d3 = Y5_GPIO_NUM;
  config.pin_d4 = Y6_GPIO_NUM;
  config.pin_d5 = Y7_GPIO_NUM;
  config.pin_d6 = Y8_GPIO_NUM;
  config.pin_d7 = Y9_GPIO_NUM;
  config.pin_xclk = XCLK_GPIO_NUM;
  config.pin_pclk = PCLK_GPIO_NUM;
  config.pin_vsync = VSYNC_GPIO_NUM;
  config.pin_href = HREF_GPIO_NUM;
  config.pin_sscb_sda = SIOD_GPIO_NUM;
  config.pin_sscb_scl = SIOC_GPIO_NUM;
  config.pin_pwdn = PWDN_GPIO_NUM;
  config.pin_reset = RESET_GPIO_NUM;
  config.xclk_freq_hz = 20000000;
  config.pixel_format = PIXFORMAT_JPEG; 
  
  if(psramFound()){
    config.frame_size = FRAMESIZE_VGA;
    config.jpeg_quality = 10;
    config.fb_count = 2;
  } else {
    config.frame_size = FRAMESIZE_SVGA;
    config.jpeg_quality = 12;
    config.fb_count = 1;
  }
  
  // Camera init
  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    Serial.printf("Camera init failed with error 0x%x", err);
    return;
  }
  // Wi-Fi connection
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WiFi connected");
  
  Serial.print("Camera Stream Ready! Go to: http://");
  Serial.println(WiFi.localIP());
  
  // Start streaming web server
  startCameraServer();
}

void loop() {
  
}

View raw code

Network Credentials

Insert your network credentials and the code should work straight away.

const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";

How the Code Works

Let’s take a look at the relevant parts to control the servo motors.

Define the pins the servo motors are connected to. In this case, they are connected to the ESP32-CAM GPIOs 14 and 15.

#define SERVO_1 14
#define SERVO_2 15

Create Servo objects to control each motor:

Servo servoN1;
Servo servoN2;
Servo servo1;
Servo servo2;

You may be wondering why we are creating four Servo objects when we only have two servos. What happens is that the servo library we’re using automatically assigns a PWM channel to each servo motor (servoN1 → PWM channel 0; servoN2 → PWM channel 1; servo1 → PWM channel 2; servo2 → PWM channel 3).

The first channels are being used by the camera, so if we change those PWM channels’ properties, we’ll get errors with the camera. So, we’ll control servo1 and servo2 that use PWM channels 2 and 3 that are not being used by the camera.

Define the servos initial position.

int servo1Pos = 0;
int servo2Pos = 0;

Web Page

The INDEX_HTML variable contains the HTML text to build the web page. The following lines display the buttons.

<table>
  <tr><td colspan="3" align="center"><button class="button" onmousedown="toggleCheckbox('up');" ontouchstart="toggleCheckbox('up');">Up</button></td></tr>
  <tr><td align="center"><button class="button" onmousedown="toggleCheckbox('left');" ontouchstart="toggleCheckbox('left');">Left</button></td><td align="center"></td><td align="center"><button class="button" onmousedown="toggleCheckbox('right');" ontouchstart="toggleCheckbox('right');">Right</button></td></tr>
  <tr><td colspan="3" align="center"><button class="button" onmousedown="toggleCheckbox('down');" ontouchstart="toggleCheckbox('down');">Down</button></td></tr>                   
</table>

When you click the buttons, the toggleCheckbox() JavaScript function is called. It makes a request on a different URL depending on the button clicked.

function toggleCheckbox(x) {
  var xhr = new XMLHttpRequest();
  xhr.open("GET", "/action?go=" + x, true);
  xhr.send();
}


Here are the requests made depending on the button that is being pressed:

Up:

/action?go=up

Down:

/action?go=down

Left:

/action?go=left

Right:

/action?go=right

Handle Requests

Then, we need to handle what happens when we get those requests. That’s what’s done in the following lines.

if(!strcmp(variable, "up")) {
  if(servo1Pos <= 170) {
    servo1Pos += 10;
    servo1.write(servo1Pos);
  }
  Serial.println(servo1Pos);
  Serial.println("Up");
}
else if(!strcmp(variable, "left")) {
  if(servo2Pos <= 170) {
    servo2Pos += 10;
    servo2.write(servo2Pos);
  }
  Serial.println(servo2Pos);
  Serial.println("Left");
}
else if(!strcmp(variable, "right")) {
  if(servo2Pos >= 10) {
    servo2Pos -= 10;
    servo2.write(servo2Pos);
  }
  Serial.println(servo2Pos);
  Serial.println("Right");
}
else if(!strcmp(variable, "down")) {
  if(servo1Pos >= 10) {
    servo1Pos -= 10;
    servo1.write(servo1Pos);
  }
  Serial.println(servo1Pos);
  Serial.println("Down");
}

To move a motor, call the write() function on the servo1 or servo2 objects and pass the angle (0 to 180) as an argument. For example:

servo1.write(servo1Pos);

setup()

In the setup(), set the servo motor properties: define the signal frequency.

servo1.setPeriodHertz(50); // standard 50 hz servo
servo2.setPeriodHertz(50); // standard 50 hz servo

Use the attach() method to set the servo GPIO and minimum and maximum pulse width in microseconds.

servo1.attach(SERVO_1, 1000, 2000);
servo2.attach(SERVO_2, 1000, 2000);

Set the motors to its initial position when the ESP32 first boots.

servo1.write(servo1Pos);
servo2.write(servo2Pos);

That’s pretty much how the code works when it comes to control the servo motors.

Testing the Code

After inserting your network credentials, you can upload the code to your board. You can use an FTDI programmer or an ESP32-CAM MB programmer. Read one of the following articles:

After uploading, open the Serial Monitor to get the board IP address.

ESP32-CAM Getting IP Address Serial Monitor

Note: if you’re using an FTDI programmer, don’t forget to disconnect GPIO 0 from GND before opening the Serial Monitor.

Open a browser and type the board IP address to get access to the web server. Click on the buttons and check on the Serial Monitor if everything seems to be working as expected.

ESP32-CAM Pan and Tilt Web Server Serial Monitor

If everything is working as expected you can wire the servo motors to the ESP32-CAM and continue with the project.

Circuit

After assembling the pan and tilt stand, connect the servo motors to the ESP32-CAM as shown in the following schematic diagram. We’re connecting the servo motor data pins to GPIO 15 and GPIO 14.

ESP32-CAM Pan and Tilt Servo Motors

You can use a mini breadboard to assemble the circuit or build a mini stripboard with header pins to connect power, the ESP32-CAM, and the motors, as shown below.

ESP32-CAM Pan and Tilt Prototype Board

The following figure shows how the pan and tilt stands looks like after assembling.

ESP32-CAM Pan and Tilt Assembled

Demonstration

Apply power to your board. Open a browser and type the ESP32-CAM IP address. A web page with real-time video streaming should load. Click the buttons to move the camera up, down, left, or right.

ESP32-CAM Pan and Tilt Web Server Video Streaming Smartphone Arduino IDE

You can move the camera remotely using the buttons on the web page. This allows you to
monitor a different area accordingly to the camera position. This is a great solution for surveillance applications.

Wrapping Up

In this tutorial, you’ve learned how to build a pan and tilt web server with video streaming to control the ESP32-CAM.

Controlling servo motors with the ESP32-CAM is the same as controlling them using a “regular” ESP32. You can read the following tutorial to learn more about servo motors with the ESP32:

If you want to control your robot outside the range of your local network, you might consider setting the ESP32-CAM as an access point. This way, the ESP32-CAM doesn’t need to connect to your router. It creates its own wi-fi network, and nearby wi-fi devices like your smartphone can connect to it.

For more projects and tutorials with the ESP32-CAM:



Build Web Server projects with the ESP32 and ESP8266 boards to control outputs and monitor sensors remotely. Learn HTML, CSS, JavaScript and client-server communication protocols DOWNLOAD »

Build Web Server projects with the ESP32 and ESP8266 boards to control outputs and monitor sensors remotely. Learn HTML, CSS, JavaScript and client-server communication protocols DOWNLOAD »


Enjoyed this project? Stay updated by subscribing our newsletter!

61 thoughts on “ESP32-CAM Pan and Tilt Video Streaming Web Server (2 Axis)”

      • No – if you can to port forwarding, then anything which is accessible on your local network, can be made accessible worldwide via the internet.

        Reply
    • It is possible followingg these steps:
      1) subscribe a DDNS service (needed if you have an ISP that assignes a dynmaic IP to your router: best thing if you check before which DDNS services your router will accept);
      2) on your router configure the DDNS service ;
      3) on your ruoter assign a static IP address to the MAC address of your microcontroller/cam ;
      4) on your router assign a TCP/HTTTP port forwording rule to the MAC/IP address of your microcontroller (WAN and LAN ports = 80);
      After that you can access your cam locally via its assigned IP address (e.g. on your browser go to the address 192.168.1.XX) or via internet using the address that was given to you by the DDNS provider (e.g. arduinocam.mypc.com).
      Many thanks to Rui and Sara for their fantastic site!!

      Reply
      • Bonjour,
        Merci pour votre mail.

        J’ai bien un lien dynamique et ca fonctionne bien.
        Le port 80 est le port par défaut et donc avec mon lien DNS je ne peut accéder qu’à un seul montage.

        Mon problème c’est que j’ai besoin d’accéder à plusieurs caméras Pan et Tilt et c’est pour cela que je voudrai pouvoir affecter un no de port différent.
        ex: arduinocam.mypc.com:49160

        Cordialement
        Bernard

        Reply
        • Bernard,
          try these steps:
          1) on your router side configure different ports in the portwarding rules: e.g. CAM1: WAN Port = 80 & LAN Port =80
          CAM2: WAN Port = 82 & LAN Port =82
          2) in the Arduino code change this line: config.server_port = 80; to config.server_port = 82; for CAM2

          Now you should be able to access CAM1 on arduinocam.mypc.com:80 (or arduinocam.mypc.com, as port 80 is the dafault one) and CAM2 on arduinocam.mypc.com:82
          Let me know …

          Reply
  1. Vielen Dank für dieses Projekt. Ich bin vermutlich einer von vielen die danach gefragt haben und freue mich sehr das ihr es realisiert habt. Durch dieses Projekt werden die ESP32-CAMs besonders nützlich und ermöglichen mir ganz neue Einblicke in die Tier Welt.
    Nochmals Vielen Dank,
    Horst Ketter

    Translated:
    “Thank you for this project. I am probably one of many who asked about it and I am very happy that you have realized it. This project makes the ESP32-CAMs particularly useful and gives me completely new insights into the animal world.
    Thanks again,
    Horst Ketter”

    Reply
  2. It Works!!!

    Just finished this project and all works fine.
    I had trouble with another ESP32 Cam with pan and tilt hanging on me.
    I see you solved that problem by creating four servo objects, nice work!

    Thanks for sharing this with us!

    Reply
  3. Great, I have that Pan and Tilt gimball, including the servo’s exactly for this purpose, but had not gotten around to program it. Thanks for making it easy for me

    Reply
  4. Hi . thank you for this project. is there any way that transfer voice of enviroment with esp32-cam to webserver or webpage?
    thanks for attention
    regards

    Reply
  5. Thank you for this project. It works very well.
    Is there a safe way to be able to access it from out side the local network without opening any ports on the router.

    Reply
  6. Your circuit diagram shows 3.3v connected to the servos but servos specs say operating voltage is 4.8v to 6.5v. Is 3.3v in your circuit diagram correct?
    Thanks

    Reply
    • Hi Dave.
      Yes, you are right.
      I was a mistake in designing the diagram.
      You should connect to the 5V pin.
      If you look closely at the stripboard, you’ll notice that it will connect 5V to the motors.
      I’ll fix the schematic now.
      Thanks for noticing.
      Regards,
      Sara

      Reply
  7. Do you have a picture or schematic of the underside of the stripboard? The wiring and circuit diagram are not making sense to me.
    Thanks

    Reply
    • Hi Dave.
      Unfortunately, we don’t have a scheme for the stripboard.
      You just need to take into account the following connections (then, you can build a stripboard to make wiring easier)

      Servo 1:
      GND –> GND
      VCC –> 5V
      Data –> GPIO 15

      Servo 2:
      GND –> GND
      VCC –> 5V
      Data –> GPIO 14

      Because the ESP32 only has one 5V pin, you need to wire both VCC pins to the same ESP32 pin.
      I hope this is clear.
      Regards,
      Sara

      Reply
  8. Hello your program is great.
    Thank you, this gives very good possibilities.
    I haven’t done this project yet, but I see the photo on the smartphone is tilted 90 degrees.
    Is it possible to straighten the streaming image for a good view please?

    Reply
  9. Hi Sara,
    I got the tilt platform to work, the only thing I do have it is that the picture is 90 degree.
    Is there any thing to rotate the picture. I know with the normal view in the browser there is a click field to turn the view.
    Regards
    Willem

    Reply
    • Hello from France,
      I saw a tutorial concerning the possibility of making 90 ° rotations, I put the link in a post above.
      I think this possibility is very interesting, but I don’t know how to integrate it into Sara’s excellent tutorial.
      Maybe this will interest Sara and she can add a modification for this 90 ° rotation function, that would be very interesting? …

      Reply
      • Hello Gérard,
        I read your previous e-mail about rotating the image.
        After the command on the original scrip:
        window.onload = document.getElementById(“photo”).src = Window.location.href.slice(0, -1) + “:81/stream”;
        Add this 2 lines:
        var deg = -90;
        window.onload = document.getElementById(“photo”).style.transform = ‘rotate(‘ + deg + ‘deg)’;
        Changing the “var deg” you can rotate the image to 180º
        Hope it helps.

        Reply
        • Hello Arlvaro Henriques,
          Great (by changing -90 to 90), it’s great.
          I just have to go down the image to be below the Title

          ESP32-CAM Pan and Tilt

          Then move down the buttons to be below the image
          I’ll do some testing so as not to make a mistake.
          thank you so much

          Reply
        • Hello Arlvaro Henriques,
          Ok to go down the image under the title, and go down the buttons under the image.
          I added :
          h2> & nbsp </ h2 under h1> ESP32-CAM Pan and Tilt </ h1 to lower the position of the image
          Then:
          h2> & nbsp </ h2 under img src = “” id = “photo” to lower the position of the buttons
          it works very well, thanks again, it’s great !!!

          Reply
  10. Hi Sara ,

    Thank you for your job !

    I try to add a button to turn on the lamp but I can not! Can you help me, thank you?

    Reply
  11. Hi Sara,

    Found this a very interesting tutorial.

    However I ordered pan & tilt mechanism from your link, but it needs a 5V signal to operate. Had to use 2 level shifters & 2 power supplies (1 for each servo), to achieve the desired effect) . Using same power supply for both servo’s resulted in interfarance between the 2.

    Reply
  12. Bonjour,

    Excellent tuto!
    Pour moi j’ai bien l’image sur mon PC mais rien sur mon telephone portable, je ne vois que les boutons.
    Pouvez vous m’aider?

    Merci

    Reply
    • Hi
      You can only watch the video streaming in one place at a time.
      Please close your computer web browser window before opening it on the smartphone.
      Regards,
      Sara

      Reply
      • Merci pour votre réponse rapide!

        OK effectivement c’était bien mon pb…. sorry.
        Par contre si je change le port 80 par un autre port dans le PGM
        je n’ai plus l’image, j’ai contrôlé dans la boxe et le port concerné est bien activé.

        Voici ou j’ai modifié:
        void startCameraServer(){
        httpd_config_t config = HTTPD_DEFAULT_CONFIG();
        config.server_port = 49161; //port validé dans la boxe

        et à quoi servent ces 2 lignes:
        config.server_port += 1;
        config.ctrl_port += 1;

        Grand merci pour votre aide et vos tutos géniaux.

        Reply
  13. To avoid problems with the servos, you need to put a 10 uf , and a 0.1 uf condenser between + and – and the source is 5V /1Amp

    Reply
  14. Hello,
    Great your program, thank you.
    I’m in the middle of testing, but there must be some problem with my setup, but I don’t know what …
    My low servo (to go from left to right or vice versa) does not stop, it seems that it is still looking for a position because it is continually panicking.
    Do you have an opinion on my problem please?
    (By the way, I posted a suggestion to rotate the image 90 °, if that helps …)
    Please let me know about my panicking servo problem.

    Reply
      • I thought I had solved my pan-tilt problem which trembles as soon as I plug it in, but no, it starts again.
        it might be necessary to put capacitors, but which ones and where, I am zero in electronics …
        Anyone have an idea, and if so can I refer me to a tutorial, please? I searched but could not find anything regarding this problem …

        Reply
        • I continue my research regarding servomotor shaking, so the esp32-cam and pan-tilt servomotors all connected to 5v, the pan-tilt shakes and is uncontrollable from the power on 5v.
          With a 3.3v / 5v power supply module (https://fr.aliexpress.com/item/1722852745.html?detailNewVersion=&categoryId=400103), the esp32-cam connected in 5v, and the pan-tilt servomotors connected in 3.3v, it works, the servomotors no longer shake.
          That’s good, but how do you make a mini board like you suggest, putting 5v for the esp32-com and 3.3v for the servos?
          Can you help me, I’m lost and I’m really bad at electronics, please?

          Reply
          • Hello from France,

            For other people who have the same pan-tilt shaking problem, I follow up: my problem is solved, I definitely hope.
            I had changed several usb cables for the 5v power supply, unfortunately with the same problem.
            And this morning, finding yet another usb cable, I tried it on the off chance, and it works perfectly!
            The esp32-cam AND the Pan-Tilt, all connected to the 5v.
            So this problem was simply the power supply, it will have made me waste a lot of time, but it works very well now with this cable found at the bottom of a drawer …

            This project and tutorial is great, thank you Sara, and well done …

  15. Good day and thanks for posting this article. I was hoping to please ask for some assistance with the esp32-cam.

    I have been attempting to enable face detection on the ESP32-Cam however it has failed on multiple boards. The boards flash ok and the webserver example runs but when I turn on face detection on the webserver web interface, the esp32-cam reboots or shows a memory heap error in Arduino serial monitor (baud rate 115200). I have tried six units and all have failed. I tried powering the esp32-cam directly to a power supply (5V), it still failed. I have flashed using various methods, ESP32-CAM-MB USB Programming Adapter, UNO and a FT232RL FTDI USB to TTL Serial Adapter; they all flash the board but face detection still crashes it.

    Thanks for any ideas or suggestions.

    Reply
    • Hi.
      Please check the version of the ESP32 Boards Installation. It should be 1.0.4. (recent versions have some sort of bug that crashes face recognition).
      In your Arduino IDE, go to Tools > Board > Boards Manager and search for ESP32. Make sure you have version 1.0.4.
      Regards,
      Sara

      Reply
      • Thank you for that tip.

        So, I tried that, changed board to 1.0.4, still no luck. A bunch of gibberish in serial monitor then it says connected to webserver but crashes when face detection is selected using the example webserver code. I tried downgrading Arduino IDE to 1.8.12 as well, but still no luck on the face detection. I have 5 of these left, I did have 6 but one met an untimely demise with a hammer (it felt sooooo good), the rest are destined to this fate soon.

        Reply
        • What’s the board that you’re using?
          Are you using an external antenna?
          Make sure it can catch a good wi-fi signal from your router.
          Regards,
          Sara

          Reply
          • Hello again and thank you for your assistance.

            I am using this board (esp32S chip) and have tried others: https://www.amazon.ca/gp/product/B08XYLSH15/ref=ppx_yo_dt_b_asin_title_o03_s00?ie=UTF8&psc=1

            No external antenna. Wi-fi Router is right beside me.

            The part that is strange, is that directly after a successful flash with Arduino IDE and before any testing, I open serial monitor where it shows a bunch of garbage characters, as if the baud rate is wrong (it’s 115200), right after the garbage it says connecting…, it connects and says successfully connected and gives me the IP of the webserver. I browse to the IP and the Webserver works until I select Face Detection.

            In Arduino IDE, I am choosing the AI THINKER board, should I be choosing Wrover?

            Thanks,
            Norton

          • Hi.
            Try selecting the Wrover module with the following settings and see if it works.
            In Tools > Board select ESP32 Wrover Module.
            In Tools > Partition Scheme select Huge App 3MB No OTA.
            Regards,
            Sara

          • Thanks so much for all the help.

            So here is what worked for me:

            Board: “ESP32 Wrover Module”
            Upload Speed: “115200”
            Flash Frequency: “40MHz”
            Flash Mode: “QIO”
            Partition Scheme: “Huge APP (3MB No OTA/1MB SPIFFS)”
            Cored Debug Level: “None”
            Port: the CH340 port
            Programmer: “AVR ISP”

            Arduino IDE version 1.8.12
            Board: esp32 version 1.0.4

            I did have to push the reset button on the esp32-Cam and timing sometimes was hit and miss however when it worked, it worked successfully.
            The part I’m not getting is that in Arduino IDE the board selected is ESP32 Wrover Module whereas the board in the code is AI_THINKER.

            Thanks again, you guys rock!

          • Hi.
            I’m also not sure why that happens.
            But, I’m glad it is working now.
            Regards,
            Sara

  16. Hello Rui & Sara
    It works!
    One question: I was using 2 Wifi analyzer apps (Netspot on Windows / WIFI Analyzer on Android) in order to measure the signal strength of the ESP32.
    On both apps the ESP32 does not show up.
    Strange because the camera was working.
    What is going on here?
    Regards
    Chris

    Reply
  17. Hi, this project is working in lovalhost, but when I tried to use port forward, using ngrok, the video is not loading (sreaming), but the servos works fine. Could you explain me why it is happening?

    Reply
  18. Hello from France,
    Always to learn, and going further to put your excellent tutorial to my taste, I would like to mark, on the web page, the current position of the servomotors at each change of position, therefore the value of servo1Pos and servo2Pos.
    Do you think this is possible, and if so, can you help me do it, please?

    Reply
    • Hello,
      To have the current position, or the requested position of the servomotor, on the WEB page, I try to take inspiration from your excellent tutorial:
      https://randomnerdtutorials.com/esp32-servo-motor-web-server-arduino-ide/
      But I am very afraid of doing anything stupid to do so.
      Could you help me please, to have the same possibility in the web page of the tutorial with the pan-tilt, I would be really grateful to you, I am not very good at programming, and I really have need to know.

      Reply
      • Hello,
        Nobody has an idea to write the current position, OR the requested position, of the servomotors (servo1Pos and servo2Pos) on the WEB page, at each rotation of the servomotors?
        I really try, but I can’t, help

        Reply
  19. When I used the verify/compile command of Arduino 1.8.13 on this project I got an error on line 299. The variable “s” was not defined. I corrected the error by un-commenting the “flip the camera vertically” setting. Did I miss that in the instructions or is it unique to my installation? Once that error was cleaned up my servos started acting correctly–before that I had some jerky motion and random movements and I would lose connection to the video stream.

    One other thing I noticed is that my up and down servo is opposite–when I click on the down button it goes up and vice versa. I’m using Tower Pro SG92R actuators. I swapped the up and down variables and it seems to do what it should.

    Thanks and as always, a well done, great tutorial.

    Reply

Leave a Comment

Download our Free eBooks and Resources

Get instant access to our FREE eBooks, Resources, and Exclusive Electronics Projects by entering your email address below.