Video Streaming from ESP32-CAM

Hi everyone,

I have ESP32-CAM and want to streaming video from it to Blynk Iot, but every tutorials telling about OLD ver. of Blynk…

I trying to create DDNS server, but it not working too.

May u knows, how to fix it.

Tell us what you’ve done with port forwarding, DDNS service, DDNS Update client and what tests you’ve done using a browser outside your network and Blynk.
Are you using the Blynk app (in which case what OS and version) or the web dashboard?

What port are you using for your ESP32 Cam?

Pete.

Hello. The simplest implementation I saw was:

  1. Make images with a camera (not video)
  2. When image is done - upload it to some server (AWS S3 for example)
  3. When upload is done, set the image widget with setProperty("url", "uploaded image path");

When it comes to video it’s much more complicated, because you need to forward the video stream somewhere: either to some 3-d party service like wowza or to your own server with ffmpeg or some 3-d party open-source server (like motion-project) that does what you need, after that you can generate a stream url and consume it from the blynk widget.

2 Likes

Hi
I use no-ip and setting my router
After it, I use this code:

/*Tech Trends Shameer
//Live Streaming*/
/*

  This is a simple MJPEG streaming webserver implemented for AI-Thinker ESP32-CAM and
  ESP32-EYE modules.
  This is tested to work with VLC and Blynk video widget.

  Inspired by and based on this Instructable: $9 RTSP Video Streamer Using the ESP32-CAM Board
  (https://www.instructables.com/id/9-RTSP-Video-Streamer-Using-the-ESP32-CAM-Board/)

  Board: AI-Thinker ESP32-CAM

*/
#include "src/OV2640.h"
#include <WiFi.h>
#include <WebServer.h>
#include <WiFiClient.h>
#include <BlynkSimpleEsp32.h>


char auth[] = "UvjQcb_fAobpiFUQ26XC_3L7eAGFhhYX";
// Select camera model
//#define CAMERA_MODEL_WROVER_KIT
//#define CAMERA_MODEL_ESP_EYE
//#define CAMERA_MODEL_M5STACK_PSRAM
//#define CAMERA_MODEL_M5STACK_WIDE
#define CAMERA_MODEL_AI_THINKER

#include "camera_pins.h"

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

OV2640 cam;

WebServer server(80);

const char HEADER[] = "HTTP/1.1 200 OK\r\n" \
                      "Access-Control-Allow-Origin: *\r\n" \
                      "Content-Type: multipart/x-mixed-replace; boundary=123456789000000000000987654321\r\n";
const char BOUNDARY[] = "\r\n--123456789000000000000987654321\r\n";
const char CTNTTYPE[] = "Content-Type: image/jpeg\r\nContent-Length: ";
const int hdrLen = strlen(HEADER);
const int bdrLen = strlen(BOUNDARY);
const int cntLen = strlen(CTNTTYPE);

void handle_jpg_stream(void)
{
  char buf[32];
  int s;

  WiFiClient client = server.client();

  client.write(HEADER, hdrLen);
  client.write(BOUNDARY, bdrLen);

  while (true)
  {
    if (!client.connected()) break;
    cam.run();
    s = cam.getSize();
    client.write(CTNTTYPE, cntLen);
    sprintf( buf, "%d\r\n\r\n", s );
    client.write(buf, strlen(buf));
    client.write((char *)cam.getfb(), s);
    client.write(BOUNDARY, bdrLen);
  }
}

const char JHEADER[] = "HTTP/1.1 200 OK\r\n" \
                       "Content-disposition: inline; filename=capture.jpg\r\n" \
                       "Content-type: image/jpeg\r\n\r\n";
const int jhdLen = strlen(JHEADER);

void handle_jpg(void)
{
  WiFiClient client = server.client();

  cam.run();
  if (!client.connected()) return;

  client.write(JHEADER, jhdLen);
  client.write((char *)cam.getfb(), cam.getSize());
}

void handleNotFound()
{
  String message = "Server is running!\n\n";
  message += "URI: ";
  message += server.uri();
  message += "\nMethod: ";
  message += (server.method() == HTTP_GET) ? "GET" : "POST";
  message += "\nArguments: ";
  message += server.args();
  message += "\n";
  server.send(200, "text / plain", message);
}

