Schwenkbarer Live-Stream mit dem Raspberry Pi

Schwenkbarer Live-Stream mit dem Raspberry Pi

Diese Woche haben wir mal wieder eine ganz heiße Kooperation mit Tutorials-RapsberryPi.de für euch erstellt.
Denn diese Woche werden wir gemeinsam einen Raspberry Pi mit einer beweglichen Überwachungskamera bauen und über unsere ganz eigene Android App per Internet und SSH fernsteuern und den Live-Stream der Kamera anzeigen.

Wer hat nicht schon einmal darüber nachgedacht per Überwachungskamera seine eigenen vier Wände zu beobachten, eine Babykamera einzurichten oder zu sehen, was der Hund so in der Abwesenheit treibt? – Leider sind vorgefertigte Systeme nicht immer ganz billig, schlecht programmiert oder stellen riesige Sicherheitslücken dar.
Wir werden also eine selbst gebaute, bewegliche, anpassbare, erweiterbare und mit etwas Eigenarbeit sicherere (diesen Part werden StaticFloat und Tutorials-Raspberrypi in diesem Artikel nicht aufgreifen) Überwachungskamera kreieren.

Für die Überwachungskamera an sich sind nur relativ wenig Bauteile nötig:

  1. Ein Raspberry Pi (25-40€) oder Raspberry Pi Zero (ca. 10€ mit Versandkosten)
  2. Eine Kamera für den Raspberry Pi (19€)
  3. Ein Servomotor (4€)
  4. Ein paar Jumper Kabel (4€)
  5. Ein USB-Ladegerät (5€)
  6. Ein WLAN-Stick (6€) (optional) oder ein Lan-Kabel (3€)

Summa Summarum etwa 45€ – 75€.
Eine fertige Überwachungskamera kostet zwischen 50€ und 150€. Dafür bekommt man aber auch ein geschlossenes, nicht anpassbares (Hard- und Software) System. Wenn man bedeckt, dass wir unser neu programmiertes System einfach erweitern können, dann würden bei einem vorgefertigten System schnell ein Euro den nächsten Jagen. Ganz zu schweigen vom fehlenden Spaß beim Programmieren/Basteln :P

Ich werde mich in diesem Artikel ganz auf die Programmierung der Android App beschränken, den dazugehörigen Raspberry Pi Part findet ihr wie immer auf Tutorials-RapsberryPi.de. Des weiteren wird dieser Artikel auf einem bereits bestehendem Artikel, zur Steuerung des Raspberry Pi per SSH Android App, aufbauen. Ich werde im folgenden Teil die kompletten Codes angeben, allerdings den SSH-Part nicht mehr näher erläutern.

Der Artikel wird auf mehrere Seiten aufgeteilt sein.

App erstellen

Wir erstellen uns ein neues Android Studio Projekt.
Ich werde dem Projekt den Namen “Pi Security Cam” geben und die Company Domain “staticfloat.de”. Damit erhält die App den Package Namen “de.staticfloat”.
Ihr dürft die Eingaben selbstverständlich verändern, solltet aber immer darauf achten, die Codes aus diesem Artikel entsprechend zu verändern.

Vorbereitungen

Das Streamen von unserem Mjpg Motion-Stream auf Android ist kein trivialer Vorgang.
Wir müssen Android und damit Android Studio etwas vorbereiten.

Wir werden eine Library benutzen, die das Streamen an Android ermöglicht.
Diese heißt ipcam-view und ist auf GitHub verfügbar. Leider benötigen wir für diese Library noch zusätzliche Vorbereitungen.

Warum?
Ganz einfach. Diese Library nutzt eine Lambda Notation, welche in der Java JDK 7 nicht unterstützt wird. Die Java JDK 8 wird wiederum allerdings von Android noch nicht unterstützt.
Ihr seht, wir befinden uns in einer Zwickmühle. Zum Glück gibt es Work-Arounds.

