Skip to Content

Überwachungskamera mit ESP32-CAM

Überwachungskamera mit ESP32-CAM

In diesem Tutorial lernst du, wie du ein bewegungsaktiviertes Überwachungskamerasystem mit dem ESP32-CAM baust. Das System zeichnet Videos auf, sobald eine Bewegung erkannt wird, und speichert den Videostream in einer Datei auf deinem Computer. Dieses Projekt eignet sich für Überwachung, Sicherheit oder Wildtierbeobachtung.

Benötigte Teile

Im Folgenden findest du die Komponenten, die für den Bau des Projekts erforderlich sind. Du benötigst einen ESP32-CAM und das USB-TTL Shield oder den FTDI USB-TTL Adapter, um das Board zu programmieren.

Ich habe zwei verschiedene Arten von Bewegungssensoren aufgelistet. Wenn du eine kleine Größe und ein kürzeres Aktivierungsintervall möchtest, nimm den AM312. Wenn du die Kamera nur nachts aktivieren möchtest, nimm den HC-SR501, da dieser leicht mit einem Lichtsensor ausgestattet werden kann. Für weitere Details siehe das Motion Activated ESP32-CAM Tutorial.

ESP32-CAM mit USB-TTL Shield

FTDI USB-TTL Adapter

Dupont wire set

Dupont-Kabelsatz

AM312 PIR-Bewegungssensor

HC-SR501 PIR-Bewegungssensor

USB data cable

USB-Datenkabel

Makerguides is a participant in affiliate advertising programs designed to provide a means for sites to earn advertising fees by linking to Amazon, AliExpress, Elecrow, and other sites. As an Affiliate we may earn from qualifying purchases.

Systemarchitektur

In diesem Projekt implementieren wir ein Videoüberwachungssystem, das den ESP32-CAM, einen Bewegungssensor und einen PC verwendet, um Überwachungsvideos zu empfangen und zu speichern.

Der ESP32-CAM läuft als Video-Streaming-Server, der Videobilder über Wi-Fi überträgt, wenn ein an den ESP32-CAM angeschlossener PIR-Sensor Bewegung erkennt. Die Videobilder werden von einer Python-Anwendung auf einem PC empfangen, die die Videodaten in eine Datei schreibt. Das Bild unten zeigt die Einrichtung:

Architecture of Surveillance System
Architektur des Überwachungssystems

Bewegungserkennungsschaltung

Wir beginnen mit dem Aufbau der Bewegungserkennungsschaltung. Dabei wird der Passive Infrarotsensor (PIR) mit dem ESP32-CAM verbunden. In diesem Beispiel verwende ich den AM312 PIR-Sensor, aber der Anschluss des HC-SR501 PIR-Sensors ist im Wesentlichen gleich. Siehe das Motion Activated ESP32-CAM Tutorial.

Anschluss des PIR-Sensors an den ESP32-CAM

Der Anschluss des AM312 PIR-Sensormoduls an den ESP32-CAM ist einfach. Verbinde eine Stromversorgung (Batterie) mit 5V (bis zu 12V) mit dem 5V- und GND-Pin des ESP32-CAM wie unten gezeigt (rote und blaue Kabel).

Connecting AM312 PIR Sensor to ESP32-CAM
Anschluss AM312 an ESP32-CAM

Du kannst dem Board mehr als 5V bis zu 12V zur Stromversorgung geben. Keine Sorge. Ein Spannungsregler ist am 5V-Pin angeschlossen und reduziert die Eingangsspannung auf das vom Board benötigte Niveau.

Ebenso verbinde das AM312 PIR-Sensormodul mit der Stromversorgung (rote und blaue Kabel). Es kann ebenfalls bis zu 12V vertragen, aber achte auf die richtige Polung, da der AM312 keinen Verpolungsschutz hat. Verbinde dann den Ausgang S oder OUT des AM312 mit dem GPIO13-Pin des ESP32-CAM (grünes Kabel).

