Forum: PC-Programmierung Mjpeg multipart streaming. Wie geht das?


von Hadmut F. (hadmut)


Lesenswert?

Ich habe fortlaufende jpeg bilder die ich an einen browser streamen 
will. Kann mir jemand erklären welche header und was noch da gesendet 
werden muss?

Ich kenne den esp32-webcam code. Dieser läuft aber mit httpd und nicht 
mit dem esp32 <WebServer.h>. Und ich kapier das multipart zeugs nicht.

Einzelbilder läuft
1
void capture_handler()
2
{
3
    fb = esp_camera_fb_get();
4
    if (fb) 
5
    {
6
      if(fb->format == PIXFORMAT_JPEG) 
7
      {
8
        s_server.send(200, "image/jpg",String(fb->buf, fb->len));  
9
        esp_camera_fb_return(fb);
10
      }
11
    }
12
    else Serial.println("Camera capture failed");
13
}

Aber streamen bring ich nicht hin. Hat jemand einen link?

: Bearbeitet durch User
von Hadmut F. (hadmut)


Lesenswert?

Ich habs mir mal rausgesucht. Das müsste so sen:
1
httpd_resp_set_type(req, "multipart/x-mixed-replace;boundary=123456789000000000000987654321");
2
3
LOOP:
4
fb = esp_camera_fb_get();
5
6
httpd_resp_send_chunk(req, "Content-Type: image/jpeg\r\nContent-Length: xxxx\r\n\r\n"); //xxxx=_jpg_buf_len
7
8
httpd_resp_send_chunk(req, (const char *)_jpg_buf, _jpg_buf_len);
9
10
strlen(_STREAM_BOUNDARY));
11
httpd_resp_send_chunk(req, "\r\n--123456789000000000000987654321\r\n");

Was ich nicht verstehe ist das "httpd_resp_set_type". Wie heisst die 
entsprechende funktion im esp32 "webserver.h" ?

: Bearbeitet durch User
von Εrnst B. (ernst)


Lesenswert?

Hadmut F. schrieb:
> httpd_resp_set_type
setzt wohl nur den Content-Type.