Wir benötigen also eine neue Library, welche die Lambda Notation auch unter der Java JDK 7 verfügbar macht, denn nur die JDK wird von Android unterstützt.
Wir öffnen die Datei “gradle.build (Project: xxx)” und fügen hinzu:

...

buildscript {

 ...
 repositories{
 mavenCentral() // Das hier hinzufügen
 }
 ...
 dependencies {
 ...

 /* Das hier hinzufügen */
 classpath 'me.tatarka:gradle-retrolambda:3.4.0'
 /* Das hier hinzufügen */

 }
 
 ...

}

RetroLambda funktioniert jedoch nur mit der Java JDK 8.
Wir benötigen also die Java JDK 8 (Download).
Diese neue und eigentlich garnicht verwendbare JDK müssen wir in Android Studio in der Datei “grade.build (Module: app)” angeben, in dem wir zwei Zeilen hinzufügen:

android{

 ...

 /* NEUE ZEILEN */
 compileOptions {
 sourceCompatibility JavaVersion.VERSION_1_8 
 targetCompatibility JavaVersion.VERSION_1_8 
 }
}

Wir müssen RetroLambda nun auch aktivieren. Wir fügen in die selbe Datei hinzu:

apply plugin: 'com.android.application' // Diese Zeile sollte bereits existent sein
apply plugin: 'me.tatarka.retrolambda' // Diese Zeile hinzufügen (unter die andere "apply plugin"-Zeile)

Zum Schluss benötigen wir für die Compilierung noch die Android NDK.
Dazu geht ihr (falls noch nicht installiert), auf File -> Other Settings -> Default Project Stucture -> Download NDK.
Nach dem Download und der Installation können wir fortfahren.

Nun ist alles für die eigentliche Library ipcam-view vorbereitet und wir fügen zu den dependencies in der Datei “gradle.build (Module: app)” diese Zeile hinzu:

compile 'com.github.niqdev:mjpeg-view:0.3.6'

Puh, ein Haufen Arbeit bisher.
Wir müssen nun noch lediglich eine weitere Sache vorbereiten.
Da wir ja einen Live-Stream aus dem Internet erhalten wollen, müssen wir natürlich auch eine Internetverbindung haben. Diese Internetverbindung müssen wir uns noch per Berechtigung einholen.
Wir öffnen die Datei “AndroidManifest.xml” und fügen innerhalb des Tags “manifest” ein:



Einstellungen erstellen

Nun haben wir unser App-Grundgerüst erstellt und brennen schon darauf, den Live-Stream einzuprogrammieren.

Ich muss euch vorerst enttäuschen. Es tut mir wirklich leid, aber unsere App muss ja ein paar zusätzliche Informationen erhalten, woher der Live-Stream kommt und wie der Live-Stream abzurufen ist.
Wir werden also wohl oder übel erst einmal eine Einstellungsnamen Seite erstellen müssen und eine Logik einbauen, wie wir die Einstellungen speichern/auslesen.

Wir erstellen vorerst eine Java Klasse.
Diese heißt “Einstellungen” und fügen ein:

public class Einstellungen {

 public static String nutzername;
 public static String passwort;
 public static String ip;
 public static int port;
 public static int motionPort;
 public static String motionSuffix;

 public static final String befehlPre = "echo ";
 public static final String befehl = " | sudo -S python servo.py ";

}

Anschließend erstellen wir ein neues Layout, welches unsere Eingabefelder einhalten soll.
Dieses Layout speichert ihr unter dem Namen “activity_einstellungen” im Ordner res->layouts:




 
 
 
 
 
 
 
 

Alles schön und gut, aber bisher können wir weder Einstellungen speichern noch diese aufrufen.
Wir wechseln nun zurück in die Datei “MainActivity.java” und ändern die Hauptfunktion “onCreate”.