Wenn du statt des AM312 den HC-SR501 PIR-Sensormodul verwendest, verbinde ihn auf die gleiche Weise (GND zu GND, VCC zu 5V-12V).

Schaltung auf dem Breadboard

Wenn du den ESP32-CAM über das USB-TTL Shield programmierst und die Schaltung auf einem Breadboard testen möchtest, kannst du den ESP32-CAM und den AM312-Sensor wie unten gezeigt mit Strom versorgen:

Circuit on Breadboard
Schaltung auf dem Breadboard

Du musst einen kleinen Spalt zwischen dem ESP32-CAM und dem Programmier-Shield lassen und dann die Kabel an die freiliegenden Pins im Spalt anschließen:

Connect wires to pins in gap
Kabel an Pins im Spalt anschließen

Die Verbindungen sind wie zuvor. Der GND-Pin ist mit dem ‚-‚ Pin verbunden, der 5V-Pin mit dem ‚+‘ und GPIO13 ist mit dem ’s‘ Pin des AM312 verbunden.

Code zum Testen des PIR-Sensors

Bevor du den kompletten Code für den Video-Streaming-Server implementierst, testen wir den PIR-Sensor und die Schaltung. Lade den folgenden Code auf deinen ESP32-CAM hoch:

void setup() {
  Serial.begin(115200);
  pinMode(GPIO_NUM_13, INPUT);
}

void loop() {
  bool isMotion = digitalRead(GPIO_NUM_13);
  Serial.println(isMotion ? "ON" : "OFF");
  delay(1000);
}

Wenn du den Serial Monitor öffnest, solltest du den Text „ON“ sehen, wenn der PIR-Sensor Bewegung erkennt, und „OFF“ sonst. Sollte dies nicht der Fall sein, überprüfe die Schaltung. Nützliche Tipps findest du auch im Motion Activated ESP32-CAM Tutorial.

Video-Streaming

In diesem Abschnitt implementieren wir den Video-Streaming-Server auf dem ESP32-CAM-Modul, der Videobilder überträgt, wenn der PIR-Sensor Bewegung erkennt. Schau dir zuerst den kompletten Code unten an, danach besprechen wir die Details.

#include "WebServer.h"
#include "WiFi.h"
#include "esp32cam.h"

const char* WIFI_SSID = "SSID";
const char* WIFI_PASS = "PASSWORD";
const char* URL = "/video";
const auto RESOLUTION = esp32cam::Resolution::find(800, 600);
const int FRAMERATE = 10;
const byte pinPIR = GPIO_NUM_13;
const byte pinFlash = GPIO_NUM_4;

WebServer server(80);

bool isMotion() {
  return digitalRead(pinPIR);
}

void handleStream() {
  static char head[128];
  WiFiClient client = server.client();

  server.sendContent("HTTP/1.1 200 OK\r\n"
                     "Content-Type: multipart/x-mixed-replace; "
                     "boundary=frame\r\n\r\n");

  while (client.connected()) {
    if (isMotion()) {
      analogWrite(pinFlash, 20);
      auto frame = esp32cam::capture();
      if (frame) {
        sprintf(head,
                "--frame\r\n"
                "Content-Type: image/jpeg\r\n"
                "Content-Length: %ul\r\n\r\n",
                frame->size());
        client.write(head, strlen(head));
        frame->writeTo(client);
        client.write("\r\n");
        delay(1000 / FRAMERATE);
      }
    } else {
      analogWrite(pinFlash, 0);
    }
  }
  analogWrite(pinFlash, 0);
}

void initCamera() {
  using namespace esp32cam;
  Config cfg;
  cfg.setPins(pins::AiThinker);
  cfg.setResolution(RESOLUTION);
  cfg.setBufferCount(2);
  cfg.setJpeg(80);
  Camera.begin(cfg);
}

