IP-Webcam Recorder

Sockets, TCP/IP, (XML-)RPC und ähnliche Themen gehören in dieses Forum
Antworten
droptix
User
Beiträge: 521
Registriert: Donnerstag 13. Oktober 2005, 21:27

Habe eine IP-Webcam, die gleichzeitig als Webserver fungiert und auf Port 80 das aktuelle Bild bereitstellt. In Firefox gebe ich einfach die IP-Adresse der Webcam ein und schon wird mir das Bild angezeigt und ständig aktualisiert. Problem: es handelt sich um ein M-JPEG Stream und kein "normales" Bild. Hat jemand eine Idee, wie man aus dem Stream aller 5 Sekunden ein JPEG-Bild auf Festplatte abspeichern kann?

Ich kann mit `f = urllib.request.urlopen("http://192.168.0.123")` verbinden, aber sobald ich `f.read()` probiere, hängt Python bis ich manuell abbreche oder die IP-Webcam abschalte (Firefox übrigens auch, wenn ich per Rechtsklick das angezeigte Bild speichern möchte).

Kann ich aus dem HTTP-Header vielleicht die Bytelänge des Einzelbilds extrahieren und mit `f.read(byteLength)` lesen oder ähnlich?

Ich habe das noch nie gemacht, daher die Frage wie man an diese Info heran kommt?
Dauerbaustelle
User
Beiträge: 996
Registriert: Mittwoch 9. Januar 2008, 13:48

Zeig doch mal die Header. Auf Linux (etc.) z.b. mit:

Code: Alles auswählen

wget -O /dev/null -S -q IP
oder

Code: Alles auswählen

curl -o /dev/null -v IP
Ansonsten gibts noch das Live HTTP Headers Addon für Firefox.
droptix
User
Beiträge: 521
Registriert: Donnerstag 13. Oktober 2005, 21:27

Firefox Live HTTP Headers zeigt:

Code: Alles auswählen

GET / HTTP/1.1
Host: 192.168.0.123
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.2.13) Gecko/20101203 Firefox/3.6.13 ( .NET CLR 3.5.30729; .NET4.0C)
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: de-de,de;q=0.8,en-us;q=0.5,en;q=0.3
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 115
Connection: keep-alive

HTTP/1.1 200 OK
Server: MiniWebCam/1.0 (iOS)
Pragma: no-cache
Content-Type: multipart/x-mixed-replace;boundary=WebCamImage
----------------------------------------------------------
Das Gemeine ist die letzte Zeile: `Content-Type: multipart/x-mixed-replace`: die sorgt dafür, dass Firefox ständig das Webcam-Bild neu abruft. Die TCP-Verbindung bleibt laut Wikipedia die ganze Zeit bestehen:
The TCP connection is not closed as long as the client wants to receive new frames and the server wants to provide new frames.
Dauerbaustelle
User
Beiträge: 996
Registriert: Mittwoch 9. Januar 2008, 13:48

Das ist ja eklig. Hast du mal das hier ausprobiert: http://code.google.com/p/mjpeg-stream-client/?
fsck
User
Beiträge: 5
Registriert: Dienstag 4. Januar 2011, 22:13

Im Stream folgt nach dem Boundary String normalerweise noch ein HTTP-Header mit der Content-Length. Folgender Code funktioniert zum Beispiel für den MJPEG-Stream von motion:

Code: Alles auswählen

def grab_image(addr):
    s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    s.connect(addr)
    f = s.makefile('r')
    length = 0
    while True:
        line = f.readline()
        if line.startswith('Content-Length:'):
            length = int(line.split(':')[1])
        if length and line == '\r\n':
            break
    image = f.read(length)
    f.close()
    s.close()
    return image
droptix
User
Beiträge: 521
Registriert: Donnerstag 13. Oktober 2005, 21:27

Dauerbaustelle hat geschrieben:Das ist ja eklig. Hast du mal das hier ausprobiert: http://code.google.com/p/mjpeg-stream-client/?
Da ist nix zum Download, nur der Verweis auf ein C-Projekt. Projekt scheint inaktiv zu sein.
-> ich habe natürlich vorab sehr lange nach M-JPEG Stream-Recordern gesucht, nur nichts Passendes gefunden. Habe auch einiges getestet, mit demselben Misserfolg, z.B. webcamXP.

@fsck: Das Programm bleibt bei mir ewig stehen, hängt in der while-Schleife. Gibt's da kein Content-lenght? Das ist ja seltsam.

P.S. Hab nach `line=...` mal `print(line)` eingefügt, passiert nix. Auch bei `print(1)` nicht -> muss an `f.readline()` liegen. Ja, wenn ich als erstes in der While-Schleife ein `print` einbaue, kommt das nur einmal und hängt dann. Komisch, oder?
BlackJack

@droptix: Das ist ja ein Webserver, also musst Du auch eine HTTP-Anfrage stellen und nicht einfach lesen und hoffen da kommt schon irgend was.
Dauerbaustelle
User
Beiträge: 996
Registriert: Mittwoch 9. Januar 2008, 13:48