also das, was bei dir:
> s_server.send(200, "image/jpg"

"image/jpg"
ist.

von Hadmut F. (hadmut)


Lesenswert?

Ich komm da nicht weiter.
Das chrome "seitenquelltext anzeigen" zu untersuchen läuft dabei auch 
nicht.

von Εrnst B. (ernst)


Lesenswert?

Erstmal schauen, ob dein Browser das überhaupt noch unterstützt.

Chromium, die OpenSource-Basis von Chrome, wollte das schon vor über 10 
Jahren aus dem Browser schmeißen:

https://issues.chromium.org/issues/41017445

Firefox könnte das vielleicht noch...


Nachtrag: Chrome kann das noch, wenn du den Bilder-Stream nicht toplevel 
aufmachst, sondern z.B. in einem img-tag als src angibst.

: Bearbeitet durch User
von Hadmut F. (hadmut)


Lesenswert?

Ist kein browser problem. Chrom macht multipart/x-mixed-replace; bei jpg 
bildern.
Der code läuft:
1
#define PART_BOUNDARY "123456789000000000000987654321"
2
static const char* _STREAM_CONTENT_TYPE = "multipart/x-mixed-replace;boundary=" PART_BOUNDARY;
3
static const char* _STREAM_BOUNDARY = "\r\n--" PART_BOUNDARY "\r\n";
4
static const char* _STREAM_PART = "Content-Type: image/jpeg\r\nContent-Length: %u\r\n\r\n";
5
6
  res = httpd_resp_set_type(req, _STREAM_CONTENT_TYPE);
7
 
8
  while(true)
9
  {    
10
    delay(10);
11
    fb = esp_camera_fb_get();
12
    if (!fb) res = ESP_FAIL;
13
     if(res == ESP_OK)
14
    {      
15
      size_t hlen = snprintf((char *)part_buf, 64, _STREAM_PART, fb->len);
16
      res = httpd_resp_send_chunk(req, (const char *)part_buf, hlen);
17
    }
18
    if(res == ESP_OK){
19
      res = httpd_resp_send_chunk(req, (const char *)fb->buf, fb->len);
20
    }
21
    if(res == ESP_OK){
22
      res = httpd_resp_send_chunk(req, _STREAM_BOUNDARY, strlen(_STREAM_BOUNDARY));
23
    }
24
    if(fb)
25
    {
26
      len = fb->len;
27
      esp_camera_fb_return(fb);
28
      fb = NULL;
29
    } 
30
    if(res != ESP_OK) break;
31
  }
32
  return res;

Ich möchte das nicht mit httpd server sondern mit
https://github.com/espressif/arduino-esp32/blob/master/libraries/WebServer/src/WebServer.h

also mit den  funktionen machen:
1
  void send(int code, const char* content_type = NULL, const String& content = String(""));
2
  void send(int code, char* content_type, const String& content);
3
  void send(int code, const String& content_type, const String& content);
4
  void send(int code, const char* content_type, const char* content);
5
6
  void setContentLength(const size_t contentLength);
7
  void sendHeader(const String& name, const String& value, bool first = false);
8
  void sendContent(const String& content);
9
  void sendContent(const char* content, size_t contentLength);

Grund is httpd erlaubt mir keine UDP kommunikation nebenher zu fahren.

Eine beschreibung unterschied send - sendContent - sendHeader habe ich 
nicht gefunden.

: Bearbeitet durch User
von Hadmut F. (hadmut)


Lesenswert?

Meiner analyse nach macht der laufende code:
1
httpd_resp_set_type(req, "multipart/x-mixed-replace;boundary=123456789000000000000987654321");
2
3
LOOP
4
// get picture
5
fb = esp_camera_fb_get();
6
7
// set length, xxxx = length
8
res = httpd_resp_send_chunk(req, "Content-Type: image/jpeg\r\nContent-Length: xxxx\r\n\r\n"); 
9
10
// send data
11
httpd_resp_send_chunk(req, (const char *)_jpg_buf, _jpg_buf_len);
12
13
// boundary to next picture
14
httpd_resp_send_chunk(req, "\r\n--123456789000000000000987654321\r\n");
15
ENDLOOP

: Bearbeitet durch User
von Frank E. (Firma: Q3) (qualidat)


Lesenswert?

Man kann das Auffrischen eines Bildes (erneutes Abrufen), ohne die ganze 
Seite neu zu laden, auch per Javascript (eingebettet in die Webseite) 
vom Browser machen lassen ...

von Hadmut F. (hadmut)


Lesenswert?

Denke ich habs gefunden.
Mit "WiFiServer server(80);"

Erstes mal:
1
  client->println("HTTP/1.1 200 OK");
2
  client->printf("Content-Type: %s\r\n", _STREAM_CONTENT_TYPE);
3
  client->println("Content-Disposition: inline; filename=capture.jpg");
4
  client->println("Access-Control-Allow-Origin: *");
5
  client->println();
Dann dauerschleife bild mit:
1
  client->print(_STREAM_BOUNDARY);
2
  client->printf(_STREAM_PART, fb);
3
  client->write(out_buf, len) // bilddaten

: Bearbeitet durch User
von Hadmut F. (hadmut)


Angehängte Dateien:

Lesenswert?

JA! Läuft. Bild über http-streaming, fernsteuerung über UDP. Alles auf 
einem ESP32.
Kater lässt sich durch die RC karre nicht mehr stören :o)
Das ohr, das war eine elster.

: Bearbeitet durch User
von Enrico E. (pussy_brauser)


Angehängte Dateien:

Lesenswert?

Hadmut F. schrieb:
> Das Ohr, das war eine Elster.

Das sieht aber eher aus wie mit einer Schere schön gerade abgeschnitten. 
Eltern haben normalerweise keine Zähne!

von Hadmut F. (hadmut)


Lesenswert?

Falls das interessiert, hier der ganze streaming code.
1
void stream_handler()
2
{
3
  WiFiClient wfc = s_server.client();
4
  String out;
5
6
  Serial.println("Image stream start");
7
  out  = "HTTP/1.1 200 OK\r\n";
8
  out += "Content-Type: ";
9
  out += "multipart/x-mixed-replace;boundary="; 
10
  out += "123456789000000000000987654321\r\n";
11
  out += "Content-Disposition: inline; filename=capture.jpg\r\n";
12
  out += "Access-Control-Allow-Origin: *\r\n"; 
13
  out += "\r\n";
14
     
15
  Serial.println(out);  
16
  s_server.sendContent(out);
17
18
  while(wfc.connected())
19
  {
20
    delay(100);
21
    fb = esp_camera_fb_get();
22
    if (fb) 
23
    {
24
25
      out  = "\r\n--123456789000000000000987654321\r\n";
26
      out += "Content-Type: image/jpeg\r\nContent-Length: ";
27
      out += fb->len;
28
      out += "\r\n\r\n";    
29
      s_server.sendContent(out);
30
31
      s_server.sendContent(String(fb->buf, fb->len)); 
32
       
33
      esp_camera_fb_return(fb);
34
    }
35
    else break;
36
  } 
37
  Serial.println("Disc"); 
38
}