void initWifi() {
  WiFi.persistent(false);
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASS);
  while (WiFi.status() != WL_CONNECTED) {
    delay(100);
  }
  Serial.printf("Stream at: http://%s%s\n",
                WiFi.localIP().toString().c_str(), URL);
}

void initServer() {
  server.on(URL, handleStream);
  server.begin();
}

void setup() {
  Serial.begin(115200);
  pinMode(pinPIR, INPUT);
  pinMode(pinFlash, OUTPUT);
  initWifi();
  initCamera();
  initServer();
}

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

Bibliotheken

Wir beginnen mit dem Einbinden der benötigten Bibliotheken:

#include "WebServer.h"
#include "WiFi.h"
#include "esp32cam.h"

Die WebServer.h Bibliothek wird verwendet, um HTTP-Server-Funktionalitäten zu handhaben, und die WiFi.h Bibliothek ermöglicht dem ESP32 die Verbindung zu einem drahtlosen Netzwerk. Die esp32cam Bibliothek bietet eine einfache Schnittstelle zur Konfiguration und Nutzung des Kameramoduls auf dem ESP32-CAM-Board. Du musst sie über den Library Manager installieren:

esp32cam library installed via Library Manager
esp32cam-Bibliothek installiert über Library Manager

Konstanten

Als nächstes definieren wir mehrere Konstanten für die Konfiguration:

const char* WIFI_SSID = "SSID";
const char* WIFI_PASS = "PASSWORD";
const char* URL = "/video";
const auto RESOLUTION = esp32cam::Resolution::find(800, 600);
const int FRAMERATE = 10;
const byte pinPIR = GPIO_NUM_13;
const byte pinFlash = GPIO_NUM_4;

Die WIFI_SSID und WIFI_PASS Variablen speichern die Zugangsdaten für das Wi-Fi-Netzwerk. Du musst die Platzhalter SSID und PASSWORD durch die Zugangsdaten deines Wi-Fi-Netzwerks ersetzen.

Die URL Variable gibt den Pfad auf dem Server an, den Clients zum Ansehen des Videostreams aufrufen. Du kannst ihn in einen anderen Namen ändern, z.B. „/video-frontdoor“, aber stelle sicher, dass der Name auf Server (ESP32-CAM) und Client (Python-App) übereinstimmt.

Die RESOLUTION Variable setzt die Kameraauflösung auf 800 mal 600 Pixel. Auch das kannst du ändern, aber die Auflösung auf Server und Client muss identisch sein.

Du kannst auch die FRAMERATE einstellen, die die Anzahl der Bilder pro Sekunde definiert, die der Server zu streamen versucht. Wie zuvor sollten Server- und Client-Frameraten übereinstimmen.

Schließlich beziehen sich die pinPIR und pinFlash Variablen auf die GPIO-Pins, die mit dem PIR-Bewegungssensor bzw. der eingebauten Blitz-LED verbunden sind.

Objekte

Als nächstes erstellen wir einen Webserver, der auf Port 80 lauscht:

WebServer server(80);

isMotion-Funktion

Die Funktion isMotion() prüft die Bewegungserkennung:

bool isMotion() {
  return digitalRead(pinPIR);
}

Diese Funktion liest das digitale Signal vom PIR-Sensor. Gibt der Sensor ein hohes Signal aus, ist Bewegung vorhanden, und die Funktion gibt true zurück. Beachte, dass der PIR-Sensor eine Aktivierungszeit hat, die beim HC-SR501 eingestellt werden kann.

handleStream-Funktion

Die handleStream() Funktion ist verantwortlich für die Handhabung des Videostreams, wenn ein Client die /video URL aufruft:

Wir beginnen mit der Deklaration eines Puffers namens head, der HTTP-Header für jedes Bild enthält. Danach holen wir das Client-Objekt, das mit der aktuellen HTTP-Anfrage verbunden ist:

void handleStream() {
  static char head[128];
  WiFiClient client = server.client();

Der Server antwortet dem Client mit einem HTTP-Header, der angibt, dass eine Serie von JPEG-Bildern gesendet wird, getrennt durch Grenzen, die als --frame markiert sind. Diese Technik ist als MJPEG (Motion JPEG) Streaming bekannt.

  server.sendContent("HTTP/1.1 200 OK\r\n"
                     "Content-Type: multipart/x-mixed-replace; "
                     "boundary=frame\r\n\r\n");

Solange der Client verbunden bleibt, läuft die Schleife weiter. Innerhalb der Schleife rufen wir die isMotion() Funktion auf, um zu prüfen, ob Bewegung erkannt wird:

  while (client.connected()) {
    if (isMotion()) {
      analogWrite(pinFlash, 20);

Wenn ja, schalten wir die Blitz-LED mit niedriger Helligkeit per PWM ein, indem wir einen Duty Cycle von 20 an pinFlash schreiben. Sei vorsichtig mit der Helligkeit des Blitzes. Wenn du den Blitz längere Zeit auf volle Helligkeit (255) setzt, kann die LED durchbrennen!

Dann versuchen wir, ein Bild von der Kamera mit esp32cam::capture() zu erfassen:

      auto frame = esp32cam::capture();

Wenn ein Bild erfolgreich aufgenommen wurde, bereiten wir die HTTP-Header für das Bild vor:

      if (frame) {
        sprintf(head,
                "--frame\r\n"
                "Content-Type: image/jpeg\r\n"
                "Content-Length: %ul\r\n\r\n",
                frame->size());
        client.write(head, strlen(head));
        frame->writeTo(client);
        client.write("\r\n");
        delay(1000 / FRAMERATE);
      }

Die sprintf() Funktion formatiert einen String mit dem Grenzmarker, dem Content-Type und der Content-Length des Bildes. Dieser Header wird mit client.write() an den Client gesendet. Das eigentliche JPEG-Bild wird mit frame->writeTo(client) gesendet, gefolgt von einem Wagenrücklauf und Zeilenumbruch. Eine kurze Verzögerung basierend auf der gewünschten Bildrate steuert die Streaming-Geschwindigkeit.

    } else {
      analogWrite(pinFlash, 0);
    }
  }
  analogWrite(pinFlash, 0);
}

Wenn keine Bewegung erkannt wird, schalten wir die Blitz-LED aus. Nach dem Verlassen der Schleife, was passiert, wenn der Client die Verbindung trennt, schalten wir den Blitz erneut aus, um sicherzugehen, dass er nicht an bleibt.

initCamera-Funktion

Die initCamera() Funktion initialisiert die ESP32-CAM-Hardware:

void initCamera() {
  using namespace esp32cam;
  Config cfg;
  cfg.setPins(pins::AiThinker);
  cfg.setResolution(RESOLUTION);
  cfg.setBufferCount(2);
  cfg.setJpeg(80);
  Camera.begin(cfg);
}

Innerhalb dieser Funktion wird ein Config Objekt namens cfg erstellt. Die setPins() Methode wird mit pins::AiThinker aufgerufen, um die GPIO-Pins für das AI Thinker Modell des ESP32-CAM zu konfigurieren. Du kannst den Code auch für andere Kameraboards verwenden, indem du eine andere Pin-Konfiguration wählst. Siehe z.B. das Stream Video with with XIAO-ESP32-S3-Sense und das Stream Video with ESP32-WROVER CAM.

Die Auflösung wird mit dem zuvor definierten Wert gesetzt. Die Pufferanzahl wird auf 2 gesetzt, was Double Buffering ermöglicht. Die JPEG-Kompressionsqualität wird auf 80% gesetzt. Schließlich wird die Kamera mit Camera.begin(cfg) gestartet.

initWifi-Funktion

Die Wi-Fi-Initialisierung erfolgt in der initWifi() Funktion:

void initWifi() {
  WiFi.persistent(false);
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASS);
  while (WiFi.status() != WL_CONNECTED) {
    delay(100);
  }
  Serial.printf("Stream at: http://%s%s\n",
                WiFi.localIP().toString().c_str(), URL);
}

Wi-Fi-Persistenz wird deaktiviert, um das Speichern der Zugangsdaten im Flash zu vermeiden. Der ESP32 wird in den Station-Modus versetzt und beginnt, sich mit dem angegebenen Wi-Fi-Netzwerk zu verbinden. Die Schleife wartet, bis die Verbindung hergestellt ist. Nach der Verbindung wird die Stream-URL im Serial Monitor ausgegeben:

Stream at: http://192.168.2.42/video

Du musst diese URL in der Client-Anwendung (Python-App) verwenden, die den Videostream in eine Datei aufzeichnet.

Beachte, dass du das Video-Streaming testen kannst, indem du diese URL in die Adresszeile deines Webbrowsers einfügst. Aber verwende entweder die Client-App ODER den Webbrowser, aber nicht beide gleichzeitig, da nur ein Client zur gleichen Zeit eine Verbindung zum Stream herstellen kann.

initServer-Funktion

Der HTTP-Server wird in der initServer() Funktion eingerichtet:

void initServer() {
  server.on(URL, handleStream);
  server.begin();
}

Diese Funktion registriert den /video Endpunkt mit der handleStream() Funktion und startet den Server.

setup-Funktion

Die setup() Funktion bereitet den ESP32-CAM für den Betrieb vor:

void setup() {
  Serial.begin(115200);
  pinMode(pinPIR, INPUT);
  pinMode(pinFlash, OUTPUT);
  initWifi();
  initCamera();
  initServer();
}

Die serielle Kommunikation wird mit einer Baudrate von 115200 initialisiert. Der PIR-Sensor-Pin wird als Eingang konfiguriert, und der Blitz-LED-Pin als Ausgang. Die Wi-Fi-Verbindung, Kamerakonfiguration und der Webserver werden der Reihe nach initialisiert.

loop-Funktion

Schließlich enthält die loop() Funktion die Hauptlaufzeitlogik:

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

Diese Funktion ruft kontinuierlich server.handleClient() auf, um eingehende HTTP-Anfragen von Clients zu bearbeiten.

Und das ist die Serverseite. Im nächsten Abschnitt implementieren wir den Client, der den Videostream empfängt und in eine Datei speichert.

Videoaufnahme

Der Client ist als Python-Skript implementiert, das sich mit dem MJPEG-Stream des ESP32-CAM verbindet und das eingehende Video in eine lokale .avi Datei schreibt. Die Aufnahme wird automatisch in tägliche Dateien segmentiert – eine pro Kalendertag. Das Skript zeigt optional auch den Live-Stream in einem Fenster an.

Unten ist der komplette Code. Schau ihn dir zuerst kurz an, dann gehen wir auf die Details ein:

import cv2
import requests
import numpy as np
import datetime
import os

# Replace with your ESP32-CAM stream URL
stream_url = 'http://192.168.2.42/video' 

# match your ESP32-CAM setting
frame_width = 800  
frame_height = 600
fps = 10
fourcc = cv2.VideoWriter_fourcc(*'XVID')

def get_output_filename():
    date_str = datetime.datetime.now().strftime("%Y-%m-%d")
    return f'recording_{date_str}.avi'

# Initialize variables
current_date = datetime.date.today()
output_file = get_output_filename()
out = cv2.VideoWriter(output_file, fourcc, fps, (frame_width, frame_height))

# Connect to MJPEG stream
print(f"Connecting to {stream_url}")
stream = requests.get(stream_url, stream=True)

if stream.status_code != 200:
    print(f"Failed to connect to ESP32-CAM. Status code: {stream.status_code}")
    exit()