void setup()
{

  Serial.begin(115200);
  //while (!Serial);            //wait for serial connection.
  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;

  // Frame parameters
  //  config.frame_size = FRAMESIZE_UXGA;
  config.frame_size = FRAMESIZE_QVGA;
  config.jpeg_quality = 12;
  config.fb_count = 2;

#if defined(CAMERA_MODEL_ESP_EYE)
  pinMode(13, INPUT_PULLUP);
  pinMode(14, INPUT_PULLUP);
#endif

  cam.init(config);

  IPAddress ip;

  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED)
  {
    delay(500);
    Serial.print(F("."));
  }
  ip = WiFi.localIP();
  Serial.println(F("WiFi connected"));
  Serial.println("");
  Serial.println(ip);
  Serial.print("Stream Link: http://");
  Serial.print(ip);
  Serial.println("/mjpeg/1");
  server.on("/mjpeg/1", HTTP_GET, handle_jpg_stream);
  server.on("/jpg", HTTP_GET, handle_jpg);
  server.onNotFound(handleNotFound);
  server.begin();
}

void loop()
{
  server.handleClient();
  
}

Of course, I replace “***” on my SSID and password :slight_smile:

THX for answer! Can u help me with Wowza? How to use Wowza and integrate it in Blynk?

That doesn’t answer my questions does it?
If you want my assistance you need to provide more info…

Pete.

I am having project connect esp32-cam and display video on blynk app but when i run this code and get ip:http://192.168.x.x/mjpeg/1 and i enter url of Blynk IoT app but it inactive
Help me please !
image

The IP address of 192.168.x.x is internal to your home network.
It can’t be resolved by the Blynk app or server.

Your WiFi router has two IP addresses - its public address, known as the WAN and its internal address, known as the LAN address. It’s the LAN (Local Area Network) address that you’ve quoted above, but that is local to your network.

Blynk can only see your WAN address (Wide Area Network). This is a single public IP address that is assigned by your ISP, and is normally dynamic (it changes from time to time, usually when you reboot your router).
Data sent to this WAN IP address is routed to the correct internal device by your router (hence its name).
For the router to correctly route data to your ESP32 CAM module you need to tell your router that any traffic on port XXXX needs to be routed to internal IP address 192.168.1.1
This is done by setting-up port forwarding rules in your router. Exactly how you do that will depend on your router, and the access to these settings that your ISP allows.

You may think that the traffic is flowing outwards from your ESP32 CAM module to Blynk, so why do inbound requests on the video streaming port need to be routed to the ESP32 CAM. This is because the Blynk widget needs to initialise the stream, so it sends an inbound message saying “give me your video”. Without this the video stream won’t initialise.

Most people have a dynamic public IP address, and to ensure that the Blynk app knows what that is whenever it changes, you need to use a Dynamic Domain Name System (DDNS) service which gives you a static URL, but forwards inbound data on that URL to your current public (WAN) IP address. For the DDNS service to know your current public IP address you need to be running an updater service on a device within your private network. This could be your router, if it has a DDNS facility, or it could be a device running an updater client.
Common free DDNS services are NoIP.com and DuckDNS.net

So, (unless you have a static public IP addresss) you need a DDNS service and an updater.
You then need to have your ESP32 CAM set-up with a static internal IP address (so that your router’s DHCP system doesn’t allocate it a different IP when it reboots) and forward inbound and outbound data packets on whichever port you’re using for video streaming to/from your ESP32 CAM’s internal IP address.
You then use your DDNS URL and the video streaming port in the Blynk video streaming app.

Pete.

2 Likes

We did a “streaming” project for one of our white label client, so now we have some expertise here. Here is my observations:

  1. You can directly stream from your camera, but this involves DDNS (or static ip), port forwarding, network setup, etc. So your streaming url could be accessible from the outside. This solution works for “makers”, home projects. But not suitable in business use cases. As end users will never be able to setup that
  2. So we come to point 2. Constant streaming is actually expensive. There are 2 main components: 1) traffic; 2) server (CPU/RAM) for streaming. Traffic is more expensive than server. In average 1 hour of streaming + 1 viewer costs ~1-3 $ / per hour on different services. Both traffic and server costs could be optimized with different approaches, but in both cases it requires some understanding of what is going on (some minimal expertise in video streaming). The typical optimization is - send the streaming only when something happens.
  3. The simplest solution is to use existing services (wowza, aws streaming). However, there is no “free” streaming services. At least I didn’t find any.

Hi kinda unrelated to this topic but do you have a guide on this? I am an noob trying to get this to work on Blynk

#define BLYNK_TEMPLATE_ID "ID"
#define BLYNK_TEMPLATE_NAME "ESP32CAMSTREAMPHOTO"
#define BLYNK_AUTH_TOKEN "Auth"