von Εrnst B. (ernst)


Lesenswert?

Schön dass es läuft. Du umgehst halt die Webserver-Klasse komplett und 
machst alles zu Fuß selber. Kann man machen.

Zwei Anmerkungen:

UDP Parallel dazu geht so auch nicht, das war doch der Grund für die 
ganze Übung?
Du steckst immer in der "while connected" Schleife fest.

und:

> s_server.sendContent(...

Das Senden direkt auf dem s_server fliegt in einer der nächsten 
Versionen des ESP32-Arduino-Frameworks raus. War nie klar definiert, auf 
welcher Verbindung (oder ob auf Allen) das senden sollte.
Besser: mach die Ausgaben auf "wfc", dafür ist das da, und damit ist 
auch klar welche Verbindung welche Daten erhält.

Und statt sendContent(String(fb->buf, fb->len)) gäb's wfc.write(const 
uint8_t *buf, size_t size) das kommt ohne Kopieren des Framebuffers in 
einen String aus, sondern kann direkt auf dem fb arbeiten.

von Hadmut F. (hadmut)


Lesenswert?

Εrnst B. schrieb:
> UDP Parallel dazu geht so auch nicht, das war doch der Grund für die
> ganze Übung?

Da läuft der bildserver auf port 81, der normale server auf port 80 und 
udp auf port 5005. Kein problem.

Die version mit #include <WebServer.h> hat den vorteil dass arduino OTA 
läuft. Das habe ich bei "esp_http_server.h / httpd" und "<WiFi.h> / 
WiFiServer server(80);" nicht hingebracht.

> Du steckst immer in der "while connected" Schleife fest.

Das muss so sein. Den loop kann man auseinander nehmen und dazwischen 
anderes machen. Der muss nicht im handler code sein. Das ist test-code!

> Besser: mach die Ausgaben auf "wfc", dafür ist das da, und damit ist
> auch klar welche Verbindung welche Daten erhält.

Ich habe gestern erstmals etwas von "wfc" gehört. etwas geduld bitte!

> Und statt sendContent(String(fb->buf, fb->len)) gäb's wfc.write(const
> uint8_t *buf, size_t size) das kommt ohne Kopieren des Framebuffers in
> einen String aus, sondern kann direkt auf dem fb arbeiten.

Werde ich anschauen. TY

Edit:
1
      //s_server.sendContent(String(fb->buf, fb->len)); 
2
      wfc.write(fb->buf, fb->len);
Geht.

: Bearbeitet durch User
von Hadmut F. (hadmut)


Lesenswert?

Εrnst B. schrieb:
> UDP Parallel dazu geht so auch nicht,

Du hast recht.
Müsste den udp-code aus dem stream handler aufrufen. Hässlich.

Die frage ist wie mache ich OTA mit WiFiClient.h ?

: Bearbeitet durch User
von Εrnst B. (ernst)


Lesenswert?

Hadmut F. schrieb:
> Ich habe gestern erstmals etwas von "wfc" gehört. etwas geduld bitte!

Hadmut F. schrieb:
> WiFiClient wfc = s_server.client();

den meinte ich :)

von Hadmut F. (hadmut)


Lesenswert?

Εrnst B. schrieb:
> den meinte ich :)

Schon klar. Der ist von gestern :)

Wo ist eigentlich der dispatcher von WiFiClient auf die verschiedenen 
WebServer.h server.on("/", handler); zu finden?
Den http decoder von WiFiClient.read() meine ich.

von Εrnst B. (ernst)


Lesenswert?

Hadmut F. schrieb:
> Wo ist eigentlich der dispatcher von WiFiClient auf die verschiedenen
> WebServer.h server.on("/", handler); zu finden?

Der Webserver hat eine Linked List der Handler, die "on()"-Methoden 
hängen da einen Eintrag an.

In Parsing.cpp (WebServer::_parseRequest) läuft er, nachdem die 
Request-Header geparst sind, über die Liste, nimmt sich den ersten 
Handler der sich zuständig fühlt:
1
  RequestHandler* handler;
2
  for (handler = _firstHandler; handler; handler = handler->next()) {
3
    if (handler->canHandle(_currentMethod, _currentUri))
4
      break;
5
  }

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.