bytes_buffer = b''
try:
    for chunk in stream.iter_content(chunk_size=1024):
        bytes_buffer += chunk
        a = bytes_buffer.find(b'\xff\xd8')  # JPEG start
        b = bytes_buffer.find(b'\xff\xd9')  # JPEG end
        if a != -1 and b != -1 and b > a:
            jpg = bytes_buffer[a:b+2]
            bytes_buffer = bytes_buffer[b+2:]

            # Decode the JPEG image to OpenCV format
            img_array = np.frombuffer(jpg, dtype=np.uint8)
            frame = cv2.imdecode(img_array, cv2.IMREAD_COLOR)

            if frame is not None:
                new_date = datetime.date.today()
                if new_date != current_date:
                    out.release()
                    current_date = new_date
                    output_file = get_output_filename()
                    out = cv2.VideoWriter(output_file, fourcc, fps, (frame_width, frame_height))
                    print(f"Started new file: {output_file}")

                out.write(frame)

                # Optional: Show live video
                cv2.imshow('ESP32-CAM Stream', frame)
                if cv2.waitKey(1) & 0xFF == ord('q'):
                    break
except KeyboardInterrupt:
    pass
finally:
    out.release()
    cv2.destroyAllWindows()
    print(f"Video saved to {output_file}")

Bibliotheken

Das Skript beginnt mit dem Import der notwendigen Bibliotheken:

import cv2
import requests
import numpy as np
import datetime
import os

Das cv2 Modul (OpenCV) wird für Bild- und Videooperationen verwendet. Die requests Bibliothek ermöglicht dem Skript, eine Verbindung zum HTTP-MJPEG-Stream des ESP32-CAM herzustellen. Die numpy Bibliothek wird verwendet, um rohe Byte-Daten in ein Bildarray umzuwandeln. Das datetime Modul verwaltet Zeit und Datum, insbesondere für die Organisation der täglichen Videodateien. Schließlich wird os für mögliche Dateipfadmanipulationen importiert, obwohl es in diesem Skript nicht verwendet wird.

Du musst die cv2, requests und numpy Bibliotheken auf deinem Computer installieren, vorzugsweise in einer virtuellen Umgebung. Siehe das Object Detection with ESP32-CAM and YOLO für ein Beispiel mit mehr Details.

Konstanten

Die IP-Adresse des ESP32-CAM ist in der stream_url Variable definiert:

stream_url = 'http://192.168.2.42/video'

Diese URL entspricht dem im Arduino-Sketch definierten Endpunkt (genauer const char* URL = "/video"). Der ESP32-CAM muss unter dieser IP-Adresse erreichbar sein, damit das Skript funktioniert.

Die folgenden Konstanten definieren die Kameraauflösung und Aufnahme-Einstellungen:

frame_width = 800  
frame_height = 600
fps = 10
fourcc = cv2.VideoWriter_fourcc(*'XVID')

Die frame_width und frame_height Werte müssen mit der im ESP32-CAM gesetzten Auflösung (esp32cam::Resolution::find(800, 600)) übereinstimmen. Die fps Variable gibt die Bildrate an, die mit der FRAMERATE Einstellung des ESP32-CAM übereinstimmen sollte. Die fourcc Variable definiert das Video-Codec-Format; hier wird XVID verwendet, ein gängiger Codec für .avi Dateien.

get_output_filename-Funktion

Als nächstes definieren wir eine Hilfsfunktion, um Dateinamen basierend auf dem aktuellen Datum zu generieren:

def get_output_filename():
    date_str = datetime.datetime.now().strftime("%Y-%m-%d")
    return f'recording_{date_str}.avi'

Diese Funktion verwendet das datetime Modul, um einen String im Format YYYY-MM-DD zu erstellen, der dann als Name für die Ausgabedatei verwendet wird, z.B. recording_2025-06-01. So wird jede Aufnahme in einer Datei gespeichert, die nach dem Aufnahmedatum benannt ist, und die Dateigröße bleibt begrenzt. Sonst könntest du eine riesige Videodatei bekommen, wenn die Kamera häufig aktiviert wird.

