10.11.2018, 18:34
(Dieser Beitrag wurde zuletzt bearbeitet: 10.11.2018, 22:23 von Bernhard45.)
Hallo zusammen,
auf Grund meines Threads "Ein minimales Internetradio für alte und neue Raspberrys" (https://radio-bastler.de/forum/showthread.php?tid=11484), erhielt ich per eMail die Anfrage ob soetwas nicht auch mit alten Android Smartphones möglich wäre. Die alten Geräte seinen heute einfach zu schwach um aktuelle Internetradio-Apps wirklich sinnig betreiben zu können. So ein altes Smartphone könnte ja, wenn es hier auch ein so minimales "iRadio" gebe, für die Zuspielung einer Internetradiostation zu einem Modulator dienen oder direkt über den Toneingang eines Radios betrieben werden. Der Mailschreiber wollte sogar ein altes Smartphone zerlegen und damit einen Radioumbau vornehmen. Zitat: "...ist doch auch nur ein Raspberry mit aufgeklebtem Touchscreen..." .
So ganz Unrecht hat der Schreiber nicht. Auch bei Android-Smartphones läuft in der Regel ein ARM-SoC mit 1 bis 8 Kernen, irgendwas ab 256 MB Ram und ganz genau ein Linux drauf.
Was die Ist-Situation mit den Apps angeht, so habe ich mal einen Selbsttest unternommen und gängige Radioplayer auf einem Uralt-Smartphone mit einem(!) Prozessorkern in der Leistungsklasse eine Raspberry Model A getestet.
Es stimmt, heutige Apps sind nicht mehr nutzbar auf solchen alten Kisten. Der Grund: Werbung, Werbung, Werbung, ..... Vor lauter Werbung kommen die Apps nicht dazu Ihre eigentliche Aufgabe zu erledigen: einfach einen Radiostream wiederzugeben!
Werbung1.JPG (Größe: 45,5 KB / Downloads: 715)
Schon beim Appstart werde ich "angemacht" und nach einem Liebesbekenntnis gefragt! Aber: So leicht bin ich nicht zu haben.
Werbung2.JPG (Größe: 51,38 KB / Downloads: 706)
Natürlich gibt es von jeder App auch eine "Premium"-Version, die man "Riskfree" testen kann. Was für ein "Risk" geht man denn da ein?
Allein das Laden der Benutzeroberfläche und dieses Werbebalkens benötigt eine Minute. Wir schauen auf die Uhr des Telefons! Musik ist noch keine zu hören!
Werbung3.JPG (Größe: 52,2 KB / Downloads: 717)
Wieder eine Minute später, endlich läuft das Radio an, ganz Langsam und mit vielen Aussetzern. Der Prozessor ist einfach überfordert.
Probieren wir eine andere aktuelle App.
Werbung4.JPG (Größe: 57,45 KB / Downloads: 709)
Auch hier wieder unerträgliche Ladezeiten und Werbung bis zum Abwinken! Das Umschalten des Senders ist wegen der überfrachteten Werbung leistungstechnisch nicht möglich. Das Telefon hat noch 5 Minuten damit zu verbringen, den Job des Wechseln eines Stream zu verarbeiten.
Ich gebe an dieser Stelle auf, bei anderen noch getesteten Apps kommt man zum gleichen Ergebnis.
Was ist eigentlich so schwierig daran, einfach in einer App einen Stream zu laden und die Ausgabe im Soundsystem des Android-OS zu machen?
Ich überfliege die Systemdokumentation und Entwicklerinfos zu Android und beschließe hier ein ähnliches "abgespecktes" Vorbild zu kreieren, wie mit dem iRadio für Ur-Raspberrys.
Zunächst lade ich das Andoid Studio, die kostenlose Entwicklungsumgebung für Apps von Google. Diese Entwicklungsumgebung gibt es für Windows-, Linux- und Mac-Systeme: https://developer.android.com/studio/
Dann schreibe ich eine kleine App mit ganz minimalistischer Benutzeroberfläche und relativ wenig Code für genau die oben definierte Aufgabe.
Hier der Quellcode in Version 0.0001 ;-) : https://mega.nz/#!OKxkmIwa!Vf4TvLcsQ57Ix...1d32G4O9RY
Eine fertig gebaute APK-Datei liegt in /iRadio/app/build/outputs/apk/debug/
Ein wunderbarer Codeeditor auf Basis der OpenSource-Version von IntelliJ der Firma JetBrains. Geschrieben in Java, aber unsere Entwicklungsmaschine (!nicht das Zielsystem!) hat ja genügend Power um solche Sachen laufen zu lassen. Braucht sie auch, denn die gesammte Android-Toolchain basiert praktisch auf viel Java und Kotlin nur weniges ist in C und C++ direkt kodiert. Auch wir werden unsere App für das Zielsystem in Java programmieren, das wird aber so wenig sein das der kleine Prozessor keine Probleme damit haben wird.
Unsere GUI können wir per Drag & Drop zeichnen, sie entspricht praktisch der minimalistischen FLTK-GUI des iRadios auf dem Raspberry Pi. Wer will kann sich mit den Sourcen oben ja eine eigene kleine (und optisch schönere) GUI erstellen.
Zuletzt noch das Manifest für unsere App, hier werden zum Beispiel die "Bedürfnisse" der App beim Betriebssystem definiert. Welche Dienste des OS möchte die App nutzen, welche Hardwarerechte braucht die App usw.
Schauen wir uns nun mal den eigentlichen Quellcode an:
Zunächst importieren wir mal alle Bibliotheken die wir benötigen.
Danach startet unsere Hauptklasse "MainActivity", vereinfacht gesagt die main()-Methode in einem C/C++ Programm. Der Rahmen des Programms wird von einem Projektwizzard im Android Studio schon generiert.
Danach legen wir erstmal ein paar Variablen/Objekte fest.
Eigentlich klar, hinter btn_play, btn_next, .... stecken unsere Bedienknöpfe von der Benutzeroberfläche.
TextView: Ist das GUI-Objekt wo später der Sendername, Titelname usw. erscheinen werden.
Mediaplayer: Das ist unser VLC-Pendant. Zwar gibt es VLC auch für Android, aber wir nehmen einfach den betriebssystemeigenen Mediaplayer, der ist fast genauso gut und man spart Ressourcen auf der Zielplatform.
radioDataThread: Dahinter verbirgt sich nachher der Thread der ständig in den Datenstrom schaut, die Metadaten (Sendername, Titelname usw.) holt und unsere Senderanzeige aktualisieren wird.
PowerManager und WifiManager: Mit diesen beiden Objekten erstellen wir später in unserer App eine Softwaresperre. Das Betriebsystem darf während die App läuft nicht das Wifi abschalten (klar warum) und auch nicht in den Schlafmodus gehen!
started: einfach ein Statusflag
URL metaDataURL: Da der Mediaplayer selbst keine Metadaten aus dem Datenstrom zieht, so wie es vlc macht, wird hier nochmal für eine anderes Objekt global die URL gehalten auf die unser Mediaplayer zeigt. Das kann man sicher auch noch rausnehmen und über Methoden realisieren, ich habe es jetzt auf die Schnelle so gemacht.
index: Ist einfach ein Index auf die aktuelle Stelle in der Senderliste, hier vorinitialisiert mit 1.
streamURLs: Unsere Senderliste als ArrayList.
Weiter gehts:
Wir überschreiben die onCreate - Methode mit unserer eigenen Variante. Diese Methode wird immer beim App-Start (also beim onCreate) ausgeführt.
Mit super.onCreate(savedInstanceState);
this.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
rufen wir die onCreate - Methode der Oberklasse auf, danach legen wir fest das unsere App immer im Breitformat (Landscape) laufen soll.
Wie beim Pimoroni- und auch beim iRadio-Paket wollen wir eine Sendeliste in ein Verzeichnis legen können. Das Verzeichnis soll der Download-Ordner des
jeweiligen Androidsystems sein. In der Variable folderDownload speichern wird zunächst das Ergebnis der Anfrage nach dem Systempfad zum Downloadordner ab.
In filePlaylist speichern wir dann den gesammten Pfad inklusive Dateinamen der Senderliste ab. Die Senderliste soll wie bei den anderen Internetradios playlist.m3u sein und diese
Senderliste ist 1:1 aufgebaut, d.h. direkt vom Pimoroni oder iRadio nutzbar. Über Log.e schicken wir den ermittelten Pfad lediglich nochmal in die Debugausgaben.
In der if-Anweisung schauen wir nun nach ob die Senderliste im Dateisystem existiert und ob diese auch lesbar ist. Ist das nicht der Fall, dann wird der Vorfall in die Debugausgabe geschrieben und eine interne Standardliste mit 5 Sendern erzeugt. Hier wären natürlich auch andere Behandlungen dieses Vorfalls denkbar, App mit Meldung beenden oder oder oder...
Wenn eine Senderliste im Dateisystem gefunden werden konnte, so lesen wir diese hier Zeilenweise aus und speichern jede Streamadresse (also jede Zeile) in der oben schon gezeigten
ArrayList (streamURLs) ab. Im catch-Zweig lassen wir uns im Fehlerfall etwas in die Debugausgabe schreiben, im finally-Zweig schließen wir die Datei nach dem Auslesen wieder.
Hier holen wir uns nun die Referenzen vom PowerManager und des WifiManagers ins Programm. Die Softwaresperren sind noch nicht aktiv!
Wir erzeugen uns jetzt einen neuen MediaPlayer und stellen Ihn darauf ein Internetradio zu empfangen.
Die Variable btn_next vom Typ Button bekommt nun ihre Verbindung zum eigentlichen Knopf auf der Benutzeroberfläche. Gleichzeitig installieren wir an diesem Knopf einen "Listener". Dieser "Listener" hört auf "OnClick"-Events, also wenn auf den Knopf gedrückt wird. Was genau passiert dann? Es wird geschaut wie groß die Sendeliste ist und ob die Indexvariable die den aktuell eingestellten Sender repräsentiert bereits auf der letzten Position der Senderliste angekommen ist. Ist das nicht der Fall, dann wird der Index um eins erhöht, ist der Index bereits am Ende der Programmliste angekommen, wird der Index wieder auf die erste Position zurückgesetzt. Danach wird dann noch die Funktion "switchProgram" aufgerufen, diese macht die eigentliche Umschaltung am Mediaplayer, mehr dazu später.
Analog erfolgt im Programmcode jetzt die Verknüpfung mit den anderen Knöpfen auf der GUI. Braucht es dazu eine Erklärung?
Jetzt Verknüpfen wir unser GUI-Element zur Anzeige der Senderinfos mit der Variablen textView. Gleichzeitig geben wir an, das als Schriftfarbe für die Senderinfos "grün" gewählt wird.
Gleich kommen wir in die heiße Phase. Unsere App meldet an, das wir ab jetzt kein Powersaving haben wollen und auch Wifi nicht abgeschaltet werden soll.
Im ersten try-Zweig versuchen wir jetzt unseren Mediaplayer zu starten. Wir übergeben Ihm die Senderadresse aus der Senderliste. Welche? Die Adrese auf die index zeigt.
Geht etwas schief, werden wir es in der Debugausgabe sehen.
Im zweiten try-Zweig übergeben wir die Senderadresse an die Variable metaDataURL. Diesmal aber nicht als String sondern als URL-Object. Auch hier, geht was schief, lesen wir es im Debugger des Android Studios.
Unser Mediaplayer läuft bereits, was brauchen wir noch? Wir erzeugen einen neuen Thread, eine Instanz der Inner-Class RadioDataThread.
Dieser Thread soll nun ständig im Datenstrom nach Metadaten suchen und uns periodisch im textView anzeigen.
Wie die Klasse des RadioDataThreads aussieht, nun das sehen wir direkt im Anschluß im Quellcode.
Die Klasse RadioDataThread wird von der Systemklasse Thread abgeleitet und erbt damit alle Eigenschaften dieser. Ein AsyncTask ginge sicher auch.
Das was in diesem Thread gemacht werden soll, steht in der Methode run() des Threads. Hier wird in einer Endlosschleife nach den Metadaten im Datenstrom gesucht. Das übernehmen die Instanzen der Klassen ParsingHeaderData und TrackData. Beide sindin der Datei ParsingHeaderData.java definiert. Diese Klassen habe ich aus dem Netz fertig übernommen und für meine Zwecke leicht modifiziert. Ein Versuch an die Metadaten mit dem Systemeigenen Metacrawler zu kommen, verlief vorerst leider negativ. Ich konnte zwar technische Daten wie Bitrate, Samplerate etc. extrahieren, Streamtitle usw. dagegen blieben immer leer, deshalb erstmal der Weg über eine Fremdklasse. Irgendwann finde ich die Zeit mir das mal genauer anzuschauen und nach dem Problem zu suchen.
Was passiert noch im Programm?
Nun wir überschreiben noch die Methode onDestroy() durch unsere eigene Version. Das ist das Gegenstück zur onCreate und wird bei Beendigung der App aufgerufen.
Hier räumen wir auf. Wir "entlassen" unseren Mediaplayer und die beiden Softwaresperren heben wir auch wieder auf.
In der switchProgram()-Methode reagieren wir auf die Änderung der Variable index durch die beiden onClick-Listener weiter oben. Wir stoppen den Mediaplayer, schicken Ihm die neue Senderadresse aus der Senderliste und bereiten Ihn auf den Neustart mit neuer Sender-URL vor. Auch für den "Parser" der Metadaten setzen wir die Adresse neu. Falls etwas schief geht, lesen wir das wieder im Debugger.
Das wäre also das Programm zu unserem kleinen Internetradio. Keine Werbung und nichts unnötiges. Wie bekommen wir das Programm jetzt auf das alte Handy, Tablet ,....?
Zunächst müssen wir für unser Android Zielsystem und falls unsere Entwicklungsmaschine eine Windowsmaschine ist, einen vom Hersteller verfügbaren ADB-Treiber installieren.
Google hat dafür schon eine Tabelle vorbereitet: https://developer.android.com/studio/run/oem-usb
Ist das Android-Gerät nicht dabei, hilft uns sicher Google weiter.
Ist der Treiber installiert, müssen wir auf unserem Android Zielsystem noch die Entwickleroptionen aktivieren. In der Regel geht man so vor:
Entwicklermodus1.JPG (Größe: 56,23 KB / Downloads: 697)
Man geht bei Android über "Einstellungen" zu den "Geräteinformationen". Dort scrollt man runter bis zur "Buildnummer". Auch wenn dieser Menüpunkt deaktiviert aussieht, mehrfach (ich glaube es waren 7 mal) anklicken!
Danach findet man in den "Einstellungen" den sonst versteckten Menüpunkt "Entwickleroptionen"
Entwicklermodus2a.jpg (Größe: 69,11 KB / Downloads: 695)
Jetzt aktiviert man jene Entwickleroptionen und wählt dabei praktischerweise noch folgende (selbsterklärenden) Einstellungen aus:
Entwicklermodus3.JPG (Größe: 60,18 KB / Downloads: 693)
Entwicklermodus4.JPG (Größe: 59,59 KB / Downloads: 692)
Nun ist das Android Zielsystem eingerichtet. Ist das Android-Smartphone oder Tablet bereits per USB-Ladekabel mit dem Entwicklungsrechner verbunden, geht es im Android Studio weiter.
Wir starten unsere Kompilierung durch klicken auf den unter 1 im Bild gezeigten Pfeil. Im sich danach öffnenden Fenster wählt man sein per USB verbundenes Android Zielsystem aus.
Alternativ wäre auch ein emuliertes Android-System möglich. So eine Emulation setzt aber wirklich eine leistungsfähige Mehrkern-Entwicklungsmaschine voraus, 8 GB RAM ist wirklich unterste Grenze, mit einem 16+x GB System und 4 Kernen macht es dann richtig Spaß auch in der Emulation zu arbeiten. Wir aber begnügen uns hier mit richtiger Android Hardware und brauchen keine Emulation.
Ein abschließender Klick auf "Okay" startet dann den Build-Vorgang und das Aufspielen und Starten der App auf dem Android-Zielsystem.
Das Ergebnis:
Eine werbefreie Internetradio-App nach dem iRadio-Vorbild, die nur genau das macht, was sie auch machen soll. Brauchten fertige Apps noch 3 Minuten zum Starten des Internetradios und 5 Minuten zum Umschalten des Senders, sind es jetzt 4 Sekunden Startzeit und weitere 5 Sekunden bis der Stream gepuffert ist und störungsfrei läuft. Das Umschalten gelingt innerhalb von Sekunden.
Da das Android-Zielsystem an der Entwicklermaschine gleich auch noch als USB-Speicherstick gemountet wird, lässt sich die Playlist meiner anderen Internetradios direkt per Dateibrowser übertragen.
Filesys1.JPG (Größe: 35,18 KB / Downloads: 685)
Filesys2.JPG (Größe: 16,9 KB / Downloads: 685)
Ich hoffe Euch hat der Ausflug in die Android-Welt gefallen, die Fragen des Mailers wurden beantwortet und einige von Euch werden motiviert auch mal in dieser Richtung aktiv zu werden. Entweder auf dem Raspberry (auf dem auch Android laufen kann) oder direkt bei der Wiederbelebung und beim Umbau alter Smartphones zu Radios.
Gruß
Bernhard45
auf Grund meines Threads "Ein minimales Internetradio für alte und neue Raspberrys" (https://radio-bastler.de/forum/showthread.php?tid=11484), erhielt ich per eMail die Anfrage ob soetwas nicht auch mit alten Android Smartphones möglich wäre. Die alten Geräte seinen heute einfach zu schwach um aktuelle Internetradio-Apps wirklich sinnig betreiben zu können. So ein altes Smartphone könnte ja, wenn es hier auch ein so minimales "iRadio" gebe, für die Zuspielung einer Internetradiostation zu einem Modulator dienen oder direkt über den Toneingang eines Radios betrieben werden. Der Mailschreiber wollte sogar ein altes Smartphone zerlegen und damit einen Radioumbau vornehmen. Zitat: "...ist doch auch nur ein Raspberry mit aufgeklebtem Touchscreen..." .
So ganz Unrecht hat der Schreiber nicht. Auch bei Android-Smartphones läuft in der Regel ein ARM-SoC mit 1 bis 8 Kernen, irgendwas ab 256 MB Ram und ganz genau ein Linux drauf.
Was die Ist-Situation mit den Apps angeht, so habe ich mal einen Selbsttest unternommen und gängige Radioplayer auf einem Uralt-Smartphone mit einem(!) Prozessorkern in der Leistungsklasse eine Raspberry Model A getestet.
Es stimmt, heutige Apps sind nicht mehr nutzbar auf solchen alten Kisten. Der Grund: Werbung, Werbung, Werbung, ..... Vor lauter Werbung kommen die Apps nicht dazu Ihre eigentliche Aufgabe zu erledigen: einfach einen Radiostream wiederzugeben!
Werbung1.JPG (Größe: 45,5 KB / Downloads: 715)
Schon beim Appstart werde ich "angemacht" und nach einem Liebesbekenntnis gefragt! Aber: So leicht bin ich nicht zu haben.
Werbung2.JPG (Größe: 51,38 KB / Downloads: 706)
Natürlich gibt es von jeder App auch eine "Premium"-Version, die man "Riskfree" testen kann. Was für ein "Risk" geht man denn da ein?
Allein das Laden der Benutzeroberfläche und dieses Werbebalkens benötigt eine Minute. Wir schauen auf die Uhr des Telefons! Musik ist noch keine zu hören!
Werbung3.JPG (Größe: 52,2 KB / Downloads: 717)
Wieder eine Minute später, endlich läuft das Radio an, ganz Langsam und mit vielen Aussetzern. Der Prozessor ist einfach überfordert.
Probieren wir eine andere aktuelle App.
Werbung4.JPG (Größe: 57,45 KB / Downloads: 709)
Auch hier wieder unerträgliche Ladezeiten und Werbung bis zum Abwinken! Das Umschalten des Senders ist wegen der überfrachteten Werbung leistungstechnisch nicht möglich. Das Telefon hat noch 5 Minuten damit zu verbringen, den Job des Wechseln eines Stream zu verarbeiten.
Ich gebe an dieser Stelle auf, bei anderen noch getesteten Apps kommt man zum gleichen Ergebnis.
Was ist eigentlich so schwierig daran, einfach in einer App einen Stream zu laden und die Ausgabe im Soundsystem des Android-OS zu machen?
Ich überfliege die Systemdokumentation und Entwicklerinfos zu Android und beschließe hier ein ähnliches "abgespecktes" Vorbild zu kreieren, wie mit dem iRadio für Ur-Raspberrys.
Zunächst lade ich das Andoid Studio, die kostenlose Entwicklungsumgebung für Apps von Google. Diese Entwicklungsumgebung gibt es für Windows-, Linux- und Mac-Systeme: https://developer.android.com/studio/
Dann schreibe ich eine kleine App mit ganz minimalistischer Benutzeroberfläche und relativ wenig Code für genau die oben definierte Aufgabe.
Hier der Quellcode in Version 0.0001 ;-) : https://mega.nz/#!OKxkmIwa!Vf4TvLcsQ57Ix...1d32G4O9RY
Eine fertig gebaute APK-Datei liegt in /iRadio/app/build/outputs/apk/debug/
Ein wunderbarer Codeeditor auf Basis der OpenSource-Version von IntelliJ der Firma JetBrains. Geschrieben in Java, aber unsere Entwicklungsmaschine (!nicht das Zielsystem!) hat ja genügend Power um solche Sachen laufen zu lassen. Braucht sie auch, denn die gesammte Android-Toolchain basiert praktisch auf viel Java und Kotlin nur weniges ist in C und C++ direkt kodiert. Auch wir werden unsere App für das Zielsystem in Java programmieren, das wird aber so wenig sein das der kleine Prozessor keine Probleme damit haben wird.
Unsere GUI können wir per Drag & Drop zeichnen, sie entspricht praktisch der minimalistischen FLTK-GUI des iRadios auf dem Raspberry Pi. Wer will kann sich mit den Sourcen oben ja eine eigene kleine (und optisch schönere) GUI erstellen.
Zuletzt noch das Manifest für unsere App, hier werden zum Beispiel die "Bedürfnisse" der App beim Betriebssystem definiert. Welche Dienste des OS möchte die App nutzen, welche Hardwarerechte braucht die App usw.
Schauen wir uns nun mal den eigentlichen Quellcode an:
Code:
package de.weiro.iradio;
import android.content.Context;
import android.graphics.Color;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.net.wifi.WifiManager;
import android.os.PowerManager;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.content.pm.ActivityInfo;
import android.os.Environment;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
public class MainActivity extends AppCompatActivity {
Button btn_play, btn_next, btn_prev, btn_exit;
TextView textView;
MediaMetadataRetriever metaRetriever = new MediaMetadataRetriever();
MediaPlayer mediaPlayer;
Thread radioDataThread;
PowerManager pm;
PowerManager.WakeLock wakeLock;
WifiManager.WifiLock wifiLock;
static boolean started = false;
URL metaDataURL;
int index = 1;
List<String> streamURLs = new ArrayList<String>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
File folderDownload = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
File filePlaylist = new File(folderDownload, "playlist.m3u");
//File file = new File("/storage/sdcard0/Download/playlist.m3u");
Log.e("Pfad zur Playlist",filePlaylist.toString());
if (!filePlaylist.canRead() || !filePlaylist.isFile()) {
Log.e("Senderliste:" , "keine gefunden, nutze Defaultliste!");
streamURLs.add("http://bbcmedia.ic.llnwd.net/stream/bbcmedia_radio2_mf_p");
streamURLs.add("http://st02.dlf.de/dlf/02/128/mp3/stream.mp3");
streamURLs.add("http://str0.creacast.com/hitradio_ohr_thema1");
streamURLs.add("http://stream.srg-ssr.ch/m/drs4news/aacp_96");
streamURLs.add("http://stream1.evasionfm.com/Chante_France");
}
BufferedReader in = null;
try {
//in = new BufferedReader(new FileReader("/storage/sdcard0/Download/playlist.m3u"));
in = new BufferedReader(new FileReader(filePlaylist.toString()));
String zeile = null;
while ((zeile = in.readLine()) != null) {
streamURLs.add(zeile);
Log.e("Senderliste lesen:" , zeile);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (in != null)
try {
in.close();
} catch (IOException e) {
}
}
pm = (PowerManager) getApplicationContext().getSystemService(Context.POWER_SERVICE);
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "myWakeLock");
wifiLock = ((WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE))
.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF , "myWiFiLock");
mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);
mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
mp.start();
started = true;
btn_play.setText("PAUSE");
}
});
setContentView(R.layout.activity_main);
btn_next = (Button) findViewById(R.id.button_next);
btn_next.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (index < (streamURLs.size()-1) )
index++;
else
index = 0;
switchProgram();
}
});
btn_prev = (Button) findViewById(R.id.button_prev);
btn_prev.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View v) {
if (index == 0)
index = streamURLs.size() - 1;
else
index--;
switchProgram();
}
});
btn_play = (Button) findViewById(R.id.button_play);
btn_play.setText("LADE");
btn_play.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View view) {
if (started) {
started = false;
mediaPlayer.pause();
btn_play.setText("PLAY");
} else {
started = true;
mediaPlayer.start();
btn_play.setText("PAUSE");
}
}
});
btn_exit = (Button) findViewById(R.id.button_exit);
btn_exit.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
finish();
}
});
textView = (TextView) findViewById(R.id.textView);
textView.setTextColor(Color.GREEN);
wifiLock.acquire();
wakeLock.acquire();
try
{
mediaPlayer.reset();
mediaPlayer.setDataSource(streamURLs.get(index));
mediaPlayer.prepareAsync();
} catch (IOException e) {
e.printStackTrace();
}
try {
metaDataURL = new URL(streamURLs.get(index));
}
catch (MalformedURLException ex) {
ex.printStackTrace();
}
radioDataThread = new RadioDataThread();
radioDataThread.start();
}
public class RadioDataThread extends Thread {
ParsingHeaderData streaming;
ParsingHeaderData.TrackData trackData;
public void run() {
while (true) {
try {
streaming = new ParsingHeaderData();
trackData = streaming.getTrackDetails(metaDataURL);
textView.setText("Sender:" + trackData.channel + "\nArtist/Titel:" + trackData.artist + " - " + trackData.title +
"\nGenre:" + trackData.genre + "\nAudioinfos: " +trackData.audioinfo);
getWindow().getDecorView().findViewById(android.R.id.content).postInvalidate();
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mediaPlayer != null) {
mediaPlayer.stop();
mediaPlayer.release();
}
if (wakeLock != null) {
if (wakeLock.isHeld()) {
wakeLock.release();
}
}
if (wifiLock != null) {
if (wifiLock.isHeld()) {
wifiLock.release();
}
}
}
protected void switchProgram() {
try {
mediaPlayer.stop();
mediaPlayer.reset();
mediaPlayer.setDataSource(streamURLs.get(index));
mediaPlayer.prepareAsync();
try {
metaDataURL = new URL(streamURLs.get(index));
}
catch (MalformedURLException ex) {
ex.printStackTrace();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
Zunächst importieren wir mal alle Bibliotheken die wir benötigen.
Zitat:import android.content.Context;
import android.graphics.Color;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.MediaMetadataRetriever;
import android.net.wifi.WifiManager;
import android.os.PowerManager;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.content.pm.ActivityInfo;
import android.os.Environment;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
Danach startet unsere Hauptklasse "MainActivity", vereinfacht gesagt die main()-Methode in einem C/C++ Programm. Der Rahmen des Programms wird von einem Projektwizzard im Android Studio schon generiert.
Zitat:Code:public class MainActivity extends AppCompatActivity {
Danach legen wir erstmal ein paar Variablen/Objekte fest.
Zitat:Button btn_play, btn_next, btn_prev, btn_exit;
TextView textView;
MediaPlayer mediaPlayer;
Thread radioDataThread;
PowerManager pm;
PowerManager.WakeLock wakeLock;
WifiManager.WifiLock wifiLock;
static boolean started = false;
URL metaDataURL;
int index = 1;
List<String> streamURLs = new ArrayList<String>();
Eigentlich klar, hinter btn_play, btn_next, .... stecken unsere Bedienknöpfe von der Benutzeroberfläche.
TextView: Ist das GUI-Objekt wo später der Sendername, Titelname usw. erscheinen werden.
Mediaplayer: Das ist unser VLC-Pendant. Zwar gibt es VLC auch für Android, aber wir nehmen einfach den betriebssystemeigenen Mediaplayer, der ist fast genauso gut und man spart Ressourcen auf der Zielplatform.
radioDataThread: Dahinter verbirgt sich nachher der Thread der ständig in den Datenstrom schaut, die Metadaten (Sendername, Titelname usw.) holt und unsere Senderanzeige aktualisieren wird.
PowerManager und WifiManager: Mit diesen beiden Objekten erstellen wir später in unserer App eine Softwaresperre. Das Betriebsystem darf während die App läuft nicht das Wifi abschalten (klar warum) und auch nicht in den Schlafmodus gehen!
started: einfach ein Statusflag
URL metaDataURL: Da der Mediaplayer selbst keine Metadaten aus dem Datenstrom zieht, so wie es vlc macht, wird hier nochmal für eine anderes Objekt global die URL gehalten auf die unser Mediaplayer zeigt. Das kann man sicher auch noch rausnehmen und über Methoden realisieren, ich habe es jetzt auf die Schnelle so gemacht.
index: Ist einfach ein Index auf die aktuelle Stelle in der Senderliste, hier vorinitialisiert mit 1.
streamURLs: Unsere Senderliste als ArrayList.
Weiter gehts:
Zitat:@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
File folderDownload = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
File filePlaylist = new File(folderDownload, "playlist.m3u");
//File file = new File("/storage/sdcard0/Download/playlist.m3u");
Log.e("Pfad zur Playlist",filePlaylist.toString());
if (!filePlaylist.canRead() || !filePlaylist.isFile()) {
Log.e("Senderliste:" , "keine gefunden, nutze Defaultliste!");
streamURLs.add("http://bbcmedia.ic.llnwd.net/stream/bbcmedia_radio2_mf_p");
streamURLs.add("http://st02.dlf.de/dlf/02/128/mp3/stream.mp3");
streamURLs.add("http://str0.creacast.com/hitradio_ohr_thema1");
streamURLs.add("http://stream.srg-ssr.ch/m/drs4news/aacp_96");
streamURLs.add("http://stream1.evasionfm.com/Chante_France");
}
BufferedReader in = null;
try {
//in = new BufferedReader(new FileReader("/storage/sdcard0/Download/playlist.m3u"));
in = new BufferedReader(new FileReader(filePlaylist.toString()));
String zeile = null;
while ((zeile = in.readLine()) != null) {
streamURLs.add(zeile);
Log.e("Senderliste lesen:" , zeile);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (in != null)
try {
in.close();
} catch (IOException e) {
}
}
Wir überschreiben die onCreate - Methode mit unserer eigenen Variante. Diese Methode wird immer beim App-Start (also beim onCreate) ausgeführt.
Mit super.onCreate(savedInstanceState);
this.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
rufen wir die onCreate - Methode der Oberklasse auf, danach legen wir fest das unsere App immer im Breitformat (Landscape) laufen soll.
Zitat: File folderDownload = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
File filePlaylist = new File(folderDownload, "playlist.m3u");
//File file = new File("/storage/sdcard0/Download/playlist.m3u");
Log.e("Pfad zur Playlist",filePlaylist.toString());
Wie beim Pimoroni- und auch beim iRadio-Paket wollen wir eine Sendeliste in ein Verzeichnis legen können. Das Verzeichnis soll der Download-Ordner des
jeweiligen Androidsystems sein. In der Variable folderDownload speichern wird zunächst das Ergebnis der Anfrage nach dem Systempfad zum Downloadordner ab.
In filePlaylist speichern wir dann den gesammten Pfad inklusive Dateinamen der Senderliste ab. Die Senderliste soll wie bei den anderen Internetradios playlist.m3u sein und diese
Senderliste ist 1:1 aufgebaut, d.h. direkt vom Pimoroni oder iRadio nutzbar. Über Log.e schicken wir den ermittelten Pfad lediglich nochmal in die Debugausgaben.
Zitat: if (!filePlaylist.canRead() || !filePlaylist.isFile()) {
Log.e("Senderliste:" , "keine gefunden, nutze Defaultliste!");
streamURLs.add("http://bbcmedia.ic.llnwd.net/stream/bbcmedia_radio2_mf_p");
streamURLs.add("http://st02.dlf.de/dlf/02/128/mp3/stream.mp3");
streamURLs.add("http://str0.creacast.com/hitradio_ohr_thema1");
streamURLs.add("http://stream.srg-ssr.ch/m/drs4news/aacp_96");
streamURLs.add("http://stream1.evasionfm.com/Chante_France");
}
In der if-Anweisung schauen wir nun nach ob die Senderliste im Dateisystem existiert und ob diese auch lesbar ist. Ist das nicht der Fall, dann wird der Vorfall in die Debugausgabe geschrieben und eine interne Standardliste mit 5 Sendern erzeugt. Hier wären natürlich auch andere Behandlungen dieses Vorfalls denkbar, App mit Meldung beenden oder oder oder...
Zitat: try {
//in = new BufferedReader(new FileReader("/storage/sdcard0/Download/playlist.m3u"));
in = new BufferedReader(new FileReader(filePlaylist.toString()));
String zeile = null;
while ((zeile = in.readLine()) != null) {
streamURLs.add(zeile);
Log.e("Senderliste lesen:" , zeile);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (in != null)
try {
in.close();
} catch (IOException e) {
}
}
Wenn eine Senderliste im Dateisystem gefunden werden konnte, so lesen wir diese hier Zeilenweise aus und speichern jede Streamadresse (also jede Zeile) in der oben schon gezeigten
ArrayList (streamURLs) ab. Im catch-Zweig lassen wir uns im Fehlerfall etwas in die Debugausgabe schreiben, im finally-Zweig schließen wir die Datei nach dem Auslesen wieder.
Zitat:pm = (PowerManager) getApplicationContext().getSystemService(Context.POWER_SERVICE);
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "myWakeLock");
wifiLock = ((WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE))
.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF , "myWiFiLock");
Hier holen wir uns nun die Referenzen vom PowerManager und des WifiManagers ins Programm. Die Softwaresperren sind noch nicht aktiv!
Zitat:mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);
mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
mp.start();
started = true;
btn_play.setText("PAUSE");
}
});
Wir erzeugen uns jetzt einen neuen MediaPlayer und stellen Ihn darauf ein Internetradio zu empfangen.
Zitat:btn_next = (Button) findViewById(R.id.button_next);
btn_next.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (index < (streamURLs.size()-1) )
index++;
else
index = 0;
switchProgram();
}
})
Die Variable btn_next vom Typ Button bekommt nun ihre Verbindung zum eigentlichen Knopf auf der Benutzeroberfläche. Gleichzeitig installieren wir an diesem Knopf einen "Listener". Dieser "Listener" hört auf "OnClick"-Events, also wenn auf den Knopf gedrückt wird. Was genau passiert dann? Es wird geschaut wie groß die Sendeliste ist und ob die Indexvariable die den aktuell eingestellten Sender repräsentiert bereits auf der letzten Position der Senderliste angekommen ist. Ist das nicht der Fall, dann wird der Index um eins erhöht, ist der Index bereits am Ende der Programmliste angekommen, wird der Index wieder auf die erste Position zurückgesetzt. Danach wird dann noch die Funktion "switchProgram" aufgerufen, diese macht die eigentliche Umschaltung am Mediaplayer, mehr dazu später.
Analog erfolgt im Programmcode jetzt die Verknüpfung mit den anderen Knöpfen auf der GUI. Braucht es dazu eine Erklärung?
Zitat:textView = (TextView) findViewById(R.id.textView);
textView.setTextColor(Color.GREEN);
Jetzt Verknüpfen wir unser GUI-Element zur Anzeige der Senderinfos mit der Variablen textView. Gleichzeitig geben wir an, das als Schriftfarbe für die Senderinfos "grün" gewählt wird.
Zitat:wifiLock.acquire();
wakeLock.acquire();
Gleich kommen wir in die heiße Phase. Unsere App meldet an, das wir ab jetzt kein Powersaving haben wollen und auch Wifi nicht abgeschaltet werden soll.
Zitat:try
{
mediaPlayer.reset();
mediaPlayer.setDataSource(streamURLs.get(index));
mediaPlayer.prepareAsync();
} catch (IOException e) {
e.printStackTrace();
}
try {
metaDataURL = new URL(streamURLs.get(index));
}
catch (MalformedURLException ex) {
ex.printStackTrace();
}
Im ersten try-Zweig versuchen wir jetzt unseren Mediaplayer zu starten. Wir übergeben Ihm die Senderadresse aus der Senderliste. Welche? Die Adrese auf die index zeigt.
Geht etwas schief, werden wir es in der Debugausgabe sehen.
Im zweiten try-Zweig übergeben wir die Senderadresse an die Variable metaDataURL. Diesmal aber nicht als String sondern als URL-Object. Auch hier, geht was schief, lesen wir es im Debugger des Android Studios.
Zitat:radioDataThread = new RadioDataThread();
radioDataThread.start();
Unser Mediaplayer läuft bereits, was brauchen wir noch? Wir erzeugen einen neuen Thread, eine Instanz der Inner-Class RadioDataThread.
Dieser Thread soll nun ständig im Datenstrom nach Metadaten suchen und uns periodisch im textView anzeigen.
Wie die Klasse des RadioDataThreads aussieht, nun das sehen wir direkt im Anschluß im Quellcode.
Zitat:public class RadioDataThread extends Thread {
ParsingHeaderData streaming;
ParsingHeaderData.TrackData trackData;
public void run() {
while (true) {
try {
streaming = new ParsingHeaderData();
trackData = streaming.getTrackDetails(metaDataURL);
textView.setText("Sender:" + trackData.channel + "\nArtist/Titel:" + trackData.artist + " - " + trackData.title +
"\nGenre:" + trackData.genre + "\nAudioinfos: " +trackData.audioinfo);
getWindow().getDecorView().findViewById(android.R.id.content).postInvalidate();
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
Die Klasse RadioDataThread wird von der Systemklasse Thread abgeleitet und erbt damit alle Eigenschaften dieser. Ein AsyncTask ginge sicher auch.
Das was in diesem Thread gemacht werden soll, steht in der Methode run() des Threads. Hier wird in einer Endlosschleife nach den Metadaten im Datenstrom gesucht. Das übernehmen die Instanzen der Klassen ParsingHeaderData und TrackData. Beide sindin der Datei ParsingHeaderData.java definiert. Diese Klassen habe ich aus dem Netz fertig übernommen und für meine Zwecke leicht modifiziert. Ein Versuch an die Metadaten mit dem Systemeigenen Metacrawler zu kommen, verlief vorerst leider negativ. Ich konnte zwar technische Daten wie Bitrate, Samplerate etc. extrahieren, Streamtitle usw. dagegen blieben immer leer, deshalb erstmal der Weg über eine Fremdklasse. Irgendwann finde ich die Zeit mir das mal genauer anzuschauen und nach dem Problem zu suchen.
Was passiert noch im Programm?
Zitat:@Override
protected void onDestroy() {
super.onDestroy();
if (mediaPlayer != null) {
mediaPlayer.stop();
mediaPlayer.release();
}
if (wakeLock != null) {
if (wakeLock.isHeld()) {
wakeLock.release();
}
}
if (wifiLock != null) {
if (wifiLock.isHeld()) {
wifiLock.release();
}
}
}
Nun wir überschreiben noch die Methode onDestroy() durch unsere eigene Version. Das ist das Gegenstück zur onCreate und wird bei Beendigung der App aufgerufen.
Hier räumen wir auf. Wir "entlassen" unseren Mediaplayer und die beiden Softwaresperren heben wir auch wieder auf.
Zitat:protected void switchProgram() {
try {
mediaPlayer.stop();
mediaPlayer.reset();
mediaPlayer.setDataSource(streamURLs.get(index));
mediaPlayer.prepareAsync();
try {
metaDataURL = new URL(streamURLs.get(index));
}
catch (MalformedURLException ex) {
ex.printStackTrace();
}
} catch (IOException e) {
e.printStackTrace();
}
}
In der switchProgram()-Methode reagieren wir auf die Änderung der Variable index durch die beiden onClick-Listener weiter oben. Wir stoppen den Mediaplayer, schicken Ihm die neue Senderadresse aus der Senderliste und bereiten Ihn auf den Neustart mit neuer Sender-URL vor. Auch für den "Parser" der Metadaten setzen wir die Adresse neu. Falls etwas schief geht, lesen wir das wieder im Debugger.
Das wäre also das Programm zu unserem kleinen Internetradio. Keine Werbung und nichts unnötiges. Wie bekommen wir das Programm jetzt auf das alte Handy, Tablet ,....?
Zunächst müssen wir für unser Android Zielsystem und falls unsere Entwicklungsmaschine eine Windowsmaschine ist, einen vom Hersteller verfügbaren ADB-Treiber installieren.
Google hat dafür schon eine Tabelle vorbereitet: https://developer.android.com/studio/run/oem-usb
Ist das Android-Gerät nicht dabei, hilft uns sicher Google weiter.
Ist der Treiber installiert, müssen wir auf unserem Android Zielsystem noch die Entwickleroptionen aktivieren. In der Regel geht man so vor:
Entwicklermodus1.JPG (Größe: 56,23 KB / Downloads: 697)
Man geht bei Android über "Einstellungen" zu den "Geräteinformationen". Dort scrollt man runter bis zur "Buildnummer". Auch wenn dieser Menüpunkt deaktiviert aussieht, mehrfach (ich glaube es waren 7 mal) anklicken!
Danach findet man in den "Einstellungen" den sonst versteckten Menüpunkt "Entwickleroptionen"
Entwicklermodus2a.jpg (Größe: 69,11 KB / Downloads: 695)
Jetzt aktiviert man jene Entwickleroptionen und wählt dabei praktischerweise noch folgende (selbsterklärenden) Einstellungen aus:
Entwicklermodus3.JPG (Größe: 60,18 KB / Downloads: 693)
Entwicklermodus4.JPG (Größe: 59,59 KB / Downloads: 692)
Nun ist das Android Zielsystem eingerichtet. Ist das Android-Smartphone oder Tablet bereits per USB-Ladekabel mit dem Entwicklungsrechner verbunden, geht es im Android Studio weiter.
Wir starten unsere Kompilierung durch klicken auf den unter 1 im Bild gezeigten Pfeil. Im sich danach öffnenden Fenster wählt man sein per USB verbundenes Android Zielsystem aus.
Alternativ wäre auch ein emuliertes Android-System möglich. So eine Emulation setzt aber wirklich eine leistungsfähige Mehrkern-Entwicklungsmaschine voraus, 8 GB RAM ist wirklich unterste Grenze, mit einem 16+x GB System und 4 Kernen macht es dann richtig Spaß auch in der Emulation zu arbeiten. Wir aber begnügen uns hier mit richtiger Android Hardware und brauchen keine Emulation.
Ein abschließender Klick auf "Okay" startet dann den Build-Vorgang und das Aufspielen und Starten der App auf dem Android-Zielsystem.
Das Ergebnis:
Eine werbefreie Internetradio-App nach dem iRadio-Vorbild, die nur genau das macht, was sie auch machen soll. Brauchten fertige Apps noch 3 Minuten zum Starten des Internetradios und 5 Minuten zum Umschalten des Senders, sind es jetzt 4 Sekunden Startzeit und weitere 5 Sekunden bis der Stream gepuffert ist und störungsfrei läuft. Das Umschalten gelingt innerhalb von Sekunden.
Da das Android-Zielsystem an der Entwicklermaschine gleich auch noch als USB-Speicherstick gemountet wird, lässt sich die Playlist meiner anderen Internetradios direkt per Dateibrowser übertragen.
Filesys1.JPG (Größe: 35,18 KB / Downloads: 685)
Filesys2.JPG (Größe: 16,9 KB / Downloads: 685)
Ich hoffe Euch hat der Ausflug in die Android-Welt gefallen, die Fragen des Mailers wurden beantwortet und einige von Euch werden motiviert auch mal in dieser Richtung aktiv zu werden. Entweder auf dem Raspberry (auf dem auch Android laufen kann) oder direkt bei der Wiederbelebung und beim Umbau alter Smartphones zu Radios.
Gruß
Bernhard45
Ansprechpartner für Umbau oder Modernisierung von Röhrenradios mittels SDR,DAB+,Internetradio,Firmwareentwicklung.
Unser Open-Source Softwarebaukasten für Internetradios gibt es auf der Github-Seite! Projekt: BM45/iRadio (Google "github BM45/iRadio")
Unser Open-Source Softwarebaukasten für Internetradios gibt es auf der Github-Seite! Projekt: BM45/iRadio (Google "github BM45/iRadio")