@Override
protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);

 //shared data vorbereiten
 pref = PreferenceManager.getDefaultSharedPreferences(this);
 editor = pref.edit();

 // Verbindungsdaten laden und ggf. mit Platzhaltern versehen
 einstellungen.nutzername = pref.getString("nutzername", "");
 einstellungen.passwort = pref.getString("passwort", "");
 einstellungen.ip = pref.getString("ip", "");
 einstellungen.port = pref.getInt("port", 22);
 einstellungen.motionPort = pref.getInt("motionPort", 8081);
 einstellungen.motionSuffix = pref.getString("motionSuffix", "/stream.mjpg");

 // Prüft, ob eines der Felder für die Verbindungsdaten unvollständig ist.
 if(pref.getInt("port", 0) == 0 || pref.getString("ip", "").equals("") || pref.getString("passwort", "").equals("") || pref.getString("nutzername", "").equals("")) {
 // Rufe die Einstellungsseite auf, um Verbindungsdaten angeben zu können.
 zeigeEinstellungen();
 }else{
 // Rufe die Startseite der App auf, auf der der Live-Stream angezeigt werden soll.
 zeigeStart();
 }

}

In Zeile 20 und 23 rufen wir zwei Funktionen auf, welche einmal die Einstellungsseite und einmal die Startseite aufrufen sollen.
Dies geschieht je nachdem, ob wir bereits Einstellungen angeben haben.
Warum machen wir das? – Ganz einfach, wenn wir versuchen würden das Livebild Kamera  auszulesen, aber noch keine Serveradresse angeben haben, würde die App abstürzen.

Zu guter letzt müssen wir für diese Seite des Artikels noch die Funktionalität der Methode “zeigeEinstellungen()” definieren.
Hierzu schreiben wir in die selbe Klasse:

// Zeigt die Einstellungsseite an.
public void zeigeEinstellungen(){
 setContentView(R.layout.activity_einstellungen);

 // Den Textfeldern Namen geben, um den Inhalt schreiben und auslesen zu können.
 final EditText edittext_nutzername = (EditText) findViewById(R.id.edittext_nutzername);
 final EditText edittext_ip = (EditText) findViewById(R.id.edittext_ip);
 final EditText edittext_passwort = (EditText) findViewById(R.id.edittext_passwort);
 final EditText edittext_port = (EditText) findViewById(R.id.edittext_port);
 final EditText edittext_motionPort = (EditText) findViewById(R.id.edittext_motionPort);
 final EditText edittext_motionSuffix = (EditText) findViewById(R.id.edittext_motionSuffix);
 Button speichern_button = (Button) findViewById(R.id.button_speichern);

 // Fehlerabfrage
 if(edittext_ip != null && edittext_nutzername != null && edittext_passwort != null && edittext_port != null && speichern_button != null) {

 // Textfelder mit bereits bestehenden Einstellungen befüllen.
 edittext_ip.setText(pref.getString("ip", ""));
 edittext_nutzername.setText(pref.getString("nutzername", ""));
 edittext_passwort.setText(pref.getString("passwort", ""));
 edittext_port.setText(pref.getInt("port", 22) + "");
 }

 // Auf den Klick, auf den Knopf "Speichern" reagieren.
 speichern_button.setOnClickListener(new View.OnClickListener() {
 @Override
 public void onClick(View v) {

 // Variablen vorbereiten, um den Inhalt der Textfelder unterspeichern zu können.
 String nutzername = "";
 String passwort = "";
 String ip = "";
 String port = "";
 String motionSuffix = "";
 String motionPort = "";

 // Inhalt der Textfelder in Variablen schreiben.
 nutzername = edittext_nutzername.getText().toString();
 ip = edittext_ip.getText().toString();
 passwort = edittext_passwort.getText().toString();
 port = edittext_port.getText().toString();
 motionSuffix = edittext_motionSuffix.getText().toString();
 motionPort = edittext_motionPort.getText().toString();

 if(!port.equals("") && !ip.equals("") && !nutzername.equals("") && !passwort.equals("")){

 // Verbindungsdaten laden
 editor.putString("nutzername", nutzername);
 editor.putString("passwort", passwort);
 editor.putString("ip", ip);
 editor.putInt("port", Integer.parseInt(port));
 editor.putInt("motionPort", Integer.parseInt(motionPort));
 editor.putString("motionSuffix", motionSuffix);

 // Einstellungen speichern
 editor.commit();

 // Gehe bei erfolgreicher Speicherung zur Startseite zurück
 zeigeStart();
 }else{

 // Fehlermeldung ausgeben, falls eines der Felder frei war.
 Toast.makeText(MainActivity.this, "Es scheinen noch Felder frei zu sein.", Toast.LENGTH_SHORT).show();
 }

 }
 });
}

