HTTPD Webserver mit externe CSS und Bilder

Python auf Einplatinencomputer wie Raspberry Pi, Banana Pi / Python für Micro-Controller
anonym01

Hallo zusammen,

ich habe einen Raspberry Pi Zero mit einer Kamera angeschlossen, um genauer zu sein, die NoIR Kamera ist angeschlossen.
Über das internet bin ich auf ein ganz tolles Script gestossen, welches mir erlaubt via den Browser den Output der Kamera anzusehen (Live Streaming).

Nun wollte ich aber das äusserliche ein bisschen hübscher machen mit externe CSS Files und mit meinem Logo.

Hier ist mal das Script welches ich angepasst habe:

Code: Alles auswählen

import io
import picamera
import logging
import socketserver
from threading import Condition
from http import server

PAGE="""\

<!DOCTYPE HTML>
<html>
<head>
<link rel="stylesheet" href="css/styles.css">
</head>
<body>
<img src="img/img_girl.jpg" alt="Girl in a jacket" width="500" height="600">
<div class="video-container">
<img src="stream.mjpg" width="1280" height="720">
</div>
</body>
</html>


"""

class StreamingOutput(object):
    def __init__(self):
        self.frame = None
        self.buffer = io.BytesIO()
        self.condition = Condition()

    def write(self, buf):
        if buf.startswith(b'\xff\xd8'):
            # New frame, copy the existing buffer's content and notify all
            # clients it's available
            self.buffer.truncate()
            with self.condition:
                self.frame = self.buffer.getvalue()
                self.condition.notify_all()
            self.buffer.seek(0)
        return self.buffer.write(buf)