Variablen

Das Skript initialisiert dann Variablen zur Verwaltung der täglichen Video-Segmentierung:

current_date = datetime.date.today()
output_file = get_output_filename()
out = cv2.VideoWriter(output_file, fourcc, fps, (frame_width, frame_height))

Die current_date Variable speichert das heutige Datum. Die output_file Variable hält den von get_output_filename() generierten Dateinamen. Das out Objekt ist ein OpenCV VideoWriter, der verwendet wird, um einzelne Frames in die .avi Datei zu schreiben.

Verbindung zum Stream

Als nächstes verbindet sich das Skript mit dem Videostream vom ESP32-CAM:

print(f"Connecting to {stream_url}")
stream = requests.get(stream_url, stream=True)

Eine GET-Anfrage wird an den ESP32-CAM gesendet. Der stream=True Parameter sorgt dafür, dass die Antwort als Streaming-Antwort behandelt wird, sodass Daten in Blöcken empfangen werden können.

Status prüfen

Unmittelbar nach dem Verbindungsversuch prüfen wir den Antwortstatus:

if stream.status_code != 200:
    print(f"Failed to connect to ESP32-CAM. Status code: {stream.status_code}")
    exit()

Wenn der HTTP-Statuscode nicht 200 (OK) ist, gibt das Skript eine Fehlermeldung aus und beendet sich. In diesem Fall überprüfe, ob der ESP32-CAM läuft, mit dem Wi-Fi verbunden ist und die URL sowie IP-Adresse für Server und Client übereinstimmen, z.B. http://192.168.2.42/video

Puffer

Das Skript initialisiert dann einen leeren Byte-Puffer, um eingehende Daten vom Stream zu speichern:

bytes_buffer = b''

Dieser Puffer wird verwendet, um Streaming-Daten zu sammeln und einzelne JPEG-Bilder daraus zu extrahieren.

Hauptschleife

Die Hauptschleife beginnt in einem try Block:

try:
    for chunk in stream.iter_content(chunk_size=1024):
        bytes_buffer += chunk

Das Skript liest den Stream in Blöcken von 1024 Bytes und fügt jeden Block an bytes_buffer an. Dieser Ansatz behandelt den Stream als eine kontinuierliche Folge von Bytes.

Innerhalb der Schleife sucht das Skript nach Start- und Endmarkern eines JPEG-Bildes:

        a = bytes_buffer.find(b'\xff\xd8')  # JPEG start
        b = bytes_buffer.find(b'\xff\xd9')  # JPEG end

Das JPEG-Format beginnt mit der Bytefolge 0xFF 0xD8 und endet mit 0xFF 0xD9. Diese Marker ermöglichen es dem Skript, vollständige JPEG-Bilder aus dem Bytestrom zu isolieren.

Wenn beide Marker gefunden und korrekt angeordnet sind, extrahiert das Skript das JPEG-Bild:

        if a != -1 and b != -1 and b > a:
            jpg = bytes_buffer[a:b+2]
            bytes_buffer = bytes_buffer[b+2:]

Der Ausschnitt jpg enthält das rohe JPEG-Bild. Der verarbeitete Teil wird aus dem Puffer entfernt, damit das Skript das nächste Bild verarbeiten kann.

Die JPEG-Daten werden dann in ein OpenCV-Bild umgewandelt:

            img_array = np.frombuffer(jpg, dtype=np.uint8)
            frame = cv2.imdecode(img_array, cv2.IMREAD_COLOR)

Die rohen Bytes werden in ein NumPy-Array von 8-Bit-Unsigned-Integern umgewandelt. Dieses Array wird mit cv2.imdecode() in ein tatsächliches Bildframe dekodiert, das angezeigt oder gespeichert werden kann.