Live-Stream verfügbar machen

Wir haben Daten und können endlich auch ein Bild empfangen! – Wohoo!

Öffnet die Datei “activity_main” und ersetzt die Datei mit:





 
 

 

Wechselt erneut zurück in die MainActivity Datei und schreibt unterhalb von “onCreate”:

public void zeigeStart(){
 setContentView(R.layout.activity_main);

 // Verbindungsdaten laden.
 einstellungen.nutzername = pref.getString("nutzername", "");
 einstellungen.passwort = pref.getString("passwort", "");
 einstellungen.ip = pref.getString("ip", "");
 einstellungen.port = pref.getInt("port", 22);
 einstellungen.motionPort = pref.getInt("motionPort", 8081);
 einstellungen.motionSuffix = pref.getString("motionSuffix", "/stream.mjpg");

 mjpegView = (MjpegView) findViewById(R.id.VIEW_NAME);
 startMjpegStream();
}

Unter “zeigeStart” kommt noch eine zusätzliche “starteMjpegStream”, welche nach der Anzeige der Startseite den eigentlichen Live-Stream startet.

// Starte die Anzeige des Live Streams des Raspberry Pi
public void startMjpegStream(){
 Mjpeg.newInstance()
 .open("http://" + einstellungen.ip + ":" + einstellungen.motionPort + einstellungen.motionSuffix, TIMEOUT)
 .subscribe(inputStream -> {
 mjpegView.setSource(inputStream);
 mjpegView.setDisplayMode(DisplayMode.BEST_FIT);
 mjpegView.showFps(false);
 },
 throwable -> {
 Log.e(getClass().getSimpleName(), "mjpeg error", throwable);
 Toast.makeText(this, "Error", Toast.LENGTH_LONG).show();
 });
}

Bewegung per SSH ermöglichen

Schlussendlich wollten wir ja eine bewegliche Kamera basteln, also werden wir nun die Bewegung der Kamera einprogrammieren.
Eine “Seekbar” wird uns bereits gezeigt. Diese Seekbar ist eine Art Schieberegler, welcher beim Start der App immer auf der Mitte steht. Verschieben wir den Regler und lassen ihn los, dann soll per SSH eine zahl zwischen 0-90 an den Raspberry Pi übergeben werden.
Diese Zahl wird dann von dem Python Servo-Skript (auf Tutorials-RaspberryPi.de erklärt) in einen Winkel übersetzt und dem Servo übergeben. Dieser sollte sich anschließend drehen.

Wir müssen nun die Funktionalität für die Seekbar liefern, welche dann im Anschluss den SSH-Befehl an den Raspberry Pi sendet.
Fügt ans Ende der “onCreate” Methode ein:

// unter die Funktion startMjpegStream();

SeekBar winkelSteller = (SeekBar) findViewById(R.id.seekBar);
winkelSteller.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
 @Override
 public void onProgressChanged(SeekBar seekBar, int i, boolean b) {

 }

 @Override
 public void onStartTrackingTouch(SeekBar seekBar) {

 }

 @Override
 public void onStopTrackingTouch(SeekBar seekBar) {
 starteSSHAbfrage( einstellungen.nutzername, einstellungen.passwort, einstellungen.ip, einstellungen.port, seekBar.getProgress());
 }
});

Die beiden Methoden zum Senden des SSH-Befehls sind in anderen Artikeln näher erläutert.
(1. Artikel dazu: 1. Raspberry Pi mit dem Smartphone steuern)

