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:


<uses-permission android:name="android.permission.INTERNET"></uses-permission>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"></uses-permission>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"></uses-permission>

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:

<!--?xml version="1.0" encoding="utf-8"?-->
<!--
 ~ Copyright 2017 www.staticfloat.de
 ~
 ~ Licensed under the Apache License, Version 2.0 (the „License“);
 ~ you may not use this file except in compliance with the License.
 ~ You may obtain a copy of the License at
 ~
 ~ http://www.apache.org/licenses/LICENSE-2.0
 ~
 ~ Unless required by applicable law or agreed to in writing, software
 ~ distributed under the License is distributed on an „AS IS“ BASIS,
 ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 ~ See the License for the specific language governing permissions and
 ~ limitations under the License.
 -->
<linearlayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent">
 <textview android:layout_width="wrap_content" android:layout_height="wrap_content" android:textappearance="?android:attr/textAppearanceSmall" android:text="Verbindungsdaten:" android:id="@+id/textView" android:layout_margin="10dp"/>
 <edittext android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/edittext_ip" android:hint="IP-Adresse:" android:layout_marginleft="10dp" android:layout_marginright="10dp"/>
 <edittext android:layout_width="match_parent" android:layout_height="wrap_content" android:inputtype="number" android:ems="10" android:id="@+id/edittext_port" android:hint="Port: (Normalerweise 22)" android:layout_marginleft="10dp" android:layout_marginright="10dp"/>
 <edittext android:layout_width="match_parent" android:layout_height="wrap_content" android:inputtype="number" android:ems="10" android:id="@+id/edittext_motionPort" android:hint="Motion Port: (Normalerweise 8081)" android:layout_marginleft="10dp" android:layout_marginright="10dp"/>
 <edittext android:layout_width="match_parent" android:layout_height="wrap_content" android:inputtype="text" android:id="@+id/edittext_motionSuffix" android:hint="Motion Adresse: (/stream.mjpg)" android:layout_marginleft="10dp" android:layout_marginright="10dp"/>
 <edittext android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/edittext_nutzername" android:hint="Nutzername:" android:layout_marginleft="10dp" android:layout_marginright="10dp"/>
 <edittext android:layout_width="match_parent" android:layout_height="wrap_content" android:inputtype="textPassword" android:ems="10" android:id="@+id/edittext_passwort" android:hint="Passwort:" android:layout_marginright="10dp" android:layout_marginleft="10dp"/>
 <button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Speichern" android:id="@+id/button_speichern" android:layout_gravity="right" android:layout_marginright="16dp" android:background="@color/colorPrimary" android:textcolor="@android:color/white" android:padding="10dp"/>
</linearlayout>


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:

<!--?xml version="1.0" encoding="utf-8"?-->
<!--
 ~ Copyright 2017 StaticFloat.de
 ~
 ~ Licensed under the Apache License, Version 2.0 (the „License“);
 ~ you may not use this file except in compliance with the License.
 ~ You may obtain a copy of the License at
 ~
 ~ http://www.apache.org/licenses/LICENSE-2.0
 ~
 ~ Unless required by applicable law or agreed to in writing, software
 ~ distributed under the License is distributed on an „AS IS“ BASIS,
 ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 ~ See the License for the specific language governing permissions and
 ~ limitations under the License.
 -->

<relativelayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:stream="http://schemas.android.com/apk/res-auto" android:id="@+id/activity_main" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingbottom="@dimen/activity_vertical_margin" android:paddingleft="@dimen/activity_horizontal_margin" android:paddingright="@dimen/activity_horizontal_margin" android:paddingtop="@dimen/activity_vertical_margin" tools:context="de.staticfloat.pisecuritycam.MainActivity">
 
 <seekbar android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/seekBar" android:layout_alignparenttop="true" android:layout_alignparentstart="true" android:max="90" android:progress="45"/>

 <com.github.niqdev.mjpeg.mjpegsurfaceview android:id="@+id/VIEW_NAME" android:layout_below="@+id/seekBar" android:layout_width="match_parent" android:layout_height="match_parent" stream:type="stream_default"/>

</relativelayout>


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:

<!--?xml version="1.0" encoding="utf-8"?-->
<!--
 ~ Copyright 2017 www.staticfloat.de
 ~
 ~ Licensed under the Apache License, Version 2.0 (the „License“);
 ~ you may not use this file except in compliance with the License.
 ~ You may obtain a copy of the License at
 ~
 ~ http://www.apache.org/licenses/LICENSE-2.0
 ~
 ~ Unless required by applicable law or agreed to in writing, software
 ~ distributed under the License is distributed on an „AS IS“ BASIS,
 ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 ~ See the License for the specific language governing permissions and
 ~ limitations under the License.
 -->
<menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto">
 <!-- Menüpunkt, der angezeigt wird, wenn auf die drei Punkte in der Actionbar geklickt wird. -->
 <item android:id="@+id/menu_start" android:title="@string/menu_start" app:showasaction="never"/>
 <item android:id="@+id/menu_einstellungen" android:title="@string/menu_einstellungen" app:showasaction="never"/>
 <item android:id="@+id/menu_ueber" android:title="@string/menu_ueber" app:showasaction="never"/>
</menu>


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


<resources>
 <string name="app_name">Pi Security Cam</string>
 <string name="ok">Ok</string>
 <string name="ueber_text">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.</string>
 <!-- Texte für unser Menü -->
 <string name="menu_start">Startseite</string>
 <string name="menu_ueber">Über</string>
 <string name="menu_einstellungen">Einstellungen</string>
</resources>

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 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.

Hat dir dieser Artikel gefallen?

Kommentar hinzufügen

*Pflichtfeld

  1. Felix Grad

    Hallo Melvin, oder Marvin (steht im Impressum)? :D vielen Dank schon mal im Voraus für dein gutes Tutorial. Deine Website gefällt mir auch sehr gut. Sehr übersichtlich aufgebaut. Nun zu meiner Frage... Und zwar ich studiere User Experience Design in Ingolstadt und bin gerade dran meine Bachelorarbeit zu schreiben. Da unser Studium eher designlastig ist, bin ich jetzt auch nicht der Programmierchamp. Wir hatten zwar 2 Semester Java aber es waren doch nur Grundlagen. Meine Bachelorarbeit befasst sich mit einem nachrüstbaren Smart-Home-System für Zweitwohnsitze. So um nun auf den Punkt zu kommen, ich will eine App bauen um unter anderem auch eine Kamera am Raspberry Pi 3b+ zu steueren wie du es hier im Tutorial zeigst. Und für meine App wäre deine App ein guter Ansatzpunkt zum Starten, da ich mich erst seit zwei Tagen mit Android Studio auseinander setze. Leider bekomm ich die App nicht zum laufen da ich noch ein paar Fehler habe und nicht draufkomme wie ich diese ändern könnte. Wäre cool wenn du mir evtl. weiterhelfen könntest. Hoffe ich störe dich nicht beim Lernen für die Prüfungen ;). Liebe Grüße Felix

  2. Melvin

    Hallo Felix, du darfst mich gerne nennen, wie du magst, aber ich heiße Marvin. Ich glaube es wäre am Einfachsten, wenn du mir einfach einen Kommentar mit deiner Frage hinterlässt. Sollten die Fragen tiefgreifender sein, dann darfst du mir auch gerne eine Mail zusenden. Ich werde meine Mails immer gegen Abend checken und versuchen dir zu helfen. Gruß, Marvin/Melvin

  3. Julian

    Hallo Marvin,

    ich bin verzweifelt, da ich neu bin bei der Programmierung und noch kein Plan von Klassen und Layouts in Java habe. Ich schreibe meine Facharbeit über Hausautomation mit dem RaspberryPi und möchte als Eigenanteil einen Überwachungslivestream programmieren. Aber die App zu programmieren, damit komme ich einfach nicht klar. Kannst du mir das fertige Projekt zukommen lassen, das würde mir ungemein helfen!!!

  4. Marvin

    Hallo Julian,
    ich werde einmal schauen, ob ich den Sourcecode noch finden kann und werde ihn dir dann als ZIP per E-Mail oder Link im Artikel bereitstellen. Außerdem scheint die Formatierung im Artikel flöten gegangen zu sein. Das werde ich heute im Laufe des Tages noch nachbessern.

    Gruß,
    Marvin

  5. Alexander

    Hallo,

    toller Artikel. Allerdings verzweifle ich gerade an der Erstellung für die App. Ich scheitere an der MainActivity.Java, da ich nicht weiß, wo genau ich etwas einfügen soll. Zudem ist mir nicht klar, ob ich eine komplett neue App erstellen soll oder auf Basis der aktuellen GithHub-Version von Ip-cam view arbeiten soll.

    Sorry, bin ein absoluter Beginner und weiß so gut wie nichts über Java und Apps... Vielleicht kann mir jemand helfen? Ich möchte im Prinzip mit diesem Tutorial und dem von Raspberry Pi Tutorials die schwenkbare Kamera erstellen.

    Diese Fehlermeldungen habe ich in Android Studio, nachdem ich die Anleitung bis kurz vor der Bearbeitung der MainActivity.Java bearbeitet habe:

    Gradle DSL method not found: 'compile()'
    Possible causes:
    The project 'ipcam-view-master' may be using a version of the Android Gradle plug-in that does not contain the method (e.g. 'testCompile' was added in 1.1.0).
    Upgrade plugin to version 4.0.2 and sync project

    "The project 'ipcam-view-master' may be using a version of Gradle that does not contain the method.
    Open Gradle wrapper file

    The build file may be missing a Gradle plugin.
    Apply Gradle plugin"