Wenn ein gültiges Frame erhalten wird, prüft das Skript, ob sich das Datum geändert hat:

            if frame is not None:
                new_date = datetime.date.today()
                if new_date != current_date:
                    out.release()
                    current_date = new_date
                    output_file = get_output_filename()
                    out = cv2.VideoWriter(output_file, fourcc, fps, (frame_width, frame_height))
                    print(f"Started new file: {output_file}")

Es vergleicht das aktuelle Datum mit dem zuvor gespeicherten Datum. Wenn ein neuer Tag begonnen hat, wird die aktuelle Videodatei mit out.release() geschlossen. Ein neuer Dateiname wird generiert und ein neuer VideoWriter wird initialisiert, um in die neue Datei aufzunehmen.

Das aktuelle Frame wird dann in das Ausgabevideo geschrieben:

                out.write(frame)

Zusätzlich zeigt das Skript das Video in einem Echtzeit-Vorschaufenster an:

                cv2.imshow('ESP32-CAM Stream', frame)
                if cv2.waitKey(1) & 0xFF == ord('q'):
                    break

Das Frame wird mit der OpenCV-Funktion imshow() angezeigt. Wenn der Benutzer die Taste ‚q‘ drückt, wird die Schleife beendet und der Stream gestoppt.

Fehlerbehandlung

Ebenso sorgt das Skript im finally Block dafür, dass bei einem Keyboard-Interrupt, z.B. mit Ctrl+C, die Ausgabedatei und das OpenCV-Fenster sauber geschlossen werden:

except KeyboardInterrupt:
    pass
finally:
    out.release()
    cv2.destroyAllWindows()
    print(f"Video saved to {output_file}")

Und das ist der Server! Wenn du jetzt den ESP32-CAM (Server) startest und dann den Client (Python-App) ausführst, hast du ein Überwachungssystem, das Video aufnimmt, solange Bewegung erkannt wird, und den Videostream in eine datumsmarkierte Datei für die spätere Ansicht schreibt.

Fazit

In diesem Tutorial hast du gelernt, wie du ein bewegungsaktiviertes Überwachungskamerasystem mit dem ESP32-CAM, einem PIR-Bewegungssensor und einem PC baust.

Der ESP32-CAM fungiert als motion-activated MJPEG-Video streaming server. Wenn ein Client die /video URL besucht, beginnt der Server, Videobilder zu streamen. Allerdings werden Bilder nur aufgenommen und gesendet, wenn Bewegung vom PIR-Sensor erkannt wird. Für mehr Informationen zur Bewegungserkennung siehe das Motion Activated ESP32-CAM Tutorial.

Zusätzlich zur Aktivierung des Videostreams wird die Blitz-LED eingeschaltet, um bei der Beleuchtung zu helfen. Wenn du Probleme hast, die Blitz-LED zu steuern, schau dir das Control ESP32-CAM Flash LED Tutorial an.

Auf der Client-Seite empfängt ein Python-Skript den bewegungsgesteuerten MJPEG-Videostream vom ESP32-CAM und schreibt jedes Frame in eine lokale .avi Datei. Es wird jeden Kalendertag eine neue Datei erstellt, was die Organisation und Archivierung des Materials erleichtert. Das Skript zeigt optional auch den Live-Video-Feed und kann vom Benutzer sauber unterbrochen werden.

Wenn du benachrichtigt werden möchtest, sobald Bewegung erkannt wird, schau dir unser ESP32 send Telegram Message Tutorial an, das dir zeigt, wie du Nachrichten an die Telegram-App auf deinem Telefon sendest.

Wenn du Objekte im Videostream erkennen möchtest, schau dir das Object Detection with ESP32-CAM and YOLO Tutorial an. Und wenn du mehr GPIO-Pins für andere Ein- oder Ausgänge brauchst, hat das More GPIO pins for ESP32-CAM vielleicht nützliche Tipps.

Hinterlasse gerne deine Fragen im Kommentarbereich.

Viel Spaß beim Tüfteln ; )