// Kleine Zusatzfunktion, die den eigentlichen SSH Befehl asynchron im Hintergrund startet.
public void starteSSHAbfrage(final String nutzername, final String passwort, final String ip, final int port, final int winkel){

 final String befehl = einstellungen.befehlPre + einstellungen.passwort + einstellungen.befehl + winkel;

 // Hintergrundaufgabe erstellen
 new AsyncTask() {
 @Override
 protected boolean doInBackground(Integer... params) {

 try {
 // Funktion ausführen
 sshBefehl(nutzername, passwort, ip, port, befehl);
 
 } catch (Exception e) {
 e.printStackTrace();
 }
 
 return true;
 }
 protected void onPostExecute(boolean ausgefuehrt) {

 }
 }.execute(1);
}


// Dies ist die eigentliche Funktion, die den SSH Befehl ausführt.
public static boolean sshBefehl(String username, String passwort, String hostname, int port, String befehl) throws Exception {
 
 // Erstelle das benötigte Objekt, um eine SSH-Verbindung aufbauen zu können.
 JSch jsch = new JSch();
 
 // Bereite ein paar Variablen vor, um Ausgaben der Konsole auslesen zu können.
 byte[] buffer = new byte[1024];
 ArrayList lines = new ArrayList<>();
 
 // Füttere das Objekt mit allen nötigen Informationen, um eine Verbindung aufbauen zu können.
 Session session = jsch.getSession(username, hostname, port);
 session.setPassword(passwort);
 
 // Umgehe das Abgleichen nach dem richtigen Key. (Es wird nun eine Man-In-Middle Attacke nicht mehr abgefangen)
 Properties prop = new Properties();
 prop.put("StrictHostKeyChecking", "no");
 session.setConfig(prop);
 
 // Stelle eine Verbindung her.
 session.connect();
 
 // Erstelle ein neues Objekt, für einen neuen SSH Channel.
 ChannelExec channelssh = (ChannelExec) session.openChannel("exec");
 
 // Füttere den Channel mit dem Befehl und schicke den Befehl ab.
 channelssh.setCommand( befehl );
 channelssh.connect();

 try {
 InputStream in = channelssh.getInputStream();

 // Lese alle Ausgaben aus, bis der Befehl beendet wurde oder die Verbindung abbricht.
 while (true) {
 
 // Warte, bis zum Ende der Ausführung.
 while (in.available() > 0) {

 }

 // Befehl beendet oder Verbindung abgebrochen.
 if (channelssh.isClosed()) {
 break;
 }
 
 // Warte einen kleinen Augenblick mit der nächsten Zeile.
 try {
 Thread.sleep(1000);
 } catch (Exception ee) {}
 }
 }catch (Exception e){}
 
 // Beende alle Verbindungen.
 channelssh.disconnect();
 session.disconnect();
 
 // Gib die Ausgabe zurück
 return true;
}

Anpassungen

Zur Komplettierung der Grundapp fügt bitte über die Methode “onCreate” ein:

int TIMEOUT = 5;
MjpegView mjpegView;
private SharedPreferences pref;
private SharedPreferences.Editor editor;
private Einstellungen einstellungen;

Super, wir können Einstellungen und Verbindungsdaten angeben, wenn noch keine eingeben wurden.
Doch was passiert, wenn wir bereits Einstellungen angeben haben und sich beispielsweise die Netzwerkadresse des Raspberry Pi’s ändert? – Bis dato gucken wir doof in die Röhre.

Ändern wir das nun!
Wer gerne weiter in die Röhre schaut oder sagt “Es bleibt alles so, wie es ist”, der kann auf die nächste Seite zum Fazit schalten. Alle Anderen sind herzlich dazu eingeladen ein Menü, mit Einstellungsunterpunkt zu programmieren :P

Schreibt in die Klasse “MainActivity”:

// Definiert, dass es ein OptionMenü gibt und wie dieses aussehen soll. (Nach Vorlage der Datei "actionbar_manu.xml")
@Override
public boolean onCreateOptionsMenu(Menu menu) {
 MenuInflater inflater = getMenuInflater();
 inflater.inflate(R.menu.actionbar_menu, menu);
 return true;
}