#include "esp_camera.h"
#include "Arduino.h"
#include "FS.h"                // SD Card ESP32
#include "SD_MMC.h"            // SD Card ESP32
#include "soc/soc.h"           // Disable brownour problems
#include "soc/rtc_cntl_reg.h"  // Disable brownour problems
#include "driver/rtc_io.h"
#include <EEPROM.h>            // read and write from flash memory
#include <WiFi.h>              //wifi lib
#include <WiFiClient.h>
#include <BlynkSimpleEsp32.h>  // Blynk library
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <WebServer.h>
// define the number of bytes you want to access
#define EEPROM_SIZE 1

// Pin definition for 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

#define I2C_SDA 15
#define I2C_SCL 14
TwoWire I2Cbus = TwoWire(0);
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 32
#define OLED_RESET -1
#define SCREEN_ADDRESS 0x3C
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &I2Cbus, OLED_RESET);


const char* ssid = "wifiname";
const char* password = "password";
const char* auth = "Blynk auth";

String local_IP;
WebServer server(80);
int pictureNumber = 0;
const unsigned long serverDuration = 5 * 60 * 1000; // 5 minutes in milliseconds
unsigned long startTime;
bool isHome;
//bool isHome;
void turnOffOLEDDisplay() {
    // Send command to turn off the display
    display.ssd1306_command(SSD1306_DISPLAYOFF);
}
void startCameraServer();

void updateOledDisplay(bool isHome) {
    display.clearDisplay();
    display.setTextSize(2);
    display.setTextColor(SSD1306_WHITE);
    display.setCursor(0, 0);
    if (isHome) {
        display.println("Owner is home");
    } else {
        display.println("Owner is away");
    }
    display.display();
}

void takePhoto() {
  if (local_IP.isEmpty()) {
        Serial.println("Error: Local IP is empty.");
        return;
  }
  uint32_t randomNum = random(50000);
  //Serial.println("http://" + local_IP + "/capture?_cb=" + (String)randomNum);
  //Blynk.setProperty(V1, "urls", "http://" + local_IP + "/capture?_cb=" + (String)randomNum);
  String photoUrl = "http://" + local_IP + "/capture?_cb=" + randomNum;
  Serial.println("Photo URL: " + photoUrl);

    // Set the URL property in Blynk
  Blynk.setProperty(V1, "urls", photoUrl);
  Serial.println("URL set in Blynk on V1");

  delay(1000);
}
void handle_mjpeg_stream() {
    WiFiClient client = server.client();
    client.write("HTTP/1.1 200 OK\r\n");
    client.write("Content-Type: multipart/x-mixed-replace; boundary=frame\r\n");
    client.write("\r\n");

    while (client.connected()) {
        camera_fb_t *fb = esp_camera_fb_get();
        if (!fb) {
            client.stop();
            return;
        }

        client.write("--frame\r\n");
        client.write("Content-Type: image/jpeg\r\n");
        client.write(String("Content-Length: " + String(fb->len) + "\r\n\r\n").c_str());
        client.write((const char *)fb->buf, fb->len);
        esp_camera_fb_return(fb);

        // Check if the client is still connected, if not, break the loop
        if (!client.connected()) {
            break;
        }

        // Add a delay to control the frame rate (e.g., 30 fps)
        delay(33);
    }
}


