From 2aee8594ee08a040a91adecbe7dfe7531b83e541 Mon Sep 17 00:00:00 2001 From: Unknown Date: Sun, 5 Mar 2017 03:36:00 +0100 Subject: [PATCH] added hls stream support --- app/build.gradle | 4 +- .../radiodroid2/IPlayerService.aidl | 1 + .../radiodroid2/ActivityPlayerInfo.java | 3 + .../radiodroid2/PlayerService.java | 28 +- .../radiodroid2/PlayerServiceUtil.java | 11 + .../radiodroid2/StreamProxy.java | 293 +++++++++++++----- .../radiodroid2/data/DataRadioStation.java | 7 + .../radiodroid2/data/PlaylistM3U.java | 88 ++++++ .../radiodroid2/data/PlaylistM3UEntry.java | 91 ++++++ .../interfaces/IStreamProxyEventReceiver.java | 2 +- 10 files changed, 433 insertions(+), 95 deletions(-) create mode 100644 app/src/main/java/net/programmierecke/radiodroid2/data/PlaylistM3U.java create mode 100644 app/src/main/java/net/programmierecke/radiodroid2/data/PlaylistM3UEntry.java diff --git a/app/build.gradle b/app/build.gradle index 9185d72b9..d26aa867f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,8 +11,8 @@ android { minSdkVersion 8 targetSdkVersion 23 - versionCode 37 - versionName "0.27" + versionCode 38 + versionName "0.28" } buildTypes { release { diff --git a/app/src/main/aidl/net/programmierecke/radiodroid2/IPlayerService.aidl b/app/src/main/aidl/net/programmierecke/radiodroid2/IPlayerService.aidl index 9c7ea6816..a8ac1c358 100644 --- a/app/src/main/aidl/net/programmierecke/radiodroid2/IPlayerService.aidl +++ b/app/src/main/aidl/net/programmierecke/radiodroid2/IPlayerService.aidl @@ -22,4 +22,5 @@ void stopRecording(); boolean isRecording(); String getCurrentRecordFileName(); long getTransferedBytes(); +boolean getIsHls(); } diff --git a/app/src/main/java/net/programmierecke/radiodroid2/ActivityPlayerInfo.java b/app/src/main/java/net/programmierecke/radiodroid2/ActivityPlayerInfo.java index 088779d08..68e2f52f3 100644 --- a/app/src/main/java/net/programmierecke/radiodroid2/ActivityPlayerInfo.java +++ b/app/src/main/java/net/programmierecke/radiodroid2/ActivityPlayerInfo.java @@ -218,6 +218,9 @@ private void UpdateOutput() { } String strExtra = ""; + if (PlayerServiceUtil.getIsHls()){ + strExtra += "HLS-Stream\n"; + } if (PlayerServiceUtil.getCurrentRecordFileName() != null){ strExtra += getResources().getString(R.string.player_info_record_to,PlayerServiceUtil.getCurrentRecordFileName()) + "\n"; } diff --git a/app/src/main/java/net/programmierecke/radiodroid2/PlayerService.java b/app/src/main/java/net/programmierecke/radiodroid2/PlayerService.java index d8c4531a2..fcd34e797 100644 --- a/app/src/main/java/net/programmierecke/radiodroid2/PlayerService.java +++ b/app/src/main/java/net/programmierecke/radiodroid2/PlayerService.java @@ -53,6 +53,7 @@ public class PlayerService extends Service implements IStreamProxyEventReceiver private PowerManager powerManager; private PowerManager.WakeLock wakeLock; private WifiManager.WifiLock wifiLock; + private boolean isHls = false; enum PlayStatus{ Idle, @@ -160,6 +161,11 @@ public int getMetadataChannels() throws RemoteException { return 0; } + @Override + public boolean getIsHls() throws RemoteException { + return isHls; + } + @Override public boolean isPlaying() throws RemoteException { return playStatus != PlayStatus.Idle; @@ -474,18 +480,22 @@ private void UpdateNotification() { } @Override - public void foundShoutcastStream(ShoutcastInfo info) { + public void foundShoutcastStream(ShoutcastInfo info, boolean isHls) { this.streamInfo = info; - Log.i(TAG, "Metadata offset:" + info.metadataOffset); - Log.i(TAG, "Bitrate:" + info.bitrate); - Log.i(TAG, "Name:" + info.audioName); - if (info.audioName != null) { - if (!info.audioName.trim().equals("")) { - itsStationName = info.audioName.trim(); + this.isHls = isHls; + if (info != null) { + Log.i(TAG, "Metadata offset:" + info.metadataOffset); + Log.i(TAG, "Bitrate:" + info.bitrate); + Log.i(TAG, "Name:" + info.audioName); + Log.i(TAG, "Hls:" + isHls); + if (info.audioName != null) { + if (!info.audioName.trim().equals("")) { + itsStationName = info.audioName.trim(); + } } + Log.i(TAG, "Server:" + info.serverName); + Log.i(TAG, "AudioInfo:" + info.audioInfo); } - Log.i(TAG, "Server:" + info.serverName); - Log.i(TAG, "AudioInfo:" + info.audioInfo); sendBroadCast(PLAYER_SERVICE_META_UPDATE); } diff --git a/app/src/main/java/net/programmierecke/radiodroid2/PlayerServiceUtil.java b/app/src/main/java/net/programmierecke/radiodroid2/PlayerServiceUtil.java index 0479b287a..676adec06 100644 --- a/app/src/main/java/net/programmierecke/radiodroid2/PlayerServiceUtil.java +++ b/app/src/main/java/net/programmierecke/radiodroid2/PlayerServiceUtil.java @@ -208,6 +208,17 @@ public static String getCurrentRecordFileName() { return null; } + public static boolean getIsHls() { + if (itsPlayerService != null) { + try { + return itsPlayerService.getIsHls(); + } catch (RemoteException e) { + Log.e("", "" + e); + } + } + return false; + } + public static long getTransferedBytes() { if (itsPlayerService != null) { try { diff --git a/app/src/main/java/net/programmierecke/radiodroid2/StreamProxy.java b/app/src/main/java/net/programmierecke/radiodroid2/StreamProxy.java index 17a6685ca..a2d3c4d91 100644 --- a/app/src/main/java/net/programmierecke/radiodroid2/StreamProxy.java +++ b/app/src/main/java/net/programmierecke/radiodroid2/StreamProxy.java @@ -3,6 +3,8 @@ import android.content.Context; import android.util.Log; +import net.programmierecke.radiodroid2.data.PlaylistM3U; +import net.programmierecke.radiodroid2.data.PlaylistM3UEntry; import net.programmierecke.radiodroid2.data.ShoutcastInfo; import net.programmierecke.radiodroid2.interfaces.IStreamProxyEventReceiver; @@ -12,10 +14,12 @@ import java.io.InputStream; import java.io.OutputStream; import java.net.InetAddress; +import java.net.MalformedURLException; import java.net.ServerSocket; import java.net.Socket; import java.net.URL; import java.net.URLConnection; +import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.HashMap; @@ -33,6 +37,7 @@ public class StreamProxy { private boolean isStopped = false; private String outFileName = null; final String TAG = "PROXY"; + boolean isHls = false; public StreamProxy(Context context, String uri, IStreamProxyEventReceiver callback) { this.context = context; @@ -85,6 +90,184 @@ public void run() { InputStream in; OutputStream out; + byte buf[] = new byte[16384*2]; + byte bufMetadata[] = new byte[256 * 16]; + + private void defaultStream(ShoutcastInfo info) throws Exception{ + int bytesUntilMetaData = 0; + boolean readMetaData = false; + boolean filterOutMetaData = false; + + if (info != null) { + callback.foundShoutcastStream(info,false); + bytesUntilMetaData = info.metadataOffset; + filterOutMetaData = true; + } + + int readBytesBuffer = 0; + int readBytesBufferMetadata = 0; + + while (!isStopped) { + int readBytes = 0; + if (!filterOutMetaData || (bytesUntilMetaData > 0)) { + int bytesToRead = buf.length - readBytesBuffer; + if (filterOutMetaData) { + bytesToRead = Math.min(bytesUntilMetaData, bytesToRead); + } + readBytes = in.read(buf, readBytesBuffer, bytesToRead); + if (readBytes == 0) { + continue; + } + if (readBytes < 0) { + break; + } + readBytesBuffer += readBytes; + connectionBytesTotal += readBytes; + if (filterOutMetaData) { + bytesUntilMetaData -= readBytes; + } + + Log.v(TAG, "in:" + readBytes); + if (readBytesBuffer > buf.length / 2) { + Log.v(TAG, "out:" + readBytesBuffer); + out.write(buf, 0, readBytesBuffer); + if (fileOutputStream != null) { + Log.v(TAG, "writing to record file.."); + fileOutputStream.write(buf, 0, readBytesBuffer); + } + readBytesBuffer = 0; + } + } else { + int metadataBytes = in.read() * 16; + int metadataBytesToRead = metadataBytes; + readBytesBufferMetadata = 0; + bytesUntilMetaData = info.metadataOffset; + Log.d(TAG, "metadata size:" + metadataBytes); + if (metadataBytes > 0) { + Arrays.fill(bufMetadata, (byte) 0); + while (true) { + readBytes = in.read(bufMetadata, readBytesBufferMetadata, metadataBytesToRead); + if (readBytes == 0) { + continue; + } + if (readBytes < 0) { + break; + } + metadataBytesToRead -= readBytes; + readBytesBufferMetadata += readBytes; + if (metadataBytesToRead <= 0) { + String s = new String(bufMetadata, 0, metadataBytes, "utf-8"); + Log.d(TAG, "METADATA:" + s); + Map dict = DecodeShoutcastMetadata(s); + Log.d(TAG, "META:" + dict.get("StreamTitle")); + callback.foundLiveStreamInfo(dict); + break; + } + } + } + } + } + } + + private void streamFile(String urlStr) throws IOException { + Log.i(TAG,"URL Stream Data: "+urlStr); + URL u = new URL(urlStr); + URLConnection connection = u.openConnection(); + connection.setConnectTimeout(2000); + connection.setReadTimeout(10000); + connection.connect(); + InputStream inContent = connection.getInputStream(); + boolean recordActive = false; + if (fileOutputStream != null){ + recordActive = true; + } + + byte[] bufContent = new byte[1000]; + while(!isStopped){ + int bytesRead = inContent.read(bufContent); + if (bytesRead < 0){ + break; + } + connectionBytesTotal+=bytesRead; + out.write(bufContent,0,bytesRead); + if ((fileOutputStream != null) && recordActive) { + Log.v(TAG, "writing to record file.."); + fileOutputStream.write(bufContent, 0, bytesRead); + } + } + } + + boolean containsString(ArrayList list, String item){ + for (int i=0;i streamedFiles = new ArrayList(); + + private void hlsStream(URL path, int size, InputStream inM3U) throws Exception{ + int readBytes = 0; + int readBytesBuffer = 0; + int bytesToRead = size; + byte bufM3U[] = new byte[size]; + + if (!isHls){ + isHls = true; + callback.foundShoutcastStream(null,true); + } + + while (!isStopped){ + readBytes = inM3U.read(bufM3U, readBytesBuffer, bytesToRead); + if (readBytes < 0){ + break; + } + readBytesBuffer += readBytes; + bytesToRead -= readBytes; + } + + String s = new String(bufM3U, 0, readBytesBuffer, "utf-8"); + Log.d(TAG,"read m3u:\n"+s); + PlaylistM3U playlist = new PlaylistM3U(path, s); + + PlaylistM3UEntry[] entries = playlist.getEntries(); + for (int i=0;i= 0){ + urlWithoutQuery = urlStr.substring(0,indexQuery); + } + + if (!containsString(streamedFiles,urlWithoutQuery)){ + Log.w(TAG,"did not find in db:"+urlWithoutQuery); + streamedFiles.add(urlWithoutQuery); + + streamFile(urlStr); + continue; + } + } else { + Log.w(TAG,"URL Stream info:"+urlStr); + URL u = new URL(urlStr); + URLConnection connection = u.openConnection(); + connection.setConnectTimeout(2000); + connection.setReadTimeout(10000); + connection.connect(); + int sizeItem = connection.getHeaderFieldInt("Content-Length",0); + hlsStream(u,sizeItem,connection.getInputStream()); + break; + } + } + } + private void doConnectToStream() { try{ final int MaxRetries = 30; @@ -92,14 +275,14 @@ private void doConnectToStream() { while (!isStopped && retry > 0) { try { // connect to stream - URLConnection connection = new URL(uri).openConnection(); + Log.i(TAG,"doConnectToStream:"+uri); + URL u = new URL(uri); + URLConnection connection = u.openConnection(); connection.setConnectTimeout(5000); connection.setReadTimeout(10000); connection.setRequestProperty("Icy-MetaData", "1"); connection.connect(); - in = connection.getInputStream(); - // send ok message to local mediaplayer out = socketProxy.getOutputStream(); out.write(("HTTP/1.0 200 OK\r\n" + @@ -107,86 +290,23 @@ private void doConnectToStream() { "Content-Type: " + connection.getContentType() + "\r\n\r\n").getBytes("utf-8")); - // try to get shoutcast information from stream connection - ShoutcastInfo info = ShoutcastInfo.Decode(connection); - - int bytesUntilMetaData = 0; - boolean readMetaData = false; - boolean filterOutMetaData = false; - - if (info != null) { - callback.foundShoutcastStream(info); - bytesUntilMetaData = info.metadataOffset; - filterOutMetaData = true; - } - - byte buf[] = new byte[16384*2]; - byte bufMetadata[] = new byte[256 * 16]; - int readBytesBuffer = 0; - int readBytesBufferMetadata = 0; - - while (!isStopped) { - int readBytes = 0; - if (!filterOutMetaData || (bytesUntilMetaData > 0)) { - int bytesToRead = buf.length - readBytesBuffer; - if (filterOutMetaData) { - bytesToRead = Math.min(bytesUntilMetaData, bytesToRead); - } - readBytes = in.read(buf, readBytesBuffer, bytesToRead); - if (readBytes == 0) { - continue; - } - if (readBytes < 0) { - break; - } - readBytesBuffer += readBytes; - connectionBytesTotal += readBytes; - if (filterOutMetaData) { - bytesUntilMetaData -= readBytes; - } - - Log.v(TAG, "in:" + readBytes); - if (readBytesBuffer > buf.length / 2) { - Log.v(TAG, "out:" + readBytesBuffer); - out.write(buf, 0, readBytesBuffer); - if (fileOutputStream != null) { - Log.v(TAG, "writing to record file.."); - fileOutputStream.write(buf, 0, readBytesBuffer); - } - readBytesBuffer = 0; - } - } else { - int metadataBytes = in.read() * 16; - int metadataBytesToRead = metadataBytes; - readBytesBufferMetadata = 0; - bytesUntilMetaData = info.metadataOffset; - Log.d(TAG, "metadata size:" + metadataBytes); - if (metadataBytes > 0) { - Arrays.fill(bufMetadata, (byte) 0); - while (true) { - readBytes = in.read(bufMetadata, readBytesBufferMetadata, metadataBytesToRead); - if (readBytes == 0) { - continue; - } - if (readBytes < 0) { - break; - } - metadataBytesToRead -= readBytes; - readBytesBufferMetadata += readBytes; - if (metadataBytesToRead <= 0) { - String s = new String(bufMetadata, 0, metadataBytes, "utf-8"); - Log.d(TAG, "METADATA:" + s); - Map dict = DecodeShoutcastMetadata(s); - Log.d(TAG, "META:" + dict.get("StreamTitle")); - callback.foundLiveStreamInfo(dict); - break; - } - } - } - } - // reset retry count, if connection was ok - retry = MaxRetries; + String type = connection.getHeaderField("Content-Type").toLowerCase(); + Integer size = connection.getHeaderFieldInt("Content-Length",0); + Log.d(TAG,"Content Type: "+type); + if (type.equals("application/vnd.apple.mpegurl") || type.equals("application/x-mpegurl")){ + // Hls stream + hlsStream(u, size, connection.getInputStream()); + // wait some time for next check for new files in stream + Thread.sleep(5000); + } else { + // try to get shoutcast information from stream connection + ShoutcastInfo info = ShoutcastInfo.Decode(connection); + + in = connection.getInputStream(); + defaultStream(info); } + // reset retry count, if connection was ok + retry = MaxRetries; } catch (Exception e) { Log.e(TAG, "Inside loop ex Proxy()" + e); } @@ -207,10 +327,17 @@ private void doConnectToStream() { public void record(String stationName){ Calendar calendar = Calendar.getInstance(); calendar.setTimeInMillis(System.currentTimeMillis()); - outFileName = String.format("%2$s - %1$tY_%1$tm_%1$td_%1$tH_%1$tM_%1$tS.mp3", calendar, Utils.sanitizeName(stationName)); + if (getIsHls()) + outFileName = String.format("%2$s - %1$tY_%1$tm_%1$td_%1$tH_%1$tM_%1$tS.ts", calendar, Utils.sanitizeName(stationName)); + else + outFileName = String.format("%2$s - %1$tY_%1$tm_%1$td_%1$tH_%1$tM_%1$tS.mp3", calendar, Utils.sanitizeName(stationName)); recordInternal(outFileName); } + public boolean getIsHls(){ + return isHls; + } + void recordInternal(String fileName){ if (fileOutputStream == null) { try { diff --git a/app/src/main/java/net/programmierecke/radiodroid2/data/DataRadioStation.java b/app/src/main/java/net/programmierecke/radiodroid2/data/DataRadioStation.java index 4cd165d17..dc8f440e7 100644 --- a/app/src/main/java/net/programmierecke/radiodroid2/data/DataRadioStation.java +++ b/app/src/main/java/net/programmierecke/radiodroid2/data/DataRadioStation.java @@ -34,12 +34,16 @@ public DataRadioStation() { public int Bitrate; public String Codec; public boolean Working = true; + public boolean Hls = false; public String getShortDetails(Context ctx) { List aList = new ArrayList(); if (!Working){ aList.add(ctx.getResources().getString(R.string.station_detail_broken)); } + if (Hls){ + aList.add("HLS"); + } if (Codec != null){ if (!Codec.trim().equals("")){ aList.add(Codec); @@ -100,6 +104,9 @@ public static DataRadioStation[] DecodeJson(String result) { if (anObject.has("lastcheckok")){ aStation.Working = anObject.getInt("lastcheckok") != 0; } + if (anObject.has("hls")){ + aStation.Hls = anObject.getInt("hls") != 0; + } aList.add(aStation); }catch(Exception e){ diff --git a/app/src/main/java/net/programmierecke/radiodroid2/data/PlaylistM3U.java b/app/src/main/java/net/programmierecke/radiodroid2/data/PlaylistM3U.java new file mode 100644 index 000000000..4e7327fe2 --- /dev/null +++ b/app/src/main/java/net/programmierecke/radiodroid2/data/PlaylistM3U.java @@ -0,0 +1,88 @@ +package net.programmierecke.radiodroid2.data; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.StringReader; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; + +/** + * Created by segler on 04.03.17. + */ + +public class PlaylistM3U { + final static String COMMENTMARKER = "#"; + final static String EXTENDED = "#EXTM3U"; + + String fullText; + URL path; + boolean extended = false; + ArrayList entries = new ArrayList(); + String header = null; + + public PlaylistM3U(URL _path, String _fullText){ + path = _path; + fullText = _fullText; + decode(); + } + + void decode(){ + String[] lines = getLines(); + for (int i=0;i list = new ArrayList(); + String line; + try { + while ((line = br.readLine()) != null){ + list.add(line); + } + } catch (IOException e) { + + } + return list.toArray(new String[0]); + } + + public PlaylistM3UEntry[] getEntries(){ + return entries.toArray(new PlaylistM3UEntry[0]); + } +} diff --git a/app/src/main/java/net/programmierecke/radiodroid2/data/PlaylistM3UEntry.java b/app/src/main/java/net/programmierecke/radiodroid2/data/PlaylistM3UEntry.java new file mode 100644 index 000000000..b2c214e09 --- /dev/null +++ b/app/src/main/java/net/programmierecke/radiodroid2/data/PlaylistM3UEntry.java @@ -0,0 +1,91 @@ +package net.programmierecke.radiodroid2.data; + +import android.util.Log; + +/** + * Created by segler on 04.03.17. + */ + +public class PlaylistM3UEntry { + final static String EXTINF = "#EXTINF:"; + final static String STREAMINF = "#EXT-X-STREAM-INF:"; + final static String STREAMINF_PROGRAM = "PROGRAM-ID="; + final static String STREAMINF_BANDWIDTH = "BANDWIDTH="; + final static String STREAMINF_CODECS = "CODECS="; + + final static String TAG = "M3U"; + + String header; + String content; + int length = -1; + String title = null; + int bitrate = -1; + int programid = -1; + boolean isStreamInfo = false; + + public PlaylistM3UEntry(String _header, String _content){ + header = _header; + content = _content; + + decode(); + } + + public PlaylistM3UEntry(String _content){ + header = null; + content = _content; + } + + void decode() { + if (header == null) { + return; + } + if (header.startsWith(EXTINF)) { + Log.d(TAG,"found EXTINF:"+header); + String attributes = header.substring(EXTINF.length()); + int sep = attributes.indexOf(","); + String timeStr = attributes.substring(0, sep); + length = Integer.getInteger(timeStr, -1); + title = attributes.substring(sep + 1); + } else if (header.startsWith(STREAMINF)) { + Log.d(TAG,"found STREAMINFO:"+header); + isStreamInfo = true; + String attributes = header.substring(STREAMINF.length()); + String[] attributesList = attributes.split(","); + for (int i = 0; i < attributesList.length; i++) { + String attr = attributesList[i]; + if (attr.startsWith(STREAMINF_BANDWIDTH)) { + String paramStr = attr.substring(STREAMINF_BANDWIDTH.length()); + bitrate = Integer.getInteger(paramStr, -1); + } + if (attr.startsWith(STREAMINF_PROGRAM)) { + String paramStr = attr.substring(STREAMINF_PROGRAM.length()); + programid = Integer.getInteger(paramStr, -1); + } + } + } + } + + public boolean getIsStreamInformation(){ + return isStreamInfo; + } + + public int getBitrate(){ + return bitrate; + } + + public int getLength(){ + return length; + } + + public String getTitle(){ + return title; + } + + public int getProgramId(){ + return programid; + } + + public String getContent(){ + return content; + } +} diff --git a/app/src/main/java/net/programmierecke/radiodroid2/interfaces/IStreamProxyEventReceiver.java b/app/src/main/java/net/programmierecke/radiodroid2/interfaces/IStreamProxyEventReceiver.java index 6cbad2913..aee42c1c2 100644 --- a/app/src/main/java/net/programmierecke/radiodroid2/interfaces/IStreamProxyEventReceiver.java +++ b/app/src/main/java/net/programmierecke/radiodroid2/interfaces/IStreamProxyEventReceiver.java @@ -5,7 +5,7 @@ import java.util.Map; public interface IStreamProxyEventReceiver { - void foundShoutcastStream(ShoutcastInfo bitrate); + void foundShoutcastStream(ShoutcastInfo bitrate, boolean isHls); void foundLiveStreamInfo(Map liveInfo); void streamStopped(); }