// Definiert, wie auf Klicks im OptionMenü reagiert werden soll.
@Override
public boolean onOptionsItemSelected(MenuItem item) {
 switch (item.getItemId()) {
 case R.id.menu_start:
 // Dieser Teil (bis zum "return") wird ausgeführt, wenn wir auf den Menüpunkt mit der ID "menu_start" klicken.
 zeigeStart();
 return true;
 case R.id.menu_ueber:
 // Dieser Teil (bis zum "return") wird ausgeführt, wenn wir auf den Menüpunkt mit der ID "menu_ueber" klicken.
 DialogFragment newFragment = new UeberDialog();
 newFragment.show(this.getFragmentManager(), "Über");
 return true;
 case R.id.menu_einstellungen:
 // Dieser Teil (bis zum "return") wird ausgeführt, wenn wir auf den Menüpunkt mit der ID "menu_einstellungen" klicken.
 zeigeEinstellungen();
 return true;
 default:
 // Standardfunktion. Sollte bestehen bleiben.
 return super.onOptionsItemSelected(item);
 }
}

// Kleine Klasse zum Erstellen eines Dialogs.
public static class UeberDialog extends DialogFragment {
 @Override
 public Dialog onCreateDialog(Bundle savedInstanceState) {
 //Dialog erstellen
 AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
 //Befülle Dialog mit dem Text "ueber_text" aus der Datei res -> strings.xml
 builder.setMessage(R.string.ueber_text)
 //Erstelle einen Button mit dem Text "ok" aus der Datei res -> strings.xml
 .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
 public void onClick(DialogInterface dialog, int id) {
 //Schließe das Dialogfeld, beim Klick auf den erstellten Button
 dismiss();
 }
 });
 //Gib das erstellte Dialogfeld an die aufrufende Klasse zurück, um es anzeigen zu können.
 return builder.create();
 }
}

Nun erstellt eine Datei “actionbar_menu” im neu erstellten Ordner “menu” unter “res”.
Fügt hier ein:




 
 
 
 

Zum Schluss benötigen wir noch ein paar wenige Strings in der Datei “strings.xml” unter res->values.


 Pi Security Cam
 Ok
 Diese App wurde nach Anleitung von www.StaticFLoat.de erstellt und steht unter der Apache 2.0 Lizenz. Beachtet die Lizenzen von JSch und MJpegView.
 
 Startseite
 Über
 Einstellungen

Fazit

Wir haben ein tanzschönes Stückchen Arbeit hinter uns und wenn ich ehrlich sein soll, dann habe ich mir auch fast zwei Tage lang halbwegs den Kopf bei dieser App zerbrochen. Der Weg ist nicht ganz trivial und die App beschränkt sich nicht gerade auf simpelste Programmierung. Zum Glück haben wir es nun geschafft und eine eigene schwenkbare Raspberry Pi Überwachungskamera gebaut uns selber programmiert. Ihr dürft stolz auf euch sein!

Wir (StaticFloat und Tutorials-RaspberryPi) werden uns die nächste zeit noch einmal genauer Gedanken, um mögliche Erweiterungen machen.
Vielleicht habt ihr ja besondere Ideen und wollt uns ein kleines Feedback hinterlassen. Gerne per Kommentar oder auch per Mail.

Ich hoffe ihr habt Spaß mit eurem eigenen Überwachungssystem (Raspberry Pi Bewegungsmelder mit Benachrichtigung an Android App) und hättet auch weiterhin Spaß an möglichen Erweiterungen.

Marvin

Ich bin 23 Jahre jung und studiere zurzeit Wirtschaftsinformatik an der Georg-August-Universität in Göttingen. Ich bin ein Mensch, der sich neben der Programmierung noch für tausend andere Dinge interessiert, die mal mehr und mal weniger verrückt sind. Vor allem aber bin ich Feuer und Flamme mit der Programmierung von eigenen kleinen Apps und Programmen, die mein Leben bereichern.

Kommentar hinzufügen

*Pflichtfeld