Falls du ein Schulprojekt suchst oder einfach ein wenig Erfahrung in NodeJs und Python in Verbindung mit dem Raspberry Pi sammeln möchtest, dann hast du mit diesem kleinen Projekt gute Chancen. Die Aufgabe ist es einen Livestream zu realisieren. Die Bilder werden hierbei mit dem Kamera-Modul des Raspberry Pi aufgenommen.

Vorbereitung

NodeJs und Python sollten auf dem Raspberry Pi, auf welchem Raspbian läuft, installiert und das Kamera-Modul angeschlossen sein. Auf dem Raspberry Pi führst nur nun folgende Befehle aus:

 

1. Projekt-Ordner anlegen und Verzeichnis wechseln

$ mkdir livestream && cd livestream

2. Die Datei package.json anlegen

$ npm init

3. Express installieren

$ npm install express --save

4. Socket.IO installieren

$ npm install socket.io --save

Client

Beginnen wir mit dem Client. Irgendwo möchtest du den Stream am Ende sehen und was eignet sich da besser als eine kleine HTML-Seite mit etwas JS. Ich erstelle die Datei index.html und speichere sie im Ordner livestream ab. Danach füge ich folgenden Quelltext in die Datei ein:

<html>
    <head>
        <title>Raspberry PI 3 Livestream</title>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
        <script type="text/javascript" src="http://code.jquery.com/jquery.js"></script>
        <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.0.3/socket.io.js"></script>
        <script type="text/javascript">
            // Konstanten definieren
            const serverAddress = '192.168.1.1';
            const port = 1234;
            // Verbindung herstellen
            var socket = io.connect('http://' + serverAddress + ':' + port);
            if (socket) {
                console.log('Connected to ' + serverAddress);
            }
            // Event 'disconnect'
            socket.on('disconnect', function(socket) {
                console.log('Disconnected');
            });
            // Event 'livestream'
            socket.on('livestream', function(img) {
                $('#stream').attr('src', 'data:image/jpeg;base64,' + img);
            });
            //Stream starten
            function startStream() {
                socket.emit('startStreaming');
            }
        </script>
    </head>
    <body>
        <h1>Livestream</h1>
        <main>
            <img id="stream" src="" />
            <button type="button" onclick="startStream()">Stream starten</button>
        </main>
    </body>
</html>

Im <head>-Bereich binde ich zuerst einmal jQuery und Socket.IO ein, bevor ich danach eine Verbindung zu meinem Server aufbaue und auf die verschiedenen Events reagiere. Der ganze Stream läuft nach einem Klick auf „Stream starten“ nur im <img>-Tag ab. Hier wird mehrmals pro Sekunde ein neuer Base64 kodierter String ausgegeben, den wir von unserem Server erhalten.

Server

NodeJs

Nun zum Server. Ich erstelle die Datei main.js und kopiere erst einmal folgenden Quelltext hinein:

// Externe Inhalte einbinden
const express = require('express');
const app = express();
const http = require('http').createServer(app);
const io = require('socket.io')(http);
const fs = require('fs');
const spawn = require('child_process').spawn;
const port = 1234;
const filePath = __dirname + '/read.jpg';
// Variablen anlegen
var streaming = false;
var py;
// Webserver starten
app.get('/', function(req, res) {
    res.sendFile(__dirname + '/index.html');
});
http.listen(port, function() {
    console.log('listening on *:' + port);
});

Zuerst einmal binde ich verschiedene Bibliotheken ein und setze den Webserver auf. Als Port wähle ich 1234.

Nun komme ich zum Socket Handling. Mein Server soll auf 3 verschiedene Events reagieren. Ich möchte in der Konsole eine Benachrichtigung erhalten, wenn ein Nutzer die Verbindung aufbaut oder verliert und wenn ein Nutzer den Stream startet. Folgender Quelltext wird am Ende der Datei main.js eingefügt:

// Event 'connection'
io.on('connection', function(socket) {
    console.log('Client ' + socket.id + ' connected');
    //Event 'startStreaming'
    socket.on('startStreaming', function() {
        console.log('Client ' + socket.id + ' started streaming');
        startStreaming();
    });
    // Event 'disconnect'
    socket.on('disconnect', function() {
        console.log('Client ' + socket.id + ' disconnected');
        if (io.engine.clientsCount == 0) {
            stopStreaming();
        }
    });
});