void setup() {
  WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); //disable brownout detector
 
  Serial.begin(115200);
  //Serial.setDebugOutput(true);
  //Serial.println();
  
  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; 

  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WiFi connected");
  Blynk.begin(auth, ssid, password);
  if(psramFound()){
    config.frame_size = FRAMESIZE_XGA; // FRAMESIZE_ + QVGA|CIF|VGA|SVGA|XGA|SXGA|UXGA
    config.jpeg_quality = 10;
    config.fb_count = 2;
  } else {
    config.frame_size = FRAMESIZE_SVGA;
    config.jpeg_quality = 12;
    config.fb_count = 1;
  }
  
  // Init Camera
  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    Serial.printf("Camera init failed with error 0x%x", err);
    return;
  }
  delay(1000);

  //config camera
  sensor_t * s = esp_camera_sensor_get();
  s->set_vflip(s, 1); 


  SD_MMC.begin("/sdcard", true);
  //Serial.println("Starting SD Card");
  if(!SD_MMC.begin("/sdcard", true)){
    Serial.println("SD Card Mount Failed");
    return;
  }

  uint8_t cardType = SD_MMC.cardType();
  if(cardType == CARD_NONE){
    Serial.println("No SD Card attached");
    return;
  }
  pinMode(4, OUTPUT);
  digitalWrite(4, LOW);
  camera_fb_t * fb = NULL;
  for (int i = 0; i < 15; ++i) {
        fb = esp_camera_fb_get();
        esp_camera_fb_return(fb);
    }

  
  // Take Picture with Camera
    digitalWrite(4, HIGH);
  fb = esp_camera_fb_get();  
  if(!fb) {
    Serial.println("Camera capture failed");
    return;
  }
    digitalWrite(4, LOW);
  // initialize EEPROM with predefined size
  EEPROM.begin(EEPROM_SIZE);
  pictureNumber = EEPROM.read(0) + 1;

  // Path where new picture will be saved in SD Card
  String path = "/picture" + String(pictureNumber) +".jpg";

  fs::FS &fs = SD_MMC; 
  Serial.printf("Picture file name: %s\n", path.c_str());
  
  File file = fs.open(path.c_str(), FILE_WRITE);
  if(!file){
    Serial.println("Failed to open file in writing mode");
  } 
  else {
    file.write(fb->buf, fb->len); // payload (image), payload length
    Serial.printf("Saved file to path: %s\n", path.c_str());
    EEPROM.write(0, pictureNumber);
    EEPROM.commit();
  }
  file.close();
  esp_camera_fb_return(fb); 
  
  
  // Turns off the ESP32-CAM white on-board LED (flash) connected to GPIO 4

  digitalWrite(4, LOW);
  //rtc_gpio_hold_en(GPIO_NUM_4);

  
  server.begin();
  //startCameraServer();
  server.on("/stream", HTTP_GET, handle_mjpeg_stream);
  server.onNotFound([]() {
        server.send(404, "text/plain", "Not found");
  });

  // Start the server
  server.begin();
  Serial.println("HTTP server started");

  Serial.print("Camera Ready! Use 'http://");
  Serial.print(WiFi.localIP());
  local_IP = WiFi.localIP().toString();
  Serial.print("/stream");
  Serial.println("' to connect");
  Blynk.run();
  Blynk.setProperty(V2, "url", local_IP + "/stream");
  delay(2000);
  Blynk.logEvent("Doorbell") ;
  //takePhoto();
  delay(10000);
  // Initialize I2C with our defined pins
  I2Cbus.begin(I2C_SDA, I2C_SCL, 100000);
  display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS);
    if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
    Serial.printf("SSD1306 OLED display failed to initalize.\nCheck that display SDA is connected to pin %d and SCL connected to pin %d\n", I2C_SDA, I2C_SCL);
    while (true)
      ;
  }
  Serial.println("Initialize display");
  
  delay(2000);
  
  updateOledDisplay(isHome);
  
  delay(2000);
  //delay(5000);
  //delay(15000);
  startTime = millis();
  //Serial.print("Going to sleep now");
  //esp_deep_sleep_start();
  //Serial.println("This will never be printed");
}

void loop() {
    
      // Check if the elapsed time has exceeded the server duration
    if (millis() - startTime >= serverDuration) {
        // Turn off the OLED display
        turnOffOLEDDisplay();
        // Enter deep sleep
        Serial.println("Entering deep sleep...");
        esp_deep_sleep_start();
        //enteredDeepSleep = true; // Set the flag to indicate deep sleep has been entered
    }
    server.handleClient();
    // Run other code in the loop
    Blynk.run();
}
BLYNK_WRITE(V0) {
    int switchState = param.asInt(); // Read the value from the switch widget on the virtual pin V0
    if (switchState == 1) {
        isHome = 1; // Convert the state to a boolean (1 means home, 0 means away)
    } else {
        isHome = 0;
    }
    Serial.printf("isHome updated to: %d\n", isHome);
    updateOledDisplay(isHome); // Update the OLED display with the home status
}
BLYNK_WRITE(V4) {
    int switchState01 = param.asInt(); // Read the value from the switch widget on the virtual pin V0
    if (switchState01 == 1) {
        digitalWrite(4, HIGH);
        Serial.printf("LED is turned on\n"); // Convert the state to a boolean (1 means home, 0 means away)
    } else {
        digitalWrite(4, LOW);
        Serial.printf("LED is turned off\n");
    }
    
    //updateOledDisplay(isHome); // Update the OLED display with the home status
}


Again, sorry for the spaghetti code but I am a total noob.

No, because it will vary according to whether you have a static or dynamic IP address, the DDNS service you use if your public IP is dynamic, whether your router allows DDNS updates with your chosen DDNS service, whether your ISP uses a Double NAT system, how your router’s UI handles port forwarding, how your router handles static internal IP addresses, and the static internal IP address you assign to your ESP32 module.

I’ve outlined the basic principals already in this post, it’s up to you to handles the non-Blynk aspects of the project to make your video stream publicly resolvable.

Pete.

1 Like

thanks