Fang doch mal so an: Lies die Antwort einfach Byte für Byte aus, bis das Ende eines JPEGs kommt. Wenn das stabil funktioniert, kannst du ja einen intelligentern Parser bauen, der vielleicht mehr Bytes auf einmal liest.
droptix
User
Beiträge: 521
Registriert: Donnerstag 13. Oktober 2005, 21:27

Siehe ganz oben: `f = urllib.request.urlopen("http://192.168.0.123")` funzt noch, aber `f.read()` kehrt nicht zurück, hängt also. Das wäre dann eine HTTP-Anfrage, die keine ordentliche HTTP-Antwort zu liefern scheint. Oder urrlib kommt damit nicht klar, so wie Internet Explorer das auch nicht kann. M-JPEG Streams scheinen was Besonderes auf der Client-Seite zu benötigen...

Ich glaube das liegt zusätzlich auch am Webserver. Ist schon komisch, wenn keiner der üblichen Stream Recorder (u.a. soll auch VLC dafür geeignet sein, den Stream einer M-JPEG Webcam aufzunehmen) funtkioniert.
BlackJack

@droptix: Wenn Du sagst `read()` kehrt nicht zurück, meinst Du denn wirklich ``read()`` ohne ein Argument? Dass *das* nicht zurückkehrt, sollte klar sein, denn das kehrt ja erst zurück wenn *alles* gelesen wurde. Die Kamera sendet ja aber "unendlich" Bilder.

Und wenn ich das richtig verstanden habe handelt es sich ja auch nicht um einen Videostream wie beispielsweise MPEG, sondern "einfach" um eine HTTP-Multipart-Antwort, die aus einzelnen JPEG-Bildern besteht, die von der Kamera in regelmässigen Abständen gesendet werden.

Die Kamera wird nach folgendem Muster senden:

<Der Header den Du weiter oben gezeigt hast>
<Header für Bild>
<Bilddaten>
WebCamImage
<Header für Bild>
<Bilddaten>
WebCamImage
...

Und das müsstest Du auf der Empfängerseite verarbeiten. Der Browser fragt nicht regelmässig nach neuen Bildern, sondern verarbeitet *eine* "unendlich" grosse Antwort. Die Verzögerung kommt von der Kamera, die nach jedem gesendeten Bild eine Pause einlegt.
Barabbas
User
Beiträge: 349
Registriert: Dienstag 4. März 2008, 14:47

Hallo,

BlackJack hat ja auf das "Grundproblem" schon hingewiesen: read() blockiert natürlich, bis die Datei gelesen wurde - was bei "unendlichen" Dateien etwas *hust* dauern könnte.

Hier mal ein grober Versuch, der bei den von mir getesteten IP-Webcams gut funktioniert:

Code: Alles auswählen

def generic_mjpeg_processor(url, chunk_size=1024):
    f = urllib2.urlopen(url)
    
    data = ""
    content = f.read(chunk_size)
    chunk_counter = 0
    
    start_boundary = "{0}{1}".format(chr(0xFF), chr(0xD8))
    end_boundary = "{0}{1}".format(chr(0xFF), chr(0xD9))
    
    start, end = None, None
    
    while content:
        data += content
        
        end = data.rfind(end_boundary, -chunk_size+len(end_boundary))
        if end:
            start = data.find(start_boundary, 0, end)
        
        if start >= 0 and end >= 0:
            yield data[start:end+len(end_boundary)]
            data = data[end+len(end_boundary):]
            start, end = None, None
            
        content = f.read(chunk_size)
    else:
        print "No more content"
Das Ding sollte theoretisch (wenn man urllib durch ein file-object ersetzt) auch normale mjpg-Daten lesen können, habe das aber noch nicht getestet. Grundsätzlich ist dieser Ansatz auch nicht besonders schön, weil er beispielsweise nur ein Bild pro Chunk verarbeitet - bei 1024 Byte sollte das aber auch durchaus realistisch sein.

Ansonsten: Auch darauf hat BlackJack schon hingewiesen: Das Zeug, was du von deiner IP-Webcam erhälst, ist kein reines MJPG, zumindest nicht so, wie es das entsprechende RFC spezifiziert. Bei diesen HTTP-Streams wird jedem Bild noch ein Head vorangestellt (Boundary, Mime-Type, Size), der in normalen MJPG-Daten nach meiner Beobachtung nicht zu finden ist. So gesehen könnte man den oben erwähnten Code dahingehend abändern, dass er die JPGs nicht nach Magic Bytes, sondern nach Boundaries trennt. Da ich bisher keine Kollisionen (sprich: vermeintliche Magic Bytes innerhalb einer JPEG-Datei) feststellen konnte, sollte das aber nicht unbedingt nötig sein.

Besten Gruß,

brb

//edit:
Habe das Ganze jetzt so abgeändert, dass nach den End-Bytes (FF D9) nur im letzten Chunk nachgesehen wird: data.rfind(end_boundary, -chunk_size+len(end_boundary)). Bei durchschnittlich 50.000 Bytes pro JPEG würde die gesamte Datenmenge bei einer Chunk-Größe von 1024 Bytes sonst ~45 Mal unnötig durchlaufen, bis endlich mal das End-Byte auftaucht.