Zu guter Letzt füge ich noch die folgenden drei Funktionen hinzu:

// Stream starten
function startStreaming() {
    if (!streaming) {
        streaming = true;
        py = spawn('python', ['mjpeg.py']);
        py.stdout.on('data', function(data) {
            sendImage();
        });
    }
}
// Stream stoppen
function stopStreaming() {
    streaming = false;
    if (py) {
        py.kill();
    }
}
// Bild senden
function sendImage() {
    fs.readFile(filePath, function(err, buffer) {
        io.sockets.emit('livestream', buffer.toString('base64'));
    });
}

Die Funktion startStreaming() wird aufgerufen, sobald den Nutzer den Stream startet. Sie erzeugt einen Prozess mit einem Python-Script namens mjpeg.py (dazu komme ich gleich) und ruft jedes Mal die Funktion sendImage() auf, wenn das Python-Script etwas in die Konsole ausgibt. Die Funktion sendImage() ließt dann nur noch das gerade abgespeicherte Bild, kodiert es mithilfe des Base64-Verfahrens und schickt es an alle Nutzer. Sind keine Nutzer mehr mit dem Server verbunden, wird die Funktion stopStreaming() aufgerufen und der Prozess beendet.

 

Python

Kommen wir nun zum Python-Script. Dieses ist eigentlich für den wichtigsten Teil verantwortlich: Das Aufnehmen der einzelnen Bilder.

Damit ich auf mehr als ~ 3 FPS komme (mit dem Kamera-Modul lassen sich ca. 3 Fotos/Sekunde aufnehmen), habe ich mich dazu entschieden mit dem Python-Modul PiCamera eine Videoaufname zu starten. Als Format wähle ich MJPEG, damit ich die einzelnen Frames anhand der „Magischen Zahlen“ FF D8 erkennen und einzeln abspeichern kann. Somit sind bis zu 30 FPS möglich.

Ich erstelle die Datei mjpeg.py und kopiere folgenden Quelltext rein:

# Erforderliche Module importieren
import io
import time
import picamera
import sys
import os
# Klasse SplitFrames
class SplitFrames(object):
    def __init__(self):
        self.output = None
        self.file = 'write.jpg'
    def write(self, buf):
        if buf.startswith(b'\xff\xd8'):
            if self.output:
                self.output.close()
            if self.file.is_file():
                os.rename('write.jpg', 'read.jpg')
                print('refreshed')
                sys.stdout.flush()
            self.output = io.open('write.jpg', 'wb')
            self.output.write(buf)
# PiCamera
with picamera.PiCamera() as camera:
    camera.resolution = (640, 360)
    camera.framerate = 30
    time.sleep(2)
    output = SplitFrames()
    camera.start_recording(output, format='mjpeg')
    camera.wait_recording(999999);

Zuerst einmal setze ich die Auflösung und die Framerate. Danach gebe ich der Kamera 2 Sekunden Zeit um sich aufzuwärmen und starte die Aufnahme. Als Output wähle ich ein custom output object. Dieses enthält die Methode write, welche nach jedem aufgenommenem Frame aufgerufen wird. Somit habe ich die Möglichkeit auf jedes Frame einzeln zu reagieren. Handelt es sich um den Anfang eines Frames und existiert eine Datei namens write.jpg, wird die Datei write.jpg in read.jpg umbenannt. Der Grund, weshalb ich mit zwei Bilddateien arbeite, ist, dass es bei einer hohen Framerate meist gleichzeitig zum Auslesen und Abspeichern eines Bildes kommt. Lese ich also eine Datei aus, während sie neu geschrieben wird, erhalte ich ein fehlerhaftes Bild.

Nachdem die Datei umbenannt ist, wird das Wort „refreshed“ in der Konsole ausgegeben. Dies registriert mein NodeJs-Script und ruft die oben beschriebene Funktion sendImage() auf, um das neue Bild zu kodieren und zu verschicken. Danach wird das nächste einzelne Frame noch als write.jpg-Datei abgespeichert.

 

Und mit

$ node main.js

können wir das Ganze nun ausführen.

Fazit

Bereits nach einer Woche konnte ich die Micro-SD-Karte meines Raspberry Pi aufgrund der vielen Lese- und Schreibzugriffe in den Müll schmeißen. Für den produktiven Einsatz ist dieses kleine Projekt also nicht gedacht, eher als Schulprojekt.

Merken