class StreamingHandler(server.BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == '/':
            self.send_response(301)
            self.send_header('Location', '/index.php')
            self.end_headers()
        elif self.path == '/index.php':
            content = PAGE.encode('utf-8')
            self.send_response(200)
            self.send_header('Content-Type', 'text/css')
            self.send_header('Content-Type', 'text/html')
            self.send_header('Content-Length', len(content))
            self.end_headers()
            self.wfile.write(content)
        elif self.path == '/stream.mjpg':
            self.send_response(200)
            self.send_header('Age', 0)
            self.send_header('Cache-Control', 'no-cache, private')
            self.send_header('Pragma', 'no-cache')
            self.send_header('Content-Type', 'multipart/x-mixed-replace; boundary=FRAME')
            self.end_headers()
            try:
                while True:
                    with output.condition:
                        output.condition.wait()
                        frame = output.frame
                    self.wfile.write(b'--FRAME\r\n')
                    self.send_header('Content-Type', 'image/jpeg')
                    self.send_header('Content-Length', len(frame))
                    self.end_headers()
                    self.wfile.write(frame)
                    self.wfile.write(b'\r\n')
            except Exception as e:
                logging.warning(
                    'Removed streaming client %s: %s',
                    self.client_address, str(e))
        else:
            self.send_error(404)
            self.end_headers()

class StreamingServer(socketserver.ThreadingMixIn, server.HTTPServer):
    allow_reuse_address = True
    daemon_threads = True

with picamera.PiCamera(resolution='1280x720', framerate=24) as camera:
    output = StreamingOutput()
    #Uncomment the next line to change your Pi's Camera rotation (in degrees)
    #camera.rotation = 90
    camera.start_recording(output, format='mjpeg')
    try:
        address = ('', 80)
        server = StreamingServer(address, StreamingHandler)
        server.serve_forever()
    finally:
        camera.stop_recording()
Irgendwie möchte der HTTPD Server diese Files nicht sehen. Zuerst dachte ich es sei ein Permission Issue aber den Ordner sowie die Files gehören allle an der Gruppe sowie dem User www-data.

Hätte da jemand eine Idee?

Danke und Grüsse
anonym01

..echt jetzt hat hier niemand einen Hinweis weswegen dies nicht funktioniert?
__deets__
User
Beiträge: 14528
Registriert: Mittwoch 14. Oktober 2015, 14:29

Du lieferst kein CSS aus, wie soll es also ankommen? Und der zweite Content-Type header (bzw. der erste) ist falsch, das funktioniert nicht. Man kann nur einen dieser Header liefern.
Sirius3
User
Beiträge: 17741
Registriert: Sonntag 21. Oktober 2012, 17:20

Der doppelte Content-Type ist quatsch. Du mußt sowohl /index.php als HTML ausliefern, als auch /css/styles.css, das noch gar nicht als Pfad bei Dir existiert.
Ebensowenig existiert ein /img/img_girl.jpg.
anonym01

Danke für die Informationen.
Das würde bedeuten ich muss jeden zusätzlichen File (CSS oder Bildchen) irgendwie so mitgeben?

Code: Alles auswählen

self.send_response(301)
            self.send_header('Location', '/css/style1.css')
            self.send_header('Location', '/css/style2.css')
            self.end_headers()
Wie würde dies den aussehen wenn ich das CSS File vom internet abrufe? Als Beispiel ich würde gerne die Bootstrap CSS nehmen.

Vielen Dank
F
Sirius3
User
Beiträge: 17741
Registriert: Sonntag 21. Oktober 2012, 17:20

Nein, Du mußt wie bei `if self.path == '/index.php'` auf Anfragen auf style.css und img_girl.jpg reagieren. Am besten, indem Du das generisch für alle Datein in einem static-Verzeichnis machst.
anonym01

Achso okay, alles klar. Wäre es dann nicht einfacher nur den gesammten Ordner für die CSS sowohl den Ordner für die Bildchen anzugeben?
Anonsten habe ich ein riesen langen Code.. oder geht das gar nicht?
anonym01

Also fürs verständnis. Nehmen wir an ich hab bei mir aufm Raspi unter css/style.css die CSS Datei rumliegen.

Dann müsste ich das Script mit diesen hier ergänzen:

Code: Alles auswählen

elif self.path == 'css/style.css':
            content = PAGE.encode('utf-8')
            self.send_response(200)
            self.send_header('Content-Type', 'text/css')
            self.send_header('Content-Length', len(content))
            self.end_headers()
            self.wfile.write(content)
Richtig?
Sirius3
User
Beiträge: 17741
Registriert: Sonntag 21. Oktober 2012, 17:20

`PAGE` ist doch Dein HTML-Code und nicht Dein CSS-Code. Wie schon geschrieben, würde man generischen Code für statische Daten in einem /static-Verzeichnis schreiben, und nicht kopieren. Jedes vernünftige HTTP-Framework bringt das übrigens schon mit. Ich würde also empfehlen auf Flask oder ähnliches umzusteigen.
anonym01

Habe mir Flask auch angeschaut. Habe da aber dann eher mühe wegen der PI Cam. Ich bin mir nicht sicher ob dies dann immer noch so funktioniert wie gewünscht.
__deets__
User
Beiträge: 14528
Registriert: Mittwoch 14. Oktober 2015, 14:29

Wenn die Menschheit nur eine Technologie erfunden haette, mit der man solche Fragen in eine allwissende Muellhalde einkippen kann, und die produziert dann weise Antworten...

https://blog.miguelgrinberg.com/post/vi ... with-flask

Nachtrag: hat eine neuere Version, die besser tut: https://blog.miguelgrinberg.com/post/fl ... -revisited
anonym01

Diesen Blog vom Miguel habe ich ebenfalls gesehen. Hat mir aber nicht geholfen bei meinem Vorhaben. PI Kamera funktioniert zwar ganz schön und gut, aber eine aktive Bedienung via Browser wird wohl so nicht möglich sein.
__deets__
User
Beiträge: 14528
Registriert: Mittwoch 14. Oktober 2015, 14:29

Wie kommst du zu dieser Einschaetzung? Die ist in meinen Augen falsch. Ich sehe auch nichts in deinem jetzigen Code, der da etwas aktiv erlaubt. Wie geht das denn da?
nezzcarth
User
Beiträge: 1633
Registriert: Samstag 16. April 2011, 12:47

Soweit ich weiß, stammt das Skript aus deinem Ausgangspost ja aus der Picamera Doku – Und dort wird es lediglich als Rezept geführt sowie deutlich darauf hingewiesen, dass Videostreaming kompliziert ist, wenn man es richtig machen möchte. Aus meiner Sicht hat das mehr Demo Charakter und ist nicht für den ernsthaften Gebrauch gedacht.

Ich würde persönlich daher darauf verzichten, mir da irgendetwas mit Python zusammen zu basteln. Je nachdem brauchst du dann auch einen passenden Player (JavaScript) usw. der im Browser läuft etc. Eine Google Suche zeigt doch recht schnell, dass es auch fertige Streaming-Lösungen gibt, mit denen Leute auf dem Raspberry Pi experimentieren.
anonym01

@nezzcarth was hast du denn so gefunden für "fertige" streaming lösungen?
anonym01

Hallo Zusammen, habe mir mal auf dem Notebook mit PyCharm mal die Umgebung nachgebaut.
Aktuell habe ich nen http server welche die index.html Informationen via PAGE ausliest und auf dem Browser tatsächlich wieder ausgibt.

Code: Alles auswählen

import socketserver
from threading import Condition
from http import server

PORT_NUMBER = 8080  # Maybe set this to 9000.

PAGE = """\
<html>
<head>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<h1>Hallo</h1>
</body>
</html>
"""


class MyHandler(server.BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == '/':
            self.send_response(301)
            self.send_header('Location', '/index.html')
            self.end_headers()
        elif self.path == '/index.html':
            content = PAGE.encode('utf-8')
            self.send_response(200)
            self.send_header('Content-Type', 'text/html')
            self.send_header('Content-Length', len(content))
            self.end_headers()
            self.wfile.write(content)
        if self.path == '/':
            self.send_response(301)
            self.send_header('Location', '/css/style.css')
            self.end_headers()
        elif self.path == '/css/style.css':
            self.send_response(200)
            self.send_header('Content-Type', 'text/css')
            self.end_headers()
        else:
            self.send_error(404)
            self.end_headers()


class StreamingServer(socketserver.ThreadingMixIn, server.HTTPServer):
    allow_reuse_address = True
    daemon_threads = True


if __name__ == '__main__':

    try:
        address = ('', 8000)
        server = StreamingServer(address, MyHandler)
        server.serve_forever()
    except KeyboardInterrupt:
        pass
    server.server_close()

Zusätzlich habe ich noch eine weitere If Schlaufe erstellt welche überprüft ob auf dem "server" den Verzeichnis css/ existiert und ob da drin auch eine style.css befindet.

Code: Alles auswählen

        if self.path == '/':
            self.send_response(301)
            self.send_header('Location', '/css/style.css')
            self.end_headers()
        elif self.path == '/css/style.css':
            self.send_response(200)
            self.send_header('Content-Type', 'text/css')
            self.end_headers()
Das hat soweit alles gut funktioniert, sogar auf dem Browser beim Inspect hat der server das Verzeichnis sowie die style.css Datei gefunden.
Jedoch kam der Inhalt der style.css leider nicht mit. Bedeutet die Datei ist leer, ob wohl die style.css mit Inhalt gefüllt ist.

Würde gerne hier das Bild hochladen, weiss aber nicht wie dies geht sorry :(

Was habe ich übersehen, könnte jemand sich dies anschauen?
Besten Dank
F
__deets__
User
Beiträge: 14528
Registriert: Mittwoch 14. Oktober 2015, 14:29

Da du den Inhalt der Datei nicht schreibst - anders als bei dem index.html - ist das ja auch kein Wunder.

Du hast die Frage nicht beantwortet, warum das mit Flask (das sowas schon fertig kann) angeblich nicht geht. Warum machst du dir das Leben schwerer als notwendig?
anonym01

Nevermind habe mir da was besseres rausgefischt.

Code: Alles auswählen

import socketserver
from threading import Condition
from http import server
import os

PORT_NUMBER = 8080  # Maybe set this to 9000.



class MyHandler(server.BaseHTTPRequestHandler):
    def do_GET(self):
        #root = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'test')
        root = '/Users/user/PycharmProjects/webserver_pi'
        if self.path == '/':
            filename = root + '/index.html'
        else:
            filename = root + self.path

        self.send_response(200)
        if filename[-4:] == '.css':
            self.send_header('Content-type', 'text/css')
        elif filename[-5:] == '.json':
            self.send_header('Content-type', 'application/javascript')
        elif filename[-3:] == '.js':
            self.send_header('Content-type', 'application/javascript')
        elif filename[-4:] == '.ico':
            self.send_header('Content-type', 'image/x-icon')
        else:
            self.send_header('Content-type', 'text/html')
        self.end_headers()
        with open(filename, 'rb') as fh:
            html = fh.read()
            # html = bytes(html, 'utf8')
            self.wfile.write(html)


class StreamingServer(socketserver.ThreadingMixIn, server.HTTPServer):
    allow_reuse_address = True
    daemon_threads = True


if __name__ == '__main__':

    try:
        address = ('', 8000)
        server = StreamingServer(address, MyHandler)
        server.serve_forever()
    except KeyboardInterrupt:
        pass
    server.server_close()

Auf dieser Art und Weise gefällt es mir umso besser. Habe die Dateien auch statisch irgendwo aufm server abgelegt und gut ist.
Sobald ich zu Hause bin wird dies mit dem Pi ausprobiert und geprüft ob ich den Stream noch rein kriege.

Warum ich das mit Flask nicht wage.. Erstens möchte ich meine Python skills verbessern und einen gewissen touch anlegen für diese Geschichte. Zweitens der Sinn dieser Übung wäre dass ich am Ende auf dem Browser sagen kann. Nun starte mir den Live stream, nehme aktuell das auf was du siehst und speichere es irgendwo ab.

Ich habe den part für den Live stream bereits, den part für die Aufnahme habe ich ebenso.

Ich hatte echt nur dieses eine kleine Issue .

Mit Sicherheit wäre dieses ganze Projekt einfacher gewesen mit einem framework wie flask, hatte und wollte die Zeit anders investieren. :D
Benutzeravatar
sparrow
User
Beiträge: 4187
Registriert: Freitag 17. April 2009, 10:28

@firedotwater: Was ist eigentlich eine if-Schlaufe?
Benutzeravatar
kbr
User
Beiträge: 1487
Registriert: Mittwoch 15. Oktober 2008, 09:27

Das ist so etwas, wie ein historischer Flußlauf. Der liegt noch neben dem aktuellen in geschleifter Form, wird aber nicht mehr durchlaufen … quasi „durchschlaufen“ … 8)
Antworten