Auch sonst kann man da sicher noch einiges optimieren: Ich glaube beispielsweise, dass man besser erstmal mit dem in-Operator überprüfen sollte, ob "end_boundary" im letzten Chunk auftaucht. "rfind()" dürfte teurer sein als "in" - habs aber nicht überprüft.
droptix
User
Beiträge: 521
Registriert: Donnerstag 13. Oktober 2005, 21:27

Die Idee finde ich super, nur läuft der Code in meinem Python 3.1 nicht. Ich musste `urllib2` und `print` wie folgt ändern.

Code: Alles auswählen

import urllib.request, urllib.error

def generic_mjpeg_processor(url, chunk_size=1024):
    print("begin")
    f = urllib.request.urlopen(url)
    data = ""
    content = f.read(chunk_size)
    chunk_counter = 0
    start_boundary = "{0}{1}".format(chr(0xFF), chr(0xD8))
    end_boundary = "{0}{1}".format(chr(0xFF), chr(0xD9))
    start, end = None, None
    while content:
        data += content
        end = data.rfind(end_boundary, -chunk_size+len(end_boundary))
        if end:
            start = data.find(start_boundary, 0, end)
        if start >= 0 and end >= 0:
            yield data[start:end+len(end_boundary)]
            data = data[end+len(end_boundary):]
            start, end = None, None
        content = f.read(chunk_size)
    else:
        print("No more content")
    print(data, start, end)
    print("end")
Aufruf dann über `generic_mjpeg_processor("http://192.168.0.133")`.

Ergebnis: Nichts, nicht mal ein einziges `print` kommt raus, aber auch kein Fehler... ??? Ich steh jetzt echt auf dem Schlauch. Vielleicht war der Umstieg auf v3.1 doch keine so gute Idee... ?
Barabbas
User
Beiträge: 349
Registriert: Dienstag 4. März 2008, 14:47

Das Ding ist ein Generator - du musst darüber iterieren:

Code: Alles auswählen

for image in generic_mjpeg_processor("http://192.168.0.133"):
   print(image)
droptix
User
Beiträge: 521
Registriert: Donnerstag 13. Oktober 2005, 21:27

Ach wegen `yield`, richtig?
Bekomme nun leider diesen Fehler hier:

Code: Alles auswählen

Traceback (most recent call last):
  File "C:\Dokumente und Einstellungen\root\Desktop\webcam.py", line 102, in <module>
    for image in generic_mjpeg_processor("http://192.168.0.133"):
  File "C:\Dokumente und Einstellungen\root\Desktop\webcam.py", line 21, in generic_mjpeg_processor
    data += content
TypeError: Can't convert 'bytes' object to str implicitly
Dauerbaustelle
User
Beiträge: 996
Registriert: Mittwoch 9. Januar 2008, 13:48

Willst du vielleicht einfach mal selbst Hand anlegen? Schreib dir das von Grund auf selbst.
droptix
User
Beiträge: 521
Registriert: Donnerstag 13. Oktober 2005, 21:27

Bin ja leider schon mit meinen eigenen Versuchen gescheitert. Den Code verstehe ich im Wesentlichen, habe auch versucht das so zu handhaben: `data += str(content)`, aber dann hängt das Programm wieder. Ich würde nicht fragen, wenn ich es selbst hinbekäme...

Da ich nicht weiß, was genau hinter so einem M-JPEG Stream steckt und wie man ihn zerlegt, wäre ich nie auf die Boundary-Sachen gekommen.

Zur Info: soweit klappt alles, es rieselt Bytes in der Konsole. Nur muss ich jetzt noch die Bilder rausfummeln:

Code: Alles auswählen

# -*- coding: UTF-8 -*-
import urllib.request

def main(url, chunk_size=1024):
    f = urllib.request.urlopen(url)
    while True:
        content = f.read(chunk_size)
        if not content:
            break
        print(content)

if __name__ == "__main__":
    main("http://192.168.0.133/")
Dauerbaustelle
User
Beiträge: 996
Registriert: Mittwoch 9. Januar 2008, 13:48

Das lies dir doch einfach mal durch, wie das Protokoll aufgebaut ist. Wikipedia hat da nen Artikel zu. Im Prinzip läuft es darauf hinaus, dass du erst mal die Header liest, und ab dort immer bis zum Ende eines Bildes. Die Position (also an welchem Byte) des Endes weißt du entweder durch irgendwelches Boundary-Geparse oder möglicherweise kommt da auch eine Byteanzahl mit (Stichwort Content-Length).

Wenn ich mich recht entsinne ist das doch Multipart, was da verschickt wird? defnull hat da mal einen wunderprächtigen Parser für geschrieben, laut README kann der auch Parts unbekannter Größe. https://github.com/defnull/multipart/tree/develop Vielleicht bist du damit ja schon fertig, wer weiß :-)
Antworten